让工作在滚动更新期间连接不断,关键步骤是正确配置Pod的preStop钩子和terminationGracePeriodSeconds,同时依赖Service的Endpoints机制配合就绪探针,确保新Pod就绪后才摘除旧Pod。
滚动更新已有连接为什么会断:先理解draining原理
很多人遇到滚动更新时连接闪断,第一反应是去调Service配置,其实问题通常出在Pod生命周期管理上,Kubernetes在滚动更新时对旧副本的处理不是直接“杀掉”,而是执行一系列有序操作这个流程就是draining,中文常译作“排空”或“优雅终止”。
官方文档对Pod终止流程有明确描述:当Deployment滚动更新发生时,旧Pod会被标记为Terminating,同时从Service的Endpoints列表中移除,但这里有个关键细节:从Endpoints移除和Pod真正停止之间有时间差,如果新连接刚好被转发到正在Terminating的Pod上,而Pod进程已经关闭了监听端口,连接就会立即失败,业内专家指出,大多数滚动更新连接中断问题,都源于没有给这个时间差留出足够缓冲。
draining状态到底是什么
draining不是Kubernetes中的正式API对象,而是对Pod终止过程的一种描述,具体到旧副本,它的生命周期是这样的:
- Deployment控制器将旧Pod的
spec.template更新为新版本,同时将旧Pod的desired数量减1 - kubelet收到终止请求,Pod进入Terminating阶段
- kubelet并行执行两个动作:将Pod从Service的Endpoints中摘除(通过EndpointSlice控制器),同时执行
preStop钩子(如果配置了) - 等待
preStop钩子执行完成,或者超过terminationGracePeriodSeconds(默认30秒) - kubelet向Pod发送
SIGTERM信号 - 如果进程在宽限期结束前未退出,kubelet发送
SIGKILL强制终止
这里最容易被忽视的是第3步和第4步之间。Endpoints摘除是异步的,从kubelet发起终止到EndpointSlice控制器真正更新,可能需要数秒时间,在这期间,Service仍然可能把新连接转发到旧Pod上,如果旧Pod执行preStop时直接睡觉,不关闭监听,连接就能正常建立;但如果preStop里什么都没做,Pod进程收到SIGTERM后立刻关闭监听端口,那么在这几秒内到达的连接就会被拒。
滚动更新期间连接中断怎么办:先检查这三个位置
如果你已经遇到连接失败,排查顺序应该是:
- 查看Pod事件:
kubectl describe pod <pod-name>,确认是否有Killing或Unhealthy事件 - 确认应用是否处理了
SIGTERM信号:很多开发语言框架默认不处理,直接退出 - 检查就绪探针的配置:
readinessProbe的initialDelaySeconds是否过短,导致新Pod还没准备好就被打进Endpoints
多数情况下,问题不是Kubernetes配置错误,而是应用本身没有优雅退出能力,比如Nginx需要nginx -s quit而不是直接kill,Java应用需要关闭Spring Boot的优雅停机开关,Node.js需要监听SIGTERM并关闭HTTP server。
Kubernetes滚动更新旧副本连接断开怎么办:配置优雅停止
要解决连接断开,核心是拉长从“摘除Endpoints”到“进程关闭监听端口”之间的窗口,最常用的做法是配置preStop钩子,让Pod在收到SIGTERM之前先等几秒钟,给EndpointSlice控制器足够时间完成更新。
实操:给Deployment添加preStop和就绪探针
下面是一个经过验证的Deployment配置片段,演示了如何正确处理滚动更新期间的连接:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
replicas: 3
selector:
matchLabels:
app: web-app
template:
metadata:
labels:
app: web-app
spec:
terminationGracePeriodSeconds: 60
containers:
- name: app
image: nginx:1.25
ports:
- containerPort: 80
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 10"]
readinessProbe:
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 5
periodSeconds: 5
startupProbe:
httpGet:
path: /healthz
port: 80
failureThreshold: 30
periodSeconds: 5
关键点说明:
preStop中的sleep 10不是无意义的延时,它让Pod在收到SIGTERM之前保持运行,等待Service Endpoints完成更新,行业共识认为,10秒是绝大多数集群中Endpoints更新最坏情况的安全值,如果集群节点数较多或网络延迟大,可以适当增加到15秒。terminationGracePeriodSeconds: 60必须大于preStop时长加应用自身优雅退出的时间,如果这里设置为30,而preStop睡眠20秒,应用只剩10秒处理收尾,可能不够。readinessProbe确保新副本在真正就绪后才被加入Service的Endpoints列表,注意initialDelaySeconds要评估应用启动耗时,不能设得太短。startupProbe是给启动很慢的应用准备的,它允许应用在启动期间不触发readiness探针的失败记录,避免滚动更新卡死。
已有连接如何处理:应用层必须做好这两件事
Kubernetes侧的配置只能保证连接不被重置,但已经建立的连接怎么办,取决于应用自身的行为,正确流程是:
preStop执行期间,Pod仍运行,已有的TCP连接不受影响- kubelet发送
SIGTERM后,应用收到信号,停止接收新请求,同时开始处理完当前正在进行的请求 - 应用主动关闭空闲连接和Keep-Alive连接,让客户端感知到连接关闭
- 全部请求处理完毕后,进程退出,kubelet回收Pod
如果你的应用是HTTP服务,建议在收到SIGTERM后禁用Keep-Alive,并设置一个最大等待时间,比如Nginx可在preStop里发送nginx -s quit,但更稳妥的方式是在preStop中先sleep,再执行quit。
滚动更新优雅停止超时设置:terminationGracePeriodSeconds到底填多少
这个参数很多人直接留默认30秒,但遇到大请求或长轮询就会出问题,给出一个计算参考:
- (总宽限期 \ge preStop时长 + 应用处理存量请求的最大时间 + 安全余量)
- 如果业务接口平均响应时间2秒,最慢请求10秒,
preStop设5秒,那么总宽限期建议为5 + 10 + 5 = 20秒以上,保守设为40秒更稳 - 注意:总宽限期不是越大越好,如果设置成600秒,而应用不响应
SIGTERM,Pod会被卡在Terminating状态很久,滚动更新会卡住,建议同时配置terminationGracePeriodSeconds和应用进程内的最大退出等待时间,两者取较小值。
滚动更新时的连接排空:两种常见场景处理方案
不同业务对连接处理的要求差异很大,这里拆成两个典型场景说明。
无状态HTTP服务(如Web API、前端Nginx)
这类服务最怕的是请求被转发到正在关闭的Pod上,操作路径:
- 确保Deployment配置了
preStop和readinessProbe - 在Service中使用
sessionAffinity: None(默认),让新请求均匀分布到新Pod - 如果使用Ingress,确认Ingress Controller与Service的
externalTrafficPolicy设置,需要保留客户端真实IP时,设externalTrafficPolicy: Local,但这会导致流量转发到无本地副本的节点时被丢弃,滚动更新期间可能出现少量请求失败,更推荐用Cluster模式配合nginx.ingress.kubernetes.io/service-upstream注解
有状态长连接服务(如WebSocket、gRPC Stream)
这些连接无法通过HTTP探针感知,需要更精细的排空策略:
- 在
readinessProbe中增加一个“排空状态”端点,当Pod开始准备终止时,应用内部设置一个标志位,让/healthz返回失败,这样Kubernetes会提前将Pod从Endpoints摘除,即使还在等preStop结束 preStop里除了sleep,还应主动向已连接的客户端发送关闭通知,比如WebSocket服务可以广播一个{"type":"shutdown"}消息,让客户端主动重连- 对于gRPC Stream,可以在
SIGTERM时调用Server.GracefulStop(),并在preStop中留出足够时间让客户端感知连接关闭
滚动更新期间旧副本连接处理的Q&A
Q1:为什么我配置了preStop,滚动更新时还是有连接中断?
最常见原因是preStop中只执行了sleep,但应用在收到SIGTERM后立即关闭了监听端口。preStop只是延迟发送信号,并不能阻止应用崩溃,请检查应用是否正确捕获了SIGTERM并执行优雅退出代码,另一个可能原因是Service的Endpoints更新比preStop执行更慢这个可以用kubectl get endpoints -w观察更新延迟,如果延迟超过preStop时长,就需要加长sleep时间或改用上述“排空状态端点”方案。
Q2:滚动更新时新Pod一直处于ContainerCreating,卡住了怎么办?
通常是因为readinessProbe失败导致新Pod永远不算就绪,旧Pod无法被替换,检查探针路径和端口是否正确,确认应用在启动期间是否能够响应探针,如果应用需要读取配置或连接数据库,确保这些依赖在探针超时时间内可用。terminationGracePeriodSeconds过长而旧Pod进程不退出,也会让滚动更新停滞,可以用kubectl rollout status deployment/web-app观察详细状态。
Q3:distributed system中draining和cluster autoscaler的节点排空有什么区别?
Kubernetes中的节点排空(kubectl drain)针对的是整台节点上的所有Pod,用于节点维护或缩容,它会先把节点标记为不可调度,然后对每个Pod执行与滚动更新相同的终止流程即preStop、SIGTERM、宽限期强制终止,如果你的Pod有PVC,drain还会检查PodDisruptionBudget,而滚动更新的draining是Deployment控制器内部的行为,只针对被替换的旧副本,两者在技术底层的Pod终止机制完全一致,但触发条件和调度策略不同,节点排空需要你手动设置--ignore-daemonsets等参数,而滚动更新不需要额外操作。
滚动更新期间的连接处理,本质上是对“Pod终止”这个事件的充分准备,记住一条主线:先摘除流量,再优雅退出,最后强杀兜底,配置好preStop和探针,让你的应用对SIGTERM保持敬意,连接中断就能从“必然”变成“偶然”。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/776083.html


