
本文详解 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。