一个 liveness 探针配置得又急又严、还去查下游的服务,在启动慢或下游抖动时被 Kubernetes 反复 kill 重启,陷入 CrashLoopBackOff:一次健康检查配置不当的深度复盘

服务部署后反复重启、状态 CrashLoopBackOff,可进到容器里看进程其实是活的、并没崩。根因是 liveness 探针配错:一是 initialDelaySeconds 太短,服务启动慢、还没就绪就被探测失败、被反复 kill;二是 liveness 端点去查了数据库/下游,下游一抖动健康检查就失败、把本来好好的服务也 kill 了。本文讲透 liveness(要不要重启,轻量只看进程自身)、readiness(要不要发流量,可查依赖)、startupProbe(给足启动时间)三种探针的职责区别,给出对症配置的正解,梳理 k8s 健康检查常见坑,最后落到'对不同问题施以对症处置、自动化保护是放大器、判断机制的有效性取决于指标是否准确对应目标'的认知。

一个 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 运维时,刻进骨子里的几条铁律:

  1. liveness 管"要不要重启",要轻量、只看进程自身。失败会被 kill 重启。
  2. liveness 绝不查依赖(DB/下游)。否则下游问题传染成自己被重启。
  3. 依赖检查放 readiness。失败只摘流量、不重启,等依赖恢复再加回来。
  4. 启动慢用 startupProbe 给足启动时间。避免启动期被 liveness 误杀。
  5. 探针参数留合理容错。timeout/failureThreshold 别太严、initialDelay 给足。
  6. 对不同问题施以对症处置。该重启的重启、该摘流量的摘流量、该等的等。
  7. 自动化保护是双刃剑,谨慎配置并给护栏。越强力越要小心驾驭。

写在最后

回头看,这场由"liveness 探针配错"引发的、健康服务被反复误杀的事故,真正教给我的,远不止"分清 liveness 和 readiness"这一个技巧。它让我对"一个为'保护'而生的机制,如果它的'判断标准'定错了,就会把它要保护的东西反过来伤害",有了一次刻骨的体会。我栽跟头,根源在于我给 liveness 探针定了一个错误的"判断标准":我让它用"依赖是否健康"来判断"本服务是否该重启"——可这两件事根本不是一回事"下游挂了"不代表"我这个进程死了、需要重启";恰恰相反,这时我的进程好好的、正确地反映了"下游有问题"——而我的探针却错误地把"下游有问题"判定成了"我自己有问题、该被重启",于是把一个忠实履职的健康进程给杀了我给"守护者(探针)"配了一双"错误的眼睛(判断标准)",于是它把朋友当成了敌人这让我领悟到一个关于"判断/监控机制"的深刻认知:任何"基于某个指标/检查来做决策"的机制(健康检查、监控告警、自动化触发、风控规则),它的有效性,完全取决于"它依据的那个指标,是否真正、准确地反映了它想判断的那件事"——如果"指标"和"想判断的事"之间错配了(用依赖健康去判断本服务存活),那么基于这个指标做的所有决策都会是错的;"测量了错误的东西、然后据此做决策",比"不测量"更危险——因为你会"自信地"做出错误的处置这给了我一种设计判断机制的审慎:设计任何"检查→决策"的机制时,要反复确认"我检查的这个指标,真的准确反映了我想判断的那件事吗?它会不会把'别的问题'误判成'这件事'?"——"判断'进程死没死'就只看进程本身,别掺入'依赖好不好'这个无关变量";"让判断的依据,精准地对应要判断的目标",是让一切监控、检查、自动决策机制真正有效而非帮倒忙的关键认清判断机制的有效性取决于指标与目标是否准确对应、别用错配的指标做决策——这,是我用一次 liveness 误杀的事故,换来的、关于 DevOps、也关于如何设计一切判断与监控机制的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次配 liveness 探针时,先问一句"这检查的是'我死没死'、还是'别人好不好'?",从而把依赖检查挪到 readiness,那我对着那个反复重启的 CrashLoopBackOff 排查的这大半天,就值了。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

一个调用下游接口后忘了关闭连接的服务,在下游主动断开后留下了一大堆 CLOSE_WAIT,把文件描述符耗尽、再也建不了新连接:一次 CLOSE_WAIT 堆积的深度复盘

2026-6-2 19:00:58

技术教程

一套给大模型功能写的断言输出完全相等的单元测试,今天通过明天就挂、同样的输入每次结果还不一样,把我整懵了:一次 LLM 非确定性的深度复盘

2026-6-2 19:29:57

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索