
本文详解如何在 kubernetes 中结合 readiness probe、prestop 生命周期钩子与 go 应用的信号处理,确保缩容时 http 连接被完整处理、服务不中断。
本文详解如何在 kubernetes 中结合 readiness probe、prestop 生命周期钩子与 go 应用的信号处理,确保缩容时 http 连接被完整处理、服务不中断。
在 Kubernetes 中执行水平缩容(如 kubectl scale 或 HPA 触发)时,若未妥善协调“服务剔除”与“进程终止”的时序,极易导致客户端收到 502/503 或连接拒绝(Connection refused)错误——这正是你观察到“缩容至单实例时短暂报错”的根本原因。问题不在于你的 Go 代码(manners 的优雅关闭逻辑本身是正确的),而在于 Kubernetes 的服务发现机制与 Pod 终止流程之间存在竞争窗口:即使应用已开始优雅关闭,Service 的 Endpoint 仍可能将新请求或重试请求转发至正在关闭的 Pod。
✅ 正确解法:三阶段协同控制
要实现真正无损的缩容,需同时满足以下三个条件:
- Pod 状态及时同步:让 Service 立即停止向即将终止的 Pod 转发流量;
- 终止前预留缓冲期:在 SIGTERM 发出后、实际关闭监听端口前,完成所有进行中的请求;
- Kubernetes 主动配合:利用原生机制而非仅依赖应用层信号。
1. 配置 Readiness Probe(关键!)
Readiness probe 是 Service 更新 Endpoints 的唯一依据。默认情况下,Kubernetes 只在 Pod 启动后就将其加入 Endpoints;但不会自动移除——除非 probe 失败或 Pod 被删除。因此,必须让 probe 在收到 SIGTERM 后立即返回失败,从而触发 Service 快速摘除该 Pod。
示例配置(Deployment 片段):
livenessProbe:
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /readyz
port: 80
initialDelaySeconds: 5
periodSeconds: 5
# 注意:failureThreshold 设为 1,确保首次失败即摘除
failureThreshold: 1你的 Go 应用需响应 /readyz 并根据状态返回不同结果:
var isReady = true
func readyz(w http.ResponseWriter, r *http.Request) {
if !isReady {
http.Error(w, "Not ready", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
}2. 使用 PreStop Hook 实现原子化状态切换
PreStop 钩子在容器收到 SIGTERM 前同步执行,且会阻塞终止流程直到其完成(默认超时 30s,可配置)。这是将 isReady = false 与优雅关闭逻辑串联的黄金时机。
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "
# 1. 立即通知应用进入不可用状态
curl -f http://localhost/readyz?down=1 || true;
# 2. 等待最多 10 秒,确保所有活跃请求完成(与 Go 的 shutdown 逻辑对齐)
sleep 10
"]⚠️ 注意:不要在 PreStop 中直接调用 kill -TERM 或 server.Close() —— 这应由 Go 主程序自行处理。PreStop 只负责状态通告 + 安全等待。
3. 优化 Go 应用的优雅关闭逻辑(精简可靠版)
现代 Go 已原生支持优雅关闭(http.Server.Shutdown),无需第三方库 manners(已归档且不兼容 Go 1.8+)。以下是推荐实现:
package main
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
srv := &http.Server{
Addr: ":80",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 示例:模拟长请求(如 DB 查询)
select {
case <-r.Context().Done():
http.Error(w, "Request cancelled", http.StatusRequestTimeout)
return
default:
fmt.Fprint(w, "Hello world!")
}
}),
}
// 启动服务器
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
panic(err)
}
}()
// 捕获终止信号
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT)
<-sigChan
fmt.Println("Received shutdown signal, stopping server...")
// 启动优雅关闭:等待最多 15 秒完成现存请求
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
fmt.Printf("Server forced to shutdown: %v\n", err)
} else {
fmt.Println("Server gracefully stopped")
}
}? 关键配置项汇总(Deployment 必填)
spec:
containers:
- name: app
image: your-go-app:v1
ports:
- containerPort: 80
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "curl -f http://localhost/readyz?down=1 || true; sleep 10"]
livenessProbe:
httpGet: { path: /healthz, port: 80 }
initialDelaySeconds: 30
readinessProbe:
httpGet: { path: /readyz, port: 80 }
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 1 # ⚠️ 核心:1 次失败即摘除
# 建议:设置 terminationGracePeriodSeconds ≥ PreStop + Shutdown 耗时
terminationGracePeriodSeconds: 30✅ 总结:为什么这样能解决你的问题?
| 阶段 | 行为 | 效果 |
|---|---|---|
| 缩容触发 | Kubelet 发送 SIGTERM 到容器 | 应用开始准备关闭 |
| PreStop 执行 | 立即调用 /readyz?down=1 → isReady=false | Readiness probe 下一周期即失败 → Service 秒级更新 Endpoints |
| Shutdown 启动 | srv.Shutdown() 等待活跃请求完成 | 已建立连接继续处理,新连接被 Service 拒绝(因已不在 Endpoints) |
| Kubelet 删除 Pod | PreStop 结束 + grace period 超时后 | Pod 彻底移除,无残留流量 |
? 提示:若仍偶发错误,请检查 terminationGracePeriodSeconds 是否小于 PreStop + Shutdown 总耗时(建议设为 30–60s),并确保 Ingress Controller(如 Nginx Ingress)也配置了 proxy-next-upstream-tries 和健康检查。
通过 readiness probe 主动声明不可用、PreStop 钩子保障状态切换原子性、以及 Go 原生 Shutdown 控制连接生命周期,三者协同即可彻底消除缩容抖动——无论从 10 实例缩至 1 实例,还是跨节点调度,均能实现真正的零请求丢失。










