在云原生架构中,服务的生命周期管理远比传统部署模式复杂。当一个 Pod 被标记为终止状态时,它并不会立刻从网络中消失,而是经历一系列状态流转。如果这个过程处理不当,就会成为线上故障的导火索。集群优雅终止与连接 draining 规范,本质上是一套让服务实例在退出时能够妥善处理未完成请求、安全释放资源并平滑从流量路由中摘除的工程实践。无论是滚动更新、节点缩容还是主动重启,都需要遵循这套规范才能保证业务连续性。

优雅终止的核心原理与信号机制
容器运行时在收到删除指令后,会向容器内的主进程(PID 为 1 的进程)发送 SIGTERM 信号。这个信号是可以被应用程序捕获和处理的,进程可以通过注册信号处理器来执行清理逻辑,比如关闭数据库连接、刷新缓存、完成正在处理的请求等。然而,如果主进程没有在规定的优雅终止期限内退出,容器运行时会发送 SIGKILL 信号强制结束进程。SIGKILL 无法被捕获或忽略,进程会立即终止,所有未完成的请求和未释放的资源都会丢失,这是导致服务下线期间出现 5xx 错误的主要原因之一。
很多开发者误以为只要在代码里监听了 SIGTERM 就能实现优雅下线,但实际情况远比这复杂。在 Kubernetes 环境中,当 Pod 被删除时,kubelet 会并行执行两个关键动作:一是向容器发送 SIGTERM 信号,二是将该 Pod 从 Service 的 Endpoints 列表中移除。问题在于,这两个动作虽然是同时触发的,但 Endpoints 的更新需要经过 API Server 传递到 kube-proxy,再由 kube-proxy 修改节点上的 iptables 或 ipvs 规则,最终才能让上游的负载均衡器感知到该 Pod 已经不可用。这个传播链路存在不可忽视的延迟。
这意味着,即使 Pod 已经收到了 SIGTERM 信号并开始执行退出逻辑,上游的负载均衡器和 API 网关可能还不知道这个 Pod 即将下线,依然会继续往这个 Pod 路由新请求。如果应用程序在收到 SIGTERM 后立即停止接收新请求,这些新路由过来的请求就会直接失败。这就是为什么单纯依赖 SIGTERM 信号无法实现真正的优雅终止,必须引入额外的 draining 机制来弥补这个时间差。理解这个信号传递与路由更新的时序问题,是设计优雅终止方案的基础前提。
连接 draining 的实现策略与配置规范
连接 draining 的核心目标是确保在服务实例真正退出之前,既不接受新请求,又能让正在处理的请求有足够的时间完成。实现这一目标需要分两个阶段执行:流量摘除阶段和连接排空阶段。流量摘除是指将该实例从服务发现注册中心和负载均衡的可用节点列表中移除,使新的请求不再被路由到这个实例上。连接排空则是指对已经建立但尚未完成的连接进行等待,直到所有活跃请求处理完毕后才允许进程退出。这两个阶段的顺序不能颠倒,时间窗口也不能重叠不足。
在 Kubernetes 中,preStop 钩子是实现流量摘除的关键手段。preStop 是一个在 SIGTERM 信号发送之前执行的钩子程序,它为应用程序提供了一个时间窗口来完成流量摘除操作。一个典型且经过验证的做法是在 preStop 中执行一段 sleep 操作,让 Pod 在收到终止信号前先等待几秒钟,给 kube-proxy 足够的时间更新网络规则。这样当 SIGTERM 真正到达时,Pod 已经从 Endpoints 中移除,不会再有新请求进来。下面是一个标准的 preStop 配置示例:
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- "sleep 10"
除了时间缓冲,应用程序自身也需要实现健康检查接口的配合。当收到终止信号后,应用应该立即将就绪探针的返回状态改为失败,返回非 200 状态码。这样即使 Endpoints 更新有延迟,Kubernetes 自身的探针机制也会将该 Pod 标记为未就绪,从而阻止新流量进入。同时,应用需要维护一个正在处理请求的计数器或使用类似 WaitGroup 的机制,在所有请求处理完成之前不退出主进程。下面是一个 Go 语言实现的优雅终止逻辑示例:
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
var (
wg sync.WaitGroup
isShuttingDown bool
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
if isShuttingDown {
w.WriteHeader(http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
})
mux.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
wg.Add(1)
defer wg.Done()
if isShuttingDown {
w.WriteHeader(http.StatusServiceUnavailable)
return
}
time.Sleep(2 * time.Second)
w.Write([]byte("request processed"))
})
server := &http.Server{Addr: ":8080", Handler: mux}
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("server error: %v", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)
<-quit
log.Println("收到终止信号,开始优雅下线")
isShuttingDown = true
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
server.Shutdown(ctx)
wg.Wait()
log.Println("所有请求处理完毕,进程退出")
}
滚动更新与扩缩容场景下的实战配置
下面给出一个完整的 Kubernetes Deployment 配置示例,整合了 preStop 钩子、就绪探针、生存探针和优雅终止期限,实现端到端的平滑下线流程。这个配置适用于 HTTP 类型的微服务,通过多层次的防护确保滚动更新期间不会出现请求丢失。配置中的每个参数都经过实际验证,可以根据具体业务场景调整数值。
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-service
spec:
replicas: 3
template:
spec:
terminationGracePeriodSeconds: 60
containers:
- name: app
image: registry.cn-hangzhou.aliyuncs.com/demo/app:latest
ports:
- containerPort: 8080
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- "sleep 10"
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
配置中需要特别关注几个关键参数的取值逻辑。terminationGracePeriodSeconds 决定了从发送 SIGTERM 到发送 SIGKILL 之间的最大等待时间,默认值是 30 秒。这个值必须大于 preStop 的 sleep 时间加上应用处理完所有活跃请求所需的时间。如果应用存在长耗时的请求处理,比如文件上传或批量数据处理,必须将这个值调大到 60 秒甚至更长。preStop 中的 sleep 时间需要根据集群规模和网络规则传播延迟来设定,通常 5 到 10 秒可以覆盖大多数中小规模集群的场景,大规模集群可能需要 15 秒以上。
对于长连接场景,比如 WebSocket 或 gRPC 流式调用,draining 的策略需要更加精细。服务端在收到终止信号后,应该向所有活跃的长连接客户端发送一个 GOAWAY 帧或自定义的关闭通知消息,让客户端主动断开当前连接并重新连接到其他健康节点。服务端不能立即关闭底层 TCP 连接,而是要等待客户端完成重连后再退出。如果直接断开长连接,客户端会经历重试和超时,不仅影响用户体验,还可能导致消息丢失。对于 gRPC,可以利用 HTTP/2 的 GOAWAY 机制实现平滑过渡;对于 WebSocket,可以发送一个约定的关闭帧让客户端感知到服务端即将下线。
最后需要强调的是,优雅终止不仅仅是应用层面的配置,还需要基础设施的配合。如果服务前面有 Nginx 或 Envoy 等反向代理,代理层本身也需要配置主动健康检查和被动健康检查。主动健康检查会定期探测后端实例的健康状态,被动健康检查则会在请求失败时自动将实例标记为不可用。只有应用层和网络层的 draining 机制协同工作,才能实现真正意义上的零中断发布。建议在上线初期通过混沌测试模拟 Pod 频繁销毁的场景,验证整个 draining 链路是否按预期工作,及时发现配置中的薄弱环节。
优雅终止连接draining集群修改时间:2026-08-25 16:10:56