
去年我们把一个核心服务从虚拟机迁移到 Kubernetes。上线第一天,P99 延迟正常,错误率正常,一切看起来完美。第二天早上看监控,发现凌晨时段有十几条 5xx 错误,集中在零点前后。
排查日志发现——零点的时候 K8s 做了滚动更新。它给旧 Pod 发了 SIGTERM,旧 Pod 立刻停止接收新请求,然后——直接杀了。那些正在处理中的请求全部中断。十几个用户在下单过程中看到了「系统繁忙」。
根因一句话:我们配了 K8s,没有配 Spring Boot。 K8s 默认给 Pod 30 秒的优雅终止时间。Spring Boot 默认收到 SIGTERM 后立刻停机,不等任何请求处理完。两个默认值打架了——K8s 说「我给你 30 秒」,Spring Boot 说「我不需要,我立刻死」。
Spring Boot 2.3 之后,优雅停机需要显式开启。不开启的话,shutdown 方法和 tomcat 都没处理完请求就被关掉了。
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 25s两行配置。shutdown: graceful 告诉 Spring Boot 收到停机信号后不要再接受新请求,但让正在处理的请求执行完毕。
timeout-per-shutdown-phase: 25s 设了一个硬上限——如果 25 秒后还有请求没处理完,强制停机。这个值必须小于 K8s 的
terminationGracePeriodSeconds,留出至少 5 秒的缓冲。K8s 给 30 秒,Spring Boot 设为 25 秒——剩下 5 秒是容器销毁的时间。
如果你的应用里还有自定义的线程池——@Async 的线程池、消息消费的线程池——这些线程池也需要优雅关闭。在 ThreadPoolTaskExecutor 配置里设
setWaitForTasksToCompleteOnShutdown(true) 和
setAwaitTerminationSeconds(20)。Spring 容器关闭时会依次关闭所有管理的线程池,等待任务完成。任务不能在规定时间内完成的,同样强制终止。
优雅停机的本质不是「让一切正常结束」,而是「给你一个有限的窗口,尽可能多地保存进度」。 你不可能让每一个请求都在 25 秒内完成——有些请求就是慢,比如上传大文件、导出报表。你要做的是让绝大多数正常请求(通常在几百毫秒内完成)安全结束,然后接受少数长请求会被截断的事实。
K8s 有三种探针。livenessProbe 问「你还活着吗」,死了就重启。readinessProbe 问「你能接流量吗」,不能就从 Service 摘掉。startupProbe 问「你启动完了吗」,没启动完就不触发另外两种探针。
最常见的配置错误是把这三种探针全指向同一个 /actuator/health。这个端点默认检查所有组件——数据库连接、Redis 连接、消息队列连接、磁盘空间。你的数据库做维护,连接池满了,/health 返回 DOWN。K8s 看到 livenessProbe 失败,认为 Pod 死了——直接 kill 掉重启。然后新 Pod 启动,数据库还没恢复,又死了。进入 CrashLoopBackOff 循环。
livenessProbe 和 readinessProbe 必须分开。 livenessProbe 只检查「JVM 进程是否活着」——磁盘空间、死锁检测。readinessProbe 检查「这个实例能不能接业务流量」——数据库连接、外部依赖。数据库挂了,readiness 失败,K8s 把 Pod 从 Service 摘掉,流量切到其他健康的 Pod。liveness 不受影响,Pod 不会被重启。
Spring Boot 2.3+ 内置了这个区分:
management:
endpoint:
health:
probes:
enabled: true
health:
livenessState:
enabled: true
readinessState:
enabled: true/actuator/health/liveness 只检查 JVM 状态。
/actuator/health/readiness 检查外部依赖。K8s 配置里 livenessProbe 指向前者,readinessProbe 指向后者。
你的应用在启动时需要预热——加载缓存、建立连接池、初始化线程池。启动花 60 秒。K8s 的 livenessProbe 默认在容器启动后立刻开始检查,initialDelaySeconds 你设了 10 秒。10 秒后 liveness 检查失败,K8s 认为 Pod 有问题——杀掉重启。然后下一个 Pod 启动,又需要 60 秒,又在 10 秒时被杀——无限循环。
startupProbe 就是解决这个问题的。它只在启动阶段生效,一旦 startupProbe 成功,livenessProbe 和 readinessProbe 才开始工作。
startupProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
failureThreshold: 30
periodSeconds: 2这个配置允许最多 30 次失败(60 秒),每次都间隔 2 秒。只要在 60 秒内启动成功一次,就把控制权交给 livenessProbe 和 readinessProbe。60 秒之后还没成功——那 Pod 确实有问题,该重启。
startupProbe 是你给应用的最长启动时间。设得太短,你的应用永远起不来。设得太长,有问题的 Pod 会占用资源太长时间。 把它设为你正常启动时间的 1.5 倍——留有缓冲,但不会无限等待。
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 25s
management:
endpoint:
health:
probes:
enabled: true
health:
livenessState:
enabled: true
readinessState:
enabled: trueK8s 侧:
spec:
terminationGracePeriodSeconds: 30
containers:
startupProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
failureThreshold: 30
periodSeconds: 2
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
periodSeconds: 5这个配置通过了两年的生产环境验证。没有一条凌晨三点的报警是因为 Pod 被杀引起的。
默认值的坑,是 K8s 和 Spring Boot 之间的信息不对称造成的。K8s 不知道你的应用需要多长时间处理完请求,Spring Boot 不知道 K8s 给了它多少时间。你的配置就是它们之间唯一的沟通方式。
优雅停机和健康检查,不是在 K8s 部署完之后的优化项。它们是部署的第一步。
转给你那个正在往 K8s 上部署 Spring Boot 的同事。他可能还不知道光靠默认配置,他的 Pod 正在被 K8s 反复 kill。
更新时间:2026-06-15
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight All Rights Reserved.
Powered By 61893.com 闽ICP备11008920号
闽公网安备35020302035593号