凌晨两点,告警群里突然炸了锅------线上服务的 Pod RESTARTS 计数狂飙,几个 Pod 已经滚进了 CrashLoopBackOff。排查了半天,应用本身没毛病,日志也干干净净。最后定位到一个你可能从未认真看过的配置:livenessProbe.initialDelaySeconds: 5。
而你的 Java 应用,冷启动需要 45 秒。
这个场景,我相信每个 SRE 都遇到过至少一次。Liveness Probe 配置不当导致的 Pod 频繁重启,是 Kubernetes 生产环境中最常见但也最容易被忽视的问题之一。今天,我把这个话题从参数原理到生产配置彻底讲透。
先搞清楚这几个参数到底在干什么
官方 API 定义中,探针的核心参数如下:
|-----------------------|------|-----|---------------|-----------------|
| 参数 | 默认值 | 最小值 | liveness 特殊限制 | 含义 |
| initialDelaySeconds | 0 秒 | 0 | --- | 容器启动后等待多久开始首次探测 |
| periodSeconds | 10 秒 | 1 | --- | 每隔多久探测一次 |
| timeoutSeconds | 1 秒 | 1 | --- | 单次探测超时时间 |
| successThreshold | 1 | 1 | 必须为 1 | 连续成功几次才判定为成功 |
| failureThreshold | 3 | 1 | --- | 连续失败几次触发重启 |
这些值来自 Kubernetes API 的 Probe 对象定义。failureThreshold 默认 3,最小值 1;periodSeconds 默认 10 秒;successThreshold 对 liveness 和 startup 必须为 1------这是 API 层面的硬性约束,不是建议。
📌 这里有个容易混淆的点 :successThreshold 对 readinessProbe 没有必须为 1 的限制,你可以把它设为大于 1 的值,表示"连续成功 N 次才判定为就绪"。但 liveness 和 startupProbe 不行,它们必须为 1。
从容器启动到第一次可能被杀的总容忍窗口是:
initialDelaySeconds + periodSeconds × failureThreshold
拿默认值算:0 + 10 × 3 = 30 秒。你可能会问:initialDelaySeconds 明明是 0,为什么还有 30 秒的窗口?因为 kubelet 不是只探测一次就下结论------它按 periodSeconds 的节奏反复探测,只有连续失败次数累积到 failureThreshold 才动手。所以即使 initialDelaySeconds 为 0,应用从启动到被杀仍有一个"容错窗口"。
⚠️ 这里有个关键区分 :上面公式里用的是 livenessProbe 的 periodSeconds 和 failureThreshold。如果你的 Deployment 只配了 readinessProbe 没配 livenessProbe,Pod 不会被重启,只会被从 Service Endpoints 中摘掉------流量不再进来,但容器本身继续运行。"重启"和"摘流量"是两个完全不同的后果,这也是为什么后面要讲 startupProbe:它是唯一能在启动阶段兜住 liveness 的机制。
另一个容易忽略的细节:livenessProbe 不会等 readinessProbe 成功之后才开始执行。在 startupProbe(如果有的话)成功之后,liveness 和 readiness 是并行独立的。官方文档明确:如果配置了 startupProbe,liveness 和 readiness 探针不会在 startupProbe 成功之前启动,以确保它们不会干扰应用启动。
三种探测方式怎么选
Kubernetes 支持三种探针执行方式,判定逻辑完全不同:
|------------|--------------|---------------|-----------------|
| 方式 | 成功判定 | 失败判定 | 适用场景 |
| HTTP GET | 响应码 200~399 | 其他状态码、连接拒绝、超时 | Web 服务、REST API |
| TCP Socket | 能建立 TCP 连接 | 连接失败或超时 | 数据库、消息队列 |
| Exec | 命令退出码为 0 | 非零退出码 | 需要自定义检查逻辑的场景 |
HTTP GET 向 Pod IP 的指定端口和路径发 GET 请求,响应码 ≥200 且 <400 算成功。但 HTTP 探针的重定向行为有一个反直觉的点:kubelet 只跟随同主机名的重定向,不跟随跨主机名的重定向 。跨主机重定向被视为成功(因为 3xx 落在 200~399 范围内),这意味着探针根本没有检查到目标端点的实际状态。但 kubelet 会生成一个 ProbeWarning事件 ,提醒你重定向被忽略了。所以如果你在 describe pod 里看到 ProbeWarning,不要忽略它------它意味着探针可能没有真正检查到你的健康端点。
这种跨主机重定向在几种典型场景下会悄悄出现,每一种都有明确的触发链路:
- 外部认证跳转:健康端点被网关拦截,未携带认证信息时返回 302,Location 指向 OAuth/OIDC 登录页。请求的目标主机名从 Pod IP 变成了认证服务器域名,kubelet 判定为跨主机重定向。
- CDN 回源 :健康检查请求经过 CDN 节点,CDN 将请求重定向到源站域名。源站域名与 Pod IP 属于不同主机名,触发
ProbeWarning。 - 多集群代理:服务网格或 API 网关将健康检查请求代理到另一个集群的端点,重定向目标的主机名与原始请求不一致。
社区中甚至记录了真实的 ProbeWarning 事件输出格式:Warning ProbeWarning 4m58s (x14458 over 43h) kubelet Liveness probe warning: Probe terminated redirects...------43 小时内出现了 14458 次,说明这种重定向在复杂网络拓扑下并不罕见。
官方文档对此的描述是:"When the kubelet probes a Pod using HTTP, it only follows redirects if the redirect is to the same host. If the kubelet receives a redirect where the hostname is different from the request, the outcome of the probe is treated as successful and kubelet creates an event to report the redirect failure。"
但这里还有一个更隐蔽的坑:同主机名、跨 scheme 的重定向(如 HTTP → HTTPS)仍然会被跟随 。kubelet 的 RedirectChecker 在决定是否跟随重定向时只校验主机名,不校验 scheme。如果你的健康端点是 http://10.42.0.6:8000/health,应用把它 302 跳到了 https://10.42.0.6:8000/health,kubelet 会跟过去,然后对仅监听 HTTP 的服务发起 TLS 握手------探测持续失败,Pod 永远无法 Ready。如果你的健康端点配置了强制 HTTPS 跳转,务必在探针里设置 scheme: HTTPS,或者直接关闭健康端点的跳转。
TCP Socket 只要端口能连上就算成功------但记住,端口通不等于服务健康。数据库连接池可能还没初始化完,但 3306 端口已经监听了。
Exec 探针有一个新手最容易踩的坑:命令不是通过 shell 执行的。官方文档明确说明:"命令只是 exec'd,它不会在 shell 中运行,因此传统的 shell 指令('|'等)将无法工作。要使用 shell,您需要明确调用该 shell。"
这意味着下面这种写法不会按预期工作:
# ❌ 错误:没有 shell 解释器,管道和 && 都不会生效
livenessProbe:
exec:
command: ["test", "-f", "/tmp/healthy", "&&", "echo", "ok"]
正确的写法是显式调用 shell:
# ✅ 正确:明确调用 sh
livenessProbe:
exec:
command: ["/bin/sh", "-c", "test -f /tmp/healthy && echo ok"]
说到选型,阿里云的健康检查最佳实践给出了明确的推荐:liveness 用 TCP 探针,initial delay 贴近应用启动时间,failureThreshold 设为 3;readiness 用 HTTP 探针,failureThreshold 设为 1。我基本认同------liveness 只关心进程是否活着,readiness 才判断服务能否处理请求。基于这个选型原则,我们来看一个完整的配置模板。
生产级完整配置:三种探针协同工作
如果你只有一个慢启动的应用,initialDelaySeconds 确实可以解决问题。但它有个致命矛盾:如果你把 initialDelaySeconds 设成 60 秒,应用在运行到第 30 秒时真的死锁了,kubelet 要等到 60 秒才开始探测,再等 30 秒才触发重启------用户整整 90 秒都在看错误页。
startupProbe 就是为解决这个矛盾而生的。核心机制很简单但极其重要:如果配置了 startupProbe,kubelet 在 startupProbe 成功之前不会执行 liveness 和 readiness 探针。一旦 startupProbe 成功,它就不再执行,liveness 和 readiness 接管。
看这个生产级配置模板(Spring Boot 3.2 应用,冷启动实测 45 秒):
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-service
spec:
replicas: 3
strategy:
rollingUpdate:
maxUnavailable: 1 # 一次只杀一个,避免级联故障
maxSurge: 1
template:
spec:
terminationGracePeriodSeconds: 60
containers:
- name: app
image: my-service:v2.1.0
ports:
- containerPort: 8080
startupProbe:
tcpSocket:
port: 8080
periodSeconds: 10
# 实测启动 45 秒,取 6 倍余量 → 300 秒窗口
# 30 × 10 = 300 秒,覆盖冷启动 + CPU 争抢 + IO 延迟
failureThreshold: 30
timeoutSeconds: 3
livenessProbe:
tcpSocket:
port: 8080
# ⚠️ TCP 探针只能验证端口是否监听,无法检测进程内死锁。
# 如果你的应用有死锁风险(线程池耗尽、GC 卡死等),
# 请改用 httpGet 端点,并确保该端点不依赖外部资源。
periodSeconds: 10
failureThreshold: 3 # 运行阶段:30 秒检测窗口
timeoutSeconds: 3
# 不需要 initialDelaySeconds,startupProbe 已经兜底了
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
periodSeconds: 5
# ⚠️ 这里 failureThreshold 与 liveness 相同(都是 3),
# 是因为本应用的 readiness 端点只检查连接池内部状态,
# 不主动探测数据库连通性。
# - 数据库短暂不可达:连接池内部状态仍正常,端点返回成功
# - 数据库持续不可达导致连接池耗尽:连接池内部状态变为异常,
# 端点返回失败(这正是 readiness 应该摘流量的时刻)
# 注意:连接池的内部状态本身就会反映数据库的可用性------
# 数据库持续不可达时,连接池中的连接会逐渐失效,
# 可用连接数下降,端点随之返回失败。
# 3 × 5 = 15 秒的窗口,是给连接池自愈机制留出的时间------
# 前提是你的连接池自愈能在 15 秒内生效。如果自愈更慢,
# 适当调大 failureThreshold;如果自愈更快,可以调小。
# 如果你遵循官方推荐的"liveness 阈值高于 readiness"模式
# (让 Pod 先被摘流量、再被硬杀,给应用一个恢复窗口),
# 应将 readiness 的 failureThreshold 设得比 liveness 更低(比如 1)。
failureThreshold: 3
timeoutSeconds: 5
几个关键设计点:
- startupProbe 用 TCP:启动阶段只关心端口是否监听,不需要应用逻辑参与。
- liveness 也用 TCP:运行时只要进程活着、端口在监听,就不该重启。但注意上面注释里的警告------有死锁风险的应用应该改用 HTTP 端点。
- readiness 用 HTTP:这才是判断"能不能服务请求"的地方,检查数据库连接池、缓存预热等依赖状态。
terminationGracePeriodSeconds: 60:给应用足够时间处理完 in-flight 请求再退出。
📌 readinessProbe 的 failureThreshold这里设为 3,而不是阿里云建议的 1,是有意的 。这个应用的 readiness 端点只检查连接池内部状态,不主动探测数据库连通性。这里的关键是区分两种失败来源:
- 数据库短暂不可达:连接池内部状态仍正常,端点返回成功------这是"不主动探测"的效果,探针不会因为数据库抖一下就摘流量。
- 数据库持续不可达导致连接池耗尽:连接池内部状态变为异常(无可用的连接),端点返回失败------这是"检查内部状态"的效果,也是 readiness 应该摘流量的正确时刻。
很多人会误以为"不主动探测数据库"意味着端点对数据库故障完全无感知,其实不然:连接池的内部状态本身就是数据库可用性的间接反映------数据库持续不可达时,连接池中的连接会逐渐失效,可用连接数下降,端点随之返回失败。这正是这个设计模式的价值所在------它既不会因为数据库短暂抖动就误摘流量,又能在数据库真正不可用时及时把 Pod 从负载均衡中摘除。
所以 failureThreshold: 3 + periodSeconds: 5 = 15 秒的窗口,是给连接池自愈机制留出的时间 。连接池耗尽是一个渐进过程------从数据库不可达到连接池真正耗尽通常有几十秒的缓冲,15 秒窗口给了连接池在耗尽前完成自愈的机会。前提是你的连接池自愈能在 15 秒内生效 :如果自愈更慢,适当调大 failureThreshold;如果自愈更快,可以调小,让流量更快从异常 Pod 上摘除。
如果你的 readiness 端点直接检查数据库连通性 (数据库不可达时返回硬失败,比如 500 错误),那 failureThreshold: 3 就意味着 15 秒内请求仍然会打到这个 Pod 上并全部失败------这种情况下 failureThreshold: 1 更合适,因为摘流量的紧迫性远高于等待自愈。
这两种模式的适用场景可以这样区分:
|-----------------------------------|----------------------------|----------------------|
| 场景 | readiness failureThreshold | 理由 |
| readiness 端点检查外部依赖(数据库、缓存、下游 API) | 低于 liveness(如 1) | 快速摘流量,避免请求打到不健康的 Pod |
| readiness 端点只检查进程内部状态 | 可与 liveness 相同 | 摘流量紧迫性低,给内部状态自愈时间 |
官方文档中推荐的模式是"liveness 阈值高于 readiness "------让 Pod 先被摘流量、再被硬杀,给应用一个恢复窗口。社区最佳实践也确认了这一方向:"Readiness failureThreshold tuned lower/faster than liveness --- you want fast rerouting, slow restarting"。这个配置模板选择两者相同,是因为该应用的 readiness 端点不检查外部依赖,摘流量的紧迫性降低了。如果你的 readiness 端点检查外部依赖,务必采用官方推荐模式------比如 readiness 用 1、liveness 用 3,让 Pod 在 5 秒内被摘流量,30 秒后才被重启。
⚠️ 但 TCP 做 liveness 有一个局限你必须知道 :官方对 liveness 探针的定义是"catch a deadlock, where an application is running, but unable to make progress"------捕获应用进程活着但无法推进的死锁状态。TCP 探针只能验证端口是否监听,无法检测进程内部的死锁。如果你的应用有进程内死锁的风险(比如线程池耗尽、死循环、GC 卡死),liveness 应该用 HTTP 端点而非 TCP------这个端点的实现要足够轻量,只检查进程自身能否响应,不依赖任何外部资源。
💡 startupProbe 的另一个关键行为 :很多人以为 startupProbe 只是"禁用 liveness 和 readiness",却不知道 startupProbe 自身失败达到 failureThreshold后,容器同样会被重启 ,行为跟 liveness 失败完全一样。官方文档确认:"If this probe fails, the Pod will be restarted, just as if the livenessProbe failed。"这意味着你的 startupProbe 的 failureThreshold 必须足够大,能覆盖应用最慢的启动场景。否则应用启动超时后,杀它的从 liveness 变成了 startupProbe,问题没有解决,只是换了个凶手。
这套配置的核心思路是:startupProbe 兜住启动阶段,liveness 管住运行阶段,readiness 控制流量入口 。三者各司其职,才能做到"启动慢不误杀,运行死锁快发现,依赖故障不摘流量"。但如果 startupProbe 的 failureThreshold 设小了,容器在启动阶段就被杀了,就会进入下面这个状态------
CrashLoopBackOff 的退避算法
一旦进入 CrashLoopBackOff,kubelet 不会无脑重启。它使用指数退避策略:
|----------|----------|
| 重启次数 | 退避延迟 |
| 第 1 次失败后 | 10 秒 |
| 第 2 次失败后 | 20 秒 |
| 第 3 次失败后 | 40 秒 |
| 第 4 次失败后 | 80 秒 |
| ...... | ...... |
| 封顶 | 5 分钟 |
GKE 官方文档确认了这一算法:"每次重启失败后,下次尝试之前的延迟时间会以指数方式增加(例如,10 秒、20 秒、40 秒),最长为 5 分钟。"
退避延迟在容器成功运行 10 分钟后重置。 注意,重置的是退避的累计延迟状态,而不是 kubectl get pod 看到的 RESTARTS 计数本身------那个计数器不会因为运行 10 分钟而清零。
有个值得关注的 KEP:社区正在讨论把初始退避从 10 秒降到 1 秒,封顶从 5 分钟降到 1 分钟。原因是 5 分钟的封顶意味着你修复配置之后,可能需要等 5 分钟才能看到新 Pod 启动。这个改动目前在 alpha 阶段。
完整排查链路:从现象到根因
发现 Pod 频繁重启时,按这个顺序走。先理解退避的时间轴,再看事件和日志 ------知道退避延迟是指数增长的,你就能对应上为什么 kubectl logs --previous 有时取不到日志(容器可能还没到日志输出阶段就被杀了)。
# 第一层:确认重启次数和当前状态
kubectl get pods -n <ns> -o custom-columns=NAME:.metadata.name,RESTARTS:.status.containerStatuses[0].restartCount,STATUS:.status.phase
# 第二层:看事件(最关键的一步)
kubectl describe pod <pod> -n <ns> | grep -B 2 -A 5 "Unhealthy\|Killing\|BackOff\|ProbeWarning"
# 第三层:看崩溃前的应用日志
kubectl logs <pod> -n <ns> --previous
# 第四层:确认探针配置是否合理
kubectl get pod <pod> -n <ns> -o jsonpath='{.spec.containers[*].livenessProbe}' | jq .
describe pod 里如果看到这样的输出,基本可以确诊:
Warning Unhealthy Liveness probe failed: Get "http://10.42.0.10:8080/healthz":
dial tcp 10.42.0.10:8080: connect: connection refused
Normal Killing Container app failed liveness probe, will be restarted
前者说明探针打到了还没监听的端口,后者说明 kubelet 已经决定动手了。如果你看到的是 ProbeWarning 而不是 Unhealthy,说明探针遇到了跨主机重定向------探针被判定为成功,但实际没有检查到目标端点。
⚠️ 还有一个容易被忽略的症状:工作负载的运行状况良好的副本数量为零,或者 HPA 扩缩容异常缓慢。如果 system 工作负载(日志代理、指标采集器)也陷入 CrashLoopBackOff,你可能会注意到监控指标出现数据缺口。
常见问题与真实报错
Q:为什么 Pod 一直在重启,但 describe pod里看不到探针失败事件?
这是 Kubernetes 的一个已知问题(Issue #131026)。在某些版本和特定配置组合下,liveness probe 的失败事件可能不会完整记录到 Event 中。如果你怀疑探针有问题但事件里没有,可以去 kubelet 日志里翻,或者检查应用日志中健康检查端点的访问记录。
Q: successThreshold能不能给 livenessProbe 设成大于 1?
不能。对于 livenessProbe 和 startupProbe,successThreshold 必须为 1,这是 API 层面的硬性限制。官方定义中明确写了 "Must be 1 for liveness and startup"。readinessProbe 则没有这个限制。
Q: initialDelaySeconds设成 0 会怎样?
Kubernetes 允许最小值是 0。设成 0 意味着容器一启动 kubelet 就开始探测。如果应用监听端口需要几秒钟,前几次探测会立即失败。好在有 failureThreshold 兜底,默认连续失败 3 次才重启,理论上有 30 秒缓冲。但我不推荐------这是在赌博。
Q:liveness 探针检查的端点能依赖数据库吗?
⚠️ 千万不要。如果 liveness 探针检查的端点依赖数据库连接,数据库短暂抖动会导致所有 Pod 同时被重启,造成级联故障。官方文档明确警告:"错误的存活探针实现可能导致级联故障。这会导致在高负载下重启容器,应用的可伸缩性降低导致客户端请求失败,以及由于部分 Pod 失败而增加剩余 Pod 的工作负载。"
社区中甚至有实际案例:BFF 服务的健康检查端点包含了后端连通性检测,后端故障时 BFF 自己的健康检查也返回错误,导致所有 BFF Pod 被 liveness 杀掉,级联故障扩散到整个调用链。
⚠️ 彩蛋:两个极少人知道的隐藏细节
第一个:探针级别的 terminationGracePeriodSeconds可以覆盖 Pod 级别的设置,但配 0 会让你死得很难看。
这个字段在 v1.21 以 Alpha 形式引入(feature gate 默认关闭) ,v1.22~v1.24 为 Beta(仍需手动开启),v1.25 起 Beta 默认启用 ,v1.28 起正式 GA,v1.29 中该 feature gate 被完全移除 ------如果你跑的是 v1.29+,这个字段已经完全稳定,也不需要再关注 feature gate 状态。它的作用是:当探针失败触发重启时,使用独立的优雅退出时间,而不是 Pod 级别的 terminationGracePeriodSeconds。最小值是 1,如果你设为 0,表示立即发送 kill 信号,不给进程任何优雅退出的机会。
我见过有人为了避免探针重启时进程 hang 住,直接把探针级别的 terminationGracePeriodSeconds 配成了 0。结果进程连 in-flight 请求都来不及处理就被 SIGKILL 了------比不配还糟糕。
第二个(历史知识):Kubernetes v1.21.0 曾有一个 startupProbe 的回归 bug,容器重启后 startupProbe 不再执行。
正常情况下,容器因 liveness 失败被重启后,startupProbe 应该重新执行一遍,给应用完整的启动窗口。但在 v1.21.0 中,startupProbe 只在"全新启动"时生效,容器重启后会被跳过------liveness 直接在应用还没启动完的时候开始探测,导致 Pod 陷入重启死循环。
好消息是这个回归在 Issue 提出后很快被修复(PR #101093),修复已合入后续的 v1.21.x 补丁版本 。今天已经没有集群还在跑这个版本了,但把这个案例放在这里,是想提醒你:startupProbe 与 liveness 的协同行为在不同版本间可能有差异。升级 Kubernetes 版本时,如果发现原本正常的探针行为突然变了,先查一下 changelog 里的探针相关条目。
💡 正确的做法 :liveness 保持灵敏(failureThreshold: 3,periodSeconds: 10),用 startupProbe 兜底启动阶段的长等待。应用一旦启动完成,liveness 就能快速检测到真正的运行时故障。
总结
记住这几条就够了:
- 总容忍窗口 =
initialDelaySeconds + periodSeconds × failureThreshold,应用启动必须在这个窗口内完成。 - Kubernetes 1.16+ 优先用
startupProbe处理慢启动。startupProbe 成功之前,liveness 和 readiness 都不会执行;但 startupProbe 自身失败达到阈值后,容器同样会被重启。 - liveness 的
successThreshold只能是 1,readiness 可以大于 1。 - Exec 探针不走 shell ,需要管道或条件判断时必须显式调用
/bin/sh -c。 - liveness 不要依赖外部状态(数据库、缓存、下游 API),否则依赖抖动会触发级联重启。有进程内死锁风险的应用,liveness 用 HTTP 端点而非 TCP。
- HTTP 探针只跟随同主机名重定向 ,跨主机重定向视为成功并生成
ProbeWarning事件------常见触发链路包括外部认证跳转(302 到 OAuth/OIDC 登录页)、CDN 回源(重定向到源站域名)、多集群代理(网关转发到另一集群端点)。同主机跨 scheme 重定向(HTTP → HTTPS)会被跟随但通常导致 TLS 握手失败,需显式设置scheme。 - readiness 端点检查外部依赖时,
failureThreshold应低于 liveness ,让 Pod 先被摘流量、再被硬杀;只检查内部状态时两者可以相同。阈值窗口(periodSeconds × failureThreshold)应与你内部自愈机制的时间匹配。连接池内部状态本身就是数据库可用性的间接反映,不需要主动探测也能感知持续故障。 - CrashLoopBackOff 退避指数增长:10s → 20s → 40s → ...... → 5 分钟封顶,成功运行 10 分钟后退避延迟重置。
你在生产环境遇到过 Liveness Probe 的什么奇葩问题?欢迎在评论区分享你的踩坑经历。如果这篇文章帮你解决了一个凌晨两点的告警,不妨分享给你的团队------毕竟,下一个被 Pod 重启折磨的人可能就是你旁边工位的兄弟。