Kubernetes的滚动更新看似只是简单地把旧Pod替换成新Pod,但在生产环境中,很多团队都遇到过发布瞬间出现一小波502、504甚至连接被拒的情况。问题的根源往往不在发布策略本身,而在于旧Pod被删除时,仍然存活的连接没有被妥善排空。连接draining(排空)就是指在Pod下线前,让正在处理的请求执行完毕、让长连接逐步迁移的一系列机制。要真正实现零中断发布,需要理解从Pod终止信号发出,到流量真正停止转发,中间到底发生了什么。

Pod终止的完整流程:排空的时间窗口在哪里
当Deployment发起滚动更新时,Kubernetes会删除旧ReplicaSet中的Pod。删除Pod并不是瞬间完成的,而是走一套固定的终止流程。kubelet收到删除请求后,Pod的状态会被标记为Terminating,此时有两个关键动作并行发生:一是端点控制器将该Pod从Service的endpoints列表中移除,二是kubelet在容器内执行preStop钩子(如果配置了),然后向主进程发送SIGTERM信号。
这里的坑在于时间差。endpoints的更新需要经过kube-proxy监听、规则下发、节点生效等多个环节,存在几百毫秒到数秒的传播延迟。也就是说,SIGTERM发出后,Pod可能还在接收新请求,而你的应用收到SIGTERM就直接退出了,新进来的请求自然被拒绝,网关层就表现为502。这就是preStop钩子存在的意义:利用一段休眠时间,等待endpoints的摘除在所有节点上真正生效。
lifecycle:
preStop:
exec:
# 先休眠10秒,等待kube-proxy的endpoints摘除在集群各节点生效
command: ["/bin/sh", "-c", "sleep 10"]
terminationGracePeriodSeconds: 30
terminationGracePeriodSeconds定义了从SIGTERM到SIGKILL的总宽限期,preStop钩子的执行时间包含在内。所以休眠10秒后,应用还有20秒去处理存量请求。这个值要根据业务最长请求耗时来定,比如文件上传接口可能需要60秒以上,宽限期就要相应放大。
应用层如何响应SIGTERM:主动停止接收新请求
排空不只是Kubernetes单方面的事,应用进程自己也得配合。收到SIGTERM后,一个规范的服务应该做三件事:停止接收新连接、继续处理存量请求、等请求全部返回后再退出进程。以Go语言为例,http.Server提供了Shutdown方法,专门用于优雅退出。
func main() {
srv := &http.Server{Addr: ":8080"}
go func() {
srv.ListenAndServe()
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
// 优雅关闭:停止接收新连接,等待存量请求处理完毕
if err := srv.Shutdown(ctx); err != nil {
log.Printf("强制关闭: %v", err)
}
}
Java应用可以用Spring Boot的server.shutdown=graceful配合spring.lifecycle.timeout-per-shutdown-phase实现同样效果。Node.js则建议在收到SIGTERM后调用server.close(),并等待连接计数归零。如果应用完全无视SIGTERM,进程会被SIGTERM默认行为直接终止,任何优雅关闭机制都形同虚设,这也是很多团队配了preStop仍然出现502的原因。
需要注意一个常见误区:有些框架默认监听了SIGTERM但只是打日志不退出,宽限期一到被SIGKILL杀掉,效果和不处理一样但排查更难。建议在压测环境里模拟kubectl delete pod,观察进程退出日志确认行为符合预期。
长连接场景的特殊处理:gRPC与WebSocket
短连接的HTTP服务,排空相对简单,存量请求几秒内就会结束。但gRPC、WebSocket这类长连接麻烦得多:SIGTERM不会断开已经建立的连接,只要客户端不主动断,服务端Shutdown会一直等到超时。结果就是Pod卡在Terminating状态直到被SIGKILL,客户端连接被硬切,正在传输的流就丢了。
对gRPC来说,标准做法是在preStop或SIGTERM处理中,先调用grpc_server.GracefulStop(Go)或者发送GOAWAY帧,同时让客户端实现重连逻辑。更通用的技巧是在preStop里执行主动的连接排空命令,比如对Nginx入口场景:
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "nginx -s quit; sleep 15"]
nginx -s quit会让Nginx停止接收新连接、等待存量连接结束后退出,随后的sleep保证endpoints摘除完毕。对于自研的WebSocket网关,建议服务端主动关闭连接前先下发一条业务层的关闭通知帧,让客户端感知后立即重连到新Pod,把中断时间压缩到毫秒级。
数据库连接池也要留意。Pod退出时,如果直接杀进程,数据库侧会留下一批半开连接,直到TCP keepalive超时才回收,高频率发布会放大这个问题。规范的退出流程应该在宽限期内调用连接池的close方法,主动归还或销毁连接。
流量入口的排空:Service与负载均衡器层面
除了Pod自身,流量入口层的排空同样重要。kube-proxy模式决定了endpoints摘除的速度:iptables模式下,每个节点上的规则更新是全量替换的,存在短暂的窗口期;ipset和IPVS模式效率更高,传播延迟更小。如果业务对中断极度敏感,可以考虑将Service的externalTrafficPolicy设为Local,减少一跳转发带来的不确定性。
使用LoadBalancer类型的Service时,云厂商负载均衡器的健康检查间隔通常在5到10秒。Pod进入Terminating后,健康检查失败、LB摘除节点、连接排空,这一整条链路的时间必须计入发布窗口。部分云厂商支持配置LB的connection draining超时,建议将其设置为大于应用最长请求耗时,比如60秒。Kubernetes的Service本身在1.26之后也提供了trafficDistribution等参数来细化流量调度策略,但核心思路不变:入口摘除必须比进程退出更早完成。
线上502排查思路与配置清单
如果发布期间还是出现502,可以按以下顺序排查:第一,确认应用是否正确处理了SIGTERM,看进程退出日志有没有优雅关闭的记录;第二,检查preStop休眠时间是否覆盖了endpoints传播延迟,集群规模越大这个延迟越明显;第三,查看宽限期是否够长,kubectl describe pod里能看到容器退出码,137代表被SIGKILL强杀;第四,检查入口层,比如Nginx Ingress的proxy-read-timeout是否小于后端处理时长,或者LB的draining是否配置。
一份相对稳妥的基线配置供参考:preStop休眠10秒,terminationGracePeriodSeconds设为45秒,Readiness探针失败阈值设为1,应用层优雅关闭超时设为30秒,LB draining设为60秒。这些数值的前提是正常请求在10秒内完成,如果你的接口存在分钟级长请求,就要整体放大这套时间参数,并优先考虑把长请求改造成异步任务,从根源上降低发布的排空压力。
Kubernetes滚动更新连接draining优雅终止修改时间:2026-09-14 11:53:19