Docker 镜像让应用能够被重复启动,但生产平台不会只启动一次。发布新版本、缩容或节点维护都会创建新实例并终止旧实例。如果旧实例立即消失,正在执行的 HTTP 请求和数据库事务可能被中断;如果新实例过早接收流量,请求又可能到达尚未完成初始化的应用。
本章把启动、接收流量和停止拆成不同状态,说明 Spring Boot 的优雅停机、资源关闭、启动任务、Actuator Readiness,以及 Docker Compose 和 Kubernetes 怎样给应用足够时间完成状态切换。
1. 进程信号
容器平台通常先向主进程发送 SIGTERM,通知它开始有序退出。等待终止宽限期后进程仍未结束,平台才发送无法被应用处理的 SIGKILL:
1平台决定终止实例2-> 发送 SIGTERM3-> JVM 关闭钩子开始关闭 ApplicationContext4-> 停止接收新请求并等待在途请求5-> 关闭 Bean 与资源6-> Java 进程退出78超过平台宽限期9-> SIGKILL 强制终止
Dockerfile 使用 exec 形式启动 Java:
1ENTRYPOINT ["java", "-jar", "application.jar"]
此时 Java 是容器主进程,可以直接收到信号。下面的 shell 形式会先启动 shell,信号转发和进程退出行为更容易出错:
1ENTRYPOINT java -jar application.jar
本地在 IDE 中点击强制停止不一定会发送正常 SIGTERM,不能据此判断生产优雅停机失效。可以使用 docker stop 或操作系统信号验证真实关闭路径。
2. 优雅停机
Spring Boot 4 对支持的内嵌 Web 服务器默认启用优雅停机。关闭 ApplicationContext 时,Web 服务器停止接收新请求,并给已经开始的请求一段完成时间。
配置每个关闭阶段的最长等待时间:
1spring:2lifecycle:3timeout-per-shutdown-phase: 20s
这个值不是让每次停机都固定等待 20 秒。在途请求和生命周期组件提前结束时,进程可以更早退出;超过时间后,框架会继续推进关闭过程。
优雅停机不能保证任何请求无限执行。接口自身仍应设置数据库、HTTP 客户端和业务操作超时,平台终止宽限期也必须大于应用需要的退出时间。
不同服务器拒绝新请求的具体表现可能不同。当前内嵌 Tomcat 会在网络层停止接受新请求,但已经建立的持久连接、负载均衡状态传播和平台网络仍会影响客户端观察结果。
如果明确需要关闭优雅停机,可以设置 server.shutdown: immediate,但生产 Web 应用通常不应这样做。遇到停机过慢,应找出未结束任务和超时设置,而不是首先改成立即终止。
3. 资源关闭
ApplicationContext 关闭时,会按生命周期规则销毁单例 Bean。Spring 管理的 HikariCP、Web 服务器和指标导出器会执行自己的关闭逻辑,应用不需要重复关闭这些容器资源。
自定义 Bean 确实持有需要释放的资源时,可以使用前面生命周期文章介绍的 @PreDestroy,或者在 @Bean 上声明销毁方法。谁创建资源,通常就由谁负责提供关闭策略,避免多个 Bean 重复关闭同一对象。当前项目直接使用容器管理的 DataSource、MeterRegistry 和 Web 服务器,不需要再添加一层手动关闭代码。
需要参与分阶段启动与停止的长期后台组件可以实现 SmartLifecycle,通过 phase 控制顺序。普通文件、客户端和线程池优先使用框架已经提供的 Bean 与销毁回调,不要仅为排序需求就把简单组件升级成复杂生命周期实现。
销毁方法应快速、有界并允许重复保护。不要在 JVM 停机期间启动新的长期任务,也不能把必须持久保存的数据只留到 @PreDestroy 才写入,因为 SIGKILL、机器故障和进程崩溃不会保证回调执行。
4. 启动与就绪
ApplicationContext 创建完成不表示应用已经准备接收业务流量。Flyway 迁移、缓存预热或必要数据检查可能还在执行。Spring Boot 建议把需要在启动阶段完成的任务放入 ApplicationRunner 或 CommandLineRunner,而不是塞进普通 Bean 的 @PostConstruct。
为当前项目增加一个轻量启动检查,复用已有 Repository 查询默认服务:
01package cn.usehook.spring.startup;0203import cn.usehook.spring.config.HealthProperties;04import cn.usehook.spring.health.HealthStatusRepository;05import org.springframework.boot.ApplicationArguments;06import org.springframework.boot.ApplicationRunner;07import org.springframework.stereotype.Component;0809@Component10public class DatabaseStartupVerifier implements ApplicationRunner {1112private final HealthProperties properties;13private final HealthStatusRepository repository;1415public DatabaseStartupVerifier(16HealthProperties properties,17HealthStatusRepository repository18) {19this.properties = properties;20this.repository = repository;21}2223@Override24public void run(ApplicationArguments arguments) {25repository.findByService(properties.service());26}27}
查询使用 health_status 的服务主键,不扫描或加载全部业务记录。默认服务尚无状态时返回空 Optional,应用仍可启动;数据库连接失败、表不存在或 SQL 映射错误则继续抛出异常,让启动失败,而不是让无法完成核心业务的实例进入流量。Flyway 已经提供结构迁移和校验时,这个额外检查可以按项目需要保留,不必为同一条件重复查询。
Spring Boot 会在应用与命令行 Runner 执行完成后把 Readiness 切换为接收流量。平台调用 /actuator/health/readiness 时,新实例在启动任务结束前不会被当作就绪。
不是所有外部依赖都必须阻止进程启动。核心数据库缺失可能让当前应用完全无法工作,非关键通知服务失败则可以保持应用启动并进入降级状态。启动失败、Readiness 失败和业务内降级要根据依赖职责选择。
5. 容器宽限期
Docker Compose 可以给 Spring Boot 比内部 20 秒更长的退出时间:
1services:2app:3image: usehook/spring-health:0.1.04stop_grace_period: 30s
执行下面的命令观察日志:
1docker compose stop app
Compose 先发送终止信号,最多等待 30 秒。应用内部每个关闭阶段允许 20 秒,两者不是简单相加,但平台上限至少要覆盖应用期望的退出窗口和少量调度余量。
同时发起一个耗时请求再停止容器,可以验证在途请求是否完成。测试不应只观察进程最终消失,还要检查请求响应、事务结果、关闭日志以及平台是否在宽限期内避免强制终止。
应用若直接写本地文件,容器结束后数据不一定保留。业务状态应该在请求事务中写入 PostgreSQL 或其他持久存储,不能把优雅停机当成数据持久化机制。
6. Kubernetes 生命周期
Kubernetes Deployment 可以组合启动、存活、就绪探针和终止宽限期:
01spec:02template:03spec:04terminationGracePeriodSeconds: 3005containers:06- name: spring-health07image: usehook/spring-health:0.1.008ports:09- name: http10containerPort: 808011startupProbe:12httpGet:13path: /actuator/health/liveness14port: http15periodSeconds: 516failureThreshold: 2417livenessProbe:18httpGet:19path: /actuator/health/liveness20port: http21periodSeconds: 1022failureThreshold: 323readinessProbe:24httpGet:25path: /actuator/health/readiness26port: http27periodSeconds: 528failureThreshold: 2
startupProbe 成功前,Kubernetes 不执行存活探针,因此启动较慢的实例不会因为尚未完成初始化而被反复重启。Readiness 失败只把 Pod 移出服务流量,Liveness 连续失败则会触发重启。
终止时,应用开始关闭与负载均衡停止转发可能并行发生,网络状态传播存在延迟。实际平台可能需要在 preStop 中预留短暂等待,让新流量先停止进入,再开始消耗 Spring Boot 的优雅停机窗口;具体时间必须通过部署环境验证,不能照搬固定秒数。
一次可靠发布的顺序应是:新实例通过启动和就绪检查后进入流量,旧实例先退出流量,再等待在途请求和事务结束,最后释放资源并退出。日志、指标和 Trace 要覆盖这一过程,才能区分启动失败、探针失败、超时强杀和正常滚动替换。
至此,主线已经从普通 Java 对象走到可部署应用。侧栏最后的 Java 模块属于独立扩展阅读,不是运行 Spring Boot 项目的必需步骤,可以在需要理解 JPMS 时再学习。