Go 微服务优雅上下线:PreStop Hook 和 Readiness Probe 的协作

📅 发布时间:2026/7/9 3:31:15
Go 微服务优雅上下线:PreStop Hook 和 Readiness Probe 的协作 Go 微服务优雅上下线PreStop Hook 和 Readiness Probe 的协作一、每次发布都有人投诉刷着刷着就报错了微服务滚动更新时的常见事故用户正在浏览商品列表页面报 502。刷新后正常了。运维查看日志发现是 Pod 被杀死时还有连接在处理。另一个常见事故新 Pod 已经就绪开始接收流量。但内部的数据库连接池还没初始化完毕。前几秒的请求全部超时。这就是没有区分进程启动了和服务就绪了。Kubernetes 提供了一套优雅上下线机制。但默认配置下它做得并不好。terminationGracePeriodSeconds 默认 30 秒。但 PreStop Hook 默认是空的。Readiness Probe 默认也没有。二、Pod 生命周期的关键阶段Kubernetes 的 Pod 生命周期有两个关键过渡点。就绪阶段容器启动了但还没准备好服务。终止阶段容器收到 SIGTERM需要优雅退出。sequenceDiagram participant K as K8s API participant P as Pod participant S as Service/Endpoint participant L as LoadBalancer Note over K,L: ---- 启动阶段 ---- K-P: 创建 Pod P-P: Init Container 执行 P-P: 主容器启动 Note over P: Readiness Probe 不断检测 P--K: Readiness 通过 K-S: 加入 Endpoints S-L: 开始分发流量 Note over K,L: ---- 终止阶段 ---- K-P: 发送 SIGTERM P-P: 执行 PreStop Hook P-S: 从 Endpoints 摘除 Note over P: 等待已有请求处理完 P-P: 关闭数据库连接 P-K: 进程退出 K-S: 清理 Pod 记录关键时序PreStop Hook 执行期间Pod 仍在 Endpoints 上。所以 PreStop Hook 的首要任务是通知负载均衡别再发新请求。三、Go 服务的优雅上下线配置Kubernetes 配置DeploymentapiVersion: apps/v1 kind: Deployment metadata: name: order-service spec: replicas: 3 selector: matchLabels: app: order-service template: metadata: labels: app: order-service spec: terminationGracePeriodSeconds: 45 # 留足时间优雅退出 containers: - name: app image: order-service:v1.2.0 ports: - containerPort: 8080 # 就绪探针服务准备好后才接收流量 readinessProbe: httpGet: path: /healthz/ready port: 8080 initialDelaySeconds: 5 # 启动后 5 秒开始探测 periodSeconds: 3 # 每 3 秒探测一次 failureThreshold: 3 # 连续失败 3 次标记未就绪 # 存活探针检测进程是否健康 livenessProbe: httpGet: path: /healthz/live port: 8080 initialDelaySeconds: 15 periodSeconds: 10 failureThreshold: 3 # 优雅停机前置操作 lifecycle: preStop: exec: command: - /bin/sh - -c - | # 1. 通知应用开始优雅关闭 curl -X POST http://localhost:8080/admin/shutdown # 2. 等待流量摘除sleep 5-10 秒 sleep 10Go 服务端实现package main import ( context fmt log net/http os os/signal sync sync/atomic syscall time ) type Server struct { httpServer *http.Server ready atomic.Bool healthy atomic.Bool shutdowning atomic.Bool activeConns int64 wg sync.WaitGroup } func NewServer(addr string) *Server { s : Server{} s.healthy.Store(true) // 进程启动即健康 mux : http.NewServeMux() // 就绪检查 mux.HandleFunc(/healthz/ready, func(w http.ResponseWriter, r *http.Request) { if s.ready.Load() !s.shutdowning.Load() { w.WriteHeader(http.StatusOK) w.Write([]byte(ready)) return } w.WriteHeader(http.StatusServiceUnavailable) w.Write([]byte(not ready)) }) // 存活检查 mux.HandleFunc(/healthz/live, func(w http.ResponseWriter, r *http.Request) { if s.healthy.Load() { w.WriteHeader(http.StatusOK) w.Write([]byte(alive)) return } w.WriteHeader(http.StatusServiceUnavailable) }) // 管理接口触发优雅关闭 mux.HandleFunc(/admin/shutdown, func(w http.ResponseWriter, r *http.Request) { if r.Method ! http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } s.shutdowning.Store(true) w.Write([]byte(shutting down)) log.Println(收到关闭信号标记为未就绪) }) // 业务接口 mux.HandleFunc(/api/order, func(w http.ResponseWriter, r *http.Request) { if s.shutdowning.Load() { w.WriteHeader(http.StatusServiceUnavailable) return } // 追踪活跃连接数 atomic.AddInt64(s.activeConns, 1) defer atomic.AddInt64(s.activeConns, -1) // 模拟业务处理 time.Sleep(100 * time.Millisecond) w.Write([]byte({status:ok})) }) s.httpServer http.Server{ Addr: addr, Handler: mux, } return s } // Start 启动服务返回就绪通知 Channel func (s *Server) Start() -chan struct{} { readyCh : make(chan struct{}) go func() { // 模拟初始化连接数据库、加载配置等 log.Println(初始化中...) time.Sleep(3 * time.Second) // 初始化完毕标记就绪 s.ready.Store(true) close(readyCh) log.Println(服务就绪开始接收流量) if err : s.httpServer.ListenAndServe(); err ! nil err ! http.ErrServerClosed { log.Fatalf(服务启动失败: %v, err) } }() return readyCh } // GracefulShutdown 监听系统信号优雅关闭 func (s *Server) GracefulShutdown(shutdownTimeout time.Duration) { sigCh : make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT) sig : -sigCh log.Printf(收到信号: %v开始优雅关闭, sig) // 1. 标记未就绪拒绝新请求 s.shutdowning.Store(true) s.ready.Store(false) // 2. 等待活跃连接处理完最多等 shutdownTimeout waitStart : time.Now() for atomic.LoadInt64(s.activeConns) 0 { if time.Since(waitStart) shutdownTimeout-5*time.Second { log.Println(等待超时强制关闭) break } time.Sleep(500 * time.Millisecond) } // 3. 关闭 HTTP 服务 ctx, cancel : context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err : s.httpServer.Shutdown(ctx); err ! nil { log.Printf(HTTP 关闭异常: %v, err) } log.Println(优雅关闭完成) } func main() { s : NewServer(:8080) readyCh : s.Start() // 等待就绪 -readyCh // 阻塞直到收到终止信号 s.GracefulShutdown(30 * time.Second) }四、常见陷阱与避坑指南PreStop Hook 的 sleep 时间需要实测。sleep 太短负载均衡还没摘除流量。sleep 太长Pod 终止时间超过 terminationGracePeriodSeconds。一般设为 5-10 秒取决于服务网格刷新频率。Readiness Probe 的 initialDelaySeconds 不可省略。如果不设延迟Pod 一启动就开始探测。而数据库连接池可能还没初始化完成。这会导致 Pod 在启动-未就绪之间反复横跳。数据库连接池的关闭时机很关键。必须在所有活跃请求处理完之后再关闭。否则正在处理的事务会因连接断开而失败。五、总结优雅上下线依赖三个机制协同PreStop Hook、Readiness Probe、Graceful Shutdown。PreStop 通知应用停止接收流量并休眠等摘除。就绪探针确保初始化完成前不接收请求。Go 服务通过 atomic.Bool 标记状态精确控制流量。核心原则先拒绝新请求再等待旧请求完成最后清理资源。