1. 从一次诡异 502 说起:探针到底在保护什么
有次凌晨一点,某服务的 Pod 状态明明显示 Running,但用户投诉首页一直在报 502。我登上去看,容器进程活着,日志也没有 panic,CPU、内存都正常。诡异的是,流量一打进来,接口响应时间直接飙到三十秒开外,然后被网关判定超时干掉。
那会儿我还没把探针当回事,觉得 Pod 起得来、进程不死,不就完事了吗?后来才反应过来,问题恰恰出在"进程活着"和"服务可用"之间那条巨大的鸿沟上。Kubernetes 里的探针(Probe)就是来填这条鸿沟的——它由节点上的 kubelet 定期执行,帮你回答三个完全不同的问题:
- 容器里的进程是不是已经完成了启动,可以开始接收健康检查了?
- 容器里的进程是不是还活着,需不需要被重启?
- 容器是不是已经准备好接收业务流量,可以摘除或加入 Service 后端?
这三个问题对应 Kubernetes 内置的三类探针:startupProbe(启动探针)、livenessProbe(存活探针)、readinessProbe(就绪探针)。它们共享同一套探测机制(HTTP 请求、TCP 连接、执行命令),但语义完全不同,配置错了、混用了,轻则 Pod 频繁重启,重则流量全部打到一个还没准备好的实例上,线上事故就是这样一点点攒出来的。
这篇就结合我自己调过的几个真实案例,把三种探针的分工逻辑、参数调优思路、常见误区和不同场景下的组合策略完整过一遍。看完你至少能回答三个问题:什么情况该配哪种探针?参数怎么写才算合理?为什么明明配了探针,服务还是炸了?
2. 三种探针的分工逻辑:谁在管启动、谁在管存活、谁在管就绪
2.1 探针是谁执行的,它和容器内进程是什么关系
先说一个很多人都忽略的基础事实:探针不是容器里的应用自己检查自己,而是节点上的 kubelet 从 Pod 的外部去"戳"这个容器。
kubelet 是运行在每个节点上的代理进程,它通过容器运行时(比如 containerd)来执行探针。拿 HTTP 探针举例,kubelet 会直接发起一个到 Pod IP 指定端口的 HTTP 请求,请求路径、端口、Host 头都由你在探针配置里指定。如果返回的 HTTP 状态码在 200 到 399 之间,就认为这次探测是健康的;否则就是失败。exec探针则是在容器里执行一条命令,看退出码是否为 0。tcpSocket探针则是尝试建立 TCP 连接,连得上就算健康。
理解了这一点,你就会明白一个关键推论:探针的探测结果,本质上是外部视角对服务可用性的判断,不是你应用内部自己觉得"我还行"就算数。很多应用的健康检查接口用/healthz,里面只查了一下进程内存,根本没检查数据库连接池、下游依赖缓存这些关键资源的状态,那这个探针对"服务是否真的可用"几乎没有任何参考价值。
另外要注意,探针是 kubelet 在主循环里串行执行的。如果一个节点的 Pod 很多,探针本身就是一种资源开销。探测太频繁、超时设太长,会把节点负载拉上去;探测太稀疏,故障发现又不及时。后面我会讲到,这就是调参需要权衡的核心矛盾。
2.2 startupProbe:给慢启动应用的一条逃生通道
先讲 startupProbe,它是 Kubernetes 1.16 引入的,也是最容易被忽略的一个。
它的职责非常单一:告诉 kubelet"容器里的应用到底完成了启动没有"。在它成功之前,其他两种探针都不会执行。
为什么需要这样一位"保安"?因为很多应用启动真的慢。比如 Java 应用启动要加载 Spring 容器、连数据库、连配置中心,一般得 30 秒到一两分钟;有些老系统冷启动甚至要三分钟。如果你只配了 livenessProbe,kubelet 从容器创建那一刻起就按initialDelaySeconds倒计时,时间一到就开始用存活探针去探测。应用还没起来,探针一打就失败,连续失败达到failureThreshold次数,kubelet 就把容器杀了重启。结果就是 Pod 陷入"启动-被杀-启动-被杀"的 CrashLoopBackOff 死循环,永远起不来。
在 startupProbe 出现之前,大家通常用两个办法绕过这个问题:
- 把 livenessProbe 的
initialDelaySeconds调得很大,比如 120 秒,赌它这个时间内能起来。 - 给 livenessProbe 配一个"宽容"的脚本,比如脚本里去判断某个启动标志文件是否存在,不存在就返回 0。
这两种都很烂。第一种是盲猜时间,启动慢一天就完蛋;第二种是自欺欺人,等于让应用自己给自己开后门,探针彻底失去意义。
startupProbe 的正确逻辑是:启动探针失败,kubelet 不会杀容器,它会一直重试,直到成功为止。一旦启动探针成功一次,它就被标记为"完成",后面所有启动探测直接停止,转而交给 livenessProbe 和 readinessProbe 接管。
这个机制意味着你可以把启动探测的failureThreshold设得很大,比如 Java 应用设个 60,periodSeconds 设 5 秒,那就有最多 300 秒的启动窗口,基本覆盖了绝大多数慢启动场景,而且完全不用担心误杀。
2.3 livenessProbe:判定"死透了",然后交给重启机制
livenessProbe 解决的是"这个容器要不要被重启"。
它的判定标准只有一条:容器里的进程是否处于不可恢复的异常状态。这个状态包括但不限于:死锁、内存泄漏导致 OOM 频发、goroutine 疯狂增长导致卡死、某个关键协程阻塞导致请求无响应。
liveness 探测失败后,kubelet 会杀掉容器,然后根据restartPolicy决定是否重启它。默认的 restartPolicy 是 Always,也就是说容器会被重建。
这里有一个特别反直觉的点,很多人理解错了:livenessProbe 不是为了"摘流量",而是为了"重启进程"。它解决不了流量调度的问题,一个 liveness 失败的容器在被杀之前,依然还在 Service 的后端列表里,依然会分到请求。所以在生产环境里,你几乎不应该只依赖 livenessProbe 保护服务可用性。
更关键的是,livenessProbe 的判定必须足够"稳",不能因为业务瞬时波动就误杀容器。比如你的接口平时响应 100ms,突然遇到流量尖峰响应变成 2 秒,如果 liveness 探针的 timeoutSeconds 只设了 1 秒,这一下就失败了。连续失败几次容器就重启了,可用性不升反降。这种情况应该是"还没做好准备接受流量",那是 readiness 该管的事;或者是"启动过程中还没起来",那是 startup 该管的事。拿 liveness 一刀切,只会制造更多事故。
2.4 readinessProbe:流量调度的开关,才是线上服务可用性的守护神
readinessProbe 解决的是"这个 Pod 能不能加入 Service 的负载均衡池,能不能被分配流量"。
它失败后,kubelet不会重启容器,而是将这个 Pod 的 Ready 状态置为 False,然后把它从所有 Service 的 EndpointSlice 里摘掉。注意,这里说的是"摘掉",不是"杀掉"。等它恢复健康了,又会被加回来。
这套机制的价值在于:它让"进程是否存活"和"是否接收流量"彻底解耦。容器活着但没准备好,就不给流量;准备好了,再给流量。这跟网关里的熔断、摘除节点思路是一脉相承的。
所以,readinessProbe 的检查内容应该尽量贴近"对外提供服务所需的所有前置条件":
- 数据库连接池是否成功初始化且有可用连接;
- 内存缓存是否加载完成;
- 依赖的下游服务(比如认证中心、配置中心)能不能连通;
- 消息队列消费组是否已经建立;
- 本地磁盘是否还有足够空间写日志;
- 业务说一句大白话:一个请求打过来,这个实例能不能在合理时间内给出正确响应。
2.5 三者协作的完整时间线:一个 Pod 从创建到稳定服务的全过程
很多人分开理解三个探针都能懂,一旦组合起来就乱了。我画个时间线你就明白了:
容器创建 ↓ [startupProbe 开始执行] ↓ 成功(或达到阈值前成功) [livenessProbe 和 readinessProbe 开始执行] ↓ readinessProbe 成功 Pod Ready,EndpointSlice 加入,Service 开始分发流量 ↓ 运行期间 readinessProbe 失败 → 摘流量,容器不重启 livenessProbe 失败 → 杀容器,按 restartPolicy 重启 ↓ 重启后 回到 startupProbe,重新走一遍流程记住一个核心逻辑:startup 是"门卫",负责放行其他探针;readiness 是"交通信号灯",管流量;liveness 是"急救人员",管生死。三者各管一段,互相不替代。
3. 探针配置参数的调优经验:从默认值到生产可用的调整链路
3.1 探针配置的五个参数,逐一拆开讲
不管是哪种探针,Kubernetes 的配置结构是一样的。看一个典型例子:
livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 15 periodSeconds: 10 timeoutSeconds: 1 successThreshold: 1 failureThreshold: 3这五个字段的含义,网上一搜一大把,但我这里要用踩过坑的方式讲一遍:
- initialDelaySeconds:容器启动后,等多久才开始第一次探测。这是给应用预留的"启动缓冲期"。但要提醒你,在配置了 startupProbe 之后,这个参数对 liveness 和 readiness 的意义已经弱化了——因为 startupProbe 成功之前,它们根本不会执行。它更多适用于没配 startupProbe 的老配置。
- periodSeconds:每隔多少秒探测一次。默认 10 秒,生产环境我一般建议还是 10 到 15 秒。调太短(比如 1 秒)会让 kubelet 和应用的健康检查接口都承受不必要的压力;调太长(比如 60 秒)故障发现速度太慢,一个端口挂着但进程没死的情况要拖一分钟才被发现。
- timeoutSeconds:单次探测的超时时间。默认 1 秒。这是最容易踩坑的参数。HTTP 探针发起请求后,如果超过这个时间没有收到响应,就算失败。如果健康检查接口本身涉及数据库查询,或者服务在 GC 停顿期间,1 秒很容易超时。我建议在服务正常响应 P99 耗时基础上再留 50% 到 100% 的余量。
- successThreshold:连续成功多少次才认为容器恢复健康。默认 1 次,一般保持默认就行。只有一种情况我会调成 2 或 3:服务的健康检查接口比较"毛躁",偶发一次失败会短暂抖动,但你不想让它抖动一下就立刻恢复流量、然后立刻又摘掉,那种频繁上下线比一直不上线更伤网关。
- failureThreshold:连续失败多少次才触发对应动作(liveness 触发重启,readiness 触发摘流量)。默认 3 次。liveness 的场景我会建议调大到 4 到 5 次,宁可多等几秒,也要避免误杀。readiness 则建议保持 3 次甚至调小到 2 次,因为摘流量本身不伤实例,反应快点没坏处。
3.2 探测方式的选型:httpGet、tcpSocket、exec 各自适合什么
三种探测方式,应用场景差别很大。
httpGet是最推荐的,也是绝大多数 Web 服务的第一选择。因为它最接近真实用户的访问路径:一个 HTTP 请求发过去,能拿到 2xx/3xx 响应码,说明从 kubelet 网络栈到容器端口、到应用进程、到 Handler 链路是通的。注意,这里说的是"链路通",不保证业务逻辑完全健康,所以健康检查接口本身的设计很重要,后面我会专门讲。
tcpSocket只检查端口能否建立 TCP 连接,比 HTTP 探针更底层。它适合四层协议的服务,比如 MySQL、Redis、gRPC(如果没暴露 HTTP 健康接口的话)。但它的检查粒度很粗:端口能被 connect,不代表服务真的能处理请求。拿它做 livenessProbe 还行,做 readinessProbe 就远远不够。
exec是在容器里执行命令,通过退出码判定健康。它最灵活,但也最危险。危险在哪?因为你在容器里执行的命令本身就依赖容器环境。常见坑我举个真实的:有人写了个健康检查脚本pidof java,容器里却用的是基础镜像,连 pidof 命令都没有,探针每次执行直接报"command not found",退出码 127,Pod 一直不 Ready。还有一个更隐蔽的坑:脚本里用grep查进程列表,但容器里跑的是精简镜像,连 /proc 的信息都不全,grep 出来的结果永远不对。
我的建议是:能走 HTTP 就走 HTTP,走不了再说别的。如果确实需要 exec,命令越简单越好,而且一定要在目标镜像里手动执行一遍验证,不要只在本地 Mac 上测。
3.3 一套可以参考的起步配置
以一个标准的 Spring Boot 服务为例,假设健康检查接口GET /actuator/health分别返回 200 和 503。给出一套我常用的起步配置:
startupProbe: httpGet: path: /actuator/health/readiness port: 8080 periodSeconds: 5 failureThreshold: 60 timeoutSeconds: 3 readinessProbe: httpGet: path: /actuator/health/readiness port: 8080 periodSeconds: 10 timeoutSeconds: 3 successThreshold: 1 failureThreshold: 3 livenessProbe: httpGet: path: /actuator/health/liveness port: 8080 periodSeconds: 15 timeoutSeconds: 3 successThreshold: 1 failureThreshold: 5几个设计思路说明一下:
启动探针周期 5 秒、阈值 60,代表最多容忍 300 秒的启动时间,给慢启动业务留足余地。正常情况下应用 30 秒就起来了,启动探针一成功就退出,不会有持续开销。
readiness 和 liveness 用不同的健康检查接口,这是一个非常关键的设计。很多团队图省事,两个探针指向同一个/healthz,结果一出问题就连锁反应:依赖抖动导致健康检查失败,readiness 摘流量,同时 liveness 也开始失败,容器被重启,整个 Pod 重建,所有连接全部断开。如果是接入层服务,这就是一次小型雪崩。而拆成两个接口之后,你可以让/healthz/liveness只检查进程内部最基本的标志(比如一个 AtomicBoolean 是否还活着),让/healthz/readiness才去检查数据库、缓存、下游依赖这些重资源。两者职责清晰,fail 的边界就不会重叠。
liveness 的 failureThreshold 设 5,配合 15 秒的 period,意味着要连续 75 秒探测失败才会重启。这个容忍度对绝大多数业务来说都够宽了,也绝不会因为偶发的 GC 停顿误杀容器。
4. 探针配置实战:从 YAML 编写到效果验证的完整链路
4.1 完整 YAML 示例:一个偏实战的 Deployment
配合上面的思路,给一个完整的 Deployment 配置,里面包含了我实际在用的探针组合:
apiVersion: apps/v1 kind: Deployment metadata: name: order-service namespace: production spec: replicas: 3 selector: matchLabels: app: order-service template: metadata: labels: app: order-service spec: containers: - name: order-service image: registry.example.com/order-service:2.4.1 ports: - containerPort: 8080 env: - name: JAVA_OPTS value: "-Xms1g -Xmx1g" startupProbe: httpGet: path: /healthz/startup port: 8080 periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 60 readinessProbe: httpGet: path: /healthz/readiness port: 8080 periodSeconds: 10 timeoutSeconds: 3 successThreshold: 1 failureThreshold: 3 livenessProbe: httpGet: path: /healthz/liveness port: 8080 periodSeconds: 15 timeoutSeconds: 3 successThreshold: 1 failureThreshold: 5眼尖的你会发现,我甚至把 startupProbe 单独拆了一个/healthz/startup接口。它和 liveness 接口的区别在于:startup 接口只返回一个"进程已经完成框架初始化"的标记,比如 Spring 容器的ApplicationReadyEvent触发后才把它置为 true;liveness 接口检查的是"运行时是否还在正常工作";readiness 是"是否具备服务能力"。三级接口逐层递进,每一层的判定条件都比上一层更严格。这种设计在业务方配合的前提下,能把探针的"误报率"降到最低。
4.2 如何验证探针配置真的生效
YAML 写完之后,别直接往集群里一扔就觉得完事了。我有几个固定的验证步骤,每次都走一遍,能拦住 90% 的脑残问题。
第一步,用 kubectl 看状态。
kubectl apply -f deployment.yaml kubectl get pods -o wide kubectl describe pod order-service-xxx如果探针配置有问题,describe里 Events 尾部一般会有Unhealthy事件,输出里会写明探针失败的具体原因,比如Liveness probe failed: HTTP probe failed with statuscode: 500。
第二步,手动触发一次失败,验证摘流量和重启逻辑。
找一个正在 Ready 的 Pod,进入容器把健康检查接口搞挂:
kubectl exec -it order-service-xxx -- sh # 在容器里把某个标志置为不健康,比如 touch /tmp/unhealthy然后观察:
kubectl get pods -w kubectl get endpointslices -l kubernetes.io/service-name=order-service如果 readiness 探针配置正确,几秒钟内你会看到 Pod 变成NotReady,同时对应的 EndpointSlice 里这个实例的 IP 被摘掉。kubectl get pods -w里显示的状态更有意思:你会看到 Pod 还在 Running,但READY列从1/1变成0/1。这就是"活着但不对外服务"的状态,明白这个状态,你就理解了探针的精髓。
第三步,模拟启动慢的场景。
把 startupProbe 的 failureThreshold 临时调小到 3,period 保持 5 秒,然后重新创建 Pod。正常情况下 Pod 会一直显示Running但READY 0/1,等到你恢复阈值后再看它是否正常变为 Ready。这一步骤是为了确认:startup 失败不会触发重启,只会让探针"等待"。
4.3 探针配置后的持续观察维度
探针不是配完就一劳永逸的。我上线新的探针配置后,头一周会盯几个指标:
- Pod 重启次数:
kubectl get pods里 RESTARTS 列持续增长,说明 liveness 误判严重,priority one 级问题。 - Ready 状态翻转次数:如果某个 Pod 频繁在 Ready 和 NotReady 之间横跳,说明 readiness 探针写得过于敏感,或者这个实例本身的稳定性有问题。频繁上下线比一直下线更可怕,因为它会让名叫 Service 的负载均衡器的连接池反复重建,长连接请求全被打断。
- 健康检查接口的响应时间:这个往往被人忽略。如果你的健康检查接口查询了一个特别慢的数据库视图,响应时间要 800ms,而 timeoutSeconds 设的是 1 秒,那你的 Pod 会在业务高峰时跳舞。在监控里专门给
GET /healthz/readiness建一个 p99 耗时的看板,能提前发现这类问题。
5. 常见误区和真实踩坑复盘:为什么探针都在报错但服务还是一团糟
5.1 误区一:liveness 和 readiness 指向同一个接口
这个坑我前面已经点名了,但值得展开复盘。
我曾经接管过一套遗留系统,里面三个服务的探针全都指向同一个/health。这个接口的逻辑是:查一次数据库,连一次 Redis,再调一次下游的订单服务,全部正常才返回 200。平时看着没啥问题,直到一次下游订单服务故障,接口开始返回 500。
接下来的连锁反应是:readiness 失败,实例摘流量;liveness 也失败,容器被杀重启。重启后的实例刚起来又开始查数据库、连 Redis、调下游,下游还是挂的,于是又失败、又被杀。整个服务在十分钟内重启了十几次,Kubernetes 集群的节点上全是这种"重启循环"的 Pod。最后拉了多少流量?业务影响范围从下单接口一路扩大到所有依赖这个服务的上游。
破解方案就是前面说的接口分层,至少把 liveness 和 readiness 拆开。liveness 接口只查进程级信号,保证它永远只在你真的需要重启容器的时候才失败。
5.2 误区二:exec 探针里的命令在目标容器里根本不存在
有次处理一个 Nginx 服务的探针问题,配置是这样的:
livenessProbe: exec: command: - /usr/local/bin/check-health.sh结果 Pod 全部处于 CrashLoopBackOff。我进容器一看,脚本文件压根不在镜像里,镜像打完包后谁都没记得把这个脚本 COPY 进去。这个案例听起来很蠢,但实际发生频率不低,尤其是当 Dockerfile 和 Kubernetes YAML 由不同人维护的时候。
还有一个更隐蔽的版本:脚本依赖的curl、jq、pgrep等命令,在本地开发镜像里有,但生产环境用的distroless精简镜像里没有。这种问题你要靠本地跑再多的kubectl describe也发现不了,必须在目标镜像里手动执行一遍命令验证。检查清单就一句话:exec 探针里的命令,以及它依赖的所有二进制,必须在目标镜像中真实存在且可执行。
5.3 误区三:timeoutSeconds 和 periodSeconds 的关系没搞懂
很多人以为periodSeconds: 10的意思是"每 10 秒发起一次探测"。其实不完全是。当一次探测超时后,下一次探测是从这次请求发出后的第 10 秒开始算,还是从这次请求返回之后开始算?不同的 kubelet 版本行为有差异,但总体逻辑是:periodSeconds 是两次探测开始时间的间隔,超时只是单次探测的内部上限。
举个例子,如果你设了timeoutSeconds: 5、periodSeconds: 10,而健康检查接口每次都耗时 4 秒,那么每个 10 秒周期里有 4 秒被探测请求占用,实际留给业务请求的处理时间只剩下 6 秒。如果是小应用无所谓,但高流量服务会明显感觉到健康检查请求和业务请求在抢占处理线程。所以健康检查接口本身必须要轻量,默认情况下单次探测耗时应该压在 100ms 以内,否则就是给系统凭空增加负担。
5.4 误区四:所有容器都套同一套探针模板
还有一次我们看到某个服务的 Pod 疯狂重启,查了半天发现是它的缓存预热要将近 40 秒,但配置却照搬了另一个服务的 liveness:initialDelaySeconds: 10, failureThreshold: 3, periodSeconds: 10。也就是说存活探针在容器启动后 10 秒开始探测,每 10 秒一次,失败 3 次就杀。40 秒的预热期内,探针早在第 30 秒就把容器杀掉了,于是永远在 40 秒前循环。
这种问题在"一个模板走天下"的团队里非常普遍。探针配置必须针对每个服务的启动耗时、服务类型、依赖强弱单独设计。没有所谓的最佳配置,只有适合你这个服务的配置。我一般要求团队在接口文档里明确写清楚"这个服务启动需要多少秒、依赖几个外部组件、读接口 P99 是多少",然后据这个信息来配置探针。
5.5 误区五:探针内部的健康检查逻辑本身就写得不对
很多人用的健康检查接口,只检查了进程自身状态,完全没有检查它依赖的下游。比如应用连的数据库挂了,但/healthz/readiness还是返回 200,因为接口里没有查数据库。这样做的结果是:数据库恢复前,所有请求打到这个实例上,全部超时,而实例在 Service 的 EndpointSlice 里依然是 Ready 状态,流量照常分发。
反过来说,如果 readiness 接口把下游全都检查一遍,而下游偶发抖动,readiness 就频繁失败,实例被频繁摘流量又加回来。所以完整的思路是:
| 场景 | readiness 检查范围 | 原因 |
|---|---|---|
| 核心应用,强依赖数据库/缓存 | 检查数据库连接、缓存连通性,但用短超时(如 200ms) | 宁可摘掉也不要让流量打到半残实例上 |
| 接入网关类服务 | 检查内存、线程池状态 | 网关自身才是关键,下游无关 |
| 有本地缓存兜底的服务 | 只要本地缓存未过期就认为 Ready | 下游挂了也不影响主要流量 |
核心原则很简单:readiness 探针的检查范围,必须和"能不能接流量"强相关。跟接流量无关的依赖,就不要放进 readiness 里。
5.6 踩坑实例复盘:一次因为探针导致的滚动发布卡死
最后分享一个我印象最深的真实事故。某次发版,Deployment 滚动更新卡住了:新创建的 Pod 一直不 Ready,旧的 Pod 也不缩容,服务一直处于"发布中"状态,用户侧偶发 5xx。
排查看新 Pod 事件,里面全是Readiness probe failed: HTTP probe failed with statuscode: 503。登录到新 Pod 里 curl 健康检查接口,返回 503,但服务本身的日志一切正常。
后来发现,新版本上线了一个功能:健康检查接口会校验内存缓存里的一个配置项,如果配置加载没完成就返回 503。而配置加载依赖一个外部的配置中心,偏偏配置中心那个时段在重启。于是新版本永远认为不健康,滚动更新卡死,流量还压在旧版本上,而旧版本没有这个逻辑,服务正常。
这件事给我的教训有三层:
- 健康检查接口的失败阈值要"向下游抖动妥协",不能把一次性外部故障放大成新实例不可用;
- 滚动发布期间,新 Pod 的 readiness 失败会影响整个发布流程,所以新版上线前必须确认健康检查接口不会因为短期依赖抖动而误报;
- 依赖外部组件的检查,必须加超时和降级逻辑,尤其是健康检查路径,不能因为下游慢就把自己的探针拖垮。
6. 不同业务场景的探针组合策略:别再一份配置打天下了
6.1 无状态 Web 服务:三探针齐全,readiness 最严格
这是最常见的场景。我的建议是三个探针都配齐:startupProbe 兜底启动时间,readinessProbe 兜底依赖可用性,livenessProbe 兜底进程死锁。
readiness 接口检查项应该包含:数据库连接池、Redis 连接、消息队列生产者状态。每个检查项要单独设超时,比如单项 200ms,总超时不超过 1 秒,避免健康检查自身变成瓶颈。
6.2 单实例且不可重启的服务:慎用 liveness
如果你跑的是单副本的数据库、有状态服务,或者容器重启代价远大于暂时不可用的服务,我强烈建议你不要配 livenessProbe,或者把 failureThreshold 调到 10 以上、periodSeconds 调到 60 以上。
为什么?因为这类服务重启一次的成本极高:主从切换、数据恢复、日志重放,可能比"让它暂时变成 NotReady"要痛得多。liveness 的本意是自动恢复,但在有状态场景下,自动恢复往往带来更大的损失。我见过一个团队给单实例 Redis 配了 liveness,结果一次 GC 停顿导致连续几次探测失败,Redis 容器被重启,缓存全部丢失,在线业务直接雪崩。这种情况的正确做法是:只配 readiness,让流量不进这个实例,人来决定何时重启。
6.3 有预热/缓存加载的服务:startupProbe 是你的救命稻草
像 Java 应用类加载、机器学习模型加载、大数据量缓存预热,这类服务启动后会有相当长一段时间不能接流量。你可以在启动逻辑里设一个标志:预热完成之前/healthz/readiness返回 503,预热完成后返回 200。
配合 startupProbe,把启动窗口拉足够长,等完整启动后再让 readiness 介入。这里有个细节:readiness 探针和 startupProbe 的判定是可以互补的——启动探针过关了,Pod 的状态就从ContainerCreating变Running;readiness 过关了,READY列才从0/1变1/1。两者各管一段,最好不要合并成一段。
6.4 批处理/离线任务:大部分场景根本不需要探针
如果你的工作负载是一次性任务(比如定时清理、离线计算),它跑完就退出,那探针对它没有意义。容器退出后 Kubernetes 会根据 restartPolicy 处理,而不是靠 liveness 判断"要不要重启"。
有一种例外:长跑的流式处理任务(比如消费消息队列的 worker),这时候 livenessProbe 有价值——如果消费者线程死了但你进程还活着,业务就静默停滞了。可以配一个 exec 探针检查队列消费 lag,或者检查 worker 内部的存活标志,让 kubelet 在消费线程死亡时重启容器。
7. 最后说几句实在的
探针配了三四年,踩过的坑比看过的文档多,我自己的体会是:探针的本质不是什么高大上的技术,而是运维同学和业务代码之间的一份契约。文档里写清楚了"进程活着不代表服务可用",你的探针就要把这句话翻译成精确的检查逻辑。
如果你现在刚接触这个主题,我建议不要急着一次把三种探针全部配齐。先从 readiness 开始,把一个服务的"接流量"条件理清楚,把它加到现有 Deployment 里,观察几周,确认它的状态翻转是合理的。然后再加 liveness,而且是从非常宽松的阈值开始(比如失败 10 次才重启),逐步收紧到你认为安全的范围。最后再看服务启动够不够快,决定要不要上 startupProbe。一步一步来,每次变更都能解释清楚,比一次性抄一个"最佳实践模板"要可靠得多。
最后分享一个调试小技巧:想知道当前节点上 kubelet 认为你的 Pod 健不健康,不用去看控制台,直接查 Pod 的.status.conditions:
kubectl get pod order-service-xxx -o jsonpath='{.status.conditions[*].type}={.status.conditions[*].status}'输出会明确告诉你Ready=True还是Ready=False、ContainersReady是真是假。配好探针之后,这个命令会变成你排查服务不可用问题时的第一条命令,比去看什么监控面板都快。