Kubernetes探针实战:解决Pod反复重启的配置与排障指南 如果你维护过 Kubernetes 集群大概率经历过一个这样的半夜业务进程没挂、日志干干净净、监控曲线也不突兀但 Pod 却在反复重启告警群里哗啦啦刷屏。我负责的模拟项目 X 就真实发生过一次。当时我盯着kubectl get pod的输出看了半天满脑子问号容器明明活着K8s 为什么要动它直到我把事件翻出来才发现真正操刀的是 k8s 探针——livenessProbe、readinessProbe、startupProbe这三个配置项。这篇文章想写给所有正在被探针问题折磨的人刚接触 K8s 的开发者、排查容器反复重启的 SRE、希望发布过程更丝滑的运维。我会从一次事故复盘讲起把三种探针的分工、参数时间线、三种探测方式的选型逻辑、高频踩坑链路和发布场景配合完整过一遍最后聊聊我调参时坚持的原则。1. 半夜两点容器被反复重启但业务进程还活着1.1 一个“看起来健康”的容器为何被判了死刑那次事故的具体表现是这样的某订单服务发完版本后部分实例在 10 分钟内被重启了 6 次但节点 CPU、内存曲线都平稳得不像话容器内的主进程也一直存活。更诡异的是业务侧的报警很轻微只是偶发超时重启完成后立刻恢复看上去就像“间歇性抽风”。我先把 Pod 的 YAML 和事件看了一遍。kubectl describe pod里躺着一行关键信息Warning Unhealthy Liveness probe failed: HTTP probe failed with status code: 500注意不是 OOMKilled不是容器退出而是 liveness 探针判定失败。也就是说是节点上的 kubelet 主动杀掉了容器并重新拉起。顺着这条线翻应用日志才发现健康检查接口偶尔会返回 500而业务接口一切正常。追进代码后发现这个/healthz接口内部会同步查询一个外部缓存组件缓存节点当时正在做滚动调整某些请求直接超时。探针一连失败几次kubelet 就按restartPolicy把容器给重启了。这个场景非常典型你以为探针监督的是“业务健康”实际上它执行的是“kubelet 视角下的判活逻辑”。只要探针接口本身写得有问题哪怕业务好端端地跑着K8s 也会把你珍爱的进程当尸体处理。1.2 三个探针三个完全不同的问题那次事故逼我把探针的定位重新理了一遍。K8s 提供三种探针很多人一开始会把它们混为一谈但它们的职责根本不重叠livenessProbe回答“进程还活着吗”。失败后 kubelet 会杀死容器并决定是否重启用来兜底死循环、死锁、内存泄漏这类“进程在但服务已经废了”的问题。readinessProbe回答“可以接流量了吗”。失败后容器不会被杀而是被从 Service 的 Endpoints 中摘除流量暂时不进来。startupProbe回答“启动流程走完了吗”。在它成功之前liveness 和 readiness 都不会启动专门给慢启动应用一个安全窗口。我后来总结了一个类比liveness 是急救电话readiness 是门店门口挂着的“暂停营业”牌子startup 则是开业准备期的保安负责拦住所有人直到店里真正收拾好。三者没有一个能互相替代。1.3 事故的真正根因把“探活”和“业务健康”混在了一起那件事的根因不是探针太敏感而是我把健康检查接口设计得太“重”了。一个 liveness 探针接口内部却去查外部缓存等于让每个实例都成了故障放大器。想象一下外部缓存抖动一次所有实例的探针几乎同时失败kubelet 就会把所有容器轮流重启一遍。新 Pod 启动、拉镜像、注册、接流量整个集群的流量在这一段时间里到处乱撞。原来的一个小故障被“探针 自愈”这套机制放大成了一次集群级抖动。所以从那个晚上之后我给自己立了一条规矩liveness 探针接口必须做到“纯本地、无外部依赖”能查内存状态就查内存状态不要碰任何外部系统。至于那些真正需要检查的依赖放进 readiness 里并且拉长探测周期让它们只负责“决定是否接流量”不负责“决定是否杀人”。2. 三种探针的分工急救、营业牌子和开业等待2.1 livenessProbe只该管“僵尸进程”livenessProbe最适合的场景是那种进程还活着、但已经无法正常工作的“僵尸状态”。比如服务里出现死循环所有工作线程全部卡死比如 goroutine 泄漏内存被一点点吃光后接口响应变成超时再比如某些原生程序长期运行后文件描述符耗尽再也无法 accept 新连接。这些问题的共同点是外部很难通过“端口能不能连通”来判断但进程本身确实已经形同废物。此时重启是成本最低的修复手段。判断一条 liveness 是否合格我一般会问自己一个问题这个检查失败时我是否真的希望立刻杀掉容器重启如果答案犹豫那这个检查就不该放在 liveness。特别要注意的是liveness 失败后的行为还受restartPolicy控制。默认Always会重启如果设为Never即使 liveness 检测失败容器也不会被重新拉起只会被标记为异常。很多人改了restartPolicy却对结果一脸困惑就是因为没把这一层想清楚。2.2 readinessProbe流量入口真正的闸门readiness 是我最建议多花心思的一个探针。它决定了 Service 是否把流量转发给这个 Pod。机制上kubelet 定期执行 readiness 探针。失败后Pod 的 IP 会从所有匹配标签的 Service Endpoints 中移除成功后再加回来。整个过程容器不会重启只是进入“不接客”的状态。这意味着我们可以放心地在 readiness 里检查相对“重”的东西依赖的数据库是否连通、配置中心是否拉取完毕、本地缓存是否预热完成。即使检查失败也只是暂时摘流量不产生杀容器这种剧烈动作。我在发布场景里尤其依赖 readiness新版本 Pod 还没就绪旧 Pod 就不会被替换流量也不会提前打入“还在热身”的新实例。可以说readiness 是发布流程里最重要的一道闸门比什么蓝绿切换的设置都实在。2.3 startupProbe给慢启动应用一条生路Java 服务、里面加载大量规则或模型的应用启动很可能会超过 60 秒。如果 liveness 的initialDelaySeconds按启动时间设置那一旦某次重启变慢就会再次被杀陷入“越重启越起不来”的死循环。startupProbe就是为这类应用设计的。它的逻辑很简单在探针成功之前liveness 和 readiness 都先不工作只有 startup 成功之后其他两种探针才开始接管。所以当一个应用是典型的“启动慢但一旦起来就很稳”时我会把启动窗口完全交给startupProbe给它一个很大的failureThreshold比如 60 甚至 120 次periodSeconds设置成 2~3 秒给足整个启动时间。启动成功后再让 liveness 按照正常节奏工作。注意在 startup 成功之前readiness 也不会启动所以这个 Pod 是不会提前接入流量的。3. 探针参数时间线从 Pod 启动到判定死亡的完整推演3.1 六个参数每个管住哪一段探针配置里常被提到的参数有六个很多人只知道字面意思但组合在一起就能画出完整的时间线参数作用典型取值initialDelaySeconds容器启动后多久开始第一次探测按启动时间留缓冲periodSeconds每次探测的时间间隔10~15 秒timeoutSeconds单次探测等待响应的上限2~5 秒successThreshold连续成功多少次才判定健康默认 1readiness 可设 1~2failureThreshold连续失败多少次才判定不健康liveness 3~5readiness 2~3terminationGracePeriodSeconds判定失败后给 Pod 的优雅退出宽限30 秒起这里有个容易踩的细节successThreshold和failureThreshold都是“连续”计数。也就是说失败两次后中间成功一次计数会重置回零。理解这一点你就知道为什么一个抖动型的接口最容易导致偶发重启——它总是差一次到失败线又总是被一次成功救回来但最终某个长时抖动突破阈值后容器还是会被杀。3.2 一个具体的时间线推演为什么探针“反应慢”很多人觉得探针“太灵敏”但按默认配置算出来探针实际并不快。我们用一个常见配置做推演livenessProbe: httpGet: path: /healthz/live port: 9090 initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3容器启动后的时间轴大致是t10s第一次探测发出。最糟糕情况下 t15s 才因超时判定为一次失败。之后每隔 10s 探测一次每次最坏消耗 5s。第三次失败判定发生在大约 t35s。这时 kubelet 开始终止容器还要再算上terminationGracePeriodSeconds默认 30s。也就是说一个彻底无响应的容器从“业务肉眼可见地挂了”到“真正被重启”中间可能拖到一分钟以上。所以感觉探针“反应慢”不是错觉而是timeout和周期共同作用的结果。我早期踩过一个坑为了追求“快速发现故障”把periodSeconds设成 1 秒、timeoutSeconds设成 1 秒。结果接口偶发慢一点就被判定失败若干个实例的探针请求像轰炸一样打在应用上反而把服务拖得更慢。探针不是越快越好它需要和应用的响应时间分布匹配。3.3 参数设计的三条建议基于上面的时间线我一般这样分配参数可以当做一个起点livenessProbeinitialDelaySeconds给足启动缓冲periodSeconds10~15timeoutSeconds3failureThreshold3以上。因为杀容器代价大所以参数要保守。readinessProbefailureThreshold可以设成 2~3让它比 liveness 更早摘流量。摘流量代价小宁可误摘也不能让流量打到不健康的 Pod。startupProbeperiodSeconds可以短通常是 1~3 秒failureThreshold按“需要等待的总时长 / periodSeconds”来算。比如想给 2 分钟启动时间period 为 2s就设 60 次。4. 探针实现选型httpGet、tcpSocket、exec 背后的逻辑4.1 三种方式的判活逻辑K8s 支持三种探测方式它们的判活逻辑完全不同httpGetkubelet 向指定路径发起 HTTP GET 请求返回码在 200~399 之间视为成功。最贴近用户请求路径是 HTTP 服务的首选。tcpSocketkubelet 尝试向容器的某个端口建立 TCP 连接能连上就算成功。只看“端口有没有被监听”不关心应用层是否真正可用。execkubelet 在容器内执行一条命令退出码为 0 就算成功。可以写任意脚本逻辑但对容器镜像有要求必须有可执行的 shell 或二进制。三个 YAML 片段分别长这样livenessProbe: httpGet: path: /healthz/live port: 9090 httpHeaders: - name: X-Health-Check value: 1readinessProbe: tcpSocket: port: 8080startupProbe: exec: command: - /bin/sh - -c - test -f /tmp/app_started4.2 选型标准探针行为必须贴近真实请求路径很多人用tcpSocket图省事我吃过亏之后非常不建议把tcpSocket用在关键服务上。它只能证明某个端口上有进程在听但进程可能已经死锁、线程池已经打满、缓存已经失效。一个能建立 TCP 连接但不返回业务数据的应用对用户来说和挂了没有区别。所以我的选型逻辑很简单HTTP 服务优先用httpGet并且探测的路径最好是真实业务链路的前置路径而不是一个空返回的/ping。非 HTTP 协议的服务比如某些内部 TCP 中间件起步可以用tcpSocket但团队最好尽快补一个支持自定义命令的exec探针做深度检查。exec适合组合判断比如检查本地状态文件是否生成、执行特定命令验证内部组件是否就绪。还有一个容易忽略的点httpGet默认请求的Host头是 Pod IP。如果应用在网关层对 Host 做了域名校验探针请求可能被判为异常流量。此时可以像上面那个 YAML 里一样通过httpHeaders显式注入 Host。另外探针由节点上的 kubelet 发起不是从集群某个中心组件发起的。如果容器内部的监听地址绑定的是127.0.0.1而不是0.0.0.0探针从外部访问就会失败但你在容器内curl localhost一切正常。这类问题只看应用日志基本发现不了必须结合探针失败的 events 和容器网络命名空间一起排查。4.3 独立健康端口把健康检查和业务请求拆开我在项目里通常会让应用同时监听两个端口业务端口比如 8080和健康检查专用端口比如 9090。/healthz/live只返回进程和本地状态/healthz/ready负责检查依赖是否就绪。这样做有几个好处健康检查产生的访问日志不会混入业务日志日志存储压力小很多。业务中间件上那些耗时过滤器、权限校验、链路追踪逻辑不会影响探针响应。两个端口的超时行为可以独立控制不至于因为业务高峰处理不过来导致探针跟着超时。这个设计看起来简单但在排障时帮我挡掉了至少三类“探针把业务拖死”的问题。5. 探针踩坑五连从 Describe 到根因的排查链路5.1 坑一liveness 被外部依赖绑架集群变成故障放大器现象Pod 反复重启但容器日志没有明显错误业务也时好时坏。排查链路先kubectl get pod看到 CrashLoopBackOff再kubectl describe pod找 events看到Liveness probe failed最后翻应用日志发现探针接口在查外部缓存。根因liveness 不适合包含外部依赖检查。修复方案把探针接口拆开/healthz/live只读本地状态/healthz/ready才检查外部依赖并把 readiness 的周期拉长到 30 秒以上。5.2 坑二readiness 没配发布时流量一波全打到新 Pod现象新版本刚上线接口瞬间出现大量 5xx 或超时但旧版本一切正常。排查链路查看滚动更新过程发现新 Pod 状态是 Running 后立刻被加入 Endpoints应用还没来得及完成初始化。根因没配置 readinessK8s 只能看到“进程活着”看不到“服务就绪”。修复方案加上readinessProbe让新 Pod 在初始化完成、具备接流量能力后才被放行。5.3 坑三exec 探针里的命令根本不存在现象容器一直重启describe 里显示exec error或者Error: Internal。排查链路使用kubectl exec进入容器手动执行同一条命令发现提示sh: xxx: not found。根因使用scratch或distroless镜像的应用容器里根本没装 shell或者命令写的是相对路径找不到二进制。修复方案尽量用绝对路径如果依赖 shell写成command: [/bin/sh, -c, check.sh]并确保镜像包含这个二进制。5.4 坑四GC 停顿撞上探针超时JVM 应用反复被杀现象Java 服务每隔一段事件就重启一次时间点恰好和 Full GC 吻合重启前监控能看到 GC 暂停达到几秒。排查链路先排除 OOMKilled再看探针超时然后对比 GC 日志和探针失败时间戳。根因timeoutSeconds设置过短Full GC 期间整个进程暂停响应探针等不到响应就判定失败。修复方案把 liveness 的timeoutSeconds调大到能覆盖 GC 暂停时长同时让探针接口本身保持轻量不要把耗时操作放在里面。5.5 坑五探针把日志和存储写爆了现象业务没出问题但日志存储突然涨了好几倍告警先从存储系统发出来。排查链路查看日志来源发现大量/healthz的访问日志如果每个副本periodSeconds1、集群有几十个副本每秒几十条日志很轻松。根因健康检查被打到了业务日志链路里。修复方案使用独立健康检查端口或者在日志框架里单独过滤/healthz路径让探针产生的请求进入独立 access log甚至直接丢弃避免冲垮日志系统。6. 发布场景里的探针组合拳readiness、preStop 与优雅排空6.1 RollingUpdate 的等待逻辑探针是发布保护器很多团队第一次遇到滚动更新一直“hang 住”都会紧张。其实这往往是探针在保护你readinessProbe一直不通过Deployment 就不会继续替换旧 Pod新 Pod 也不会被接入流量。这时候正确的处理不是强行删掉新 Pod而是先看新 Pod 是否真的准备好了。如果应用启动慢给足startupProbe时间如果应用本身有问题那发布 hang 住反而是好事避免你把故障直接带到生产环境。6.2 preStop readiness 摘流量优雅下线的黄金组合滚动更新最怕“旧 Pod 还在接流量就被 SIGTERM 杀掉”。K8s 删除 Pod 的流程里Pod 进入 Terminating 的同时会从 Endpoints 摘除但摘除动作在全集群的传播需要时间。这个窗口里负载均衡仍然可能把新请求转发到 Terminating Pod。业界最常见的解法就是用preStop钩子做一个缓冲lifecycle: preStop: exec: command: - /bin/sh - -c - sleep 15这段 sleep 不是为了拖延删除而是给 Endpoints 摘除和kube-proxy规则刷新留出时间。配合 readiness 在收到删除信号前先返回失败几乎可以做到“先断流量再停进程”。更讲究的做法是在应用层做“主动下线”应用收到 SIGTERM 前先把 readiness 接口切到失败状态然后 sleep 几秒最后再退出。这个方案对长连接服务的帮助非常明显能有效避免大量连接被半途掐断。6.3 探针和资源限额的联动先别急着调探针参数容器反复重启时我要求团队先看终止原因而不是一股脑调探针OOMKilled这是内存 limit 不够探针是无辜的。先调内存配置或排查内存泄漏。Error可能是探针失败也可能是容器本身异常退出。CPU limit 设置太紧同样会坑探针。CPU 被限流后健康检查接口的响应时间会变长轻量的探针请求也会超时。所以如果你发现“探针偶发失败、失败规律跟业务高峰高度相关”先检查 CPU throttling 再动探针参数。7. 给探针调参时我死守的几条铁律7.1 默认值永远不是银弹K8s 里探针不写参数就完全没有默认探测行为。很多人以为不配探针系统会有什么默认健康检查其实不会。更常见的误区是把initialDelaySeconds随便写个 10 秒结果应用启动要 40 秒liveness 早早杀掉了它。每个服务的启动时长、响应延迟分布都不一样探针参数必须按实际压测数据来定。7.2 liveness 要保守readiness 要敏感这是探针设计里最重要的一句话。杀容器是代价很大的操作所以 liveness 的阈值要拉宽尽量只对“彻底没救”的情况触发而摘流量是低成本操作readiness 可以更灵敏宁可多摘几次也别把流量送给不健康的 Pod。7.3 探针不是监控系统别拿它当业务健康上报探针的唯一使命是给 kubelet 的调度决策提供依据。业务黄金指标应该走专门的 metrics 体系比如每个接口的成功率、延迟分位数、队列积压量。把这些重逻辑塞进探针接口最后收获的只会是一堆误杀和难排查的偶发重启。7.4 改探针配置本身也是高风险变更我见过团队为了修一个偶发重启直接把failureThreshold改成 100结果服务真正死锁之后十几分钟都没人发现。探针参数的改动本质上是在调整“K8s 对故障的反应速度”属于高风险变更。每次改动我都要先在测试环境模拟极端延迟再到金丝雀集群观察两轮发布最后才全量更新。最后再分享一个小习惯每次调整探针我会先去压测环境观察服务的真实响应延迟分布用 P99 和 P99.9 的数值来反推timeoutSeconds和failureThreshold而不是靠感觉拍脑袋。探针不是三个配置字段那么简单它直接决定 K8s 什么时候“动手”。调教好了K8s 才真正从调度器变成可靠的自愈系统调教不好它就是你深夜告警群里的主角。