一个 liveness 探针配置得又急又严、还去查下游的服务,在启动慢或下游抖动时被 Kubernetes 反复 kill 重启,陷入 CrashLoopBackOff:一次健康检查配置不当的深度复盘
那次故障让我对"健康检查"又爱又怕:我们一个跑在 Kubernetes 上的服务,部署后反复重启,状态显示 CrashLoopBackOff(崩溃-重启循环)。诡异的是,我进到容器里看,进程其实是活的、日志也在正常输出,它并没有真的崩啊!可 k8s 就是隔一会儿就把它 kill 掉重启一次。我排查了大半天,才终于看清是我配置的 liveness(存活)探针在"误杀"它,后背发凉:我犯了两个错。错误一:liveness 探针的 initialDelaySeconds(初始延迟)设得太短——我们的服务启动要预热、加载缓存,得花几十秒才能就绪,可探针在启动后很快就开始检查,这时服务还没启动完、自然探测失败,k8s 就以为它"挂了",一刀把它 kill 了重启;重启后又是同样的过程……陷入了无限重启。错误二:我的 liveness 探针,去查了一个依赖下游的端点——这个健康检查端点内部会去 ping 一下数据库/下游服务,只要下游一抖动、一变慢,这个健康检查就失败,于是 k8s 就把我本来好好的服务 kill 了重启。问题的根,是我没搞懂 liveness 探针的职责——它是用来判断"这个进程是不是死了、需要重启"的,应该又快又轻、只看进程自己;而我把它配得又急(没给启动留时间)、又严(查了重活和下游),让它频繁地误判"该重启",把一个健康的服务反复 kill。这篇就把这次"liveness 探针配置不当、CrashLoopBackOff"的坑,从头到尾复盘一遍。
故障现场:又急又严还查下游的 liveness 探针
问题在一段健康检查的配置和实现上:
# ✗ 出问题的 liveness 探针配置
livenessProbe:
httpGet:
path: /health # ✗ 这个/health端点内部会查数据库/下游(见下)
port: 8080
initialDelaySeconds: 5 # ✗ 太短! 服务要几十秒才启动完, 5秒就开始查 → 还没起来就被判失败
periodSeconds: 5
timeoutSeconds: 1 # ✗ 太严! 1秒超时, 稍慢就算失败
failureThreshold: 2 # ✗ 太严! 失败2次就重启, 容错太小
# ✗ /health 端点的实现(查了下游, 不该出现在liveness里)
# GET /health 内部:
# - 检查数据库连接(ping db)
# - 检查下游服务可用性(调 downstream/health)
# - → 这些是"依赖项"的健康, 不是"我这个进程是否活着"!
# - → 下游一抖动/变慢, /health 就失败 → liveness判定本服务"挂了" → kill重启(冤枉!)
# 两个错误导致的恶果:
# 1. initialDelay太短: 服务启动慢, 探针在它就绪前就开始查 → 一直失败 → 反复重启(CrashLoopBackOff);
# 2. liveness查下游: 下游问题被"传染"成本服务被kill —— 下游抖一下, 我被重启, 雪上加霜。
# liveness vs readiness 的区别(关键, 我混淆了):
# - livenessProbe(存活探针): 判断"进程是不是死了/卡死了, 要不要【重启】它";
# 失败 → k8s【杀掉并重启】容器; 应该【轻量、只看进程自身】(如检查进程能否响应)。
# - readinessProbe(就绪探针): 判断"进程是不是【准备好接收流量】了";
# 失败 → k8s【把它从Service摘除, 不发流量】(但【不重启】); 可以查依赖(没就绪就别接流量)。
# - → 我把"该用readiness判断的、含依赖检查的逻辑", 错放进了liveness → 导致误重启。
# 关键: liveness探针管"要不要重启"、要轻量只看进程自身、给足启动时间; readiness管"要不要发流量"、
# 可查依赖; 把依赖检查放进liveness、或initialDelay太短, 会导致健康服务被反复误杀。
第一次分清 liveness 和 readiness 时,我恍然大悟又懊恼:"我把两个职责完全不同的探针搞混了——用'判断要不要重启'的探针,去做了'判断能不能服务'的检查,还查了下游,难怪它乱杀。"这个坑最讽刺的地方在于:健康检查本是为了提高可用性(自动发现并重启真正挂掉的实例),可配置不当的健康检查,反而成了"制造故障"的元凶——它把健康的服务反复误杀,自己制造了 CrashLoopBackOff。一个"守护可用性的机制",因为没配对,变成了"破坏可用性的机制"。下面就来拆解,两种探针的职责和正确配置。
第一件事:搞懂 liveness 和 readiness 探针的职责区别
我认真梳理了 k8s 的健康检查,才彻底理解这个坑。
liveness 与 readiness 探针: 职责完全不同
【核心: liveness管"要不要重启"(轻量、只看进程自身); readiness管"要不要发流量"(可查依赖); 别搞混】
1. livenessProbe(存活探针):
- 回答的问题: "这个容器是不是【死了/彻底卡死了, 自己恢复不了, 需要重启】?"
- 失败的后果: k8s 【杀掉容器并重启】它;
- 应该怎么查: 【轻量、只检查进程自身是否还能正常工作】(如能否响应一个简单请求);
- 【不该】查: 依赖项(数据库/下游)——那不是"我死没死", 而是"别人有没有问题"。
2. readinessProbe(就绪探针):
- 回答的问题: "这个容器是不是【准备好了, 可以接收流量】?"
- 失败的后果: k8s 【把它从Service端点摘除, 不再给它发流量】(但【不重启】, 等它好了再加回来);
- 应该怎么查: 可以查"是否就绪", 【可以包含依赖检查】(如数据库连不上就别接流量);
- → 它是"要不要给流量"的开关, 失败只是暂时不接流量、不会重启。
3. 我的两个错:
- ① liveness的initialDelaySeconds太短: 服务启动需要时间(预热/加载), 探针在它就绪前就查、
一直失败 → k8s反复重启 → CrashLoopBackOff(根本没机会启动完);
- ② liveness查了下游依赖: 下游抖动 → liveness失败 → 把本服务(其实没死)kill重启 → 冤枉且雪上加霜;
- → 这俩检查本该用readiness(或startupProbe)做, 我错用了liveness。
4. 还有 startupProbe(启动探针, k8s较新):
- 专门用于"启动慢"的应用: 启动期间只跑startupProbe(给足时间), 启动成功后才启用liveness/readiness;
- → 解决"启动慢被liveness误杀"的问题(比单纯调大initialDelay更优雅)。
5. 配置参数也要合理:
- initialDelaySeconds: 给足启动时间(别太短); timeoutSeconds/failureThreshold: 别太严(留容错);
- 太急太严 → 偶尔的慢/抖动就被判失败 → 误杀。
一句话: liveness管"要不要重启"(轻量、只看进程自身、给足启动时间)、readiness管"要不要发流量"(可查依赖);
别把依赖检查放进liveness、别让initialDelay太短/阈值太严, 否则健康服务会被反复误杀成CrashLoopBackOff。
这套区别,是整个坑的根。livenessProbe(存活探针)回答"这容器是不是死了、需要重启",失败则 k8s 杀掉并重启它,应该轻量、只检查进程自身,不该查依赖项(那不是"我死没死")。readinessProbe(就绪探针)回答"是不是准备好接收流量",失败则 k8s 从 Service 摘除不发流量(但不重启),可以包含依赖检查。我的两个错:①liveness 的 initialDelay 太短(服务启动慢、探针在就绪前就查、一直失败→反复重启 CrashLoopBackOff);②liveness 查了下游依赖(下游抖动→liveness 失败→把没死的服务 kill 重启)——这俩本该用 readiness/startupProbe 做。还有 startupProbe(启动探针)专门给启动慢的应用,启动期间只跑它、启动成功后才启用 liveness/readiness。参数也要合理:initialDelay 给足启动时间、timeout/failureThreshold 别太严(留容错)。一句话:liveness 管"要不要重启"(轻量、只看进程自身、给足启动时间)、readiness 管"要不要发流量"(可查依赖);别把依赖检查放进 liveness、别让 initialDelay 太短/阈值太严,否则健康服务会被反复误杀成 CrashLoopBackOff。
第二件事:正解——liveness 轻量只看自己,readiness 查依赖,startupProbe 给足启动时间
搞懂了原理,正解就清晰了:liveness 探针轻量、只检查进程自身、给足初始延迟/容错;依赖检查放 readiness;启动慢用 startupProbe;参数(延迟/超时/阈值)配合理。
# ✓ 正解: 三种探针各司其职 + 合理参数
# startupProbe: 专门处理"启动慢", 给足启动时间, 期间不跑liveness/readiness
startupProbe:
httpGet: { path: /livez, port: 8080 } # 查轻量端点
failureThreshold: 30 # 最多探测30次
periodSeconds: 5 # 每5秒一次 → 给最多30*5=150秒启动时间(启动期间不会被liveness杀)
# livenessProbe: 只判断"进程是否还活着/卡死", 轻量, 不查依赖
livenessProbe:
httpGet: { path: /livez, port: 8080 } # ★ /livez 只检查进程本身能否响应(不查DB/下游)
periodSeconds: 10
timeoutSeconds: 3 # 别太严
failureThreshold: 3 # 连续3次才重启, 留容错(偶尔一次慢不重启)
# readinessProbe: 判断"是否准备好接流量", 这里可以查依赖
readinessProbe:
httpGet: { path: /readyz, port: 8080 } # ★ /readyz 可检查DB/下游是否就绪
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3 # 失败则摘流量(不重启), 等依赖恢复再加回来
# ✓ 三个健康端点的实现(职责分清)
# GET /livez (给liveness/startup): 只回答"我这个进程还活着、没卡死"
# - 最简单: 直接返回200("我还能响应HTTP, 说明进程没卡死");
# - 【绝不】查数据库/下游(那不是"我死没死");
# - → 只有进程真的卡死/无响应时才失败 → 这时重启才对。
# GET /readyz (给readiness): 回答"我准备好服务了吗"
# - 检查"我提供服务【必需】的依赖是否就绪"(如数据库连接池是否可用);
# - 没就绪 → 返回非200 → k8s不给我发流量(但不重启), 等就绪了再发;
# - → 用它来"在依赖没好时, 优雅地不接流量", 而不是被重启。
# ====== 关键原则 ======
# 1. liveness = "要不要重启" → 轻量、只看进程自身、给足启动时间、留容错(别误杀);
# 2. readiness = "要不要发流量" → 可查依赖、失败只摘流量不重启;
# 3. startupProbe = "启动慢的保护" → 启动期间给足时间, 别被liveness误杀;
# 4. 依赖(DB/下游)的健康, 放readiness, 【绝不放liveness】(否则下游问题传染成自己被重启);
# 5. 参数: initialDelay/startupProbe给足启动时间; timeout/failureThreshold留合理容错。
# 核心: liveness轻量只看进程自身+给足启动时间+留容错; readiness查依赖+失败只摘流量; 启动慢用startupProbe;
# 依赖检查绝不放liveness; 健康检查是为提升可用性, 配错了反而误杀健康服务、自造故障。
修复的核心,是"让 liveness、readiness、startupProbe 各司其职,别让健康检查误杀健康服务"。正解:startupProbe 给足启动时间(启动期间不跑 liveness/readiness、不会被误杀);livenessProbe 查轻量端点 /livez(只检查进程本身能否响应、不查 DB/下游)、留容错(连续 3 次才重启);readinessProbe 查 /readyz(可检查 DB/下游是否就绪、失败只摘流量不重启)。关键原则:liveness=要不要重启(轻量、只看进程自身、给足启动时间、留容错)、readiness=要不要发流量(可查依赖、失败只摘流量)、startupProbe=启动慢的保护;依赖检查绝不放 liveness(否则下游问题传染成自己被重启)。归根结底:liveness 轻量只看进程自身+给足启动时间+留容错;readiness 查依赖+失败只摘流量;启动慢用 startupProbe;依赖检查绝不放 liveness;健康检查是为提升可用性,配错了反而误杀健康服务、自造故障。
第三件事:Kubernetes 健康检查与运维的其他常见坑
排查后我把 k8s 健康检查/运维相关的其他常见坑也系统梳理了一遍。
Kubernetes 健康检查 / 运维的其他常见坑
# 1. liveness配置不当(本文): initialDelay太短/查依赖 → 误杀、CrashLoopBackOff。→ 分清三种探针。
# 2. liveness和readiness用同一个端点: 职责混了。→ 分开 /livez 和 /readyz。
# 3. 探针超时/阈值太严: 偶尔的慢就被判失败。→ 合理的timeout/failureThreshold留容错。
# 4. 没配readiness: 服务还没就绪流量就打进来, 报错。→ 配readiness, 就绪了才接流量。
# 5. 健康检查端点太重: /health做了复杂计算/查很多东西, 探测本身耗资源/慢。→ 轻量。
# 6. 资源requests/limits没配好: OOMKilled(超内存)、被驱逐(超资源)。→ 合理配资源。
# 7. 日志只在容器内: 容器重启日志就没了。→ 日志输出到stdout并采集(EFK)。
# 8. 滚动更新没配合好探针: readiness没好就被加流量, 或停机没优雅(见优雅停机篇)。
# 共同根源: 健康检查/探针是k8s保障可用性的机制, 但它"按你配的规则行事"; 配错了规则
# (职责不分、参数太严、查错东西), 这个"保障机制"就会反过来制造故障(误杀、CrashLoop)。
# 核心: 分清liveness(重启)/readiness(流量)/startup(启动)三种探针的职责; 端点轻量、依赖检查放readiness;
# 参数留容错、给足启动时间; 配好资源requests/limits; 健康检查配对了是守护, 配错了是杀手。
排查让我把 k8s 健康检查的其他坑也梳理清了。一、liveness 配置不当(本文)。二、liveness 和 readiness 用同一端点(职责混)。三、探针超时/阈值太严。四、没配 readiness(没就绪就接流量)。五、健康检查端点太重。六、资源 requests/limits 没配好(OOMKilled/被驱逐)。七、日志只在容器内(重启就没了)。八、滚动更新没配合好探针。它们的共同根源是:健康检查/探针是 k8s 保障可用性的机制,但它"按你配的规则行事";配错了规则(职责不分、参数太严、查错东西),这个保障机制就会反过来制造故障。核心是:分清 liveness(重启)/readiness(流量)/startup(启动)三种探针的职责;端点轻量、依赖检查放 readiness;参数留容错、给足启动时间;配好资源 requests/limits;健康检查配对了是守护,配错了是杀手。下面这张图,是这次 liveness 误杀坑的成因与解法:
第四件事:三种探针职责对比表
这次踩坑后,我把 k8s 三种探针的职责和用法对比成一张表。
| 探针 | 回答 | 失败后果 |
|---|---|---|
| livenessProbe | 进程是否死了/卡死、要不要重启 | 杀掉并重启容器 |
| readinessProbe | 是否准备好接收流量 | 从Service摘除, 不发流量(不重启) |
| startupProbe | 是否已启动完成 | (启动期)失败超阈值才重启 |
| liveness该查 | 进程自身能否响应(轻量) | 不查依赖 |
| readiness该查 | 可含依赖(DB/下游)是否就绪 | 没就绪就不接流量 |
这张表把三种探针钉清了。核心是:三种探针对应三个不同的问题、三种不同的处置——liveness 问"死没死"(死了就重启)、readiness 问"能不能服务"(不能就别给流量)、startup 问"起来没有"(起来前别打扰它);它们各管一摊,把它们的职责对应到正确的'处置动作'(重启/摘流量/等待),是配对的关键。它给我的最大启发是:这背后是一种"对不同问题,要有不同的、恰当的应对"的设计智慧——"进程卡死了"的恰当应对是"重启"(它自己恢复不了);"依赖暂时不可用"的恰当应对是"暂时不接流量、等它恢复"(而不是重启——重启了依赖也不会好);"正在启动"的恰当应对是"耐心等待";我犯错,正是因为对"依赖不可用"这个问题,用了"重启"这个错误的应对(放进了 liveness)——重启一个健康的服务,根本解决不了下游的问题,只是徒增故障。这给了我一种处理故障的清醒:面对不同性质的问题,要诊断清楚"这是什么问题",再施以"对症的处置"——"该重启的重启、该等待的等待、该隔离的隔离、该降级的降级",而不是"不管什么问题都用同一招(比如一律重启)";"对错问题下错药"(对依赖问题用重启)往往比"不处理"更糟——它在原问题上又叠加了新的故障。分清三种探针的职责、对不同问题施以对症处置——是这个坑带给我的运维认知。
第五件事:健康检查这类"自动化保护"的双刃性
这次让我深刻体会到,自动化的保护机制是把双刃剑。我把它配好和配坏的两面整理成表。
| 机制 | 配对了(守护) | 配错了(伤害) |
|---|---|---|
| liveness探针 | 自动重启真挂的实例 | 反复误杀健康实例(本文) |
| 自动扩缩容 | 按负载弹性伸缩 | 阈值不当致频繁抖动/缩没了 |
| 熔断 | 下游故障时快速失败 | 阈值太敏感误熔断好的下游 |
| 自动重试 | 应对偶发抖动 | 无脑重试放大压力(重试风暴) |
| 自动回滚 | 发现问题自动回退 | 误判致来回反复横跳 |
这张表道出了自动化保护的双刃性。核心是:liveness 探针(以及自动扩缩容、熔断、自动重试、自动回滚)这类"自动化的保护/响应机制",配置得当时是守护者(自动应对故障、提升可用性),但配置不当时会变成施害者(误判、误操作,反而制造故障);它们越"自动、强力",配错时的破坏也越大(它会自动地、反复地执行错误的操作)。它给我的深刻启发是:"自动化"是一把放大器——它把"正确的决策"自动、高效地执行(放大了好处),但也会把"错误的判断"自动、反复地执行(放大了坏处);一个错误的人工操作,可能只发生一次;但一个配错的自动化机制,会不知疲倦地、一遍遍地执行那个错误(反复误杀、反复误熔断);"自动化让对的更对,也让错的更错"。这给了我一种对待自动化的审慎:引入任何"自动化的、会自主采取行动"的机制(探针、扩缩容、熔断、自动化运维)时,要格外谨慎地配置和测试它的"触发条件和行为"——"它会在什么情况下自动行动?行动对不对?会不会误判?误判了破坏多大?";给强力的自动化机制配上'护栏'(合理的阈值、容错、灰度、可观测、紧急关闭开关),确保它"在该出手时出手、不该出手时别乱动";越自动、越强力的机制,越要小心地驾驭——因为它替你做决定的能力,既是便利也是风险。认清自动化保护是放大器、谨慎配置并给强力自动化机制配护栏——是这个坑带给我的更高层认知。
第六件事:配健康检查时,我现在的检查习惯
现在每当我给服务配 k8s 健康检查,我都会按这张图先想清楚:
这张图的精髓,是"启动慢用 startup、liveness 轻量不查依赖、依赖检查放 readiness、参数留容错"。启动慢配 startupProbe;liveness 查轻量 /livez 只看进程自身、绝不查依赖;依赖检查放 readiness /readyz、失败只摘流量;参数留足容错和启动时间。这套习惯,让我从"随手配个 /health 给 liveness"变成了"分清三种探针职责再配"——核心始终是:liveness 管重启要轻量不查依赖、readiness 管流量可查依赖、startup 给启动时间,别让健康检查误杀。
我立下的几条规矩
这场"liveness 误杀、CrashLoopBackOff"的事故,换来了我做 k8s 运维时,刻进骨子里的几条铁律:
- liveness 管"要不要重启",要轻量、只看进程自身。失败会被 kill 重启。
- liveness 绝不查依赖(DB/下游)。否则下游问题传染成自己被重启。
- 依赖检查放 readiness。失败只摘流量、不重启,等依赖恢复再加回来。
- 启动慢用 startupProbe 给足启动时间。避免启动期被 liveness 误杀。
- 探针参数留合理容错。timeout/failureThreshold 别太严、initialDelay 给足。
- 对不同问题施以对症处置。该重启的重启、该摘流量的摘流量、该等的等。
- 自动化保护是双刃剑,谨慎配置并给护栏。越强力越要小心驾驭。
写在最后
回头看,这场由"liveness 探针配错"引发的、健康服务被反复误杀的事故,真正教给我的,远不止"分清 liveness 和 readiness"这一个技巧。它让我对"一个为'保护'而生的机制,如果它的'判断标准'定错了,就会把它要保护的东西反过来伤害",有了一次刻骨的体会。我栽跟头,根源在于我给 liveness 探针定了一个错误的"判断标准":我让它用"依赖是否健康"来判断"本服务是否该重启"——可这两件事根本不是一回事。"下游挂了"不代表"我这个进程死了、需要重启";恰恰相反,这时我的进程好好的、正确地反映了"下游有问题"——而我的探针却错误地把"下游有问题"判定成了"我自己有问题、该被重启",于是把一个忠实履职的健康进程给杀了。我给"守护者(探针)"配了一双"错误的眼睛(判断标准)",于是它把朋友当成了敌人。这让我领悟到一个关于"判断/监控机制"的深刻认知:任何"基于某个指标/检查来做决策"的机制(健康检查、监控告警、自动化触发、风控规则),它的有效性,完全取决于"它依据的那个指标,是否真正、准确地反映了它想判断的那件事"——如果"指标"和"想判断的事"之间错配了(用依赖健康去判断本服务存活),那么基于这个指标做的所有决策都会是错的;"测量了错误的东西、然后据此做决策",比"不测量"更危险——因为你会"自信地"做出错误的处置。这给了我一种设计判断机制的审慎:设计任何"检查→决策"的机制时,要反复确认"我检查的这个指标,真的准确反映了我想判断的那件事吗?它会不会把'别的问题'误判成'这件事'?"——"判断'进程死没死'就只看进程本身,别掺入'依赖好不好'这个无关变量";"让判断的依据,精准地对应要判断的目标",是让一切监控、检查、自动决策机制真正有效而非帮倒忙的关键。认清判断机制的有效性取决于指标与目标是否准确对应、别用错配的指标做决策——这,是我用一次 liveness 误杀的事故,换来的、关于 DevOps、也关于如何设计一切判断与监控机制的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次配 liveness 探针时,先问一句"这检查的是'我死没死'、还是'别人好不好'?",从而把依赖检查挪到 readiness,那我对着那个反复重启的 CrashLoopBackOff 排查的这大半天,就值了。
—— 别看了 · 2026