我不会起名字322· 后端 / 算法 / 数据库 技术栈 力扣 Hot100 Go 项目 Redis MySQL文章目录滚动更新时请求 502Pod 终止与 endpoint 摘除的时间差一、现象每次发布都有几个 502二、Pod 终止的完整时间线三、为什么会有这个时间差四、解法 1preStop 里 sleep五、解法 2应用必须优雅处理 SIGTERM六、解法 3就绪探针提前摘流量七、解法 4滚动更新参数八、带走物滚动更新时请求 502Pod 终止与 endpoint 摘除的时间差一、现象每次发布都有几个 502最典型的表现发布期间监控上出现一小撮 502/503持续几十秒应用日志里没有任何错误——那些请求服务端压根没收到发布结束后自动恢复副本数越多、发布越频繁越容易观察到。这不是应用的问题是Pod 被终止和流量被摘除这两件事不同步。二、Pod 终止的完整时间线kubectl delete pod或者滚动更新时实际发生的是这样一串事件t0 API Server 把 Pod 标记为 Terminating t0 kubelet 开始终止流程 t0 ├─ 执行 preStop hook如果配了 t0 └─ 同时Endpoint Controller 开始把 Pod 从 Service 的 Endpoint 移除 ↑ 这两条路径是独立的、并行的 t0 Pod 收到 SIGTERM应用开始优雅退出 t0 Endpoint 变更需要同步到所有节点的 kube-proxyiptables/ipvs 规则更新 t0 这个同步有延迟通常几秒 tX terminationGracePeriodSeconds默认 30s到期 tX 强制 SIGKILL问题就出在t0到Endpoint 同步完成之间的这几秒Pod 已经开始关闭了不再接受新连接但其他节点上的 kube-proxy 规则还没更新流量继续转发到这个正在关闭的 Pod连接被拒 →502。更麻烦的是另一半正在处理中的长请求被强制中断 →请求失败如果 Pod 是唯一的后端或者同时全部在滚动影响会被放大。三、为什么会有这个时间差两条独立的路径各自有延迟路径 1kubelet 侧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 的时间。两端不同步就有了窗口期。四、解法 1preStop里 sleep最经典也最有效的做法lifecycle:preStop:exec:command:[sh,-c,sleep 10]terminationGracePeriodSeconds:45这个 sleep 不是拖延时间而是在等两件事完成Endpoint 从 Service 里摘除并同步到所有节点Ingress Controller 更新 upstream 列表如果流量走 Ingress。sleep 的时长要大于Endpoint 同步 Ingress 更新的总耗时。经验值小集群5 秒 中等集群10 秒 大集群 / 用 Ingress15~20 秒注意terminationGracePeriodSeconds要相应调大总时间 preStop 应用优雅退出时间 余量。如果 preStop 睡 10 秒而 grace period 还是默认 30 秒应用就只剩 20 秒来处理完在手请求。五、解法 2应用必须优雅处理 SIGTERMpreStop只是争取时间应用本身的优雅退出才是关键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: 1periodSeconds: 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 就是在等控制面把这条转发规则改掉而应用侧的优雅退出决定了在手请求会不会被硬打断。