如果你维护过 Kubernetes 集群大概率遇到过这种诡异情况某个 Pod 状态明明显示 Running 且 Ready但业务方反馈接口一直超时另一个 Pod 看着一切正常却在凌晨毫无征兆地被重启了好几回。查到最后问题往往都出在同一个地方——健康检查探针的配置。Kubernetes 的健康检查机制本身并不复杂文档里的概念十几分钟能看完但真正放到生产环境探针用不好引发的连锁故障能让你在监控屏前耗上一整天。这篇内容不是把官方文档翻译一遍而是我在维护集群和帮业务团队排障过程中沉淀下来的探针配置思路、参数计算逻辑和故障复盘。无论你是在图形界面上点按钮部署还是直接写 Deployment YAML这些经验应该都能帮上忙。1. 三种探针各管一段生命周期别只盯着 liveness 一个很多团队最早接触健康检查抄的第一份 YAML 基本只有 livenessProbe因为看到的效果很直接探针失败、Pod 被杀、重启看起来服务被保护住了。但真实生产里只配 liveness 往往是不够的甚至可能起反作用。1.1 探针机制的分工逻辑为什么 K8s 要把健康检查拆成三份Kubernetes 在 Pod 级别提供三种可配置的探针livenessProbe存活性探针。探测失败后 kubelet 会杀掉容器按 restartPolicy 重启。它回答的问题是进程还活着吗。readinessProbe就绪性探针。探测失败后 Pod 会从对应的 Service Endpoints 中移除不再接收新流量但 Pod 不会被杀。它回答的问题是这个 Pod 能接流量了吗。startupProbe启动探针。用于保护启动很慢的应用在它成功之前另外两种探针都不会启动。它回答的问题是应用真正起来了吗。我见过不少团队把存活和就绪混为一谈总觉得探针失败就该重启。实际上readiness 失败只是隔离流量让 Pod 暂时离线等恢复之后再挂回去只有 liveness 才涉及杀容器。两者职责完全不同。1.2 三种探针同时配置时的生效顺序实际执行顺序是这样的Pod 启动后kubelet 先看有没有配置 startupProbe如果配置了就先执行 startupProbestartupProbe 一旦成功kubelet 才开始执行 livenessProbe 和 readinessProbe。这个顺序很容易被忽略。很多人以为 startupProbe 只是用来延迟检查的其实它是三道闸门的第一道。对一个启动需要 30 秒的 Java 应用如果不配 startupProbe就得把 liveness 的 initialDelaySeconds 调成很大的值配了 startupProbeinitialDelaySeconds 就可以保持很小因为 startupProbe 会在应用真正起来之后才放行后续探针。1.3 三种探针失败后的不同结果决定了你的选择方便对照我列一个表探针类型探测失败后果典型用途常见误区livenessProbe容器被杀并重启检测死锁、OOM、进程僵死用它做业务依赖检查readinessProbe从 Endpoints 摘除不接收新流量检测依赖服务、连接池是否就绪以为它也会重启 PodstartupProbe容器被杀并重启保护冷启动时间长的应用用固定 initialDelay 代替我实际遇到的典型事故是团队把 liveness 指向一个查数据库的接口数据库抖动所有实例的 liveness 同时失败整个服务在一分钟内被全部重启流量直接雪崩。所以第三章我会专门拆一个探针端点设计的细节。2. 探测方式与参数计算从 HTTP 到 TCP选错就会误杀2.1 三种探测机制的原理与选型建议Kubernetes 支持三种探测动作对应不同的服务形态exec在容器内执行命令退出码为 0 视为健康。适合进程内状态可以自检、又不想额外暴露端口的组件。但 exec 有个隐藏成本命令是 kubelet 在容器里 fork 执行的如果命令本身耗资源或可能卡住会拖累 kubelet。tcpSocket向容器 IP 的指定端口发起 TCP 连接三次握手成功即视为健康。适合数据库、缓存、消息队列这类端口通基本可用的组件。httpGet请求指定路径的 HTTP 接口返回状态码在 200-399 之间视为健康。适合 Web 服务。值得说一点tcpSocket 是最轻的探测方式但也是最容易误判的。比如一个缓存组件连接池耗尽端口照样在监听TCP 握手照常成功readiness 依然通过但业务请求进来就是超时。所以对关键无状态服务能用 httpGet 就不要贪省事用 tcpSocket。2.2 探针参数的计算逻辑别拍脑袋填无论哪种探针都有五个参数需要理解initialDelaySeconds容器启动后等多久开始第一次探测。periodSeconds每隔多少秒探测一次。timeoutSeconds单次探测等待响应的超时时间。successThreshold连续成功多少次才视为健康。failureThreshold连续失败多少次才触发对应动作。一个常见的误判是只算 periodSeconds忘了 timeoutSeconds 也算进失败判定时间。假设 failureThreshold3、periodSeconds10、timeoutSeconds1那么从第一次探测失败开始大约需要 33 秒才会触发重启如果 timeoutSeconds 是 30、periodSeconds 是 10实际判定时间会明显变长。数值之间还有一个容易踩的雷timeoutSeconds 最好小于 periodSeconds。如果 timeout 比 period 还大上一轮探测还没结束下一轮又发起了kubelet 侧会出现探针叠加日志里全是超时业务看到的则是探针请求密集涌入。2.3 到底该怎么填我用一个场景算一遍假设一个网关服务正常启动要 5 秒依赖 MySQL、Redis 和两个内部 RPC。健壮的配置大概是readinessProbeperiodSeconds5timeoutSeconds1failureThreshold3。也就是说依赖任何一个抖动最多 18 秒左右就会把实例摘掉。livenessProbeperiodSeconds10timeoutSeconds1failureThreshold3。startupProbeperiodSeconds2timeoutSeconds2failureThreshold15最多给它 30 秒完成启动。有人会问failureThreshold 调大一点是不是更稳对也不对。调大了确实能减少瞬时抖动导致的误杀但也会延长故障发现时间。一个已经死锁的实例本来 30 秒内能被清理failureThreshold10 可能要等 2-3 分钟才被处理这期间流量一直打在一个坏实例上。我的习惯是liveness 的 failureThreshold 保持 3-5readiness 的 failureThreshold 可以略大因为它只是摘流量不涉及杀进程。3. 三个真实落地场景的探针配置拆解3.1 常规 Web 服务liveness 和 readiness 路径必须分开大多数 HTTP 服务的正确姿势是提供两个健康检查端点/livez 和 /readyz。一个只管进程活着一个真正检查依赖。readinessProbe: httpGet: path: /readyz port: 8080 initialDelaySeconds: 10 periodSeconds: 5 timeoutSeconds: 1 successThreshold: 1 failureThreshold: 3 livenessProbe: httpGet: path: /livez port: 8080 initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 1 failureThreshold: 3/livez 的实现要足够简单内存、goroutine 正常就返回 200不要查数据库不要查远程调用。/readyz 可以去查依赖但要控制超时比如 1 秒内必须返回结果否则直接判失败。原因如果把依赖检查放进 liveness数据库一抖动所有实例的 liveness 同时失败K8s 会把整个服务快速重启这是最典型的自残式故障。3.2 缓存和存储组件tcpSocket 更适合对 Redis、MySQL 这类组件通常配置 tcpSocket 就够了。readinessProbe: tcpSocket: port: 6379 periodSeconds: 10 timeoutSeconds: 3注意事项tcpSocket 只能证明端口通了。如果你的缓存组件状态需要在 readiness 里体现比如主从切换或集群握手状态建议在业务里提供一个命令行检查工具用 exec 方式探测。但 exec 命令不能太重控制在 100ms 内完成避免每次探活都在容器里做一次完整的状态汇总。3.3 冷启动很慢的应用startupProbe 兜底一个典型的 Java 微服务依赖配置中心、注册中心冷启动经常需要 60-90 秒。如果按传统方式把 initialDelaySeconds 调成 120又会导致真正故障时发现太慢。这个时候 startupProbe 最有用。startupProbe: httpGet: path: /healthz port: 8080 periodSeconds: 5 timeoutSeconds: 2 failureThreshold: 30即最多等待 30 次探测每次最长 2 秒超时相当于给应用约 210 秒的启动窗口。startupProbe 成功之前liveness 和 readiness 不会执行所以 initialDelaySeconds 可以放心保持较小值。我在一个线上团队接手过一个服务启动要 50 秒但原来的 YAML 里 initialDelaySeconds60periodSeconds5。表面看刚好能过但一旦启动过程中 CPU 被抢占第 60 秒还没监听端口马上被杀重启陷入启动失败-被杀-再启动的死循环。改用 startupProbe 之后这个问题一次都没再出现。3.4 健康检查端点的实现细节无论用哪种探针端点本身的实现质量决定整套机制的可靠性。我的建议是端点要轻。不要在健康检查里打印大量调用栈、采集完整指标。端点响应要快。探针 timeoutSeconds 通常只有 1-3 秒不能做重计算。端点要稳定。不要把一些偶发抖动也判定成失败可利用状态码区分200 表示健康503 表示依赖未就绪让 readiness 和 liveness 能够按不同的状态码处理。这些建议看起来是常识但我在生产环境里见过太多人把健康检查端点写成一个迷你压测接口每次探测都要查全量配置、预热缓存结果探针请求本身把 CPU 打满了。4. 一次滚动更新卡死的完整排查链路与级联故障复盘4.1 案例一Pod 反复 CrashLoopBackOff那次事故发生在一次普通版本升级之后。新版本发布上去Pod 没有像预期那样 Running而是进入了 CrashLoopBackOff。我按顺序做了这几步排查kubectl get pods看到新版本 Pod 状态 CrashLoopBackOff重启次数一直在涨。kubectl describe pod name看到 Events 里有一行关键信息Liveness probe failed: HTTP probe failed with statuscode: 500。kubectl logs 容器名没发现异常堆栈应用进程本身是起来的。进入容器手动curl /livez返回 500。查路由配置发现新版本把 /livez 放进了需要鉴权的路由组探针请求不带 token 返回 500。修复方式就是把健康检查路由放在独立分组不加鉴权或者给探针配置 httpHeaders让它带上内部 token。复盘下来这条经验最通用健康检查端点不要和服务业务路由混在一起。有时候开发为了省事把 healthz 塞进了 web 框架的全局拦截器里一旦做权限改造探针就跟着挂。4.2 案例二滚动更新卡死新 Pod 一直不 Ready另一个 case 是滚动更新一直不完成新 Pod 起不来旧 Pod 也不退出。kubectl describe pod看到的是 Readiness probe failed而不是 Liveness probe failed所以 Pod 不会被杀只是永远不 Ready。进入容器检查后惊讶地发现服务监听的是 8081而探针配的是 8080。原因很简单测试环境容器端口恰好和探针配置一致部署到集群时端口映射变了但探针配置还在用老端口。这类问题用一条命令就能区分kubectl exec进容器再curl localhost:端口立刻能判断是监听问题还是探针配置问题。4.3 案例三数据库抖动引发全站级联雪崩前面提过这种事故我展开讲一下完整链路。某个晚上数据库出现了一次 5 秒的抖动正常情况下这只是一次小告警。但因为 A 服务的 /livez 内部查询数据库探针超时liveness 连续失败A 服务的所有 Pod 被快速重启。A 不可用之后依赖 A 的下游服务开始报错B 服务的 /readyz 又检查了 A 服务B 的 Pod 也被摘流或重启。最终结果是数据库一次 5 秒的抖动演变成 20 多个服务同时雪崩。正确的做法是liveness 永远不检查依赖。readiness 可以检查依赖但要给依赖检查设置很短的超时比如 300ms并考虑退避策略。4.4 排查健康检查问题的实用命令kubectl get pod name -o wide看状态、重启次数、IP。kubectl describe pod name重点看 Events 里 probe failure 的报错。kubectl logs name对照容器日志和探针时间点。kubectl get events --sort-by.lastTimestamp看全集群事件。建议是把探针请求日志在应用侧打印一下或统计到 metrics这样对比 kubelet 事件的时间线定位是探针没发出来还是端点响应失败能省下大量瞎猜时间。5. readiness 失效之后优雅停机也在保护你的流量5.1 从 readiness 失败到摘流量之间存在时间窗口很多人以为探针一失败Pod 马上就从 Service Endpoints 摘掉了。实际不是探针是周期性执行的失败后 kubelet 更新 Pod 状态控制平面再更新 Endpoints这中间有几秒到十几秒的延迟。在这段时间里如果 Pod 还继续收到新请求就会出现部分请求失败。所以当我们要下线一个 Pod 时应该先让它失去 readiness给控制平面一点时间把流量摘空再真正退出。这就要靠优雅停机机制。5.2 preStop 钩子和 terminationGracePeriodSeconds 的配合lifecycle: preStop: exec: command: [/bin/sh,-c,sleep 15] terminationGracePeriodSeconds: 30流程是kubelet 收到删除 Pod 请求后把 Pod 标记为 Terminating发送 SIGTERM 信号给主进程同时执行 preStop 钩子。preStop 执行完之后容器才会进入后续终止流程。如果 preStop 执行时间超过了 terminationGracePeriodSecondskubelet 会直接发 SIGKILL 强杀。所以配置要保证 terminationGracePeriodSeconds 大于 preStop 的 sleepTime。我见过有人把 preStop sleep 设为 30但宽限期还是默认 30结果 sleep 还没跑完就被强杀等于白配。5.3 应用侧也要响应 SIGTERMKubernetes 之外的问题如果应用不捕获 SIGTERM优雅停机无从谈起。常见做法是在应用里注册一个 shutdown hook收到 SIGTERM 后按顺序做这几件事从注册中心摘除自己、关闭 HTTP 连接池、等待存量请求结束、刷新缓冲数据、退出进程。很多老服务只处理了 SIGINTCtrlC没处理 SIGTERM到了 K8s 里一删 Pod 就直接被杀连接池里的请求全部断掉。这类问题排起来很隐蔽因为 Pod 状态和探针都正常只有业务反馈发布时总有报错。6. 复盘完这几个故障我重新设计了探针规范最后分享一些我现在的做法不算标准答案但都是被生产环境教育的探针路径在所有服务中统一/livez、/readyz不允许各自起名。liveness 只允许做进程存活检查readiness 才能检查依赖依赖检查超时必须小于 500ms。启动阶段一律用 startupProbeinitialDelaySeconds 只保留 5-10 秒。探针的 successThreshold、failureThreshold 进入发布规范改动要走评审。端点耗时和成功率纳入监控探针平均耗时超过 200ms 就告警排查。这些规范看起来严格但都是从事故里反推出来的。我原来也觉得健康检查能用就行直到连续经历了几次探针引发的故障才明白它决定了 Kubernetes 对服务状态的判断边界。你把它当普通接口配它就会在关键时刻用一场事故教你重新认识它。多说一句健康检查只是判断健康的第一步真正的稳定性还要靠资源限制、弹性伸缩、容灾设计配合。但把探针这件事做对了至少能让 K8s 在异常发生时做出一个你不会后悔的决定。