jimyag's Blog

Kubernetes 探针的作用与实现:容器启动、流量接入与故障重启

Pod 仍然显示 Running,Service 却不再把新流量发给它;另一个 Pod 则不断重启。两者可能都打印过 Unhealthy,但处理方向完全不同:前者可能只是暂时没有准备好接请求,后者可能已经被 kubelet 判定需要重新启动。

要解释这些现象,不能只看“探针失败”四个字,还要确认失败的是 startup、readiness 还是 liveness,以及失败触发了什么动作。

三个探针对应三个判断

探针要回答的问题失败后最直接的影响
startupProbe应用是否完成启动达到失败阈值后终止容器;成功前不执行该容器的 readiness/liveness
readinessProbe当前是否适合接收流量容器变为未就绪,不因这个结果单独重启
livenessProbe当前进程是否需要通过重启恢复达到失败阈值后终止容器,再按重启策略处理

kubelet 为配置的探针执行周期检查,并按连续成功或失败次数判断是否达到阈值。startup 成功前,readiness 和 liveness 不接管该容器;readiness 的结果用于容器及 Pod 的就绪状态;liveness 和 startup 达到失败阈值则进入容器终止与重启处理。官方定义见 Liveness、Readiness 和 Startup Probes,执行逻辑见 v1.35.1 的 probe worker。

readiness 影响流量还要经过一条控制链:kubelet 更新 Pod 状态,EndpointSlice 控制器更新 Service 对应端点,kube-proxy 或其他 Service 数据面再更新转发规则。探针本身不拦截应用端口,也不检查每个业务请求。本文使用未设置 publishNotReadyAddresses 的普通 Service;直接访问 Pod IP,或使用不同的服务发现策略,不一定遵循同样的就绪筛选。

探针检测程序可能是 HTTP 请求、命令或其他受支持的机制,但“检测什么”仍然由应用决定。把三个探针都指向同一个无条件返回 200 的接口,只能说明那个接口还会响应。

实验怎样控制故障

本次通过 SSH 在 zj 上运行 kind v0.33.0、Kubernetes v1.35.1。一个 Python HTTP 服务提供三个路径:

  • /startup:只有 /state/started 文件存在才返回 200。
  • /ready:存在 /state/not-ready 时返回 503。
  • /live:存在 /state/not-live 时返回 503。

/state 挂载 emptyDir,所以容器重启后文件仍然保留。每次实验通过创建或删除文件改变一个条件,其他接口保持不变;这比让应用随机报错更容易确认因果。

关键配置如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 以下字段放在应用容器中;三个接口分别控制三个独立状态。
startupProbe:
  httpGet:
    path: /startup
    port: 8080
  periodSeconds: 2 # 周期性检查启动条件。
  failureThreshold: 60 # 留出实验操作时间,避免启动阶段过早被终止。
readinessProbe:
  httpGet:
    path: /ready
    port: 8080
  periodSeconds: 2
  failureThreshold: 2 # 连续失败后标记未就绪。
livenessProbe:
  httpGet:
    path: /live
    port: 8080
  periodSeconds: 2
  failureThreshold: 2 # 连续失败后终止容器。

这是为了缩短反馈时间的实验参数,不是生产环境的通用建议。实际阈值应覆盖正常启动时间和短暂波动;探测周期、单次超时、连续失败阈值和终止宽限期共同影响最终恢复时间。

启动没有完成,进程也可以已经 Running

实验先不创建 started 文件。HTTP 服务进程已经启动,但 startupProbe 返回 503,事件记录 Startup probe failed,容器状态中的 started 为 false。

这说明 Running 只描述运行阶段,并不证明应用启动逻辑已经完成。此时增大 readiness 的初始延迟并不能替代明确的启动判断。

创建标记后,startupProbe 成功,容器开始接受后续的 readiness/liveness 检查:

1
2
kubectl --context kind-deep-dive -n dd-probes exec app -- touch /state/started
kubectl --context kind-deep-dive -n dd-probes wait --for=condition=Ready pod/app --timeout=60s

startupProbe 不是每次请求前的检查。当前容器实例启动成功后,它的职责已经完成;容器重新启动时才需要重新通过启动判断。

readiness 失败:退出流量集合,但进程继续运行

实验创建 not-ready 文件后,容器的 ready 变为 false,但 restartCount 仍然是 0。对应 Service 的 EndpointSlice 也观察到 ready=false。

1
2
3
kubectl --context kind-deep-dive -n dd-probes exec app -- touch /state/not-ready
kubectl --context kind-deep-dive -n dd-probes get pod app -o yaml
kubectl --context kind-deep-dive -n dd-probes get endpointslice -o yaml

删除标记后,同一个容器恢复 Ready,无需重启。对于暂时无法接新请求、但仍能自行恢复的情况,这种行为通常更符合预期。

EndpointSlice 更新也不等于所有连接瞬间断开。已有连接和应用中的请求有自己的生命周期,具体讨论见 Service 连接排空文章。探针不负责替应用完成事务、关闭连接或刷新缓冲区。

liveness 失败:重启是否真的能够恢复

接着让 /live 返回 503。连续失败后,实验观察到 restartCount 增加,新容器启动。由于 emptyDir 保留,not-live 文件不会因为容器重启自动消失;脚本随后删除标记,才让应用稳定恢复。

这个安排也说明了 liveness 设计中最重要的问题:如果故障原因没有被重启消除,重启只会重复相同的失败。数据库不可用、配置错误、权限不足,都不一定能通过重启当前进程解决。

是否把数据库连接纳入探针,应该从失败动作反推。数据库短暂断开时,若应用还能重连,就不能仅因为数据库不可达而认定当前进程必须重启;readiness 是否失败,则取决于应用是否仍能提供可接受的服务。把所有下游依赖同时纳入所有副本的 liveness,还可能在下游故障时让上游一起反复重启。

连续失败计数怎样避免一次抖动就改变状态

v1.35.1 的 probe worker维护当前结果和连续出现次数。新结果与上一次一致时累加,变化时从 1 开始;达到相应阈值才发布结果。因此 failureThreshold: 3 表示连续三次失败,不是任意时间窗口内累计三次失败,也不是失败率达到某个比例。中间出现成功会打断这串失败计数。

发布给 results manager 的结果以实际 container ID 为键。worker 发现容器 ID 改变后,会移除旧 ID 的结果,为新实例处理初始结果,并解除暂停;代码还对 kubelet 自身重启后的初始结果设置做了特殊判断,避免立即把原本就绪的运行中容器标记为失败。这里不能把“换了 container ID”概括为“所有内部计数都清零”,连续计数与已发布结果是不同状态。

还有一处处理专门照顾终止过程:liveness 达到失败阈值,或 startup 达到成功、失败阈值后,worker 会进入 onHold 并把连续计数清零,等待新的 container ID,不继续反复执行检查。源码注释指出,尤其要避免对正在停止的容器持续执行 exec 探针。readiness 失败后则继续探测,否则它就无法在应用恢复时把结果改回成功。

所以三个探针虽然复用检查方式,却有不同的状态转换。本文 readiness 实验中进程不重启而端点就绪性改变,liveness 实验中 restart count 增长,正好对应这两条处理路径。阈值可以过滤短暂抖动,但无法修复一个把“外部依赖故障”误判成“必须重启本进程”的检查。

看到 Unhealthy 后看什么

先读事件中的探针名称和错误内容,再看容器的 ready、started、restartCount、lastState。如果重启次数没有增加,应该先解释未就绪;如果确实重启了,需要核对退出原因,不能仅因为存在 liveness 就把所有重启归因于探针。

1
2
kubectl --context kind-deep-dive -n dd-probes describe pod app
kubectl --context kind-deep-dive -n dd-probes logs app --previous

实验代码与使用说明见 k8sdev/deep-dive-probes。

#Kubernetes #Kind #Troubleshooting