场景:为什么"停机"这件事不简单
一次普通的发布滚动更新,监控却报出一批 5xx 和连接重置。排查后发现:Kubernetes 给 Pod 发了 SIGTERM,JVM 立刻退出,而此时还有几十个 HTTP 请求正在处理、数据库事务尚未提交、消息消费者刚拉了一批消息还没 ack。这些请求被硬生生掐断,对外表现就是偶发的失败。
优雅停机(graceful shutdown)要解决的核心问题是:收到停机信号后,先停止接收新流量,把存量请求处理完,再释放资源、退出进程。 而健康检查则是流量调度层判断"这个实例还能不能接活"的依据。两者必须配合,否则单独打开优雅停机反而可能拉长 502 窗口。
机制拆解:一次干净停机的完整时序
理解优雅停机,要先理解信号和容器调度的交互。一个典型的 K8s 下线流程:
1 | 1. 控制面将 Pod 标记为 Terminating,同时: |
这里有一个极其容易被忽略的竞态:步骤 1a(摘除 Endpoint)和 1b(发 SIGTERM)是并行的,且摘除会经过 kube-proxy / iptables / ingress 多级传播。如果应用收到 SIGTERM 后立刻拒绝新连接,但此时上游负载均衡还没更新路由,新请求仍会被打过来,直接连接拒绝。
所以正确做法不是"立即停",而是"先睡一会儿,等路由收敛,再开始停"。这就是所谓的 pre-stop 静默期。
Spring Boot 的实现
Spring Boot 内建了优雅停机支持。关键配置只有两行:
1 | server: |
开启后,当容器关闭时,WebServerGracefulShutdownLifecycle 会被触发,内部对 Tomcat/Netty 做这样的事:暂停 connector 的 acceptor 线程,然后轮询工作线程池,直到活跃请求归零或超时。
源码层面,Spring 通过 SmartLifecycle 的 phase 机制来编排关闭顺序。Web server 属于最高 phase(Integer.MAX_VALUE),所以最先关闭——这保证了在关掉业务线程池、数据源之前,先停掉流量入口:
1 | // GracefulShutdown(Tomcat 实现简化) |
注意一个边界:优雅停机只覆盖"在途的同步 HTTP 请求"。异步任务、@Async 线程池、Kafka/RabbitMQ 消费者、定时任务,Spring 不会自动等它们——这些需要你自己声明 DisposableBean 或注册 shutdown hook,并通过 phase 排在 Web server 之后关闭。
健康检查:liveness 与 readiness 的本质区别
很多人把这两个探针混为一谈,但它们的语义完全不同:
- readiness(就绪):我现在能不能接收流量?失败 → 从 Endpoints 摘除,但不重启。
- liveness(存活):我这个进程还活着、有救吗?失败 → 直接重启容器。
把它们搞反会出严重事故。典型反模式:把 readiness 探针配成检查下游数据库连通性。当数据库抖动,所有实例的 readiness 同时失败,全部被摘流量,服务整体雪崩——即使数据库只是短暂慢了一下。liveness 探针更要谨慎:如果它依赖外部资源,一次外部抖动会触发全集群滚动重启,把小故障放大成大故障。
经验法则:
- liveness 只检查进程自身是否死锁/卡死,不依赖任何外部系统。
- readiness 可以检查关键依赖,但要对"非致命依赖"宽容。
Spring Boot Actuator 把这套模型直接暴露了出来:
1 | management: |
更妙的是,Spring 的 ApplicationAvailability 会在优雅停机开始的瞬间,把 readiness 状态自动切到 REFUSING_TRAFFIC。配合 K8s 的 readiness 探针,就能在进程还活着、还在处理存量请求时,主动告诉调度层"别再给我发新的了"。
工程权衡:把时间预算对齐
最常见的线上踩坑是各层超时没对齐,导致优雅停机形同虚设。必须满足这个不等式:
1 | preStop 静默期 + Spring 在途请求超时 < terminationGracePeriodSeconds |
举例,一套自洽的配置:
1 | # K8s |
1 | # Spring |
时间账:15s 静默 + 最多 30s 处理在途 = 45s < 60s,留 15s 余量给连接池等其它资源关闭。如果反过来,terminationGracePeriodSeconds 设成 30 而 Spring 超时设了 30,那么静默期一过就被 SIGKILL,优雅完全失效。
另一个权衡是静默期的代价。sleep 15 意味着每个 Pod 下线都要多花 15 秒,大规模滚动发布时整体发布时长被显著拉长。静默期不是越长越好,取决于你的负载均衡路由收敛速度——纯 K8s Service 通常几秒内收敛,而经过外部 LB / Ingress / 网格的链路可能需要更久,要按实测调。
还要注意:即使配置完美,长连接(WebSocket、SSE、gRPC 流) 仍然是难点。它们的"在途请求"可能持续数分钟,优雅停机超时根本等不起,只能靠客户端重连机制兜底。
小结
优雅停机的精髓不是"等请求处理完"这一句话,而是理解摘流量与停进程之间的竞态:先用 preStop 静默期等上游路由收敛,再借 Spring 的 SmartLifecycle phase 让 Web 入口最先关闭、业务资源随后释放,最后用对齐的超时预算兜底。健康检查则要严守 liveness 与 readiness 的语义边界——liveness 不碰外部依赖,readiness 对非致命依赖宽容——否则探针本身就会成为故障放大器。把这三者(信号时序、关闭顺序、超时预算)拧成一个整体,发布时那批莫名其妙的 5xx 才会真正消失。