Kubernetes探针终极指南:区分Pod‘活着’与‘可用’的高可用基石

发布时间:2026/10/16 15:01:23
Kubernetes探针终极指南:区分Pod‘活着’与‘可用’的高可用基石
做Kubernetes平台维护久了你会越来越清楚一件事Pod显示Running不代表服务真的能用。之前我在某容器平台项目里业务方反复反馈接口偶发超时我登上去看kubectl get pods所有Pod都是Running但实际流量已经打到了几个状态异常的副本上造成大量5xx。后来把所有问题绕回到一个点上探针配置没做好。Kubernetes里的livenessProbe和readinessProbe再加上经常被忽略的startupProbe就是用来区分“进程活着”和“服务可用”这两个完全不同的概念。这篇文章会把探针的原理、参数、写法、验证手段以及我在真实环境里踩过的坑全部摊开讲。无论你是刚接触Kubernetes的开发者还是被Pod各种“假活”问题折磨的SRE都能从里面找到可以直接抄作业的内容。1. 探针到底在解决什么问题1.1 从一个“假活”故障说起先还原一个很典型的故障场景。某服务有5个副本某次发布后代码里出现了一个死循环进程没有退出端口也在监听但所有线程都被占满根本处理不了新请求。这时如果你只看Pod Running这个状态完全看不出问题。流量被Service转发到这几个“僵尸”Pod上请求全部卡死或超时。为什么容器本身没有退出因为Kubernetes判断容器存活的标准是进程是否还在而不是业务是否正常。只要主进程没挂容器就是Running。探针的作用就是让Kubelet主动去问应用“你还正常吗”如果应用回答说“我不行了”探针就会触发后续动作要么重启容器要么从Service负载均衡里摘掉这个Pod。所以探针不是锦上添花而是高可用服务的基本底线。没有探针一个健康检查接口写得再好的应用也只相当于一个有人发命令才去治疗的病人而不是实时监测体征的ICU。1.2 三类探针的分工与配合Kubernetes一共提供三种探针存活探针、就绪探针、启动探针。livenessProbe决定容器是否要重启。它是“救死扶伤”的角色一旦失败次数达到阈值Kubelet会杀掉容器并按重启策略重新创建。它能兜住进程卡死、死锁、OOM后还残留之类的场景。readinessProbe决定Pod是否接收流量。它是“开关闸门”的角色失败时Kubernetes会把Pod从Service的Endpoints中摘除流量就不再打到它身上。但Pod不会被重启。它解决的是应用已经启动但暂时不具备服务能力的问题比如依赖的数据库还没有连上、本地缓存还没预热完成。startupProbe是后加的“保护壳”。它的目的是给启动特别慢的容器一个缓冲期在这段时间内Kubelet会忽略livenessProbe和readinessProbe的结果。很多老旧的Java应用启动要几十秒甚至几分钟如果存活探针过早开始检查刚启动的容器还没准备好就被反复杀掉形成“启动-被杀-再启动”的恶性循环。这三者配合起来才是一套完整的生命周期管理启动探针管“什么时候能开始查”就绪探针管“什么时候可以接流量”存活探针管“活不下去了就重启”。1.3 为什么说探针是高可用的基石你可以把Service、Deployment、HPA这些机制想成一张回流路网。Deployment保证副本数量Service保证流量分发HPA保证容量伸缩。但如果没有探针这张路网里就混入了大量“看起来还在、实际已经瘫了”的节点。负载均衡策略再好把请求发给一个不能处理的Pod结果依然是超时和报错。反过来探针又是路网里“自动上报拥堵”的传感器。一个Pod就绪探针失败Service马上把它摘掉HPA可能因为只能处理请求的Pod数变少而扩容。整个系统像有了免疫系统会自动隔离异常节点。这就是高可用的核心不是保证某个节点永远不坏而是让集群能快速发现坏节点并把影响控制在最小范围。2. 探针配置格式与关键参数2.1 四个必须搞懂的探针参数探针配置看起来就是几行YAML但真正决定行为的是这四个参数initialDelaySeconds、periodSeconds、timeoutSeconds和failureThreshold。有一个successThreshold在就绪探针里也会用到。initialDelaySeconds表示容器启动后等多长时间才开始第一次探测。这个值不能拍脑袋写。如果应用平均启动时间需要8秒你就不能设成3秒否则探针一上来就会失败。但它也不能设得太大否则Pod已经在硬撑了探针还赖在“不闻不问”的窗口期里。periodSeconds是两次探测之间的间隔。间隔越短发现异常越快但会给应用带来额外的本地请求压力。比如你的是一个高QPS的服务健康检查接口本身没做什么消耗但每2秒一次和每30秒一次的差别还是很大的。我一般倾向于生产环境用10s到15s除非业务对故障恢复时间有很高要求。timeoutSeconds是单次探测的超时时间。如果应用响应慢超过这个时间也算失败。注意它不是从请求发出开始排队算的而是整个探测请求的等待上限。这个值不能小于健康接口的P99响应时间否则正常请求也会被误杀。failureThreshold是连续失败多少次后才把探针状态置为失败。默认是3次。它本质上是给网络抖动和瞬时卡顿留的缓冲。比如健康接口偶发一次超时不能因为一次失败就直接把Pod重启了。successThreshold比较容易被忽略。就绪探针可以设置为连续成功N次才认为就绪用于防止刚启动的服务在“第一次探测成功、第二次又失败”之间反复横跳。但存活探针和启动探针的successThreshold必须为1这个限制在Kubernetes的API校验里有强制约束不要试图改成2。2.2 HTTP、TCP、Exec三种探测方式怎么选探针的探测方式主要有三类httpGet、tcpSocket和exec。新版Kubernetes还支持grpc探测但场景相对少先把前三种吃透。httpGet最常用也最接近业务真实状态。它主动请求一个HTTP接口只要返回状态码在200到399之间就算成功否则失败。适合所有有HTTP端口的Web服务。关键点在于这个接口不能只查进程是否在跑最好能检查核心依赖是否正常。比如数据库连接池状态、缓存连接是否可用但也要注意不要把所有弱依赖都放进去否则一个旁路组件抖动就会带崩主流程。tcpSocket表示Kubernetes会和容器的指定端口建立TCP连接能连上就成功。它比HTTP更初级只能证明端口在听无法证明应用能处理业务。对纯TCP协议的非HTTP服务比较合适比如Redis、MySQL或RPC框架。但很多场景下TCP探针的“假阳性”会非常严重端口能连不代表应用能真正处理请求。exec探针则是在容器内部执行一个命令命令退出码为0就算成功。它最灵活什么都能测但也最容易写出坑。注意exec不会经过shell解析除非你明确写[/bin/sh, -c, curl ...]。而且如果健康检查依赖的脚本或工具没打进镜像探针就会因为命令不存在而一直失败。这种问题在改镜像时最容易发生。另外无论用哪种探针它都是Kubelet从节点侧发起的不会经过Service负载均衡。所以探针请求不会因为流量切换被丢到别的Pod上它是直接访问Pod的IP和端口。这也是为什么探针能反映单Pod状态的原因。2.3 一个直接可用的Deployment配置示例下面这个YAML是我在实际项目中常用的模板做成了一个“探针三件套”的示例。服务是一个叫order-api的HTTP服务容器端口是8080健康检查路径有两个一个/healthz给存活探针用一个/readyz给就绪探针用。apiVersion: apps/v1 kind: Deployment metadata: name: order-api namespace: production spec: replicas: 3 selector: matchLabels: app: order-api template: metadata: labels: app: order-api spec: containers: - name: order-api image: registry.example.com/order-api:1.2.3 ports: - containerPort: 8080 protocol: TCP startupProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 0 periodSeconds: 2 timeoutSeconds: 2 failureThreshold: 30 readinessProbe: httpGet: path: /readyz port: 8080 initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 2 successThreshold: 1 failureThreshold: 3 livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 15 periodSeconds: 10 timeoutSeconds: 2 failureThreshold: 3简单解释一下这个模板startupProbe每2秒探测一次最多失败30次也就是给了最多60秒的启动缓冲期。在启动探针成功之前存活探针和就绪探针的结果都会被忽略。启动探针成功之后就绪探针开始按5秒的周期检查/readyz存活探针开始按10秒的周期检查/healthz。提示不要给存活探针和就绪探针配同一个HTTP路径。如果共用同一个接口且这个接口因为依赖故障返回500那么就绪探针会把Pod摘掉存活探针又会把Pod重启。虽然“摘掉重启”不一定会造成全网故障但会让一个仅仅是数据库短暂不可用的问题升级成Pod反复重启故障响应更加混乱。3. startupProbe 的正确打开方式3.1 启动慢的服务为什么需要它先说一个我遇到过的场景。某个老系统用的是重量级Java框架容器从开始启动到第一个HTTP端口能响应平均要40秒。一开始我没有配置启动探针只配了存活探针initialDelaySeconds设的是10秒。结果就是每个新Pod起来后10秒存活探针开始探测发现端口不通连续3次失败后Kubelet杀掉容器重启。新Pod又被杀又重启最后整个发布过程一团糟。这个问题的根源是存活探针太早介入了。有人会说那把initialDelaySeconds调大不就行了。确实可以但如果你把initialDelaySeconds调到60秒就会遇到另一个问题所有Pod都要等60秒才开始接受任何探测真正需要快速恢复的时候探针的反应被拖慢了。startupProbe就是来解决这个两难问题的。启动探针的语义是如果这个探针还没成功容器就还处于“启动阶段”其他探针先靠边站。一旦启动探针成功它就不再进行探测后面的生命周期交给存活和就绪探针。所以它既给了启动慢的应用足够的缓冲时间又不会耽误启动完成后探针的快速生效。3.2 典型配置与数值计算启动探针的配置核心在于计算periodSeconds * failureThreshold的乘积要大于最坏启动时间。拿上面的例子来说启动最慢需要60秒我就可以设置periodSeconds: 2、failureThreshold: 30理论上能容忍60秒内没有任何一次成功。如果业务方说“高峰期启动要90秒”这时候你必须扩到位比如periodSeconds: 3、failureThreshold: 30而不是凭感觉填。timeoutSeconds同样重要。启动阶段的应用响应可能很慢比如JVM在做类加载或者连接池初始化健康检查接口可能在等待某个锁。如果单次超时设成1秒哪怕启动总时间足够了也会因为单次请求超时导致探针失败。我一般把启动探针的timeoutSeconds放到和存活探针一致或更大一点比如2到3秒。需要特别提醒的是启动探针一旦失败次数达到阈值同样会触发重启。而且由于启动探针在做它的工作期间会屏蔽存活探针如果启动探针持续失败Kubelet不会管存活探针的结果直接进入重启流程。所以启动探针的失败阈值本质上就是“你允许这个应用最多启动多久”。3.3 三探针组合下的行为逻辑把三个探针放在一个Pod里看它们的关系是这样运转的容器创建成功后启动探针立刻开始探测。如果启动探针还没成功存活探针和就绪探针的探测结果被忽略Kubelet不会因此重启或摘除Pod。启动探针成功以后启动探针使命结束存活探针和就绪探针按各自的周期开始工作。一旦某个时刻就绪探针失败Pod从Endpoints摘除但容器不清除。存活探针连续失败容器被重启Pod重新回到启动探针阶段。启动探针再次失败到阈值继续重启。这套逻辑里最容易出问题的阶段是容器已经重启过多次。由于镜像拉取时间和容器启动时间都要算进去Pod的restartCount会快速上涨。如果此时Deployment正在滚动更新还会导致发布变慢甚至卡住。所以三探针组合配置完以后一定要做一轮“发布演练”确认从旧版本滚动到新版本的全过程中Pod能稳定地走完启动、就绪、接管流量这三个阶段。4. 实操过程从写探针到验证效果4.1 场景设计与需求确认动手配探针之前先不要急着改YAML。你需要确认三件事健康检查路径是什么、目标启动时间大概多长、业务能容忍的单次失败时间是多少。首先是健康检查路径。我建议应用团队至少暴露两个端点/healthz只查进程和基础运行环境返回200表示“我还活着”/readyz综合检查数据库连接、缓存、消息队列等核心依赖返回200表示“我现在能接流量”。如果你不想区分只做一个/healthz也可以但你需要想清楚这个接口失败时到底是希望Pod被重启还是希望暂时摘流量。如果两个探针都打同一个接口出现的就是前面说的“既重启又摘除”的叠加态排查起来非常痛苦。其次是启动时间。如果是新接手的服务我一般会在本地或测试环境先直接运行容器统计从容器开始到健康检查接口返回200的时间。统计3到5次取最大值再乘以1.5到2作为启动探针可容忍的时长。最后是恢复时间预期。如果业务要求故障恢复时间在30秒内那periodSeconds就不能设太长建议用5秒以下failureThreshold设2到3。如果业务对短暂故障不敏感可以放宽到10秒和3次。探针的灵敏度本质上是拿故障检测性能换稳定性越灵敏越容易误伤一定要根据业务容忍度来定。4.2 编写并应用探针配置确认好需求后把前面那段Deployment YAML里的镜像、路径、端口和参数替换成自己的值。在替换时我习惯先在测试环境做一次灰度只更新一个副本观察日志和指标再逐步放量。执行下面的命令应用配置kubectl apply -f deployment.yaml如果只是想临时调试也可以直接给正在运行的Deployment打补丁kubectl patch deployment order-api \ --typejson \ -p[{op:replace,path:/spec/template/spec/containers/0/livenessProbe,value:{httpGet:{path:/healthz,port:8080},initialDelaySeconds:15,periodSeconds:10}}]不过生产环境我强烈不建议用kubectl patch这种方式改探针。探针参数每次调整都应该走Git提交和镜像版本绑定方便回滚和审计。部署完成后先观察kubectl get pods -w看到Pod从ContainerCreating到Running再到1/1说明就绪探针已经通过了。如果卡在0/1说明就绪探针还没通过需要进一步排查。4.3 验证探针真实效果的几个手段配置探针后光看Pod状态还不够必须主动验证探针是不是真的在起作用。这里我一般会做四步。第一步观察探针相关的事件。执行kubectl describe pod pod-name在Events里能看到ProbeSuccessful、ProbeWarning或者Unhealthy的详细记录。如果看到Unhealthy会附上失败原因比如HTTP probe failed with statuscode: 500。第二步确认就绪探针失败后是否真的摘除了流量。执行kubectl get endpoints service-name正常情况下就绪的PodIP会出现在Endpoints里。人为触发某个Pod的就绪探针失败后它的IP应当从Endpoints列表里消失。如果IP还在说明可能是Service的selector没对到Pod标签或者使用的是EndpointSlice需要刷新时间。第三步验证存活探针重启流程。可以对容器内进程做一次强制“假死”比如执行kubectl exec -it pod-name -- sh -c kill -STOP 1这个操作只是暂停主进程不是杀死容器。主进程还在所以容器不会自己退出。但如果存活探针配好了Kubelet探测超时后会判定不健康进而重启容器。看到restartCount增加并且容器重新创建说明存活探针生效了。注意这个动作一定要在测试环境做生成环境别这么玩。第四步看健康检查接口是否被真实流量命中。可以临时在健康接口里打一行访问日志然后观察探针请求日志的周期是不是和periodSeconds一致。这一步能确认探测确实发到了容器而不是被Ingress或Service转发错位置。5. 常见问题与避坑指南5.1 就绪探针失败但还有流量打过来这个问题的表象是Pod状态已经是0/1但业务日志里仍然有大量请求进来。我排查过几次真正原因往往不在探针本身而是下面几个方向。首要怀疑对象的Service的selector是不是太宽泛。比如Service选中的Pod标签是app: order-api但Deployment模板里加了一个新标签version: v2而Service的selector没有调整或者反过来selector匹配到了多个不同版本的Pod。这种情况下某些Pod会被Service选中但不在目标Endpoints正常更新范围内。其次要怀疑是否走的是Headless Service或StatefulSet。Headless Service没有ClusterIP在kubectl get endpoints里能看到Pod列表但客户端直接通过DNS解析拿到所有PodIP绕过了就绪探针的控制。对于无头服务一定要在业务侧自己实现健康检查或者通过双Endpoint的方式隔离。最后还有一个容易被忽略的点readinessProbe更新到Endpoints之间有延迟。新版Kubernetes使用EndpointSlice更新会批量同步正常情况下秒级完成但如果集群网络或API Server负载很高可能有一定的滞后。排查的时候别只盯Pod状态还要看kubectl get endpointslice里的实际列表。5.2 存活探针太敏感导致Pod频繁重启Pod一直在重启重启原因里全是被探针杀掉的记录这是新手最容易踩的坑。核心原因只有一个探针灵敏度超过了应用本身的抗抖动能力。我之前遇到一个服务健康接口实现里直接查一个外部缓存。某一个下午第三方缓存服务出现两次短暂超时每次持续1秒左右。我的存活探针timeoutSeconds是1秒periodSeconds是2秒failureThreshold是3。结果就是这1秒的超时非常容易触发连续失败Pod在10分钟内被重启了七八次。解决思路分两块。第一块把timeoutSeconds调到一个更宽容的值比如3秒。前提是健康接口本身的P99响应时间不能太高否则探针每10秒都要等3秒压力测试时会把健康检查请求堆起来。第二块把failureThreshold调到3以上甚至5。这个参数就是用来吸收偶发毛刺的不要舍不得。Btw有一种很隐蔽的情况健康检查接口和普通业务接口共用同一个网络栈和线程池。当业务QPS过高线程池被占满健康检查请求也会排不上队导致探针超时。这种问题不是调探针参数能解决的归根结底是容量规划没做好。你调大超时阈值只是隐藏问题流量一上来照样会被打爆。5.3 HTTP探针返回码和路径的坑很多人以为HTTP探针只要请求能连上就算成功这是错的。httpGet探针明确要求响应状态码在200到399之间401、403、404、500一律算失败。如果你的健康检查接口需要鉴权或者路径写错了探针就会一直失败而业务日志里根本看不到有效请求。路径的坑主要体现在重定向。如果健康接口从/healthz重定向到了/healthz/探针默认不会跟随重定向直接按失败处理。我建议在配置前用curl -I看一眼健康接口返回的是200还是3xx。如果返回3xx要么把路径改成最终地址要么在应用里关掉重定向。还有端口问题。容器里监听的是8080但探针写的是8443Kubelet连不上就自然失败。排查方法很简单先执行kubectl exec -it pod-name -- netstat -ltnp看端口监听情况再对着探针里的端口逐一对。很多时候就是一个数字抄错了能卡你半小时。5.4 Exec探针的Shell陷阱与镜像依赖问题exec探针给人的错觉是可以直接写“一条命令”实际上它内部使用的是ExecAction在容器内直接执行你提供的命令组不做Shell解析。所以如果你写[curl, -f, http://localhost:8080/healthz]容器里必须有curl这个可执行文件。很多精简镜像里根本没有curl探针就会报“executable file not found”。正确的做法有几种。一种是用/bin/sh -c显式拉起Shell执行exec: command: - /bin/sh - -c - curl -f http://localhost:8080/healthz另一种是镜像里内置一个探针脚本比如/app/healthcheck.sh然后在探针里执行它。这样能把健康检查逻辑和镜像绑定避免在YAML里写一堆复杂的命令还容易错。但要记住镜像升级时必须同步验证脚本还能用否则探针会因为脚本缺失而直接失败。另外exec探针是在容器内执行它的输出不会打到Pod的标准控制台所以排查问题时要主动去容器的临时文件系统看脚本日志或者让脚本把结果写到同目录下的文件里。我在一个项目里就因为脚本里用了绝对路径而镜像的WORKDIR被改过导致探针一直失败。这种问题不看脚本根本发现不了。5.5 其他值得记住的实战细节探针是Kubelet发起的本地操作但它依然会消耗容器资源。健康检查接口不要做重逻辑更不要在每个探针请求里都重新建立数据库连接池。我见过有团队在健康检查里做全量表查询探针一跑数据库负载直接飙高。健康检查就应该像“体检”而不是“马拉松”只做轻量的连通性判断。还有一个和优雅终止相关的问题。存活探针决定重启但容器被重启时走的还是terminationGracePeriodSeconds的流程。如果你没有给业务预留优雅退出时间默认30秒可能不够也可能太长。探针配置和优雅终止必须一起治理否则探针发现故障后应用还在处理存量请求就被强制杀掉了。最后说下监控。探针本身不是万能的它只负责在异常时做动作但它不告诉你“异常正在发生”。生产环境要对探针失败事件做告警特别是readinessProbe失败的次数和时长。能用Prometheus这类监控系统采集Kubelet的探测器指标把“探针失败”和“Pod重启次数”直接挂到告警页面上。没有告警的探针就像没有警铃的火灾报警器失效了都没人知道。6. 围绕探针进一步加固高可用服务6.1 结合优雅终止、PodDisruptionBudget与多副本调度探针把“单Pod不健康”这个问题管住了但服务整体高可用还需要另外几块拼图。优雅终止和探针是天然搭档。Pod要退出时先进入Terminating状态从Endpoints摘掉然后等待preStop钩子和terminationGracePeriodSeconds完成收尾。如果你配合了就绪探针Kubelet会更平滑地从流量列表里摘掉Pod。我建议给需要优雅退出的服务配置preStop里的sleep命令给业务一点时间处理存量请求lifecycle: preStop: exec: command: - /bin/sh - -c - sleep 5同时给关键服务配置PodDisruptionBudget保证在主动运维节点维护、滚动更新期间至少有最低数量的Pod可用。探针只能处理“意外故障”不能替你做“计划内维护”的容量保护。多副本调度也要和探针配合着看。如果一个服务只有1个副本探针配得再完美故障时也会有一段空窗期。尽量把副本数放到至少2个并且用反亲和性把Pod分散到不同节点。这样即使某个节点的Kubelet或容器运行时异常也不会把整个服务的请求都拖垮。6.2 从探针到全链路健康检查探针解决的是单Pod的可用性问题但它不解决集群级依赖的可用性。比如数据库整个实例挂掉所有Pod的就绪探针可能都会失败Service里一个健康后端也没有。这种情况探针能告诉你“都在失败”但没办法自动恢复。所以从探针再往上走一步你应该在业务侧建立“依赖健康度”的分层模型。核心依赖必须fail-fast让探针反映出来非核心依赖可以做降级不要让它影响主流程的可用性。探针本身不是架构设计的替代品但它会逼着你把“依赖关系”和“故障临界点”想清楚。监控告警层面除了关注单次探针失败还要关注探针失败的时间窗口。短时间失败可能是抖动持续失败一定是有问题。用探针的值来做告警时我习惯设置两条线一条是“连续失败超过1分钟”的警告另一条是“Pod重启次数在10分钟内超过3次”的严重告警。这样既能早点发现问题又不会让探针本身的抖动把值班人员搞得草木皆兵。6.3 我的一点经验收尾探针配置这个东西看起来简单实际是最能体现“运维功底”的地方。我在实际项目里踩过几次坑之后养成了一套自己的习惯任何新服务上线前都必须先回答三个问题——健康检查路径是什么、启动需要最长时间是多少、业务能容忍多久的故障检测时间。这三个问题答不上来探针参数就是空中楼阁。还有一个小技巧想分享给正在看这篇文章的你探针不是配完就一劳永逸的每次应用版本升级、依赖变更、启动流程调整都要重新审视一遍探针配置。尤其是startupProbe很多服务第一次上线时不需要它后来某个版本启动变慢了没有它就会开始莫名其妙地重启。把探针参数纳入发布评审比事后再去排查要省力得多。最后说一句不要迷信某一份配置模板。别人家好用的探针参数换到你的服务上可能就是灾难。拿着这篇文章里的模板当起点结合自己服务的真实行为去调整多观察几个版本的Pod生命周期你就能找到最适合自己的那组参数。