ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

滚动更新时请求 502:Pod 终止与 endpoint 摘除的时间差

滚动更新时请求 502:Pod 终止与 endpoint 摘除的时间差

我不会起名字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 同步完成之间的这几秒:

  1. Pod 已经开始关闭了(不再接受新连接);
  2. 但其他节点上的 kube-proxy 规则还没更新;
  3. 流量继续转发到这个正在关闭的 Pod;
  4. 连接被拒 →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 不是"拖延时间",而是在等两件事完成:

  1. Endpoint 从 Service 里摘除,并同步到所有节点;
  2. 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 就是在等控制面把这条转发规则改掉,而应用侧的优雅退出决定了在手请求会不会被硬打断。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进