☰
Kubernetes探针完全指南:liveness/readiness/startup实战与避坑
2026/10/18 21:31:43 网站建设 项目流程

做 Kubernetes 运维久了,你会慢慢发现一个真相:大部分服务“看起来正常”,实际上早就病入膏肓了。容器进程还在,端口还通着,监控面板上的 CPU、内存都正常,可用户体验就是卡顿、报错、超时。这种情况下,指望 Kubernetes 自己发现问题基本不可能,除非你给它装上一双“眼睛”——探针(Probe)。

今天我想把 livenessProbe 和 readinessProbe 这套东西彻底聊透。不只是把 YAML 参数背一遍,而是结合我自己在真实环境里踩过的坑、调过的参、救过的命,讲清楚探针的工作原理、参数选型逻辑,以及怎么靠它把服务可用性从 99% 拉到 99.99%。内容适合刚入门 K8s 的开发者,也更适合那些被“诡异故障”折磨过、想真正理解探针机制的运维同学。

1. 探针到底是什么:先从三个故障场景说起

1.1 没有探针时,服务是怎么“假死”的

先别急着看配置。我在讲探针原理之前,习惯先让团队想一个问题:你部署了一个 Web 服务,容器起来之后,Kubernetes 怎么知道这个服务是“活的”还是“死的”?

答案很残酷——默认情况下,它不知道。

Kubernetes 只知道 Pod 里的容器进程是否还在。只要进程没退出,它就认为这个容器是健康的,哪怕这个进程已经陷入死锁、内存溢出、或者内部线程池全部耗尽,对外请求全部卡住。这种情况就是典型的“假死”:进程活着,服务死了。

我见过一个真实案例。某个内部系统用的是某个第三方 HTTP 框架,运行几小时后会出现连接泄漏,端口仍然监听,curl也能握手成功,但请求永远不会返回。没有探针的时候,K8s 对此毫无感知,流量照常打进来,用户请求全部超时,最后靠人工发现重启才恢复。整个过程里,容器一直处于 Running 状态,重启策略根本触发不了。

这个例子非常典型地说明了:进程存活不等于服务健康。探针的存在,就是把“容器活着”这个粗粒度判断,细化成“服务是否真正可用”的精细判断。

1.2 三种探针的分工:各管一段生命周期

Kubernetes 提供了三种探针,很多人一开始记不住它们有什么区别,其实用生活里开餐馆来类比,一下就通了。

  • livenessProbe(存活探针):相当于餐厅的“食品安全检查”。检查的是这个店还活着吗?如果发现厨师晕倒了(进程卡死),直接关门整顿(重启容器)。
  • readinessProbe(就绪探针):相当于餐厅门口的“营业中”牌子。牌子没亮,客人就不该进来(流量不转发);亮了,代表厨师准备好了、菜能做了,可以接待客人。
  • startupProbe(启动探针):相当于餐厅开业的“装修期”。新店装修没完成之前,不让客人进来,也不催厨师干活,等装修完了再正常营业。

放在 K8s 的语境里更精确一点:

探针类型判断依据失效后动作典型场景
livenessProbe容器是否还“活着”杀掉容器,按重启策略重建死锁、内存泄漏、goroutine 泄漏
readinessProbe服务是否“可用”摘除 Service 端点,流量不转发依赖未就绪、缓存未加载、正在过载
startupProbe进程是否完成初始化存活/就绪探针暂停工作启动耗时长的 Java 应用、AI 模型加载

特别强调一点:readinessProbe 失败不会重启容器,只会把 Pod 从 Service 的 Endpoints 里摘掉。这是很多初学者最容易搞混的地方。它俩职责完全不同,一个是“要不要杀掉你”,另一个是“要不要给你流量”。

1.3 三种检测机制:exec、HTTP、TCP 怎么选

探针要判断“健康与否”,总得有检测手段。Kubernetes 提供了三种机制:

HTTP 请求(httpGet):向容器的某个 HTTP 端口发 GET 请求,只要返回码在 200 到 399 之间,就判定为健康。这是互联网应用最常用的方式,因为它检测的是“应用层”,能真实反映业务是否就绪。

TCP 连接(tcpSocket):尝试建立 TCP 连接,成功就算健康。适合那些没有 HTTP 接口的服务,比如数据库、消息队列、缓存中间件。但它最多只能检测到“端口能连上”,端口能连不代表业务逻辑正常,这在后面会成为坑。

Exec 命令(exec):在容器内执行一条命令,以退出码是否为 0 作为判断标准。适合那些既不开 HTTP 端口、又不监听 TCP 的服务,比如定时任务容器,或者进程内部有自检接口的服务端。

我自己的选型经验是:凡是能做 HTTP 健康检查的,一律优先用 httpGet。TCP 检查虽然简单,但它看不到应用内部状态;Exec 检查灵活,但每次执行都会产生进程开销,而且容器里不一定有完整的 shell 环境,排查问题时会很难受。

2. 探针参数深度拆解:每个时间含义都要懂

2.1 四个时间参数:一次完整的探针检查生命周期

探针的 YAML 配置里有一堆Seconds,很多人只是照抄默认值,出了问题根本不知道怎么调。这里我用一条时间线把它们串起来。

假设场景是这样的:

livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 15 periodSeconds: 10 timeoutSeconds: 2 failureThreshold: 3

容器启动后,探针不会立刻开始工作。K8s 会先等initialDelaySeconds(初始延迟,15 秒),给应用留出初始化时间。这个延迟结束后,探针开始第一次检查,之后每periodSeconds(周期,10 秒)执行一次。

每次请求发出后,K8s 最多等待timeoutSeconds(超时,2 秒)。如果请求在 2 秒内没返回,这次检查判定为失败。注意,一次失败并不会立即触发动作,而是连续失败达到failureThreshold(失败阈值,3 次)才会执行探针对应的动作——对于 livenessProbe 来说就是重启容器。

对应地,每个探针还有一个successThreshold(成功阈值),默认值是 1,意思是连续成功 1 次就算恢复健康。一般保持默认就好,不需要改。

来看一个具体的时间线计算。假设:

  • 容器启动时刻:T0
  • initialDelaySeconds = 15,那么第一次探针在 T0+15 发起
  • periodSeconds = 10,第二次在 T0+25,第三次在 T0+35
  • 假设从 T0+15 开始连续失败,
    • 第一次失败:T0+15(等待 2 秒超时后判定失败)
    • 第二次失败:T0+25
    • 第三次失败:T0+35
  • 在 T0+35 这次失败之后,kubelet 触发 kill 操作,容器重启

这也就是说,从“探测开始”到“容器被杀”,最坏情况下需要(failureThreshold - 1) * periodSeconds + timeoutSeconds的时间。代入数值就是(3-1)*10 + 2 = 22 秒,加上 initialDelaySeconds 的 15 秒,容器从启动到被强制重启,最快路径约 37 秒。

2.2 为什么 initialDelaySeconds 是最容易配错的参数

实话说,我排查过大量探针导致的连环重启事故,八成都能归结到 initialDelaySeconds 没配好。

配小了:应用还在做数据库连接池初始化、加载配置文件、建立缓存连接,探针就来了。区区 2 秒的 timeout,应用内一个慢查询就把健康检查拖超时,连续 3 次之后容器被重启。重启之后又是一轮初始化,又超时,又重启……形成经典的 CrashLoopBackOff。

配大了:虽然不会有重启风险,但 readinessProbe 的 initialDelaySeconds 如果设置过大,会导致服务启动后迟迟不对外提供流量。我见过一个团队配了 120 秒,结果应用其实 20 秒就就绪了,白白浪费了 100 秒的等待时间。发布高峰期,这种浪费会直接影响扩容和滚动更新的效率。

正确做法:不要靠猜。在本地环境里先用kubectl logs观察应用日志,记录从进程启动到“对外服务就绪”的实际耗时,在这个耗时基础上增加 20% 到 30% 的安全余量。Java 应用尤其注意,JVM 启动、Spring 容器加载、连接池预热的耗时可能远超预期。

2.3 成功阈值、失败阈值到底该怎么定

failureThreshold 这个参数同样值得仔细想。它本质是一个“容忍度”设计,目的是防止因为网络抖动或单次超时导致误杀。

  • 设置太小(比如 1):只要健康检查一次超时,立刻重启容器。网络偶发抖动、负载瞬时升高都会触发重启,导致服务频繁重建。
  • 设置太大(比如 5 以上):容器要连续失败很多次才被重启,相当于容忍一个病态服务继续对外提供服务,用户的体验已经崩了,你却还在 “观察”。

正常 HTTP 服务的健康检查接口通常响应很快(毫秒级),一次检查失败确实是异常信号。我一般把 livenessProbe 的 failureThreshold 设置在 2 到 3 之间,readinessProbe 可以稍微宽松一些,设置在 3 左右。因为 readinessProbe 失败只是暂时摘流量,不应该太“激动”;livenessProbe 则要相对灵敏,避免让坏实例持续接收流量。

另外注意一个隐藏细节:livenessProbe 和 readinessProbe 如果配置完全一样,就没有意义了。存活探针管的是容器进程层面的“活着”,就绪探针管的是业务依赖层面的“就绪”,两者检测的内容和频率应该有不同的设计。liveness 可以放宽周期(比如 20 秒),readiness 可以设得更密(比如 5 秒),这样才能快速感知依赖故障、及时摘除流量。

3. 实战:用探针组合拳构建高可用服务

3.1 一套标准的 HTTP 探针配置

先给出一份完整的 Deployment YAML。这是一个模拟的订单服务,内部有/healthz接口,会检查数据库连接池、本地缓存状态,返回 JSON 格式状态码。

apiVersion: apps/v1 kind: Deployment metadata: name: order-service 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:1.4.2 ports: - containerPort: 8080 name: http readinessProbe: httpGet: path: /healthz port: http initialDelaySeconds: 10 periodSeconds: 5 timeoutSeconds: 2 successThreshold: 1 failureThreshold: 3 livenessProbe: httpGet: path: /healthz port: http initialDelaySeconds: 30 periodSeconds: 15 timeoutSeconds: 3 successThreshold: 1 failureThreshold: 3

这份配置里有几个细节值得讲。

readinessProbe 的 periodSeconds 比 livenessProbe 更短。因为就绪状态变化需要被快速感知,比如数据库挂了,最好在 5 秒内把流量从故障实例摘掉,避免用户请求打到坏实例上。而 liveness 关心的是“容器要不要重启”,节奏可以慢一点,15 秒足够了。

initialDelaySeconds 分别是 10 和 30。这是我在模拟环境里观察日志后定出来的:应用启动到 HTTP 端口可访问大约是 8 秒,readiness 设 10 秒刚好;但业务真正达到“健康”状态要 25 秒左右(要预热缓存),liveness 设 30 秒是比较安全的。如果你两个探针共用一个健康检查接口,但 initialDelaySeconds 没有区分,就可能出现“端口通了但内部还没就绪”的尴尬期。

httpGet 的 port 字段可以用端口名字。比如port: http,引用的是上面定义的容器端口名。这样以后改实际端口号时,不需要同步改探针配置,减少出错机会。

3.2 startupProbe 的正确使用姿势:解决启动慢的 Java 应用

有些人想不通:为什么有了 initialDelaySeconds,还要搞一个 startupProbe 出来?它是不是多余的?

原因在于 initialDelaySeconds 是固定的“死等”。你给多了浪费启动时间,给少了又会导致存活探针误杀。但很多真实应用,启动时间不是稳定的。同样是 Spring Boot 服务,冷启动可能要 40 秒,热启动只要 20 秒。如果 livenessProbe 的 initialDelaySeconds 按冷启动配置了 45 秒,那么在热启动场景下,它会延迟探活节奏,拖慢发布速度;如果按热启动配 20 秒,冷启动时 liveness 就会误杀容器。

startupProbe 的价值在于:它成功之前,livenessProbe 和 readinessProbe 全部不生效。而 startupProbe 自己可以用相对宽松的周期和阈值,给慢启动应用足够的“宽限期”。

看这个配置,一个加载模型文件的应用,启动可能需要 1 到 2 分钟:

startupProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 0 periodSeconds: 10 timeoutSeconds: 3 failureThreshold: 30 successThreshold: 1

periodSeconds: 10乘以failureThreshold: 30,意味着 startupProbe 给了应用最多 300 秒的启动时间。一旦 startupProbe 成功一次,livenessProbe 和 readinessProbe 才开始接管。

我个人强烈建议:所有初始化耗时不确定的新服务,都配上 startupProbe。它彻底解决了 “initialDelaySeconds 拍脑袋” 的问题。配置上,failureThreshold 可以放大一点,因为 startupProbe 只是在启动阶段生效,不会导致频繁重启。比如 10 秒一次、连续 60 次,等于允许 10 分钟内慢慢启动,这对 AI 模型加载、大数据组件启动这类场景非常实用。

3.3 滚动更新中的探针:readinessProbe 是流量切换的开关

探针的作用不只是在运行期。在滚动更新、扩容缩容这些场景里,readinessProbe 可以说是控制流量切换的开关。

想象一下滚动更新流程:

  1. 新 Pod 被创建,执行 readinessProbe。
  2. 探针一直失败,新 Pod 状态仍然是Running但不接收流量,旧 Pod 继续提供服务。
  3. 探针成功,新 Pod 的 IP 被加入 Service 的 Endpoints,开始接收流量。
  4. 继续创建下一个新 Pod,直至所有副本更新完成。

如果没配 readinessProbe,K8s 会在 Pod 进入 Running 状态后立刻把流量切过去。假如你的应用需要预热 JIT 编译、加载远程配置、建连数据库,流量切过去时应用还没准备好,大量请求会直接报错。

这里我吃过一个实亏。某一次发布,因为配置中心临时抖动,新 Pod 启动后配置文件拉不下来,业务端口没有监听。由于当时没有配置 readinessProbe,Service 照样把流量转发给新 Pod。发布期间线上大量 502,查了大半天才定位到是“没就绪的 Pod 接收了流量”。从那以后,我给自己定了一条铁律:凡是接入 Service 的 Deployment,必须配置 readinessProbe,否则不允许上生产。

另外,还有一个细节容易被忽略:rolling update 的 maxUnavailable 与 readinessProbe 的关系。K8s 滚动更新的默认策略会先停掉一个旧 Pod,再创建新 Pod。如果你的应用依赖数据库迁移或接口兼容性,而 readinessProbe 检测又不够严格,发布期间新旧版本都会在 Service 里,流量可能打到不兼容的旧版本上。要缓解这个问题,可以把 maxSurge 和 maxUnavailable 配置得保守一点,比如同时最多多启动 1 个新 Pod,同时最多停 1 个旧 Pod,保证服务容量不出现断崖。

4. 探针的排查实录与最佳实践

4.1 我在生产环境遇到的四种探针故障

故障一:探针接口返回 500,导致滚动更新永远无法完成

某个服务的/healthz接口内部做了强依赖检查,下游数据库一旦有延迟,接口就返回 500。readinessProbe 一直失败,新 Pod 永远进不了 Endpoints,滚动更新卡住,旧 Pod 也迟迟不缩容。这是很典型的“就绪探针太严”导致的问题。

解决思路:健康检查接口要区分“存活”和“就绪”的语义。存活检查只查进程和基础资源,就绪检查才查外部依赖。更合理的做法是/healthz检查本地状态,/readyz检查下游依赖。用不同路径分别挂到 liveness 和 readiness 上,互不干扰。

故障二:TCP 探针把“半连接”误判为健康

一个缓存中间件服务用的是 tcpSocket 探针,后端进程发生线程池耗尽,新的 TCP 连接虽然在操作系统层面能握手成功,但应用层根本不处理任何命令。此时 TCP 探针依旧报告健康,流量持续分发到故障节点。

这类问题的根源是 TCP 探针检测不到应用层状态。建议这类服务如果能暴露 HTTP 健康检查接口,就换成 httpGet;实在做不到,也要配合 metrics 和告警做兜底,避免探针成为唯一的健康依据。

故障三:Exec 探针命令执行时间过长

某个容器内没有 curl,团队就用 exec 探针执行自定义 shell 脚本做检测。脚本里有一次远程 HTTP 调用,响应稍微慢一点,脚本执行时间就会超过 timeoutSeconds,探针判定失败,容器被反复重启。

教训有两条:第一,exec 探针里的命令必须尽量轻量,不要做网络调用,最好是读文件、算资源、查本地端口这类毫秒级操作;第二,给探针脚本设置自己的内部超时(如timeout 2 /bin/sh check.sh),防止脚本挂死拖垮 kubelet 的资源。

故障四:livenessProbe 和依赖故障形成“死循环”

服务启动后,readinessProbe 因为数据库连接失败而持续失败,这本身是安全行为——流量不会进来。但如果同时配置了 livenessProbe 也依赖数据库,那就有麻烦了:liveness 连续失败触发重启,容器重启后再次初始化,数据库还没恢复,又失败又重启。整个 Pod 在 CrashLoopBackOff 里循环,而问题源头是数据库,重启容器毫无意义。

最终方案是:livenessProbe 永远不要检查外部依赖,只检查本进程的存活状态。外部依赖挂了,应该让服务继续运行、readiness 摘流量、等待依赖恢复。这样既避免了反复重启,也保留了故障恢复的能力。

4.2 排查命令与调试思路

遇到探针问题,第一件事不是看 YAML,而是看事件和日志。我常用的排查命令整理成了一套组合拳:

# 查看 Pod 的详细状态和最近事件 kubectl describe pod <pod-name> # 查看探针是否真的在发请求(日志里会有 /healthz 的记录) kubectl logs <pod-name> --tail=50 # 进入容器,手动模拟探针请求 kubectl exec -it <pod-name> -- curl -v http://127.0.0.1:8080/healthz # 查看容器是否被重启过,以及重启次数 kubectl get pod <pod-name> -o jsonpath='{.status.containerStatuses[0].restartCount}' # 查看 Service 后端是否包含该 Pod 的 IP kubectl get endpoints <service-name>

有两个小技巧值得分享。

通过 describe 里的事件信息定位探针失败。如果是 liveness 失败,事件里通常会出现Liveness probe failed: HTTP probe failed with statuscode: 500;如果是 readiness 失败,会出现Readiness probe failed字样。注意区分事件里出现的探针类型,不要一看到 probe 失败就以为容器要被杀。

用kubectl get endpoints验证流量摘除是否生效。当服务的某个 Pod 就绪探针失败后,它的 IP 应该从 Endpoints 列表中消失。如果 IP 还在,说明 readinessProbe 的配置没有按预期工作,或者 Service 选择的标签名没有匹配到对应 Pod。这个命令在排查“探针失败但流量还在”的问题时,几乎是百试百灵。

4.3 探针设计的最佳实践清单

最后把我这几年的配置经验和设计原则汇总成一张速查表,发布前对照检查一遍,能帮你少走很多弯路。

关注点推荐做法理由
探针接口语义liveness 查 /healthz,readiness 查 /readyz区分进程存活与应用就绪,避免互相干扰
HTTP 状态码判断200-399 算成功兼容重定向类返回码,避免把 302 当失败
liveness 周期10-20 秒不用太频繁,避免无谓压力
readiness 周期5-10 秒缩短故障摘除时间窗口
initialDelaySeconds日志实际就绪耗时 + 20%-30%防止启动初期误杀
startupProbe初始化时间不确定时优先使用替代拍脑袋的 initialDelay
外部依赖检查readiness 可以检查,liveness 不要防止外部故障引发容器反复重启
exec 探针脚本只做本地毫秒级操作避免脚本耗时过长导致误判
探针资源开销基于 exec 时控制进程创建频率kubelet 频繁创建进程会占用节点资源
发布验收滚动更新前人工摘除一个副本观察验证 readiness 摘流量真实生效

我自己在设计探针方案时,还有一个深层心法:探针的本质是给 K8s 一个明确的“健康信号协议”。你希望 K8s 在什么条件下重启你、在什么条件下不给你流量、在什么条件下认为你准备好接客,都要通过探针明确表达出来。这套协议设计得越清晰,K8s 的自动化能力发挥得就越充分,人力介入的次数就越少。

说到底,高可用不是靠 K8s 帮你扛出来的,而是靠你把应用的真实状态准确告诉 K8s,让它替你做最优决策。探针就是你们之间沟通的那根信号线。把这根线接对、接稳,比你多写一百行业务代码都管用。

最后分享一个我自己一直在用的小习惯:每次写完探针配置,我都会故意在模拟环境里把业务端口停掉,然后观察 K8s 的反应。该重启的重启、该摘流量的摘流量,全部符合预期之后,再放到生产。这个“故障演练式验证”花不了几分钟,却能在真正出故障时帮你守住底线。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询