滚动更新卡在 Terminating:Pod 优雅退出链路的实战梳理
在 Kubernetes 集群里做滚动更新,大多数团队都关注"新 Pod 起没起来",却常常忽略另一个方向:旧 Pod 退得干不干净。一次看似普通的发布,可能因为退出链路没理顺,演变成旧实例卡在 Terminating 半小时、连接池里的请求持续 502、发布进度条永远停在最后一步。这篇文章把 Pod 优雅退出的完整链路拆开梳理一遍,给出每个环节的排查点和配置建议。
一、Terminating 之后到底发生了什么
当执行 kubectl delete pod 或滚动更新触发替换时,控制面的动作顺序是固定的:先从 EndpointSlice 中摘除该 Pod 的端点,然后并发执行 preStop 钩子与发送 SIGTERM 信号,同时等待 terminationGracePeriodSeconds(默认 30 秒)倒计时。超时后 kubelet 才会强制 SIGKILL。
这里有一个非常常见的误区:很多人以为摘除端点发生在 SIGTERM 之后,于是把"等待负载均衡摘流"的逻辑全部塞进 preStop 里。实际上端点摘除是异步的,从 EndpointSlice 更新到每个代理(如 Ingress 控制器、Service 转发面)真正生效,可能需要数秒。如果应用在 SIGTERM 一到就立刻关闭监听端口,而上游代理还缓存着旧端点,就会出现一小段 502 或 connection refused 窗口。
二、卡在 Terminating 的三大根因
1. preStop 阻塞超过优雅退出窗口
如果 preStop 里做了耗时的排空逻辑(比如等待长连接自然结束),而 terminationGracePeriodSeconds 还是默认 30 秒,kubelet 会按超时强杀,但 Pod 对象层面可能因为卷卸载、CNI 清理等后置步骤仍然停留很久。排查方法是看 Pod 的 containersDeletedAt 与实际删除完成时间的差值。
2. 容器内进程忽略 SIGTERM
PID 1 问题是最经典的坑。用 shell 形式写的 ENTRYPOINT(ENTRYPOINT ["sh", "-c", "java -jar app.jar"])会让 sh 成为 PID 1,而 sh 默认不转发信号给子进程。结果是 SIGTERM 发给了 sh,Java 进程毫不知情,只能等 30 秒被强杀。解法是改用 exec 形式,或者在入口脚本里用 trap 转发信号。
3. 挂载卷或 Finalizer 未释放
如果 Pod 挂载了需要卸载的云盘、NFS 卷,或者被某个控制器加了 finalizer(比如等待日志采集侧确认),删除动作会被卡住。此时 kubectl describe pod 里能看到 "Warning: ContainerTerminating" 之外的事件,比如卷卸载失败重试。处理方式是检查 finalizer 列表和存储类的回收策略。
三、推荐的优雅退出配置组合
结合多次发布事故的复盘,一套比较稳的组合是:
- 应用侧:SIGTERM 处理器先停止接收新请求,再等待存量请求完成,超时上限设为 20 秒左右;
- preStop:sleep 5 秒,给端点摘除与代理生效留出缓冲窗口,命令写法用 ["sleep", "5"] 而不是 shell 形式;
- terminationGracePeriodSeconds 设为 40 秒,大于 preStop 加应用排空时间之和;
- 就绪探针在收到 SIGTERM 后立即返回失败,让代理更早把流量切走。
最后提醒一点:发布策略建议配置 maxUnavailable 与 maxSurge 的组合为 25%/25%,并在发布窗口观察 Terminating Pod 的停留时长分布。如果 p99 停留时间明显超过 grace period,说明退出链路里一定有一环在阻塞,值得在下次发布前把根因找出来,而不是靠重试发布来掩盖问题。
- 点赞
- 收藏
- 关注作者
评论(0)