【K8s运维实战】Kubernetes 容器健康检查三探针实战:从原理到生产配置避坑指南

先说个真实故事

凌晨 2 点,某金融公司监控报警——支付服务异常中断。几分钟内多个微服务进入 CrashLoopBackOff 状态,业务全面瘫痪。

事后复盘发现根因就一句话:存活探针配得太敏感了

高流量期间,服务响应稍微慢了 1 秒,存活探针超时→kubelet 重启容器→重启过程中更多请求失败→雪崩。

这不是段子。CrashLoopBackOff 占了 Kubernetes 支持工单的 40%,而其中相当部分是探针配置问题。我干运维这十几年,半夜被叫起来处理这类事故的次数两只手数不过来。

所以今天把 Kubernetes 三种探针的原理、配置方法和避坑指南彻底讲清楚。


三种探针,各司其职

Kubernetes 的探针(Probe)是 kubelet 对容器定期执行的诊断操作。三种探针各有各的活,千万别搞混用途

Liveness Probe(存活探针)—— “活着吗?”

决定要不要重启容器

比如应用死锁了——进程还在跑,但卡住不动了。存活探针发现了,kubelet 就把容器杀了重建。

关键理解:存活探针失败 = “这进程彻底废了,得重启”。不是“暂时有点忙”。

Readiness Probe(就绪探针)—— “能接客吗?”

决定要不要把流量切过来

应用启动需要加载配置、连数据库、暖缓存,这段时间虽然进程在跑,但还没准备好服务。就绪探针失败时,Pod 的 IP 会从 Service 的 EndpointSlice 中移除,流量不会打过来。

关键理解:就绪探针失败 ≠ 重启,只是暂时“靠边站”。

Startup Probe(启动探针)—— “启动完了吗?”

解决 “慢启动被误杀” 的问题。

有些 Java/Spring Boot 应用启动要一两分钟。如果只配了存活探针,kubelet 在 initialDelaySeconds 之后就开始检查,此时应用还没起来→探针失败→容器被重启→永远起不来。

启动探针的作用是:只要启动探针还没成功,存活探针和就绪探针就不干活

三种探针的执行顺序:startupProbe(启动时)→ 成功后 → livenessProbe + readinessProbe(持续运行)。


搞清楚三种探针各自管什么之后,接下来看怎么“检查”——Kubernetes 为所有探针统一提供了四种检查手段,按需选用即可。

探针的四种实现方式

不管哪种探针,都支持四种检查方式:

方式

适用场景

关键配置

exec

容器内有检查脚本或命令

command 数组,返回 0 表示成功

httpGet

提供 HTTP/HTTPS 服务的应用

pathportschemehttpHeaders

tcpSocket

只关心端口是否监听的场景

port

grpc

gRPC 服务(Kubernetes v1.24 引入 Beta,v1.27 正式 GA

portservice

生产环境最常用的是 httpGet——让应用自己暴露 /healthz/ready 端点,由应用决定自己是否健康。


生产级配置实战

公共参数说明

所有探针共享以下参数:

参数

默认值

说明

initialDelaySeconds

0

容器启动后多久开始探测(注意:存在 startupProbe 时行为有变,见下方警告

periodSeconds

10

每隔多少秒探测一次

timeoutSeconds

1

探测超时时间

successThreshold

1

从失败变成功所需连续成功次数。对于 livenessProbe 和 startupProbe,此值必须为 1(API 强制约束);readinessProbe 可设为 >1(比如在依赖不稳定时避免状态频繁抖动)

failureThreshold

3

判定为失败所需的连续失败次数

⚠️ 关键行为差异(initialDelaySeconds 的隐藏逻辑)
如果 Pod 中定义了 startupProbe,则 livenessProbereadinessProbe 的探测动作会等到 startupProbe 成功之后才开始执行,但 initialDelaySeconds 本身仍从容器启动时刻开始计时。这意味着实际首次探测时间 = startupProbe 耗时 + initialDelaySeconds。如果没有定义 startupProbe,则 initialDelaySeconds 从容器启动时刻开始计时,首次探测就在这个时间点发生。两者行为不同,配置时务必区分。

我见过有人同时配了启动探针又把存活探针的 initialDelaySeconds 设成 120 秒,以为双保险,实际上那 120 秒是从容器启动开始计时的,而探测动作要等启动探针成功后才触发。最终首次探测远晚于他的预期——完全不是他想要的效果。

完整配置示例

下面是一个生产级 Deployment 配置,涵盖三种探针:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  labels:
    app: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: app
        image: my-app:1.0.0
        ports:
        - containerPort: 8080
        # ---------- 启动探针 ----------
        startupProbe:
          httpGet:
            path: /healthz/startup   # 专用于启动检查的端点
            port: 8080
          initialDelaySeconds: 10     # 可选项,主要用于规避极端情况(如容器瞬间启动但端口尚未监听)
          periodSeconds: 5            # 核心启动窗口靠下面这个 failureThreshold 控制
          failureThreshold: 30        # 30 * 5 = 150 秒启动窗口
          timeoutSeconds: 5
        # ---------- 存活探针 ----------
        livenessProbe:
          httpGet:
            path: /healthz/liveness   # 轻量级端点,只检查进程是否卡死
            port: 8080
          periodSeconds: 30           # 别太频繁
          timeoutSeconds: 5
          failureThreshold: 3         # 连续 3 次失败才重启(90 秒)
        # ---------- 就绪探针 ----------
        readinessProbe:
          httpGet:
            path: /healthz/readiness  # 检查依赖是否就绪
            port: 8080
          periodSeconds: 10           # 可以频繁一点
          timeoutSeconds: 5
          failureThreshold: 3

参数调优的核心公式

启动探针的容忍时间 = failureThreshold × periodSeconds

上面的例子里:30 × 5 = 150 秒,给应用 150 秒完成启动。如果你的应用启动需要 60 秒,设 failureThreshold: 20periodSeconds: 5 就是 100 秒窗口,留点余量。

我的经验公式

  • 启动探针容忍时间 = 应用正常启动时间 × 1.5
  • 存活探针容忍时间 = 应用正常响应 P95 延迟 × 2 × failureThreshold

验证方法

1. 查看 Pod 状态

kubectl get pods

正常状态应该是 RunningRESTARTS 列应该为 0 或稳定不变。

2. 查看探针事件(附“怎么看”解读)

kubectl describe pod <pod-name>

输出中会包含探针相关的 Events。如果探针失败,会看到类似:

Warning  Unhealthy  10s  kubelet  Liveness probe failed: HTTP probe failed with status code: 500

怎么看:事件里会明确标明是 LivenessReadiness 还是 Startup。如果你看到 Readiness 频繁失败但 Liveness 正常,说明应用依赖有问题但容器没被重启——这是预期行为,说明探针在正确工作,不要慌,去修依赖就行。反之如果 Liveness 失败,容器会被重启,你得赶紧看日志。

3. 查看容器日志

kubectl logs <pod-name> --previous  # 查看上一次崩溃的日志

4. 实时监控探针状态

kubectl get pod <pod-name> -o yaml | grep -A 10 -E "livenessProbe|readinessProbe|startupProbe"

我踩过的五个坑

坑一:存活探针检查外部依赖

错误做法:存活探针去检查数据库、Redis、消息队列是否可达。

livenessProbe:
  httpGet:
    path: /healthz  # 这个端点会检查 DB、Redis、MQ...

后果:数据库维护期间短暂不可用→所有 Pod 存活探针同时失败→kubelet 重启所有容器→雪崩。

正确做法:存活探针只检查进程自身是否卡死(如死锁检测);就绪探针才检查外部依赖。

livenessProbe:
  httpGet:
    path: /healthz/liveness   # 只检查本进程状态
readinessProbe:
  httpGet:
    path: /healthz/readiness  # 检查 DB、缓存等依赖

官方文档明确警告:“Liveness probes must be configured carefully to ensure that they truly indicate unrecoverable application failure”。外部依赖暂时不可用是可恢复的,不该触发重启。

官方文档还特别指出,存活探针实现不当会导致级联故障:在高负载下容器被重启、客户端请求失败、剩余 Pod 负载加重——“Incorrect implementation of liveness probes can lead to cascading failures. This results in restarting of container under high load; failed client requests as your application became less scalable; and increased workload on remaining pods due to some failed pods.” 这段话我每次给团队培训都会念一遍,不是教条,是血泪教训。

坑二:慢启动应用没有配启动探针

错误做法:只配存活探针,initialDelaySeconds 设了个固定值(比如 30 秒)。

livenessProbe:
  initialDelaySeconds: 30  # 假设 30 秒一定能起来

后果:某次发布后启动变慢到 45 秒→存活探针在第 30 秒开始检查→失败→kubelet 在第 60 秒重启→永远起不来。

正确做法:用启动探针,不要依赖 initialDelaySeconds

startupProbe:
  httpGet:
    path: /healthz/startup
    port: 8080
  failureThreshold: 30
  periodSeconds: 5   # 最多等 150 秒

坑三:存活探针和就绪探针用同一个端点

错误做法

livenessProbe:
  httpGet:
    path: /healthz
readinessProbe:
  httpGet:
    path: /healthz   # 同一个端点

后果:应用启动过程中,/healthz 返回 503→存活探针失败→容器被重启。应用永远启动不起来。

正确做法:区分端点语义——

  • /healthz/liveness:只返回 200(进程活着就返回 200)
  • /healthz/readiness:依赖就绪才返回 200,否则返回 503
  • /healthz/startup:启动完成才返回 200

坑四(隐藏款):terminationGracePeriodSeconds 太长,导致重启“不果断”

探针失败了,kubelet 不会直接 SIGKILL,而是先发 SIGTERM,等待 terminationGracePeriodSeconds(默认 30 秒)再强杀。

如果你的应用捕获了 SIGTERM 但清理逻辑很慢(比如在等数据库连接池关闭、在等 HTTP 长连接排干),这 30 秒会被计入“从探针失败到容器真正重启”的时间。在高频探测场景下,这段时间内新请求依然可能被路由过来(尤其是就绪探针还没来得及更新),造成间歇性 5xx。

我的做法:如果你的应用对 SIGTERM 没有复杂的状态清理需求,可以酌情调小这个值:

spec:
  terminationGracePeriodSeconds: 10   # 默认 30,按需调短

注意:调短的前提是你确认业务能接受这么短的优雅终止时间。有状态应用(如正在处理事务的 worker)请谨慎。

坑五(隐藏款):探针超时被 CPU 节流(Throttling)坑了

这个坑我排查了整整一个下午。

现象是:/healthz 逻辑极简单(只读一个内存标志位),timeoutSeconds 已经设到了 5 秒,但还是偶发超时。查了一圈发现根因是 CPU Limit 设得太小

当容器的 CPU Limit 较低且流量上来时,CFS 会对容器进行节流(Throttling)。此时你的 /healthz 请求虽然逻辑简单,但可能因为线程被调度出去,实际响应时间远超 timeoutSeconds

解决方法

  1. 先用 kubectl top pod 看实时 CPU 使用率
  2. 检查监控指标里的 container_cpu_cfs_throttled_seconds_total,如果这个值在持续增长,说明 Limit 太紧了
  3. 适当调大 CPU Limit,或者把 /healthz/liveness 端点做得更轻量(比如不经过业务线程池,直接在 Netty/框架层返回)

这个不算探针配置错误,但它会导致“探针配置看起来完全正确,但就是偶尔重启”的玄学现象。希望你不用像我一样花一下午才找到它。


总结

五个核心要点收个尾:

  1. 三探针各司其职:liveness = 重启决策,readiness = 流量决策,startup = 保护慢启动。别混用
  2. 存活探针只查进程自身,外部依赖交给就绪探针——这是防止雪崩的关键。
  3. 慢启动应用必须配启动探针,别依赖 initialDelaySeconds 硬编码。
  4. 探针失败了不是秒杀terminationGracePeriodSeconds 会影响重启速度,按需调优。
  5. 探针超时不一定是你配置的事,检查 CPU Limit 是否过小导致节流。

最后送大家一句话:探针配得好,半夜睡得好;探针配得烂,凌晨三点见。

如果觉得有用,欢迎分享给更多正在被探针折磨的同事。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

运维老郭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值