1. 先搞清楚 Kubernetes 健康检查到底在检查什么
1.1 健康检查不是“应用自检”,而是 kubelet 的外部视角
做 Kubernetes 运维这几年,“健康检查”这个词我几乎每天都在碰。很多刚上手的朋友会把探针当成“容器内的一个定时器”,以为只要写了 liveness 就能保证服务不出事,结果生产上一个误杀就把整条链路打挂了。这篇文章不吃理论大餐,直接讲 Kubernetes 健康检查的三种探针怎么配、参数怎么推导、常见翻车场景怎么排查。
首先要纠正一个认知:健康检查是 kubelet 从宿主机视角对容器做“外部探测”,不是应用自己往控制面报告“我还活着”。你在 Pod 里写一个脚本检测自身状态,那只是应用逻辑,Kubernetes 是不认的。kubelet 会按照你配置的探针,定期发起 HTTP 请求、TCP 连接或者 exec 命令,根据结果决定这个容器的生死和流量归属。
这个外部视角非常重要。容器进程存在不代表业务就绪,端口监听不代表依赖可用。我见过很多案例:Java 进程还在,但线程池耗尽、数据库连接池打满,接口全部超时;这时候从容器层面看,PID 是活的,CPU 也正常,但用户请求已经全挂在等待队列里。如果没有 Readiness 探针把这些异常实例从负载均衡端点里摘掉,流量会继续灌进去,雪崩就是这么一步步扩大的。
健康检查解决的是两个完全不同的目标:一是“要不要继续给它发流量”,二是“出问题了要不要把它重启掉”。这两个目标如果混在一个探针里,后果往往很严重——要么流量打到坏实例上,要么一个瞬时抖动触发大规模重启。
1.2 三类探针怎么分工
Kubernetes 里内置了三类探针:livenessProbe、readinessProbe、startupProbe。名字很直白,但很多人栽在“以为字面意思就是全部”上。
livenessProbe 管存活,失败后 kubelet 会杀掉容器并按 restartPolicy 决定是否重启。这是自愈机制,适合兜底死锁、内存泄漏导致的进程僵死。readinessProbe 管就绪,失败后 Pod 会被标记为 NotReady,然后从 Service 的 Endpoints 中剔除,但它不会被重启。startupProbe 是后来加的保护机制,专门对付启动特别慢的容器,在它成功之前,liveness 和 readiness 都会被暂时禁掉。
用一个表格看清楚:
| 探针类型 | 失败后果 | 适用场景 | 核心关键词 |
|---|---|---|---|
| livenessProbe | 容器被 kill、可能重启 | 进程死锁、业务僵死、内存泄漏无法自愈 | 要不要活 |
| readinessProbe | 从 Service Endpoints 摘除,不重启 | 依赖未就绪、流量过载、启动未完成 | 要不要接流量 |
| startupProbe | 容器被 kill、可能重启 | 老应用启动特慢、liveness 误杀启动过程 | 等它慢慢起 |
注意一个细节:liveness 失败时,kubelet 通常只重启“容器”,不一定是整个 Pod。具体取决于 restartPolicy,如果策略是 Always,容器会在同一个 Pod 内重新拉起,restartCount 加 1。如果策略是 Never,容器不会被重启,Pod 会进入 Failed。这个差异在排查“Pod 一直 Running 但容器不停重启”的时候很关键。
我个人的理解是:startupProbe 是给 livenessProbe 上的一道保险。很多老旧应用从进程启动到真正可服务需要 40 秒甚至 2 分钟,如果 liveness 从容器启动那一刻就开始算,极大概率在应用还没起来时就触发重启,形成 CrashLoopBackOff。有了 startup 探针,你只需要告诉 kubelet:“先给它 XX 秒,别急着杀它。” 等它起来了,再把“存活判断权”交还给 liveness。
2. 探针配置:参数含义与“踩坑”阈值计算
2.1 五个标准参数逐个拆解
所有探针都有一组共同的配置参数:initialDelaySeconds、periodSeconds、timeoutSeconds、failureThreshold、successThreshold。刚接触的人往往只关心 path 和 port,把后面这些默认值当成空气,实际上这才是最容易埋雷的地方。
initialDelaySeconds 表示容器启动之后,等多少秒才开始第一次探测。期间 kubelet 不执行任何探针,给应用留出“进程初始化”的时间。periodSeconds 是两次探测之间的间隔,默认 10 秒,最小可以设成 1 秒,但我不建议小于 3 秒。timeoutSeconds 是每次探测的超时时间,默认只有 1 秒,这个值在很多业务系统里根本不够用,接口响应只要超过 1 秒就会被判定为失败,这一点我后面会专门讲。
failureThreshold 是连续失败多少次之后才认定探针失败。默认 3 次,看的是“连续失败”,只要中间成功一次,计数器就会清零。successThreshold 正好相反,连续成功多少次才认为探针通过,默认 1 次。Readiness 探针的 successThreshold 可以设置成 2 或 3,用来防止“刚 Ready 又被打掉”的抖动;但 liveness 和 startup 探针的 successThreshold 必须保持 1,这是硬限制。
我见过的生产事故里,至少有三分之一和“默认值直接上线”有关。默认 periodSeconds=10、failureThreshold=3,意味着一次失败判定至少要经过约 20 到 30 秒窗口,如果业务调用方超时只有 2 秒,这个恢复速度根本不够看。反过来,如果你把一个纯 liveness 探针设成 period=1、failureThreshold=1,那就是在制造不稳定——稍微抖一次就把容器杀了。
2.2 一次完整重启周期推导
探针的失败判定时间是有公式的,但是网上很多文章没说透。最坏情况的下,持续失败到判定失败的时间大约是:
初始等待时间 + (连续失败次数 - 1) * 探测周期
也就是说,如果一个探针配置是initialDelaySeconds: 10, periodSeconds: 10, failureThreshold: 3,那么最坏情况下,容器启动后 10 秒开始第一次探测,20 秒第二次,30 秒第三次,第三次失败后 kubelet 认定探针失败。这里的“判定失败时刻”是 10 + (3-1)*10 = 30 秒。
举例来说,一个 Java 服务进程起来大约需要 20 秒,Spring 上下文加载完要 45 秒。如果你只配了 liveness:
livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 10 periodSeconds: 10 failureThreshold: 3按公式算,第 3 次失败发生在 30 秒左右,可应用要到 45 秒才起来,这必然触发重启。容器被杀了之后又会重新启动,30 秒后又杀,形成一个永远起不来的循环。这就是“启动太慢导致 CrashLoopBackOff”的典型原因。
正确的做法是引入 startupProbe,让它来覆盖启动窗口。比如:
startupProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 0 periodSeconds: 5 failureThreshold: 12这个 startup 探针在容器启动后立刻开始探测,每 5 秒一次,连续 12 次失败才会判定失败,也就是最多给容器约 55 秒的启动时间。如果应用在 45 秒就绪,那 startup 就能成功,随后 liveness 才正式接管。
这里有个很多人都忽略的时序问题:startup 探针成功之前,readiness 和 liveness 都会被禁用;startup 成功之后,它们才开始执行。所以你在配 liveness 的时候,initialDelaySeconds 不要比 startup 窗口大太多,一般只要保证“在 startup 成功之后,liveness 还能有至少一到两个周期的缓冲”就够了。
2.3 参数设置的“宁可长一点,也别过早下结论”
探针参数的调优原则,我一直跟团队说的是:宁可长一点,也别过早下结论。这个“长”是有上限的,不是无脑大。
如果是普通无状态 HTTP 服务,我通常这样定初始值:
- readiness:initialDelaySeconds=10,periodSeconds=5,timeoutSeconds=2,failureThreshold=3,successThreshold=1
- liveness:initialDelaySeconds=30,periodSeconds=10,timeoutSeconds=2,failureThreshold=3
- startup:initialDelaySeconds=0,periodSeconds=5,timeoutSeconds=2,failureThreshold=12
遇到老系统、依赖多、启动慢的,startup 的 failureThreshold 直接给到 20 甚至 30。比如一个应用需要 90 秒才能就绪,period=5、failureThreshold=20,窗口大约是 (20-1)*5 = 95 秒,够用了。注意不要把启动等待全压在 initialDelay 上,因为 initialDelay 只影响探针开始时间,不参与失败计数逻辑;startup 才是更合理的“启动保护伞”。
还有一点,periodSeconds 不宜追求过分激进。readiness 设成 5 秒可以接受,liveness 我建议至少 10 秒。因为 liveness 每次失败都可能导致重启,周期太短会把一次瞬时抖动放大成全量重启;而 readiness 失败只是摘流量,可以更敏感一点,但也要配合 failureThreshold 和 successThreshold,避免频繁抖动。
3. 三种探针实现的具体写法与适用场景
3.1 httpGet 探针:最常用,但路径和状态码要盯紧
绝大多数 Web 服务都用 httpGet 探针,因为它最直观:kubelet 定期向容器内某个 HTTP 接口发起 GET 请求,只要返回的状态码在 200 到 399 之间就视为成功,否则视为失败。也就是说 4xx 和 5xx 都算失败。
写起来很简单:
livenessProbe: httpGet: path: /healthz/live port: 8080 httpHeaders: - name: X-K8s-Probe-Token value: "some-static-token" initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 2 failureThreshold: 3这里有几个坑必须说清楚。
第一,探针是从宿主机发起的,不是从 Pod 内部发起的。如果应用只监听了 127.0.0.1,httpGet 探针会失败,因为 kubelet 访问的是 Pod IP。应用监听地址要写成 0.0.0.0 或者 Pod IP。这一点在网络插件和防火墙策略复杂的集群里特别容易踩。
第二,健康检查接口要轻量。别在/healthz里做全量数据库查询、清缓存、跑定时任务,否则一次探针请求就可能把实例拖垮。liveness 的职责是“进程还活着”,readiness 的职责是“依赖基本可用”,都不适合做重型检查。
第三,timeoutSeconds 要大于接口的真实响应时间。很多开发同学默认把健康检查接口写成“即时返回”,但实际上应用在高负载下,一个健康检查请求也可能排队 1 到 2 秒。如果 timeoutSeconds 保持默认 1 秒,高峰期探针就会连续失败,触发误判。我一般在生产环境至少给到 2 秒,复杂的给到 3 到 5 秒。
第四,健康检查接口如果需要登录或者鉴权,要么用 httpHeaders 把 token 显式带上,要么做成无需鉴权的独立端点。我见过有团队把健康检查放到需要 SSO 登录的路径后面,探针全部 302 跳转,最后一堆 Pod 从 Endpoints 里消失,业务直接中断。
3.2 tcpSocket 探针:只通端口不等于业务可用
tcpSocket 探针做的事情更底层:kubelet 尝试与容器指定端口建立 TCP 连接,只要能连上就判定成功,连不上就失败。
readinessProbe: tcpSocket: port: 3306 initialDelaySeconds: 5 periodSeconds: 10 timeoutSeconds: 2 failureThreshold: 3tcpSocket 的适用场景很明确:非 HTTP 协议的服务,比如 MySQL、Redis、消息队列、自研 TCP 网关。它不需要应用实现一个专门的健康检查协议,只要端口能监听起来就行。
但它的局限也非常明显:端口通,不等于业务可用。一个服务可能 TCP 三次握手能成功,但内部线程池已经打满,新的连接虽然建立了却得不到响应。另一个常见例子是:代理服务监听端口正常,但后端的真实服务已经挂了,TCP 探针仍然显示成功。
所以 tcpSocket 更适合作为“进程是否在监听”的兜底探针,而不是业务质量探针。如果你的服务既能用 HTTP 探针,就尽量用 HTTP 探针;实在只能 TCP,也要接受它的判定粒度比较粗。
3.3 exec 探针:适合容器内没有 HTTP 服务的场景
exec 探针是在容器内执行一条命令,通过退出码判断结果:0 表示成功,非 0 表示失败。
livenessProbe: exec: command: - /bin/sh - -c - pgrep -f myapp || exit 1 initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3exec 探针最典型的用处是:容器里的主程序不是 Web 服务,或者不方便暴露 HTTP 健康端点。比如一个后台任务进程、一个消息消费 worker,你想知道它是否还在运行,可以用 pgrep 或者检查 PID 文件。
但 exec 探针有三个非常容易踩的坑。
第一个是性能开销。每次探测都要在容器内 fork 一个进程,如果反复执行复杂 shell 脚本,对节点 CPU 和容器文件系统都有压力。健康检查频率越高,开销越明显。所以 exec 探针的 periodSeconds 不建议低于 10 秒,command 也要尽量短。
第二个是 shell 的退出码陷阱。很多同学写成/bin/sh -c "pgrep myapp && exit 0",或者干脆写pgrep myapp,但没注意到sh -c返回的是最后一个命令的退出码,有时候最后一条 pipeline 成功了,前面核心判断却失败了,探针结果完全偏离预期。
第三个是“进程在但业务死”的问题。exec 探针通常只能判断进程是否存在,判断不了线程池是否可用、依赖是否健康。如果你的 worker 进程陷入了死循环但还没退出,exec 探针照样显示成功。这种情况还得靠业务层自己上报心跳。
3.4 探针类型怎么选:先看进程在不在,再看业务通不通
我给团队定的选型逻辑很简单:第一层确认进程在不在,第二层确认业务通不通。
| 应用类型 | 推荐 readiness | 推荐 liveness | 推荐 startup |
|---|---|---|---|
| Web/HTTP 服务 | httpGet/healthz/ready | httpGet/healthz/live | httpGet/healthz/live |
| TCP/MySQL/Redis 类 | tcpSocket 端口 | tcpSocket 端口 或 exec | 根据启动速度而定 |
| 后台 Worker/Consumer | exec 或业务心跳接口 | exec 判断进程存活 | 启动慢则加 startup |
| 短时任务/批处理 | 不建议配 readiness | 通常不配 liveness | 一般不配 startup |
还有一个容易忽略的组合:一个容器可以同时配置三个探针,也可以只配其中两个。比如纯批处理任务,跑完就退出,如果你配了 liveness,任务退出时 liveness 会判定失败,然后容器被重启,业务逻辑重新执行一遍,这很可能是灾难。短时任务的容器通常不需要探针,或者只需要在任务入口处做 readiness 控制,让 Job 的调度逻辑来判断。
4. 从零配置一套健康检查:完整实战流程
4.1 先确认业务健康检查点
很多团队做健康检查是在部署文件里抄一段模板,根本没有想清楚健康检查接口要长什么样。我的习惯是:先让应用侧把健康检查端点拆分出来,再写 Kubernetes 配置。
一个比较稳的分法是两个端点:
/healthz/live:只检查进程是否活着、主线程是否没有死锁。它不依赖数据库、Redis、消息队列等外部组件。这个端点给 liveness 和 startup 用。/healthz/ready:检查当前实例是否具备服务能力,比如数据库是否能连、缓存是否能读写、关键依赖是否可用。这个端点给 readiness 用。
为什么要分开?因为 liveness 失败会导致容器重启,如果把数据库检查放进去,数据库一抖动,所有实例就可能同时被重启,这是最典型的雪崩诱因。数据库恢复之后,服务还得重新冷启动,恢复时间反而更长。
你可以把/healthz/ready设计成“容忍短时抖动”的检查:依赖出问题时可以延迟判断,而不是一失败立刻返回 500。例如连续 3 次从数据库读取失败才标记依赖不健康,单次失败不要立刻影响 readiness。
4.2 一份可以直接改的 Deployment YAML
下面是一份我常用的示例配置,你可以直接套用:
apiVersion: apps/v1 kind: Deployment metadata: name: demo-app spec: replicas: 3 selector: matchLabels: app: demo-app template: metadata: labels: app: demo-app spec: containers: - name: demo-app image: registry.example.com/demo-app:v1.0.0 ports: - containerPort: 8080 resources: requests: cpu: 100m memory: 256Mi startupProbe: httpGet: path: /healthz/live port: 8080 initialDelaySeconds: 0 periodSeconds: 5 timeoutSeconds: 2 failureThreshold: 12 readinessProbe: httpGet: path: /healthz/ready port: 8080 initialDelaySeconds: 10 periodSeconds: 5 timeoutSeconds: 2 failureThreshold: 3 successThreshold: 1 livenessProbe: httpGet: path: /healthz/live port: 8080 initialDelaySeconds: 60 periodSeconds: 10 timeoutSeconds: 2 failureThreshold: 3这份配置对应的是一个需要大约 45 秒才能完成启动、之后进入稳定状态的 Java 服务。
startupProbe 的窗口是(12-1)*5 = 55秒,默认放行窗口约 55 秒;如果你的应用需要更长时间,把 failureThreshold 增大,比如 15 次就是 70 秒,20 次就是 95 秒。readinessProbe 每隔 5 秒探测一次,启动完成后如果依赖没就绪,最多连续失败 3 次就把实例从 Endpoints 摘掉。livenessProbe 的 initialDelaySeconds 给了 60 秒,目的是让 startup 保护窗口结束后,liveness 仍然有一段缓冲期,避免刚完成启动就被误杀。
这里的端口不一定要写在 containerPort 里,探针可以直接探测任何容器内可访问的端口,但写出来更清晰,也方便其他配置引用。
4.3 部署后如何验证探针是否生效
写完配置不是结束,验证才是关键。部署之后我会按这样的顺序来检查:
先用kubectl apply发布,然后kubectl get pods -w观察 Pod 状态。正常情况下,Pod 会从 Pending 走到 ContainerCreating,再走到 Running,然后 Ready 列变成1/1。如果 Ready 一直是0/1,说明探针已经判定失败,需要进一步排查。
用kubectl describe pod <pod-name>可以看到探针的详细配置和最近事件。比如事件里出现Unhealthy,后面会跟着探针类型和失败原因;如果是 HTTP 探针失败,会显示HTTP probe failed with statuscode: 500;如果是 exec 失败,会显示退出码或错误信息。这些事件是排错的第一手资料。
之后做两个主动故障演练:
第一个演练是“kill 主进程”。进入容器后把应用主进程杀掉,比如 Java 应用ps -ef | grep java找到 PID,再 kill。如果配置了 liveness,kubelet 会在下一轮探测时发现进程不存在,或者健康检查接口无响应,然后容器被重启。你会在kubectl get pod -o yaml里看到restartCount增加。
第二个演练是“让 readiness 失败”。你可以在容器内临时停掉/healthz/ready所依赖的组件,或者直接改返回值,让 healthz 返回 500。这时 Pod 的状态会变成 NotReady,但容器不会重启。同时用kubectl get endpoints或kubectl get endpointslices观察,这个 Pod 的 IP 会从 Ready 地址列表里消失。等恢复健康后,它又会自动加回来。
我非常建议在测试环境把这两个演练做成自动化用例,每次改完探针配置都跑一遍。这样能避免“上线时探针配错了,但流量少没暴露,到高峰期突然流量全部打挂”的情况。
4.4 滚动更新里的“串行放量”为什么要靠 readiness
Deployment 滚动更新时,探针的作用会被放大。默认情况下,Deployment 会先创建一个新 Pod,等它 Ready 之后再删除旧 Pod。如果新 Pod 一直没有 Ready,滚动更新会卡住,旧 Pod 不会被删,新 Pod 也不会继续扩容。
这其实是保护机制:新版本有问题时,readiness 探针会拦住它上量。但前提是你把 readiness 配对了。如果 readiness 探针只是“端口通就行”,新 Pod 即使业务初始化失败,也会被标记为 Ready,流量照样打进去。
建议给 Deployment 额外配一个minReadySeconds。它不算探针,但和探针是黄金搭档。minReadySeconds表示 Pod 转为 Ready 之后,至少要保持多少秒才算真正可用。比如:
spec: minReadySeconds: 30这个参数可以防止“新 Pod 刚 Ready,进入流量后马上抖动,readiness 又失败,滚动更新中断”的恶性循环。配合 readinessProbe,新 Pod 只有“探针通过 + 稳定运行 30 秒”这两个条件都满足,才会被纳入流量。
maxSurge 和 maxUnavailable 是控制滚动更新速率的,但如果探针配置有问题,调这两个参数只能事倍功半。我的经验是:先把探针调稳,再根据发布速度去调滚动参数。
5. 常见探针翻车现场与排查套路
5.1 场景一:容器疯狂重启,但业务日志没有报错
这是最迷惑人的问题。你去看应用日志,一切正常,连异常堆栈都没有,但 Pod 里的容器一直重启。这时候十有八九是探针把你杀了。
用kubectl describe pod <pod-name>查看事件,通常能看到类似:
Warning Unhealthy 3s Liveness probe failed: HTTP probe failed with statuscode: 500如果看到这条,直接去访问应用的/healthz接口,看看 500 是哪里来的。常见原因有三个:应用监听地址绑定了 localhost,探针访问不到;健康检查路径写错,返回 404 被当成失败;健康检查接口内部依赖某个组件,该组件当前抖动。
还有一种情况是健康检查接口本身响应太慢,超过了 timeoutSeconds。事件里可能显示context deadline exceeded或者timeout。这时把 timeoutSeconds 从 1 秒调到 2 到 3 秒,多半能解决。但如果一个健康检查接口经常需要超过 2 秒才能响应,那这个接口本身就太重了,建议把业务逻辑拆分出去。
5.2 场景二:服务被 Readiness 频繁摘流量,接口超时量增加
某天监控里突然发现服务时不时从 Endpoints 里掉出去,但容器没重启,一看事件全是Readiness probe failed。
先别急着调探针,先想清楚这个实例是不是真的不健康。如果它只是响应慢,readiness 把它摘掉是对的;但如果它只是偶尔一次抖动,摘掉之后流量转移到其他实例,又导致其他实例过载,形成连锁反应,那就说明 readiness 太敏感。
我的调法有几个方向:
一是把 successThreshold 从 1 调到 2。对于新启动的实例,这个参数要求连续两次成功才加入流量,可以避免“刚 Ready 又挂了”的振荡。
二是把 failureThreshold 从 3 调到 5。这样单次网络抖动不会触发摘除,只有持续失败一段时间才判定不健康。
三是检查健康检查端点是否包含了过于严格的依赖检查。比如 readiness 接口一旦查不到 Redis 就返回 500,而 Redis 恰好重启了一次,那所有实例会在同一时间被摘掉,这非常恐怖。
四是结合 minReadySeconds。很多时候不是探针参数问题,而是滚动更新太快,新 Pod 一 Ready 就接流量,瞬间负载上来,readiness 被打掉。加 30 到 60 秒稳定期,问题就会消失。
5.3 场景三:老应用启动特别慢,一上线就 CrashLoopBackOff
这种问题几乎都出在“没有配置 startupProbe 或者配置不当”。老应用启动慢、依赖多,甚至第一次请求前要做一堆初始化,期间健康检查接口要么不响应,要么返回 503。
解决办法是给启动过程一个明确的宽限窗口。比如应用需要 90 秒启动,配置:
startupProbe: httpGet: path: /healthz/live port: 8080 initialDelaySeconds: 0 periodSeconds: 5 timeoutSeconds: 2 failureThreshold: 20这样最坏情况下有(20-1)*5 = 95秒的启动窗口。如果 95 秒还不够,就把 failureThreshold 继续加大,直到覆盖实际启动时间,再留出 20% 到 30% 的余量。
注意一个反面教训:有人为了省事,不给应用配 startupProbe,而是把 livenessProbe 的 failureThreshold 调到 100,并设置一个超长的 initialDelay。这个方法表面上也能让应用启动起来,但代价是:业务真正死锁后,需要很久才能被探测出来并重启,故障恢复时间被严重拉长。启动这件事就交给 startupProbe,liveness 只负责运行时兜底,职责边界要清晰。
5.4 排查工具箱与速查表
探针排错的核心工具链其实就那么几个,整理成速查表:
| 现象 | 可能原因 | 优先检查 |
|---|---|---|
| 容器不断重启,应用日志正常 | liveness/startup 误判 | kubectl describe 查看 Unhealthy 事件 |
| Pod 长时间 NotReady,但容器运行 | readiness 失败 | curl 健康检查端点,观察 Endpoints |
| 探针事件显示 timeout | 接口响应慢 / timeoutSeconds 过小 | 调大 timeoutSeconds,压测响应耗时 |
| 事件无探针告警,容器仍重启 | 进程本身崩溃退出 | kubectl logs --previous,查看退出码 |
| 滚动更新卡住 | 新版本 readiness 一直不通过 | 查看新版 Pod 的健康检查状态 |
| 健康检查请求被拒绝 | 网络插件/探针来源 IP 被安全策略拦截 | 在宿主机手动 curl Pod IP 验证 |
还有两条实用心得。第一,Pod 的/status里会显示 containerStatuses 的 restarts 次数和 lastTerminationReason,这个是确认容器是否被探针杀掉的最快途径。第二,很多 CNI 安全策略可能会拦截 kubelet 到 Pod IP 的探针流量,排查时要记住“探针是从宿主机发起的”,不能只看容器内部。
最后给一个平时不太有人提的小技巧:如果你发现探针事件里频繁出现 Unhealthy,但应用整体表现正常,可以先看看最近有没有集群 Node 的网络或者内核抖动。kubelet 探针发出后如果短时间内收不到响应,也会被判失败,这种情况下应用是“被无辜牵连”的。你可以在 Node 上执行journalctl -u kubelet -f --grep probe,观察是否有大量探测超时,再决定是调探针参数还是优先修基础设施问题。
我个人在实际操作中的体会是:健康检查不是上线前最后一个想到的配置项,而是服务可用性设计的一部分。每个服务从开发第一天就应该把/healthz/live和/healthz/ready两个端点定好,探针参数写进发布变更单里,每次业务改动都要重新过一遍启动时间和慢接口情况。你愿意在这上面多花半小时,后面能少熬好几个凌晨的故障恢复夜。