我不会起名字322· 后端 / 算法 / 数据库
📘 技术栈 | 🧩 力扣 Hot100 | 🐹 Go 项目 | 🟥 Redis | 🐬 MySQL
文章目录
- 滚动更新时请求 502:Pod 终止与 endpoint 摘除的时间差
- 一、现象:每次发布都有几个 502
- 二、Pod 终止的完整时间线
- 三、为什么会有这个时间差
- 四、解法 1:`preStop` 里 sleep
- 五、解法 2:应用必须优雅处理 SIGTERM
- 六、解法 3:就绪探针提前摘流量
- 七、解法 4:滚动更新参数
- 八、带走物
滚动更新时请求 502:Pod 终止与 endpoint 摘除的时间差
一、现象:每次发布都有几个 502
最典型的表现:
- 发布期间监控上出现一小撮 502/503,持续几十秒;
- 应用日志里没有任何错误——那些请求服务端压根没收到;
- 发布结束后自动恢复;
- 副本数越多、发布越频繁,越容易观察到。
这不是应用的问题,是Pod 被终止和流量被摘除这两件事不同步。
二、Pod 终止的完整时间线
kubectl delete pod或者滚动更新时,实际发生的是这样一串事件:
t=0 API Server 把 Pod 标记为 Terminating t=0 kubelet 开始终止流程 t=0 ├─ 执行 preStop hook(如果配了) t=0 └─ 同时,Endpoint Controller 开始把 Pod 从 Service 的 Endpoint 移除 ↑ 这两条路径是独立的、并行的 t=0 Pod 收到 SIGTERM,应用开始优雅退出 t=0+ Endpoint 变更需要同步到所有节点的 kube-proxy(iptables/ipvs 规则更新) t=0+ 这个同步有延迟,通常几秒 t=X terminationGracePeriodSeconds(默认 30s)到期 t=X 强制 SIGKILL问题就出在t=0到Endpoint 同步完成之间的这几秒:
- Pod 已经开始关闭了(不再接受新连接);
- 但其他节点上的 kube-proxy 规则还没更新;
- 流量继续转发到这个正在关闭的 Pod;
- 连接被拒 →502。
更麻烦的是另一半:
- 正在处理中的长请求被强制中断 →请求失败;
- 如果 Pod 是唯一的后端(或者同时全部在滚动),影响会被放大。
三、为什么会有这个时间差
两条独立的路径,各自有延迟:
路径 1:kubelet 侧
kubelet 收到终止指令 → 执行 preStop → 发 SIGTERM 给容器这条很快,毫秒级。
路径 2:控制面侧
Pod 变 Terminating → Endpoint Controller 更新 Endpoint 对象 → 各节点的 kube-proxy watch 到变更 → 更新本机的 iptables/ipvs 规则 → 流量不再转发过去这条慢得多,包括:
- Endpoint Controller 的处理延迟;
- watch 事件的传播延迟;
- kube-proxy 更新 iptables 规则的时间(大集群里
iptables-restore可能要几秒); - 如果用了 Ingress,还要加上 Ingress Controller 更新 upstream 的时间。
两端不同步,就有了窗口期。
四、解法 1:preStop里 sleep
最经典也最有效的做法:
lifecycle:preStop:exec:command:["sh","-c","sleep 10"]terminationGracePeriodSeconds:45这个 sleep 不是"拖延时间",而是在等两件事完成:
- Endpoint 从 Service 里摘除,并同步到所有节点;
- Ingress Controller 更新 upstream 列表(如果流量走 Ingress)。
sleep 的时长要大于"Endpoint 同步 + Ingress 更新"的总耗时。经验值:
小集群:5 秒 中等集群:10 秒 大集群 / 用 Ingress:15~20 秒注意terminationGracePeriodSeconds要相应调大:总时间 = preStop + 应用优雅退出时间 + 余量。如果 preStop 睡 10 秒而 grace period 还是默认 30 秒,应用就只剩 20 秒来处理完在手请求。
五、解法 2:应用必须优雅处理 SIGTERM
preStop只是争取时间,应用本身的优雅退出才是关键:
funcmain(){srv:=&http.Server{Addr:":8080",Handler:router}gofunc(){iferr:=srv.ListenAndServe();err!=http.ErrServerClosed{log.Fatal(err)}}()quit:=make(chanos.Signal,1)signal.Notify(quit,syscall.SIGTERM,syscall.SIGINT)<-quit log.Println("收到 SIGTERM,开始优雅退出")// 1. 先从注册中心反注册(如果用服务发现)deregister()// 2. 停止接收新请求,处理完在手的ctx,cancel:=context.WithTimeout(context.Background(),20*time.Second)defercancel()iferr:=srv.Shutdown(ctx);err!=nil{log.Printf("强制关闭: %v",err)srv.Close()}// 3. 关闭资源(DB 连接、MQ 消费者)closeResources()log.Println("退出完成")}关键点:
srv.Shutdown()会停止接受新连接,但等在手请求处理完;- 必须设超时(
context.WithTimeout),否则卡住的请求会让 Pod 一直不退,最后被 SIGKILL; - 先反注册,再 Shutdown—— 顺序不能反(详见服务注册那篇)。
六、解法 3:就绪探针提前摘流量
还有一个更主动的思路:在 Pod 还没被终止时,就先让它"不健康"。
readinessProbe:httpGet:path:/readyzport:8080periodSeconds:2failureThreshold:1# 一次失败就摘掉(快速生效)应用收到 SIGTERM 后立刻让/readyz返回失败:
varshuttingDown atomic.Boolfuncreadyz(w http.ResponseWriter,r*http.Request){ifshuttingDown.Load(){http.Error(w,"shutting down",503)return}w.WriteHeader(200)}// SIGTERM 处理里<-quit shuttingDown.Store(true)// 1. 先让就绪探针失败time.Sleep(3*time.Second)// 2. 等 kubelet 探测到并摘流量srv.Shutdown(ctx)// 3. 再关闭配合failureThreshold: 1+periodSeconds: 2,能在 2 秒内完成摘除。
注意:readiness 摘除也要同步到所有节点的 kube-proxy,所以同样有延迟。它和preStop是互补的,不是替代关系——可以同时用。
七、解法 4:滚动更新参数
spec:strategy:type:RollingUpdaterollingUpdate:maxUnavailable:0# 保证不减少可用副本maxSurge:1# 一次只多起一个maxUnavailable: 0 → 先起新的,就绪后再杀旧的 → 任何时刻可用副本数不低于期望值代价是发布变慢(要等新 Pod 就绪才能杀旧的),而且需要集群有额外资源。对可用性要求高的服务,这个代价值得付。
另外还有一个容易忽略的点:新 Pod 必须真的 Ready 才会接流量。如果readinessProbe的initialDelaySeconds太短,Pod 还没完全就绪就被认为 Ready,会接过流量然后失败 —— 这也会产生发布期的 502。
八、带走物
一份能直接用的配置:
spec:strategy:rollingUpdate:maxUnavailable:0maxSurge:1template:spec:terminationGracePeriodSeconds:45containers:-name:applifecycle:preStop:exec:command:["sh","-c","sleep 10"]# 等 endpoint 摘除同步readinessProbe:httpGet:{path:/readyz,port:8080}periodSeconds:2failureThreshold:1# 快速摘除配合应用的优雅退出顺序:
1. shuttingDown = true(让 readyz 返回 503) 2. 从注册中心反注册 3. 等 2~3 秒(让探针/注册中心生效) 4. srv.Shutdown(ctx)(停止新连接,等在手请求) 5. 关闭资源 6. 退出时间账要算清楚:
terminationGracePeriodSeconds ≥ preStop(sleep) + 应用最长处理时间 + 余量 例:10 + 20 + 15 = 45一句话结论:发布期 502 不是应用报错,是"Pod 已经不接客了但流量还在往它这打"——preStop的 sleep 就是在等控制面把这条转发规则改掉,而应用侧的优雅退出决定了在手请求会不会被硬打断。