说明:本文由 Codex 根据作者提供的主题、素材与公开资料辅助生成,属于 AI 生成/整理内容,非作者原创。请读者自行甄别并交叉验证。
目标:让下线可控,而不是让进程多活一会儿#
Pod 的优雅退出要同时处理两件事:
- 流量面:尽快不再接收新请求。
- 进程面:给正在处理的请求、消息和任务一个可控的收尾时间。
理想结果是:
停止新流量
-> 完成 in-flight 请求
-> 提交消息 offset / 释放锁 / 关闭长连接
-> 进程主动退出
-> 超时才由 Kubernetes 强制杀死这里只设置一个较长的 terminationGracePeriodSeconds 并不够。应用必须能响应退出信号,也必须有清晰的“我不再接新活”的状态。
先区分七个概念#
这几个对象名字相近,但处在不同层次:
| 概念 | 所在层次 | 谁控制它 | 核心含义 |
|---|---|---|---|
/ready | 应用 | 应用代码 | 应用是否愿意接收新请求 |
readinessProbe | kubelet 探测 | Pod 配置 + kubelet | 定期检查应用是否就绪 |
Pod Ready condition | Pod 状态 | kubelet / readiness gates | Pod 是否就绪的 Kubernetes 状态 |
EndpointSlice conditions.ready | Service 后端 | EndpointSlice 控制器 | 是否作为普通可用后端 |
Terminating | Pod 删除生命周期 | API Server / 控制器 | Pod 已被请求删除 |
preStop | 容器生命周期钩子 | kubelet | 容器退出前同步执行的动作 |
SIGTERM | 容器进程 | kubelet / runtime | 通知进程开始退出的信号 |
最容易混淆的一点是:
Pod 进入 Terminating != 容器已经收到 SIGTERM
EndpointSlice ready=false != Pod Ready condition 一定已经变成 False它们有关联,但不是同一个字段,也不会严格在同一时刻改变。
默认终止流程#
删除 Pod、Deployment 滚动更新、通过 Eviction API 驱逐 Pod 等,最终都会触发 Pod 终止。典型流程如下:
T0 API Server 为 Pod 设置 deletionTimestamp,Pod 显示 Terminating
|
+-- 控制面异步更新关联的 EndpointSlice
| terminating=true
| ready=false
| serving 可能仍为 true
|
+-- kubelet 开始容器的优雅终止
|
+-- 执行 preStop(如果已配置且宽限时间不为 0)
|
+-- preStop 完成后,向容器主进程发送 TERM(默认是 SIGTERM)
|
+-- 应用停止接新活、处理存量工作并退出
Tgrace 仍未退出时,kubelet 发送 KILL(通常是 SIGKILL)EndpointSlice 更新和 kubelet 的容器终止是并发发生的,不能把它理解成一条严格串行的流水线。数据面、Ingress、云负载均衡器和客户端连接也都有各自的传播延迟。
Kubernetes 默认的 terminationGracePeriodSeconds 是 30 秒。宽限时间在 preStop 之前就开始计时,因此它同时覆盖:
preStop 执行时间 + 应用收到 SIGTERM 后的退出时间若 preStop 已经超过宽限时间,kubelet 会给一个很短的额外延长(当前文档描述为 2 秒);不要把它当作可依赖的预算。耗时较长的关闭动作应显式调大 terminationGracePeriodSeconds。
preStop、SIGTERM 与 sleep 10#
preStop 配在容器的 lifecycle 下:
spec:
terminationGracePeriodSeconds: 45
containers:
- name: web
image: example/web:1.0
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "/app/bin/drain && sleep 5"]preStop 是同步钩子。kubelet 会等待它结束,再向该容器发送 TERM 信号。因此:
preStop sleep 10 的十秒内,进程通常仍在运行,且通常尚未收到 SIGTERM。单独的 sleep 10 不等于停止接流量。它只是给以下状态传播留出时间:
- Service / kube-proxy 或 eBPF 数据面感知 EndpointSlice 变化。
- Ingress 或云负载均衡器完成后端摘除。
- 应用主动 drain 后,readinessProbe 失败并被 kubelet 观察到。
如果应用没有先进入 drain 状态,它还在监听端口,也可能继续处理来自已有连接、直连 Pod IP、自研注册中心或尚未收敛的负载均衡器的请求。
因此更常见的模式是:
主动 drain + 很短的等待 + SIGTERM graceful shutdown而不是只写一个长 sleep。这个等待值没有通用答案,应由实际的 Ingress/LB 传播时间和压测结果决定;它会吃掉总宽限时间。
preStop 可以做什么#
适合在 preStop 中做的动作包括:
- 调用本地
/drain接口或执行应用内置的 drain 命令。 - 停止 MQ、Kafka、任务队列的新消息拉取。
- 停止新建定时任务、抢占任务或租约续约。
- 从自研服务发现或外部负载均衡器主动注销。
- 让应用通知长连接客户端进入关闭流程。
不适合把 preStop 当作一个长时间后台任务。钩子卡住会拖住整个 Pod 终止,并侵占应用真正处理 SIGTERM 的时间。
readinessProbe 如何影响 EndpointSlice#
在 Pod 正常运行、尚未终止时,典型链路是:
应用 /ready 返回成功
-> readinessProbe 成功
-> kubelet 写入 Pod Ready=True
-> EndpointSlice endpoint ready=true
-> Service 的实现将它作为可用后端反过来,应用的 /ready 失败后:
应用 /ready 返回失败
-> readinessProbe 失败
-> Pod Ready=False
-> EndpointSlice endpoint ready=false
-> 普通 Service 新流量不应再选择它所以 readinessProbe 会间接影响 EndpointSlice 的后端可用性;它不是直接修改 EndpointSlice 的 API。
但 Pod 进入终止时有一条独立规则:终止中的 EndpointSlice endpoint 会被标为 ready=false,以使常规负载均衡停止把普通新流量导向它。此时一个典型状态是:
conditions:
ready: false
serving: true
terminating: true这里的含义是:
ready: false:不应再作为常规新请求的后端。terminating: true:该端点所属的 Pod 正在终止。serving: true:它可能仍可用于连接排空;支持这个条件的消费者可以据此保留已有连接的收尾能力。
Endpoint 不一定会在 Pod 刚显示 Terminating 时立刻从 EndpointSlice 对象中消失。反过来,Pod 的 Ready condition 也未必会立刻变为 False。排查时要分别看 Pod status 和 EndpointSlice,别把二者混为一个“readiness 状态”。
例外:
publishNotReadyAddresses、自定义 readiness gates、Headless Service、外部 Ingress/LB 的实现细节,都会改变实际观测到的转发行为。结论应以当前集群的数据面实现和压测结果为准。
主动 drain 到底解决什么#
主动 drain 指应用自己先进入“停止接新活”的状态,常见做法是一个 /drain 接口把内部 draining 标记打开,使 /ready 之后返回失败。
它有两种时机,意义并不相同:
| 时机 | 主要价值 |
|---|---|
删除 Pod 之前主动调用 /drain | 让 readinessProbe、EndpointSlice 和外部流量入口先完成摘流量,再开始真正删除。适合发布平台或应用控制器可编排的场景。 |
在 preStop 中调用 /drain | 此时 Pod 已在 Terminating,Kubernetes 已会开始将 Service 端点标为非就绪;它的主要价值转为应用侧自我保护,例如停止队列消费、拒绝直连请求、停止自研注册中心流量,以及优雅关闭长连接。 |
也就是说,preStop 里的 /drain 并不会直接 patch Kubernetes 的 Pod Ready condition。它改变的是应用的 /ready 返回值;kubelet 下一次探测后才可能更新 Pod Ready。并且在终止路径中,EndpointSlice 的 ready=false 本来就会由终止状态保证。
这正是为什么不能把“主动 drain 影响 Ready 接口”和“Pod Terminating 影响 EndpointSlice endpoint 状态”说成同一件事。
一份可落地的 Deployment 配置#
下面的示例适用于一个 HTTP 服务。/drain 由应用实现:它立即拒绝新业务请求、使 /ready 失败,并停止接收新的后台工作;SIGTERM 处理逻辑负责等存量请求结束。
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
terminationGracePeriodSeconds: 45
containers:
- name: web
image: example/web:1.0
ports:
- name: http
containerPort: 8080
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- |
curl -fsS -X POST http://127.0.0.1:8080/drain
sleep 5
readinessProbe:
httpGet:
path: /ready
port: http
periodSeconds: 2
timeoutSeconds: 1
failureThreshold: 1
livenessProbe:
httpGet:
path: /live
port: http
periodSeconds: 10
timeoutSeconds: 1
startupProbe:
httpGet:
path: /live
port: http
periodSeconds: 2
failureThreshold: 30注意事项:
terminationGracePeriodSeconds位于 Podspec,不是containers[]内。preStop、readinessProbe、livenessProbe和startupProbe都位于具体容器的配置下。- 示例依赖镜像中存在
curl;更稳妥的生产做法是提供一个应用自身的 drain 命令,或选用镜像中已有的 HTTP 客户端。 /ready与/live应分开。进入 drain 时通常应该让/ready失败,但不应因为正在优雅关闭而把存活检查的语义混乱化。startupProbe用于慢启动保护,避免 livenessProbe 在应用尚未启动完成时反复重启它;它不参与正常的退出摘流量。- 某些较新的 Kubernetes 版本还支持配置容器的停止信号;是否使用取决于集群版本、容器运行时和操作系统。绝大多数 Linux 服务保留默认
SIGTERM即可。
上面 sleep 5 的角色只是“传播缓冲”。45 秒的总预算可粗略拆为:5 秒给摘流量传播,剩余约 40 秒给应用处理存量请求和退出。若关闭一个任务需要 60 秒,这份配置仍然会失败,应调大宽限时间或改变任务的可中断/可续跑设计。
应用收到 SIGTERM 后应该做什么#
应用侧的收尾顺序通常比 Kubernetes YAML 更重要:
收到 SIGTERM
-> 再次切换为 draining(保证没有遗漏)
-> 停止 HTTP/gRPC 接受新连接
-> 停止 poll 新消息、停止派发新任务
-> 等待 in-flight 请求、消息处理和关键事务完成
-> 提交 offset / ack,释放锁和连接
-> 主进程退出不同类型应用的重点不同:
| 应用类型 | 退出时的关键动作 |
|---|---|
| 普通 HTTP 服务 | 关闭 listener,等待正在处理的请求完成。 |
| gRPC / WebSocket 服务 | 停止新连接,向客户端发送关闭或迁移信号,设置最大等待时间。 |
| Kafka / MQ Consumer | 先停止拉取新消息,再处理当前消息并提交 offset / ack。 |
| Worker / 定时任务 | 不再领取新任务,完成或安全中断当前任务,释放分布式锁。 |
进程必须正确接收到 TERM。若容器的入口脚本启动了子进程却没有使用 exec,信号可能只到 shell,应用本体收不到,从而让优雅退出形同虚设。
kubectl drain、Eviction API 与删除 Pod 的区别#
“drain”有两个不同语境,应该分开说:
| 名称 | 目标 | 做什么 |
|---|---|---|
| 应用主动 drain | 一个应用实例 | 停止接新请求、停止取新任务,保留存量工作收尾。 |
kubectl drain <node> | 一个 Node | cordon 节点,并把符合条件的 Pod 从该节点驱逐出去。 |
| Eviction API | 一个 Pod | 向 API Server 请求驱逐这个 Pod,并遵守 PodDisruptionBudget。 |
kubectl delete pod | 一个 Pod | 直接请求删除;通常不经 Eviction API,因此可绕过 PDB 的自愿中断保护。 |
kubectl drain 是节点级命令,不能直接写成“drain 一个 Pod”。它会遍历节点上的可驱逐 Pod,并通常通过 Eviction API 发起逐个驱逐;Eviction API 本身则是Pod 级的 API。
常用节点维护命令:
kubectl drain <node-name> --ignore-daemonsets --delete-emptydir-data
kubectl uncordon <node-name>几个边界需要牢记:
drain会受 PodDisruptionBudget 限制;PDB 不允许时,驱逐会等待或失败。- DaemonSet Pod 默认不能被
drain正常驱逐,所以通常需要--ignore-daemonsets。 --disable-eviction会改用删除路径,可能绕过 PDB;除非明确接受可用性风险,不要随手使用。- 节点宕机、进程 OOM、硬资源压力驱逐等异常场景,未必能给应用完整的优雅退出时间。应用仍需具备幂等、可重试和可恢复能力。
无论是用户删除、Deployment 更新,还是一个正常的 Eviction,进入 kubelet 可执行优雅终止的路径后,应用配置的 preStop 和 SIGTERM 处理才会发挥作用;节点已经失联时,任何本地钩子都无法保证执行。
常见误区#
“Pod 进 Terminating 后已经不会有任何新请求”#
不够准确。Kubernetes 会让终止中的 Service endpoint ready=false,目标是停止常规新流量;但这是异步传播的。已有长连接、LB/Ingress 缓存、直连 Pod IP、Headless Service 或外部注册中心的流量需要单独验证和处理。
“sleep 10 时应用已经在优雅关闭”#
不一定。若 sleep 是 preStop,那么 TERM 还未发送。除非此前已调用应用 drain 逻辑,否则应用可能仍在接受请求。
“preStop drain 可以立刻改变 Kubernetes Ready”#
不直接可以。它只能改变应用接口或本地状态;readinessProbe 需要被 kubelet 探测到,才会影响 Pod Ready。在 Pod 已经 Terminating 的路径中,EndpointSlice ready=false 又是另一条独立规则。
“serving=true 表示 Service 会继续发送新请求”#
不是。终止端点的 ready 会是 false。serving 与 terminating 是给支持连接排空语义的消费者提供的更细粒度状态,不能把它当作普通新流量的放行开关。
“设置很长的 grace period 就完成了优雅退出”#
不是。没有 drain、信号处理和后台任务收尾逻辑时,只是更晚收到 SIGKILL。
验证与排查#
发布前最好做一次真实终止演练:保持持续请求或一个长请求,删除一个 Pod,并同时观察状态变化。
# 观察 Pod 是否进入 Terminating
kubectl get pod -l app=web -w
# 查看 Pod 的就绪条件和终止时间
kubectl get pod <pod-name> -o yaml
# 查看 Service 后端中的 ready / serving / terminating
kubectl get endpointslice \
-l kubernetes.io/service-name=<service-name> \
-o yaml
# 查看 preStop、探针或终止相关事件
kubectl describe pod <pod-name>
kubectl get events --sort-by=.lastTimestamp验收时关注这些事实,而不只是“Pod 最后消失了”:
- 终止期间 EndpointSlice 是否出现
ready=false与terminating=true。 - 新请求在传播完成后是否不再落到该实例。
- 已在执行的请求、消息或任务是否在宽限时间内正确完成或安全中断。
- 应用日志是否记录到 drain 和 SIGTERM 收尾,而不是直接被 KILL。
terminationGracePeriodSeconds是否覆盖最慢但仍应完成的工作。
生产检查清单#
- 应用实现了独立的
/ready与/live语义。 - 应用能处理默认 TERM 信号,并能停止新工作、等待存量工作。
- 入口脚本确保信号能传给真正的应用主进程。
-
terminationGracePeriodSeconds基于实际最大收尾时间设置。 -
preStop只做必要动作,且不把长时间工作塞进去。 - 使用 Ingress、云 LB、自研注册中心或长连接时,已验证实际的摘流量延迟。
- 消费者/Worker 的消息处理是幂等、可重试或可恢复的。
- 节点维护场景为 Deployment 配置了合适的副本数和 PodDisruptionBudget。
小结#
可以把 Pod 优雅退出压缩成下面这句话:
Terminating 负责声明“这个 Pod 正在下线”;
EndpointSlice 负责让 Service 停止把普通新流量给它;
preStop 负责在 TERM 前做同步准备;
SIGTERM 负责通知应用完成真正的收尾;
SIGKILL 只是超时兜底。最稳妥的实践是让 Kubernetes 的端点摘除、应用自身的主动 drain 和 SIGTERM 处理三者协作,而不是依赖某一个单独配置。