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

我不会起名字322 · 后端 / 算法 / 数据库

📘 技术栈 | 🧩 力扣 Hot100 | 🐹 Go 项目 | 🟥 Redis | 🐬 MySQL

文章目录

  • [滚动更新时请求 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 同步完成 之间的这几秒:

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

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

相关推荐
我不会起名字3223 小时前
服务注册了却调不通:注册中心与心跳的 3 个时间参数
微服务·nacos·注册中心·排查
L0208245 小时前
Kubernetes Service 与 kube‑proxy 深度详解
运维·算法·云原生·容器·贪心算法·kubernetes
SatanII5 小时前
Kubernetes Ingress
kubernetes·ingress
SatanII5 小时前
kubernetes service && proxy
云原生·容器·kubernetes
艾伦_耶格宇5 小时前
【kubernetes - 1.28版本】-1 kubernetes基础
云原生·容器·kubernetes
Initialize-le6 小时前
PVC 一直 Pending?五步排查存储绑定
云原生·容器·kubernetes
9624566 小时前
一次 Kubernetes 临时存储压力引发的小程序授权雪崩:从 Pod 驱逐到定时任务漏执行
容器·小程序·kubernetes
SatanII6 小时前
Kubernetes 网络
网络·容器·kubernetes
闲云野鹤在人间20 小时前
Kubernetes Service 与 kube-proxy 详解
云原生·容器·kubernetes