微服务无损发布:Spring Boot 优雅停机与 K8s 探针联动
发版时出现 502/超时,根因通常有两个:处理到一半的请求被进程退出直接掐断;以及 Kubernetes 摘除 Endpoint 是异步的,网关还在往已开始停机的 Pod 送新流量。对应的解法是三件事配合:Spring Boot 开启 server.shutdown=graceful 保护在途请求、K8s 用 preStop 等待覆盖路由刷新时间差、Readiness 探针保证启动完成前不接流量。
边界也要先说清:优雅停机不是无损的绝对保证——超过 timeout-per-shutdown-phase 宽限期的请求仍会被掐断,kill -9(SIGKILL)更是直接绕过整个机制。宽限期要按系统最慢接口的实际耗时来评估。
1. 什么是优雅停机?
Section titled “1. 什么是优雅停机?”优雅停机的逻辑是:停止接收新流量,把手里的请求处理完,再退出。
当应用收到关闭信号(通常是 K8s 发出的 SIGTERM)时:
- 底层的 Tomcat/Undertow web 容器立即拒绝接收任何新的网络连接。
- 通知网关或注册中心:该节点准备下线。
- 给当前正在处理的 HTTP 请求一个宽限期(Grace Period),比如 30 秒,让它们执行完并返回响应。
- 宽限期结束后,无论是否处理完,强制关闭连接并退出 JVM。
2. Spring Boot 侧的配置
Section titled “2. Spring Boot 侧的配置”在 Spring Boot 2.3+ 以及后续的 3.x 版本中,官方原生内置了这套机制(Spring Boot 文档:Graceful Shutdown),只需要在 application.yml 里开启它:
server: # 将停机模式从默认的 IMMEDIATE 切换为 GRACEFUL shutdown: graceful
spring: lifecycle: # 设定给正在处理的请求多少宽限期,默认是 30s。根据你系统最慢的接口来评估。 timeout-per-shutdown-phase: 30s就这么简单。只要你在 Linux 中用 kill -15 <PID>(千万不要用 kill -9)通知进程,Spring Boot 控制台就会优雅地打印出:
Commencing graceful shutdown. Waiting for active requests to complete...
3. Kubernetes 下的关键盲区
Section titled “3. Kubernetes 下的关键盲区”只加上面的配置就部署到 K8s,502 依然可能出现。
K8s 的路由刷新是异步的。 当 K8s 决定停掉一个 Pod 时,它会同时做两件事:
- 发送
SIGTERM给你的 Spring Boot 容器。 - 通知网络组件(kube-proxy / Ingress)把这个 Pod 从可用 Endpoint 列表里摘除。
但网络组件更新 iptables 或网关配置需要时间(通常是秒级)。于是会出现这样的窗口:Spring Boot 收到 SIGTERM 已开始优雅停机、不再接收新连接,而网关还没完成摘除,继续把流量送进这个 Pod,得到的是 Connection Refused(表现为 502)。
解法:K8s PreStop Hook
Section titled “解法:K8s PreStop Hook”用一个 PreStop Hook 把这个时间差覆盖掉:K8s 在发送 SIGTERM 之前先执行 sleep,这期间 Pod 仍正常处理请求,同时网关完成摘除广播。
修改你的 Deployment.yaml 配置:
apiVersion: apps/v1kind: Deploymentmetadata: name: user-servicespec: template: spec: containers: - name: user-service image: company/user-service:1.0.0 lifecycle: preStop: exec: # 收到停止指令后先等待 10 秒: # 这期间 Pod 仍正常处理请求,同时 K8s 正在把摘除消息同步到网关 command: ["/bin/sh", "-c", "sleep 10"]4. 就绪探针与活性探针 (Readiness & Liveness)
Section titled “4. 就绪探针与活性探针 (Readiness & Liveness)”除了优雅停机,要做到平滑的启动(无损上线),你必须配置 K8s 探针。 Spring Boot Actuator 原生提供了针对 K8s 容器的探测接口:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId></dependency>开启 K8s 探针感知:
management: endpoint: health: probes: enabled: true在 K8s yaml 中配置:
readinessProbe: # K8s 会不断访问这个接口,只有返回 200 OK,K8s 网关才会把真实流量放进来 # 这样就避免了 Spring 刚启动一半,流量打进来报错的问题 httpGet: path: /actuator/health/readiness port: 8080 initialDelaySeconds: 10 periodSeconds: 5 livenessProbe: # K8s 会用这个接口检测你的服务是不是死锁或 OOM 假死了,如果挂了就自动重启你 httpGet: path: /actuator/health/liveness port: 8080 initialDelaySeconds: 30 periodSeconds: 10三个配置分别对应三个失效点:server.shutdown=graceful 保护在途请求,preStop sleep 覆盖网关路由刷新的时间差,Readiness 探针防止未就绪的实例提前接流量。配置时的两个注意点:timeout-per-shutdown-phase 要大于系统最慢接口的耗时;preStop 的等待时长要大于所在集群 Endpoint 摘除的实际传播耗时,这两个值都需要在自己的环境验证。