探针(Probes)决定了:

  • Pod 是否 接收流量(readiness)
  • 容器是否会被 自动重启(liveness)
  • 启动慢的服务是否会被 过早判死(startup)

很多“发布一上来就抖/经常重启”的问题,根因常在探针语义用错。

先看故障长什么样

探针问题很少以“探针配置错了”这个名字出现。它通常伪装成下面几类事故:

  • 发布后新 Pod 的 EndpointSlice 条目长期不是 ready=true,Service 没有可用后端。
  • 应用启动慢,被 liveness 提前杀掉,形成重启风暴。
  • 下游依赖短暂抖动,liveness 跟着失败,把本来可恢复的问题放大成集群级重启。
  • readiness 过宽,Pod 还没准备好就接流量;readiness 过严,Pod 明明可用却长期被摘除。

排查时先问一个问题:这个检查到底是在回答“能不能接流量”,还是“该不该重启”?如果这两个问题由同一个接口回答,后面大概率会出事。

最常见的误判

  • 把 liveness 当流量开关,结果依赖抖动时把容器杀掉。
  • 用很深的业务链路做 readiness,导致外部系统抖动时本服务也被摘除。
  • 用 initialDelaySeconds 硬猜启动时间,而不是用 startupProbe 保护慢启动。
  • 只看 Pod Running,不看 readiness condition 和 endpoints。

心智模型(最重要的三句话)

  • Readiness:现在适合接流量吗?
  • Liveness:进程是不是卡死/不可恢复,需要重启吗?
  • Startup:启动期给我额外时间,先别用 liveness 折腾我。

最常见的坑:用 liveness 充当“流量开关”

如果你把 liveness 当成“没 ready 就死给你看”,你会得到:

  • 还在 warmup 或迁移的 Pod 被不断重启
  • 发布速度变慢,错误率变高
  • 甚至形成“重启风暴”

正确做法:

  • 用 readiness 把 EndpointSlice 中的端点标记为未就绪,使常规 Service 流量不再选择它
  • 用 startupProbe 保护启动期
  • 用 liveness 只处理“重启能解决的卡死/异常”

一个靠谱的默认配置(HTTP 服务)

startupProbe:
  httpGet:
    path: /startupz
    port: 8080
  failureThreshold: 30
  periodSeconds: 2
  timeoutSeconds: 2

readinessProbe:
  httpGet:
    path: /readyz
    port: 8080
  periodSeconds: 5
  timeoutSeconds: 2
  failureThreshold: 3

livenessProbe:
  httpGet:
    path: /livez
    port: 8080
  periodSeconds: 10
  timeoutSeconds: 2
  failureThreshold: 3

这套配置的核心是:启动期先过 startup,再谈 liveness;流量交给 readiness 控制。

/readyz、/livez 应该检查什么

/readyz:能否正确服务请求(“现在”)

readiness 应该在以下场景失败:

  • 依赖还没连上(DB、缓存、上游服务)
  • 迁移未完成(但你又不希望接流量)
  • 预热未完成(关键缓存/配置未加载)

readiness 的本质是“业务可用性”,不是“进程是否活着”。

/livez:进程是否卡死/不可恢复(“重启能救”)

liveness 适合检查:

  • 死锁/事件循环卡死
  • 内部状态不可恢复(严重异常)

不推荐:把 DB 可用性作为 liveness 的硬条件。DB 抖一下你就把所有 Pod 重启一遍,往往会把事故扩大。

探针类型:HTTP / TCP / gRPC / exec(用得越简单越好)

Kubernetes 支持:

  • httpGet:最推荐(语义清晰、可观测)
  • tcpSocket:只检查端口是否能连(语义弱)
  • grpc:调用标准 gRPC Health Checking Protocol
  • exec:在容器里跑命令(灵活但可能昂贵)

优先 HTTP 探针

Web 服务尽量暴露小而快的检查接口:

  • /livez:进程是否健康
  • /readyz:现在能否接流量

readiness 检查如果偶尔要跑 5-10 秒,通常是把过深的依赖检查塞进了探针。

TCP 探针什么时候用

它只能证明“端口能建立连接”,不能证明“请求会成功”。简单代理或没有 HTTP 接口的旧系统可以用,但要接受它的语义更弱。

gRPC 探针什么时候用

gRPC 探针从 Kubernetes 1.27 起进入 Stable,适合已经实现标准 gRPC Health Checking Protocol 的服务:

readinessProbe:
  grpc:
    port: 50051
    service: readiness
  periodSeconds: 5
  timeoutSeconds: 2

port 必须写数字,不能使用命名端口。基础模式使用明文连接;如果服务只开放 TLS,先核对当前集群版本与对应 feature gate,不要直接照搬 HTTP 探针配置。

exec 探针什么时候用

exec 可以检查文件、socket、进程状态,但它:

  • 在容器内执行;
  • 可能较慢;
  • 每个 Pod、每几秒执行一次,规模变大后会带来额外负载。

如果一定要用,命令必须轻量、快速。

startupProbe 比 initialDelaySeconds 更靠谱

传统做法用 initialDelaySeconds 延迟探针启动,但缺点明显:

  • 延迟是固定值,启动快也要等
  • 启动慢超过延迟就被误判

startupProbe 更智能:

  • 在 startupProbe 成功之前,liveness 和 readiness 都不会开始
  • 启动快就快通过,启动慢就给时间

实用换算:

最长允许启动时间 ≈ failureThreshold * periodSeconds

例如希望给 60 秒启动窗口,可以:

  • periodSeconds: 2
  • failureThreshold: 30

发布友好视角(Mermaid)

flowchart LR A[Pod 启动] --> B{startupProbe 通过?} B -- 否 --> A B -- 是 --> C{readiness 通过?} C -- 否 --> D[不接流量] C -- 是 --> E[接收流量] E --> F{liveness 通过?} F -- 否 --> A F -- 是 --> E

Readiness 在发布中到底影响什么

Deployment 滚动更新时:

  • 新 Pod 启动;
  • readiness 决定 EndpointSlice 端点什么时候变为 ready=true;
  • 旧 Pod 按策略终止。

如果 readiness 太宽:配置还没加载、缓存还没预热、迁移还没完成,Pod 就开始接请求,于是“rollout succeeded”但错误率上升。如果 readiness 太严:DB 抖 1 秒就把所有新 Pod 摘掉,发布卡住,甚至形成级联故障。

一个更稳的边界是:readiness 表示“现在能正确处理请求”,允许短时依赖抖动;liveness 只判断“重启是否有帮助”。

与优雅下线配合(否则探针也救不了)

探针只决定“是否在 endpoints 里”,但如果 Pod 退出太粗暴,还是会有错误。

建议:

  • 合理设置 terminationGracePeriodSeconds
  • 收到 SIGTERM 立即让 readiness 失败(快速摘流量)
  • 等待 in-flight 请求完成后再退出
  • 必要时加 preStop 做 drain/flush

调参避免抖动(timeout/threshold/period)

常见问题:

  • timeoutSeconds 太小(真实 p99 可能 > 1s)
  • periodSeconds 太密(探针负担大,反而影响业务)
  • failureThreshold 太小(一次抖动就摘流量/重启)

实践经验:

  • readiness:period 5s、timeout 1-2s、failureThreshold 3
  • liveness:period 10s、timeout 1-2s、failureThreshold 3
  • startup:按启动最长时间换算

Liveness 别绑定外部依赖

这一点值得反复强调:如果 DB 一抖,liveness 就失败,你会把一次依赖故障放大成全集群重启风暴。

更合理的分工是:

  • readiness 在依赖不可用时摘流量;
  • liveness 只在进程真的卡死或不可恢复时失败。

排查探针失败(必备命令)

先看 endpoints,再看 Pod 状态:

kubectl get endpointslice -n <ns> -l kubernetes.io/service-name=<svc> -o wide
kubectl describe pod -n <ns> <pod>
kubectl logs -n <ns> <pod> -c <container> --previous
kubectl get events -n <ns> --sort-by=.lastTimestamp

在容器内部验证:

kubectl exec -n <ns> -it <pod> -- sh
curl -sS -i http://127.0.0.1:8080/readyz

HTTP、TCP 和 gRPC 探针由 kubelet 直接访问 Pod IP,不经过 Service。Pod 内 curl 127.0.0.1 成功只能证明处理器可用;如果探针仍失败,继续检查应用是否只监听 loopback、端口与路径是否一致,以及基于 Host 的路由是否接受 kubelet 请求。

如果镜像是 distroless 没有工具,用 ephemeral container(kubectl debug)来跑 curl 更安全。

可观测性建议:让 readiness 失败有“原因”

即使探针只看状态码,你也应该在服务内部记录:

  • 哪个依赖导致 readiness 失败
  • 失败次数与持续时间
  • 失败发生在发布/扩容/依赖抖动的哪个阶段

如果 /readyz 是组合检查(缓存、DB、队列),建议返回带 reason code 的 JSON。探针虽然只看状态码,但运维和监控能立刻看到具体原因。

这样你才能把“探针失败”从现象变成可行动的根因。

上线前自检

  • 启动慢的服务启用 startupProbe
  • readiness 控制接流量,liveness 控制重启
  • liveness 不依赖外部依赖(避免重启风暴)
  • timeouts/thresholds/period 贴近真实延迟分布
  • 下线流程与 readiness 配合(优雅摘流量)

探针配置的三个判断

Q: readiness 和 liveness 要用同一个接口吗? A: 通常不建议。readiness 关注依赖与可接流量,liveness 只需要快速判断是否“卡死”。

Q: 启动很慢怎么办? A: 用 startupProbe,或提高 failureThreshold/period,别用很长的 initialDelaySeconds。

Q: readiness 失败会重启吗? A: 不会。只有 liveness 失败才会触发重启。

参考链接