如何在 Kubernetes 中实现 Pod 的优雅缩容

3次阅读

如何在 Kubernetes 中实现 Pod 的优雅缩容

本文详解 kubernetes 中实现 http 服务优雅缩容的关键机制:结合 prestop hook、就绪探针(readiness probe)与应用层信号处理,确保请求零丢失、连接平滑终止。

本文详解 kubernetes 中实现 http 服务优雅缩容的关键机制:结合 prestop hook、就绪探针(readiness probe)与应用层信号处理,确保请求零丢失、连接平滑终止。

在 Kubernetes 中实现真正的“优雅缩容”(graceful scaling),不能仅依赖应用自身对 SIGTERM 的响应——如您在 Go 示例中使用 manners 库关闭 HTTP 服务器。问题在于:Kubernetes Service 的 Endpoint 更新存在延迟,而 Pod 的终止流程可能早于流量的完全摘除。当缩容至单副本时出现短暂 HTTP 错误,正是这一时序竞争的典型表现:新请求仍被负载均衡转发至正在关闭的 Pod,而此时其 HTTP server 已停止接收新连接或拒绝处理中请求。

要彻底解决,需构建三层协同机制:

✅ 1. 就绪探针(Readiness Probe):主动声明“不可服务”

就绪探针是 Service 将 Pod 从 Endpoint 列表中移除的唯一依据。默认情况下,Pod 在收到 SIGTERM 后仍处于 Ready: true 状态,Service 继续转发流量,直到 Pod 实际终止(触发 Endpoint 删除)。因此,必须让应用在收到终止信号后立即通知 Kubernetes 自身已不再就绪

推荐做法:在 Go 应用中维护一个可变就绪状态,并通过 HTTP 探针暴露:

var isReady = true  func readinessHandler(w http.ResponseWriter, r *http.Request) {     if isReady {         w.WriteHeader(http.StatusOK)         w.Write([]byte("ok"))     } else {         w.WriteHeader(http.StatusServiceUnavailable)         w.Write([]byte("shutting down"))     } }  // 在 SIGTERM 处理逻辑中置为 false signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) go func() {     <-sigChan     fmt.Println("Received SIGTERM, marking as not ready...")     isReady = false // 立即影响 readiness probe 结果     fmt.Println("Shutting down server gracefully...")     server.Close() }()

对应 Kubernetes Deployment 需配置:

livenessProbe:   httpGet:     path: /healthz     port: 80   initialDelaySeconds: 30   periodSeconds: 10 readinessProbe:   httpGet:     path: /readyz     port: 80   initialDelaySeconds: 5   periodSeconds: 5   failureThreshold: 1  # 一次失败即标记 NotReady

✅ 2. PreStop Hook:强制同步等待,保障探针生效

即使设置了就绪探针,Kubernetes 默认在发送 SIGTERM 后直接进入 terminationGracePeriodSeconds 倒计时,而探针状态变更(如 /readyz 返回 503)需等待下一次探测周期(默认 5s)。这中间的窗口期仍可能导致流量误入。

解决方案:使用 preStop 生命周期钩子,同步阻塞 Pod 终止流程,直至确认就绪状态已更新且存量请求完成

lifecycle:   preStop:     exec:       command: ["/bin/sh", "-c", "         # 等待就绪探针返回失败(最多 10s)         for i in $(seq 1 10); do           if ! curl -f http://localhost/readyz &>/dev/null; then             echo 'Readiness probe failed, proceeding...';             break;           fi;           sleep 1;         done;         # 可选:额外等待 2s 确保 Endpoint 更新传播         sleep 2;       "]

⚠️ 注意:preStop 执行期间,Pod 仍处于 Running 状态但已标记为 Terminating,Service 会逐步将其从 Endpoint 中剔除(取决于 kube-proxy 更新频率,通常

✅ 3. 应用层优雅关闭:配合超时与连接 draining

您的 manners 方案方向正确,但需增强鲁棒性:

  • 设置合理的 server.Close() 超时(避免无限等待);
  • 显式调用 server.Shutdown(ctx)(Go 1.8+ 原生支持,比 manners 更轻量且官方维护);
  • 在 SIGTERM 处理中先禁用就绪状态,再启动关闭流程。

优化后的 Go 关键逻辑:

func main() {     http.HandleFunc("/", hello)     http.HandleFunc("/readyz", readinessHandler)      server := &http.Server{Addr: ":80", Handler: nil}      // 启动服务     go func() {         if err := server.ListenAndServe(); err != http.ErrServerClosed {             log.Fatalf("HTTP server error: %v", err)         }     }()      sigChan := make(chan os.Signal, 1)     signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt)      <-sigChan     fmt.Println("Shutting down gracefully...")      // 1. 立即标记不就绪(影响下一次 readiness probe)     isReady = false      // 2. 创建带超时的 context 控制 shutdown     ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)     defer cancel()      // 3. 执行优雅关闭(等待活跃连接完成)     if err := server.Shutdown(ctx); err != nil {         log.Printf("Server shutdown error: %v", err)     }     fmt.Println("Server stopped.") }

? 总结:优雅缩容的黄金组合

组件 作用 必须配置?
Readiness Probe 让 Service 主动摘流,是优雅缩容的前提 ✅ 强烈推荐
PreStop Hook 同步等待就绪状态变更 + 缓冲传播延迟,消除竞态窗口 ✅ 生产环境必备
应用层 Shutdown 安全终止 HTTP server,拒绝新请求、完成存量请求 ✅ 基础保障

三者缺一不可:缺少就绪探针,Service 不知“该停”;缺少 PreStop,探针更新赶不上终止节奏;缺少应用层关闭,连接被粗暴中断。当三者协同,无论缩容至 1 个副本还是跨节点调度,均可实现毫秒级无损切换。

最后提醒:务必设置合理的 terminationGracePeriodSeconds(默认 30s),确保 PreStop 和 Shutdown 有足够时间完成;同时监控 kube_pod_status_phase{phase=”Terminating”} 和 container_restarts_total,及时发现未按预期退出的 Pod。

text=ZqhQzanResources