我不会起名字322 · 后端 / 算法 / 数据库
文章目录
- [滚动更新时请求 502:Pod 终止与 endpoint 摘除的时间差](#滚动更新时请求 502:Pod 终止与 endpoint 摘除的时间差)
-
- [一、现象:每次发布都有几个 502](#一、现象:每次发布都有几个 502)
- [二、Pod 终止的完整时间线](#二、Pod 终止的完整时间线)
- 三、为什么会有这个时间差
- [四、解法 1:`preStop` 里 sleep](#四、解法 1:
preStop里 sleep) - [五、解法 2:应用必须优雅处理 SIGTERM](#五、解法 2:应用必须优雅处理 SIGTERM)
- [六、解法 3:就绪探针提前摘流量](#六、解法 3:就绪探针提前摘流量)
- [七、解法 4:滚动更新参数](#七、解法 4:滚动更新参数)
- 八、带走物
滚动更新时请求 502:Pod 终止与 endpoint 摘除的时间差
一、现象:每次发布都有几个 502
最典型的表现:
- 发布期间监控上出现一小撮 502/503,持续几十秒;
- 应用日志里没有任何错误------那些请求服务端压根没收到;
- 发布结束后自动恢复;
- 副本数越多、发布越频繁,越容易观察到。
这不是应用的问题,是 Pod 被终止和流量被摘除这两件事不同步。
二、Pod 终止的完整时间线
kubectl delete pod 或者滚动更新时,实际发生的是这样一串事件:
text
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 侧
text
kubelet 收到终止指令 → 执行 preStop → 发 SIGTERM 给容器
这条很快,毫秒级。
路径 2:控制面侧
text
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
最经典也最有效的做法:
yaml
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 10"]
terminationGracePeriodSeconds: 45
这个 sleep 不是"拖延时间",而是在等两件事完成:
- Endpoint 从 Service 里摘除,并同步到所有节点;
- Ingress Controller 更新 upstream 列表(如果流量走 Ingress)。
sleep 的时长要大于"Endpoint 同步 + Ingress 更新"的总耗时。经验值:
text
小集群:5 秒
中等集群:10 秒
大集群 / 用 Ingress:15~20 秒
注意 terminationGracePeriodSeconds 要相应调大:总时间 = preStop + 应用优雅退出时间 + 余量。如果 preStop 睡 10 秒而 grace period 还是默认 30 秒,应用就只剩 20 秒来处理完在手请求。
五、解法 2:应用必须优雅处理 SIGTERM
preStop 只是争取时间,应用本身的优雅退出才是关键:
go
func main() {
srv := &http.Server{Addr: ":8080", Handler: router}
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}()
quit := make(chan os.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)
defer cancel()
if err := 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 还没被终止时,就先让它"不健康"。
yaml
readinessProbe:
httpGet:
path: /readyz
port: 8080
periodSeconds: 2
failureThreshold: 1 # 一次失败就摘掉(快速生效)
应用收到 SIGTERM 后立刻让 /readyz 返回失败:
go
var shuttingDown atomic.Bool
func readyz(w http.ResponseWriter, r *http.Request) {
if shuttingDown.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:滚动更新参数
yaml
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0 # 保证不减少可用副本
maxSurge: 1 # 一次只多起一个
text
maxUnavailable: 0 → 先起新的,就绪后再杀旧的
→ 任何时刻可用副本数不低于期望值
代价是发布变慢(要等新 Pod 就绪才能杀旧的),而且需要集群有额外资源。对可用性要求高的服务,这个代价值得付。
另外还有一个容易忽略的点:新 Pod 必须真的 Ready 才会接流量 。如果 readinessProbe 的 initialDelaySeconds 太短,Pod 还没完全就绪就被认为 Ready,会接过流量然后失败 ------ 这也会产生发布期的 502。
八、带走物
一份能直接用的配置:
yaml
spec:
strategy:
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
template:
spec:
terminationGracePeriodSeconds: 45
containers:
- name: app
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 10"] # 等 endpoint 摘除同步
readinessProbe:
httpGet: {path: /readyz, port: 8080}
periodSeconds: 2
failureThreshold: 1 # 快速摘除
配合应用的优雅退出顺序:
text
1. shuttingDown = true(让 readyz 返回 503)
2. 从注册中心反注册
3. 等 2~3 秒(让探针/注册中心生效)
4. srv.Shutdown(ctx)(停止新连接,等在手请求)
5. 关闭资源
6. 退出
时间账要算清楚:
text
terminationGracePeriodSeconds ≥ preStop(sleep) + 应用最长处理时间 + 余量
例:10 + 20 + 15 = 45
一句话结论:发布期 502 不是应用报错,是"Pod 已经不接客了但流量还在往它这打"------preStop 的 sleep 就是在等控制面把这条转发规则改掉,而应用侧的优雅退出决定了在手请求会不会被硬打断。