1. 探针到底在解决什么问题
1.1 容器“活着的假象”
我一直觉得,容器编排里最容易让人忽略、但坑最大的概念,就是健康检查。
举一个我自己真实遇到过的场景:某服务发布到集群里,Pod 状态一溜烟的 Running,日志也没有异常,但业务方反馈接口超时率奇高。我去看监控,CPU 和内存都正常,端口也在监听。折腾半天才发现,这个服务里有一个后台线程池已经全部卡死,主流程还能响应几个固定接口,但真正的核心链路已经完全不工作了——进程没死,端口还通,Pod 看起来非常健康。可实际上呢?它已经是一个“植物人”容器。
这时候如果没有健康检查探针,kubelet 会认为容器一切正常,不会重启它,也不会把它从服务负载均衡里摘掉。流量照样往里打,用户照样超时。这就是容器化之后最常见的一类故障:进程在,但业务不健康。
探针的核心价值就在这里:它不是判断“进程活没活”,而是判断“这个服务能不能正常干活”。能干活,就留下;不能干活,要么把它摘掉流量,要么直接重启容器。
1.2 没有探针的调度灾难
探针影响的还不仅仅是单个容器的重启问题。它跟 Service 的 Endpoints、Deployment 的滚动更新、Pod 的就绪状态都直接挂钩。
比如一个服务没有配置 readinessProbe,那么只要容器启动完成,Pod 就会进入 Ready 状态,立刻被加入 Service 的后端列表。如果你的应用启动初始化需要 30 秒,而容器进程 1 秒就拉起来了,那这 30 秒内所有流量都会被导到一个还没准备好的实例上。用户看到的现象就是:刚发布完,立刻一批 502、504。很多人这时候去查代码、查网关,其实根因就是探针没配或者配得不合理。
还有滚动更新场景。Deployment 做滚动更新时,要不要把新 Pod 接入流量、什么时候算更新完成,依赖的就是 readinessProbe。如果探针没配,新 Pod 秒级 Ready,老 Pod 立刻被销毁,而新 Pod 其实还没真正就绪,服务就相当于在悬崖边上换轮子。
所以探针不是在 Pod 上随便加几行 YAML,而是整个发布流程和流量治理的基础设施。它解决的问题可以归纳为三句话:故障了能不能被及时发现,发现了以后流量往哪儿走,以及新实例什么时候可以接客。
1.3 kubelet 是那个执笔医生
理解探针的执行机制,对后面调参和排障很有帮助。
所有探针都不是容器内部自己上报状态的,而是由节点上的 kubelet 按照你配置的频率主动发起探测。你配了 httpGet,kubelet 就以 HTTP 客户端身份去请求 Pod 的某个路径;你配了 tcpSocket,kubelet 就去尝试建立 TCP 连接;你配了 exec,kubelet 就在容器内执行这条命令。
这个机制有一个容易被人忽略的细节:探针探测结果反映的是 kubelet 视角的服务状态,所以探测路径、端口、协议必须站在 kubelet 的角度去验证。比如你写了一个健康检查接口,默认监听在 127.0.0.1 上,自己在容器里 curl 一下没问题,但 kubelet 访问 Pod IP 上的这个端口时根本连不通。这种案例非常常见。
另外探针本身是有开销的。每次探测都会创建连接、执行命令、消耗一次进程资源。频率越高,资源开销越大,特别是 exec 类型的探针,每次都要 fork 一个进程。探针设计不是拍脑袋写上去就完事的,它需要结合资源限制和应用的真实启动速度来权衡。
2. 三种探针,各有各的职责
2.1 livenessProbe:容器挂了要重启
livenessProbe 负责回答一个问题:这个容器要不要重启?
如果探测失败,kubelet 会根据 restartPolicy 来决定是否重启容器。默认 restartPolicy 是 Always,所以常见的效果就是 Pod 反复重启。liveness 探针判断的是“进程是不是僵死到已经无法恢复”,一旦判定失败,核心动作就是干掉容器,重新拉一个。
这里要特别注意:livenessProbe 是一种“熔断式”的检测手段,不是用来做流量摘除的。它的失败会导致容器重启,非常暴力,所以不适合用来探测外部依赖是否可用。比如你的数据库抖动了一下,健康检查接口因为依赖数据库而返回 500,liveness 判定失败,容器被重启;重启之后数据库还处于抖动期,容器又失败,又重启。结果就是问题不但没解决,反而引入了一堆无意义的重启开销,整个服务的可用性反而更差。
我建议把 liveness 的检测目标放在“我这个进程是否还具备基本工作能力”上,而不是“我的所有依赖是否全绿”。一个比较经典的实现是:健康检查接口内部只检查进程状态,不检查数据库、缓存、消息队列这些外部组件;外部依赖的健康状态单独上报到监控系统,或者用 readiness 来承担。
2.2 readinessProbe:准备好了再接流量
readinessProbe 负责回答另一个问题:这个 Pod 要不要接收业务流量?
它和 liveness 的核心区别在于失败后的动作。readiness 失败不会重启容器,而是把 Pod 的 Ready 状态置为 False,kubelet 会将该 Pod 从 Service 的 Endpoints 列表里摘掉。也就是说,流量不再往这个实例上打,但容器本身还继续运行。
等 readiness 重新探测成功,Pod 会再次进入 Ready 状态,重新加入 Endpoints,开始接收流量。所以 readiness 适合做依赖检查:应用启动时,先检查数据库连接、配置中心、下游服务是否可用,全部可用才返回健康;任何依赖不可用,就从负载均衡里摘掉,等恢复后再接流量。
最典型的使用场景是蓝绿发布和滚动发布。新版本 Pod 启动后,readiness 探针会持续探测,直到确认新实例真正就绪,才把它加入 Service。这样旧的实例在被销毁前,会有很短的一段新旧共存时间,流量平滑切换,不会出现“服务还没起来就已经接了流量”的尴尬。
2.3 startupProbe:慢启动容器的保命符
startupProbe 是这三者里比较年轻的一个,解决的问题是:容器启动很慢,但启动后运行稳定,该怎么做健康检查?
以前没有 startupProbe 的时候,慢启动应用只能靠调大 initialDelaySeconds 和一些阈值去硬扛。但 initialDelaySeconds 是写死的,设短了,应用还没初始化完就开始探测,连续失败触发重启;设长了,原本 5 秒就能起来的应用,硬要等 60 秒才开始探测,滚动更新非常拖沓。
startupProbe 的思路完全不同:它只负责判断“容器是否完成启动”,一旦 startupProbe 探测成功,kubelet 就会停止继续执行 startupProbe,转而执行配置的 livenessProbe 和 readinessProbe。在 startupProbe 成功之前,其他探针是不会生效的。
这就给慢启动应用留出了一条通道:你可以把 startupProbe 的 failureThreshold 设成很大,比如 30 次、60 次,周期 5 秒,等于给了应用 150 秒到 300 秒的启动窗口;而一旦启动完成,后续的 liveness 和 readiness 仍然保持快速响应,不会因为启动慢而承担重启风险。
2.4 三种探针的选型对比
我自己在项目里总结了一套选择逻辑,先按这个来判断再加配置:
| 探针类型 | 失败后的行为 | 核心用途 | 是否适合检查外部依赖 |
|---|---|---|---|
| livenessProbe | 重启容器 | 检测僵死、无法恢复的进程 | 不适合 |
| readinessProbe | 踢出 Service 端点,停止流量 | 检测是否就绪,管理流量接入 | 适合 |
| startupProbe | 暂时不起作用,只等启动 | 给慢启动应用争取初始化时间 | 不适合 |
大部分无状态服务,readiness 是必须有的;liveness 则需要谨慎配置,不是所有服务都建议加;startup 主要面向启动特别慢的应用。三者之间不是互相替代的关系,而是配合使用。比如一个启动需要 20 秒的 Java 服务,可以配置 startupProbe 给足启动时间,同时配 readinessProbe 管理流量接入,再配一个 livenessProbe 保障运行期僵死能被发现。
3. 四种探测方式,各自的水有多深
3.1 httpGet:最常用,但细节容易翻车
httpGet 是最常见的探测方式,kubelet 会对 Pod 的某个路径发起 HTTP GET 请求,根据响应状态码判断是否健康。判断逻辑是:状态码在 200 到 399 之间算成功,其他都算失败。
它比较简单直观,只要应用暴露一个 HTTP 健康检查端点就行,大部分 Web 服务都很容易接上。但细节上有个坑:很多组件会把健康检查端口单独监听在一个管理端口上,而不是业务端口。这时 httpGet 的 port 字段就必须写成管理端口,而不是业务端口,否则 kubelet 探测的是另一个服务,结果自然不稳定。
还有一个坑是 httpHeaders 和 host 字段。如果你的健康检查端点需要自定义 Header 才能访问,比如内部接口需要 X-Health-Check: true,那必须在探针配置里加上这个 Header。另外,默认情况下 HTTP 请求的 Host 是 Pod IP,如果应用做了基于 Host 的路由校验,可能返回 404,需要显式配置 host 字段。
httpGet 还支持配置 scheme,默认是 HTTP,如果健康端点只支持 HTTPS,需要改成 HTTPS。但注意,如果是自签名证书,kubelet 的客户端默认校验证书的话可能会失败,这种情况下一般建议让健康检查端点保持 HTTP,避免证书打扰探针逻辑。
3.2 tcpSocket:端口通就行?不一定
tcpSocket 探测方式的理解很简单:kubelet 尝试和 Pod 的某个端口建立 TCP 连接,连接成功就认为健康,失败就认为不健康。
它适合那些没有 HTTP 接口的服务,比如传统 RPC 服务、数据库代理、消息队列客户端所依赖的端口等。但这里要警醒:端口能建立不代表业务正常。只要进程监听了端口,哪怕内部已经完全卡死,TCP 握手照样可以成功。你的服务可能已经处于“端口通、业务死”的状态,但探针检测完全是绿的。
所以 tcpSocket 一般当作“端口可用性检测”来用,适合判断进程是否挂掉,不适合判断复杂的业务健康状态。如果服务同时暴露了 HTTP 管理接口,那肯定是优先用 httpGet,而不是 tcpSocket。
3.3 exec:灵活但消耗最大
exec 类型的探针会在容器内部执行一条命令,通过命令的退出码判断是否健康:退出码为 0 表示成功,非 0 表示失败。
它非常灵活,因为你可以写任意脚本来进行判断。比如检测一个 pid 文件是否存在、检查一个本地队列的长度是否超过阈值、执行一条 SQL 查询确认数据库通不通等,都可以用 exec。
但 exec 的坑也不少。每次探针都要在容器内创建一个进程,如果 periodSeconds 设得很小,比如 5 秒,那么每个节点上每 5 秒就要 fork 一次进程,对高密度部署的节点会造成不小压力。另外,容器镜像里如果缺少 bash、curl 等命令,或者脚本使用了绝对路径但镜像里不存在,探针会持续失败。最常见的例子是脚本开头用了#!/bin/bash,但镜像里只有 sh,没有 bash,一条看似无害的脚本直接报错退出。
使用 exec 的时候,脚本里的所有命令都要尽量用绝对路径,并且在部署前先手动kubectl exec进容器执行一遍,确认在容器内能跑通,而不是只在本地验证。
3.4 grpc:面向新一代微服务协议
grpc 探测是后来才加入的,适用于暴露 gRPC 接口的服务。kubelet 会作为 gRPC 客户端,使用标准的 gRPC Health Checking 协议去探测服务,要求服务端实现了这个协议。
这套方案最大的优势是:不需要额外暴露一个 HTTP 端口或执行命令,只要 gRPC 服务本身支持健康检查协议就行。目前很多 gRPC 框架都内置了健康检查能力,比如 Go 的 grpc-go 可以注册 health server,Java 的 grpc-java 也有对应的实现。
不过 grpc 探针也有前置条件:容器里的 gRPC 服务必须实现了grpc.health.v1.Health服务,并且启动时注册到 gRPC server 上。如果服务本身没有实现,探针再怎么配也没用。所以使用前一定要先确认服务端是否支持。
3.5 四种探测方式的选型建议
| 探测方式 | 适用场景 | 优点 | 风险点 |
|---|---|---|---|
| httpGet | Web 服务、有 HTTP 管理接口 | 直观、判断业务逻辑 | 端点逻辑别太重,会加大探测开销 |
| tcpSocket | 无 HTTP 接口的服务 | 简单、轻量 | 端口通不代表业务正常 |
| exec | 自定义判断逻辑 | 灵活 | 资源开销大,镜像命令依赖多 |
| grpc | gRPC 微服务 | 协议标准、无需额外端口 | 服务端必须实现健康协议 |
组合使用时,一般建议 probe 类型和端口单独设计。业务端口管流量,管理端口管健康检查,这样探测动作不会干扰业务请求,也不会出现因为流量的波动导致探针误判。
4. 参数调优,别拿默认值当免死金牌
4.1 核心参数逐项拆解
探针的 YAML 配置里有一组参数,看起来平平无奇,但在生产环境里每一个都值得反复推敲。
initialDelaySeconds 是容器启动后多少秒开始探测。这个值不是越大越好,设置太大会让启动完成后的就绪时间被拖长,给用户的感觉就是“发布特别慢,半天不接流量”。设置太小则容器可能在初始化还没开始时就被探测,连续失败触发重启。
periodSeconds 是每次探测的间隔。默认值是 10 秒,适合大多数场景,但如果你的服务流量波动大、故障恢复要求快,可以适当缩小到 3 秒或 5 秒。反过来,如果健康检查端点本身有较大开销,比如要查数据库,那间隔太短会放大数据库压力。
timeoutSeconds 是单次探测超时时间。这是很微妙的一个参数:如果应用健康检查接口偶发慢响应,比如要连一次下游,偶尔超过 1 秒,而 timeoutSeconds 还是默认的 1 秒,那探针就会误判失败。不要把 timeoutSeconds 设得和接口平均响应时间一样,要留出余量。
failureThreshold 是连续失败多少次才判定探针失败。默认值是 3。如果业务上是那种“抖动一下就能自己恢复”的服务,可以适当调大,比如 5 或 8,避免一次抖动就触发重启或者摘流量。但也不能调太大,否则真正不可用的时候,系统要很久才能反应过来。
successThreshold 是连续成功多少次才判定探针恢复。默认值是 1,意思是只要探测成功一次就算恢复。这个值一般不需要改大,但如果你担心偶发成功造成的抖动脉冲,也可以设成 2,不过代价是恢复时间的延长。
4.2 生产配置的计算思路
实际配置的时候,我一般不会直接拍脑袋填数,而是先想清楚“服务最长启动时间”、“健康检查接口的最长耗时”和“业务上能够容忍的中断时长”这三个前提。
举个例子。有一个服务,启动时间平均 15 秒,极端情况下可能到 40 秒;健康检查接口逻辑很轻,只有一次本地内存检查,耗时通常在 20ms 以内;发布时用户能容忍的最长不可用时间是 60 秒左右。
那么配置可以是:
startupProbe: httpGet: path: /health/startup port: 8080 initialDelaySeconds: 0 periodSeconds: 5 timeoutSeconds: 2 failureThreshold: 30 successThreshold: 1startupProbe 给足了 150 秒的启动窗口,即使慢启动到 40 秒也在安全范围内。
然后配置 readinessProbe:
readinessProbe: httpGet: path: /health/ready port: 8080 initialDelaySeconds: 10 periodSeconds: 5 timeoutSeconds: 2 failureThreshold: 3 successThreshold: 1一旦 startupProbe 成功,readinessProbe 就会开始工作,周期 5 秒,连续失败 3 次后摘流量,大约最快 15 秒内完成摘除。
livenessProbe 如果确实需要加,可以设置得保守一点:
livenessProbe: httpGet: path: /health/live port: 8080 initialDelaySeconds: 20 periodSeconds: 15 timeoutSeconds: 2 failureThreshold: 5 successThreshold: 1它检测的是“进程是否彻底僵死”,所以周期可以稍微拉长,阈值也可以调大。这里有个默认值需要警惕:timeoutSeconds 默认是 1 秒,很多服务的健康检查接口一次调用可能超过 1 秒,虽然看起来是极少数,但生产环境每天几十万次探测,偶发几次超时是必然的。所以只要健康检查接口里涉及远程调用,我都建议把 timeoutSeconds 至少调到 2 秒或 3 秒。
4.3 参数调优的几个实战原则
原则一:探针频率不是越高越好。有些同学刚接触探针,喜欢把 periodSeconds 调到 1 秒,觉得这样故障发现快。实际上这会给系统和应用带来持续压力,尤其是经健康检查接口透传到数据库的探针,频次高了之后数据库会最先扛不住。故障往往不是服务本身挂的,而是被探针打挂的。
原则二:window 大小的概念要清晰。判定失败所需的估算时间大约等于initialDelaySeconds + failureThreshold × (periodSeconds + timeoutSeconds),在规划业务可容忍中断时间时可以参考这个公式。默认配置的粗略值大约是 33 秒,时间不算长,也不算短。
原则三:不同环境用不同参数。联调环境里可以把探针阈值调小一些,便于快速暴露问题;生产环境则适当调大,避免抖动引发误杀。不要一套 YAML 走天下。
5. 常见问题与排查实录
5.1 Pod 一直重启,查日志没异常
这是最经典的问题:kubectl get pods 看到某个 Pod 的 RESTARTS 一直在涨,但是 kubectl logs 里又看不到明显的异常日志。
这种情况十有八九是 livenessProbe 探测失败导致的,而容器内的应用认为自己的业务逻辑没问题,自然不会打印错误日志。排查命令的方向是:
- 用
kubectl describe pod <pod>看 Events 里有没有Liveness probe failed的记录; - 看探针失败时返回的是什么错误,比如
HTTP probe failed with statuscode: 500; - 确认探针路径是否真实存在、状态码判断是否符合预期。
有一次我遇到一个服务,自带的健康检查端点在早期启动阶段会返回 200,但真正的内部组件还没初始化完成。导致 liveness 判定成功,等到流量进来的时候又不可用。这个问题的根因是健康检查端点太“诚实”了,没有反映真实的就绪状态。后来把 liveness 和 readiness 拆成两个不同的端点才解决。
5.2 服务一直 NotReady,流量撕裂
另一个高发问题是:Pod 状态是 Running,但 Ready 列一直是 0/1,服务也没有流量。
这通常是 readinessProbe 持续失败。可能原因包括探针配置指向了错误的端口或路径、应用依赖的下游服务不可用导致健康检查接口报错、健康检查接口本身有 bug,在特定情况下返回 500。
其中一种是:健康检查接口路径拼写错误。比如配置的是/healthz,但应用实际提供的路径是/actuator/health。这种问题在手动配置 YAML 时太常见了。
遇到 NotReady,我一般按这个顺序排查:先 describe 看探针失败原因;然后用kubectl exec进入容器,手动 curl 一下探针路径,看返回的到底是什么;再检查容器内监听的端口是否和配置一致。需要提醒的是,探针访问走的是 Pod IP,不是 localhost,所以手动验证时要访问 Pod IP 或者直接用容器内curl 127.0.0.1也可以,但要注意 listen 地址是否限制了仅本机访问。
5.3 exec 探针的脚本坑
exec 探针最大的坑就是“本地跑得通,容器内跑不通”。
镜像为了精简体积,常常不会装 shell 脚本里要用的命令。比如你写了一个检测脚本,用了grep、curl、find,本地环境全都有,但基础镜像里可能一个都没有。还有脚本的换行符问题:如果在 Windows 下编辑了脚本,换行符是 CRLF,放进 Linux 容器里执行可能直接报not found。
这些问题在配置 exec 探针时几乎都会遇见,我的做法是:脚本统一用 POSIX sh 语法,不依赖 bash 特性;脚本中所有命令都用绝对路径;部署前先在容器里手动执行一遍完整的探针命令,确认退出码为 0。
5.4 httpGet 用 localhost 导致的误判
还有一个高频翻车点是把 httpGet 的 host 或端点地址配置成 127.0.0.1,结果导致带网络策略或者监听地址受限的服务被误判失败。
默认情况下,kubelet 发起 httpGet 请求时访问的是 Pod 的 IP,并非容器的 localhost。如果你在探针配置里写了host: 127.0.0.1,那么 kubelet 实际上是在容器网络命名空间外访问,除非 Pod 使用了 hostNetwork,否则这个探测大概率会失败。
另一个类似问题是健康检查接口只监听了 IPv6 地址,而 kubelet 使用 IPv4 访问。这种情况不常见,但一旦遇到会非常隐蔽。排查时可以重点看一下应用启动日志里监听的 IP 是什么版本。
5.5 依赖启动顺序与探针的配合
多个服务同时启动时,如果服务 A 强依赖服务 B 已经就绪,但没有探针来协调,就可能出现 A 疯狂重试、B 还没起来的情况。
两种处理思路:一种是依赖方把启动阶段的错误重试做得足够稳健,启动慢一点没关系;另一种是依赖方配了 readiness 探针,只有依赖方自身就绪了才会接流量,本质上解决的是“发布期间新实例的流量接入时机”问题。两者之间要配合好:上游服务要等待下游服务的 readiness 全部为 Ready,再开始批量放流量。
探针只解决单个 Pod 的就绪评估,跨服务的依赖编排还需要结合流量调度策略来做,不要指望探针能解决整个拓扑的依赖关系。
5.6 探针排查工具清单
我平时排查探针相关问题,常用这几招:
kubectl get pods -o wide:看节点分布和 Pod 状态;kubectl describe pod <name>:查看探针失败的原因和被 kill 的时间点;kubectl logs <name> --previous:看容器上一次退出的日志;kubectl exec -it <name> -- curl http://<pod-ip>:<port>/health:手动模拟探针请求;- 查看 kubelet 日志,里面有每次探针的执行结果,能帮确认探针是否在预期时间点被执行。
6. 生产落地:从“能用”到“好用”的几个关键点
6.1 探针端点的设计与分层
在生产环境,我不会让业务主接口和健康检查端点混在同一个逻辑里。比较推荐的做法是:单独开一个管理端口,专门放健康检查端点,和业务端口分开。
比如一个 Spring Boot 服务,业务端口是 8080,管理端口是 8081;8080 放正常业务接口,8081 放/health/live和/health/ready。这样健康检查请求不会和业务流量抢线程池,也不会因为业务接口偶发慢请求影响探针结果。
端点的分层也很重要:/health/live只检查进程是否存活,/health/ready才检查缓存、数据库、消息队列等依赖是否可用。liveness 探针指向/health/live,readiness 探针指向/health/ready,两者各司其职。这样的好处是,外部依赖抖动时,readiness 先摘流量,不会触发重启;真正僵死时,liveness 才出手重启容器。
6.2 探针与优雅终止的配合
Pod 被删除时,kubelet 会先执行 preStop 钩子,再发送 SIGTERM 信号,等待 terminationGracePeriodSeconds 之后强制 SIGKILL。
如果你只配置了探针,但没有和优雅终止配合,发布时还是会看到少量请求失败。原因是:Pod 收到 SIGTERM 后,如果立即停止接收新请求,但负载均衡里还可能残留旧连接,这期间打进来的流量就会失败。
优雅终止的标准做法是:readiness 探针在收到 SIGTERM 之前先开始失败,让 Pod 被摘除;preStop 钩子里 sleep 几秒,给现有请求一个排空的时间窗口。大多数情况下,preStop sleep 5 秒左右就能解决绝大部分发布时的连接断开问题。
6.3 探针结果的可观测性
探针是系统自动执行的动作,但如果不把探针结果纳入监控,它反而可能变成盲区。生产环境里我会配合以下几点:
- 将容器的 Ready 状态变化接入告警,当 Ready 副本数长期低于期望副本数时触发告警;
- 记录探针失败事件到审计系统,便于事后回溯;
- 对健康检查端点的响应耗时做监控,探针端点一旦变慢,本身就说明服务有一定负载压力了。
探针不是一劳永逸的配置。服务的启动时间、依赖的响应速度、流量高峰的变化都会影响探针的参数选择。每次发版前花一点时间审视探针配置是否仍然匹配当前业务,比故障发生以后再手忙脚乱要划算得多。
说回开头那个场景,我后来在那套服务上补了 readinessProbe,并重新设计了健康检查端点,把依赖检查从 liveness 里彻底拿掉。之后同样的故障再也没有对线上流量造成影响。
探针这门功夫,看起来只是几个 YAML 字段,但背后的坑和取舍,真的值得反复打磨。如果你刚接触 Kubernetes,建议从最简单的 httpGet + readinessProbe 组合开始,先把“流量不乱打”这个问题解决掉,再慢慢考虑 liveness 和 startup 的组合策略。配置完之后,别忘了在测试环境故意停一次依赖、关一次健康检查接口,看看探针的响应是否符合预期——这一步,我每次发布前都会做。