K8s 滚动更新与 Spring Boot 优雅停机实战:如何做到真正的平滑发布?

K8s 滚动更新与 Spring Boot 优雅停机实战:如何做到真正的平滑发布?

文章目录

引言:发布时的"幽灵报错"

在微服务和云原生时代,将 Spring Boot 应用部署到 Kubernetes (K8s) 已经是标准操作。然而,很多团队在初次上云时都会遇到一个极其头疼的问题:每次滚动更新或重启,前端总会零星报错,甚至用户会投诉"系统卡了一下"或"提示服务不可用"。

排查日志后发现,往往是因为应用启动极慢(例如启动需要 100 秒以上),但 K8s 看到 Pod 处于 Running 状态就把流量打进来了;或者,当旧 Pod 被销毁时,它正在处理的长耗时任务被直接"腰斩"。

本文将结合实际运维场景,从 就绪探针(Readiness Probe) 到 优雅停机(Graceful Shutdown),手把手教你如何组合配置,实现真正的平滑发布。

适用边界提示 :本文的"Readiness Probe + preStop Hook + 宽限期"组合主要面向 Web 服务:就绪探针负责摘除入站流量,preStop 为在途请求争取缓冲时间。对于定时任务/Job 类 Pod,没有 Service 流量可摘,就绪探针不能解决任务中断问题,preStop sleep 也只是缓冲,并不保证当前任务一定完成。此类应用需要额外实现:收到 SIGTERM 后停止调度新 Job、等待或主动中断当前 Job、保存 checkpoint 或保证任务幂等,并把 terminationGracePeriodSeconds 当作硬上限而非"等完承诺"。不建议用超长宽限期或固定 sleep 来掩盖任务时长不可预测的问题。


第一阶段:解决"启动慢导致流量过早打入"问题

痛点

K8s 默认只要容器的进程启动了(状态变为 Running),就会认为该 Pod 可以接收流量,并立刻将其加入 Service 的 Endpoints 中。但此时 Spring Boot 的内部组件(如 Tomcat、数据库连接池、工作流引擎)可能还在初始化,导致请求直接报 502 或超时。

解决方案:引入就绪探针 (Readiness Probe)

我们需要通过探针告诉 K8s:"虽然我活着,但我还没准备好,别给我发流量。"

在 Deployment 的 YAML 中,针对容器加入如下配置:

yaml 复制代码
          readinessProbe:
            httpGet:
              path: /actuator/health
              port: 8080
              scheme: HTTP
            initialDelaySeconds: 130  # 留足启动时间(根据实际启动耗时调整)
            periodSeconds: 10         # 每10秒探测一次
            failureThreshold: 3       # 连续3次失败视为未就绪

效果: 在新 Pod 启动后的前 130 秒,它处于 0/1(未就绪)状态,不接收任何流量。130 秒后探针成功,变为 1/1 开始接管流量。

⚠️ 避坑指南:Actuator 健康检查"误报"

很多项目引入了 spring-boot-starter-actuator 作为探针接口,但往往忽略了它的自动装配机制。例如,项目引入了 Redis 依赖但实际并未使用,导致 /actuator/health 探测 Redis 失败,返回 DOWN,进而导致 Pod 永远无法就绪。

修复方法: 在 application.yml 中关闭无用的健康检查,并开启详情:

yaml 复制代码
management:
  health:
    redis:
      enabled: false  # 关闭误报的依赖检查
  endpoint:
    health:
      show-details: always

第二阶段:解决"旧 Pod 被销毁时中断任务"问题

痛点

当新 Pod 就绪后,K8s 会开始销毁旧 Pod。此时,旧 Pod 会从 Endpoints 中被剔除(停止接收新流量)。但是,那些已经在处理中(正在进行中)的请求怎么办?

K8s 的终止流程与隐患

当 Pod 被标记为删除时,K8s 会发送 SIGTERM 信号给容器主进程。在 Spring Boot 中,如果不做特殊配置,Tomcat 会立刻关闭,正在处理的请求直接报错断开。

虽然 K8s 有 terminationGracePeriodSeconds(默认30秒)的宽限期,但如果是较老的 Spring Boot(2.3 版本之前),它根本不认识什么优雅停机,收到信号直接"自杀"。

解决方案:利用 preStop 钩子"争取时间"

如果你的 Spring Boot 版本较低,无法使用自带的 server.shutdown: graceful,我们可以利用 K8s 的 preStop 钩子,上演一出"缓兵之计"。

修改 Deployment YAML,在容器生命周期中加入钩子:

yaml 复制代码
          lifecycle:
            preStop:
              exec:
                command: ["sh", "-c", "sleep 15"]

这 15 秒在底层发生了什么?

  1. K8s 决定销毁旧 Pod,先将其从 Service 路由表中剔除(不再有新流量进入)。
  2. 此时不发送 SIGTERM ,而是执行 preStop 脚本,即睡 15 秒。
  3. 在这 15 秒内,旧 Pod 进程完全存活,TCP 连接不中断,正在处理的接口可以顺利跑完并把响应返回给客户端。
  4. 15 秒后,preStop 结束,K8s 发送 SIGTERM。此时旧 Pod 手里基本没活了,瞬间安全关闭。

⚠️ 时间配比铁律

使用了 preStop 后,必须确保 K8s 的宽限期足够长,否则会发生"强杀":

  • terminationGracePeriodSeconds: 30(总计 30 秒)

  • preStop sleep 15(拿走 15 秒)

  • 留给 JVM 关闭的时间:15 秒(足够安全)

该配比适用于请求可排干的 Web 场景;对于运行时长不定的定时任务,应先让任务具备可中断、可收尾或可断点续跑能力,再谈宽限期预算。


终极平滑发布配置模板

结合以上所有经验,一份标准的、能够应对绝大多数生产级场景的 Deployment 容器配置片段如下:

yaml 复制代码
    spec:
      containers:
        - name: my-app
          image: my-registry/my-app:v1.0.0
          ports:
            - containerPort: 8080
          
          # 1. 就绪探针:防止未启动完就接客
          readinessProbe:
            httpGet:
              path: /actuator/health
              port: 8080
            initialDelaySeconds: 130
            periodSeconds: 10
            failureThreshold: 3
            
          # 2. 优雅停机钩子:防止正在处理的请求被强杀
          lifecycle:
            preStop:
              exec:
                command: ["sh", "-c", "sleep 15"]
      
      # 3. 宽限期:必须大于 preStop 的 sleep 时间,给 JVM 留足关闭时间
      terminationGracePeriodSeconds: 30

(注:如果你的应用中有耗时超过 30 秒的重度任务,建议将其改造为异步任务机制,而非无限延长 K8s 的宽限期。)


附录:关于 Revision 的小知识

在 K8s Deployment 的 YAML 中,常常会看到一个元数据注解 deployment.kubernetes.io/revision: '88'。

这是 K8s 自动维护的版本号。只有当你修改了 Pod 模板(spec.template) ,比如改了镜像、改了探针配置、加了 preStop 时,它才会自动 +1。

如果仅仅是修改了副本数(replicas),它不会变化。这个版本号是 K8s 保存历史 ReplicaSet 用于回滚的核心依据,默认最多保留 10 个历史版本。

总结

平滑发布不是单一的配置,而是一个系统性的工程。Readiness Probe(控制入流量) + preStop Hook(控制出流量缓冲) + 宽限期(兜底强杀),这套组合拳打下来,才能让用户在无感知的情况下完成版本迭代,彻底告别发布时的深夜报警。

相关推荐
Z21-X1 小时前
云原生运维笔记:硬核拆解OpenStack计算与存储——从主机聚合到Cinder挂载全流程
运维·笔记·学习·云原生·openstack
CodeGL1 小时前
一文了解:K8s
云原生·容器·kubernetes
java_logo2 小时前
Docker 部署 Portainer CE 中文版:轻松搭建容器管理平台
运维·docker·容器·portainer·容器管理·轩辕镜像·portainer ce
LazyHunter92 小时前
Spring Boot 3 微服务实践指南
java·spring boot
zzu123zsw2 小时前
从 SPI 契约到独立 ClassLoader 隔离:Spring Boot 插件热插拔架构设计与内存泄露防御
java·spring boot·后端·架构设计
yz_aiks11 小时前
Spring AI 2.1 ChatMemory 已过时用什么替代:迁移到 Session Memory 实战(JDBC 存储与会话压缩)
java·spring boot·大模型·ai agent·spring ai
学长毕设12 小时前
基于SpringBoot的上门洗车服务系统设计与实现
java·spring boot·后端
SelectDB15 小时前
安全评审卡了三周,我把部署形态的四种选项重新排了一遍
大数据·数据库·云原生
阿里云云原生16 小时前
完整还原已经服务千家企业的 AIOps Agent 的评测和调优实践
云原生