每次滚动发布都有几分钟大量 502、半夜还莫名其妙被重启,我查到底才发现是 K8s 的就绪探针没配、存活探针又配得太敏感:一次健康检查探针配置失当的深度复盘
那两个问题是发布和监控告警双重暴露的:我们的服务跑在 K8s 上。第一个症状是——每次滚动发布新版本,都会有那么几分钟,接口大量报 502 / 连接被拒,等所有新 Pod 都起来了才恢复;运维一开始以为是"发布期抖动正常"。第二个症状更诡异——有些 Pod 半夜会莫名其妙地被重启,日志里没崩溃、没 OOM,可 kubectl describe 显示它被 Killed 然后 Restarted。我把这两件事一起查到底,才看明白,后背发凉:根因都出在 K8s 的两个健康检查探针配置上,而且是两个方向相反的错:第一,readiness(就绪)探针没配——K8s 不知道一个 Pod"什么时候才准备好接收流量",于是容器进程一启动(但应用还在加载、还没真正就绪)它就把流量打进来了,而此时应用还连不上/还没监听,就报 502/拒绝;第二,liveness(存活)探针配得太敏感——它检查的超时太短、失败阈值太低,结果一个健康但偶尔慢一下(比如在 Full GC、或某次请求略卡)的 Pod,被探针误判为"死了",K8s 就把这个本来好好的 Pod 给杀了重启。问题的根,是我没理解 readiness 和 liveness 这两个探针各自的职责:readiness 管"能不能接流量"(没配好就把流量打给没就绪的 Pod),liveness 管"要不要重启"(配太敏感就误杀健康 Pod)。这篇就把这次"健康检查探针配置失当"的坑,从头到尾复盘一遍。
故障现场:没配就绪探针 + 存活探针太敏感
问题在于 readiness 探针缺失(流量打给没就绪的 Pod)和 liveness 探针太敏感(误杀健康 Pod):
# ✗ 出问题的配置: 没有readiness探针, liveness探针又太敏感
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: my-app
image: my-app:1.2.0
# ✗ 问题一: 没有配 readinessProbe!
# → K8s不知道Pod何时"就绪", 容器进程一起来就往里打流量;
# → 但应用可能还在启动(加载配置/连DB/预热), 还没真正能服务 → 502/拒绝。
# ✗ 问题二: liveness探针配得太敏感
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5 # ✗ 太短! 应用可能要30s才启动完, 5s就开始查 → 启动期就被判死
periodSeconds: 5
timeoutSeconds: 1 # ✗ 太短! 一次GC/慢请求就可能超过1s → 误判
failureThreshold: 1 # ✗ 太低! 失败1次就重启 → 偶尔抖一下就被杀
# → 结果: 健康但偶尔慢一下(Full GC等)的Pod, 被误判为死, K8s把它杀了重启。
# 现象:
# - 滚动发布时大量502(没readiness, 流量打给还没就绪的新Pod);
# - Pod半夜莫名重启(liveness太敏感, 把GC中/偶尔慢的健康Pod误判为死, 杀掉重启)。
# 两个探针的职责(关键):
# - readinessProbe(就绪探针): 回答"这个Pod现在能接收流量吗?"
# 不通过 → K8s把它从Service的endpoints里【摘掉】(不打流量给它), 但【不重启】它;
# 用途: 启动未完成时、临时过载时, 先别给它流量, 等它就绪/缓过来。
# - livenessProbe(存活探针): 回答"这个Pod是不是死了/卡死了, 需要重启吗?"
# 不通过 → K8s【重启】这个容器;
# 用途: 检测进程死锁/卡死等"重启才能恢复"的情况。
# 关键: 没配readiness → 流量打给没就绪的Pod(502); liveness太敏感 → 误杀健康Pod;
# 两个探针职责不同(readiness管摘流量、liveness管重启), 配错方向相反, 都会出事。
第一次理清"原来是两个探针、一个没配一个太凶"时,我又懊恼又恍然:"我一直以为健康检查就是'配个 /health 让 K8s 探一下'这么笼统的一件事,完全没分清 readiness 和 liveness 是两个职责完全不同的探针。"这个坑最坑的地方在于:两个症状看起来毫不相关(一个是发布期 502、一个是半夜重启),根因却都在探针配置上,且是两个相反方向的错(一个该配的没配、一个配得过激);而且发布期 502 容易被当成"正常抖动"忽略,半夜重启又因为"重启后就好了"而难抓现行。下面就来拆解,这两个探针到底各管什么、该怎么配。
第一件事:搞懂 readiness 和 liveness 探针的不同职责
我顺着这次事故,把 K8s 三种探针的职责彻底理清了。
K8s 的健康检查探针: readiness / liveness / startup 各管什么?
【核心: readiness管"能否接流量"(不过就摘流量不重启); liveness管"要否重启"(不过就重启); startup管"启动完没"(保护慢启动); 三者职责不同, 别混用】
1. readinessProbe(就绪探针): "现在能接收流量吗?"
- 通过 → Pod留在Service的endpoints里, 接收流量;
- 不通过 → 把Pod从endpoints【摘掉】(停止给它流量), 但【不重启】, 等它恢复;
- 典型用途: ①启动未完成(还在加载/连DB/预热)时, 别给流量; ②临时过载/依赖不可用时, 暂时摘出去缓一缓。
- ★没配的后果(本文): 容器一启动就被打流量, 而应用还没就绪 → 502/拒绝。
2. livenessProbe(存活探针): "死了/卡死了吗?需要重启吗?"
- 通过 → 啥也不做;
- 不通过(达到failureThreshold) → 【重启容器】;
- 典型用途: 检测"进程还在但已死锁/卡死、只有重启才能救"的情况;
- ★配太敏感的后果(本文): 健康但偶尔慢(GC/瞬时卡)的Pod被误判为死, 被无谓重启。
3. startupProbe(启动探针, 慢启动应用必备):
- 专门保护"启动慢"的应用: startup没通过前, 暂停liveness/readiness检查;
- → 给应用足够的启动时间, 避免"还没启动完就被liveness判死重启"(陷入重启循环);
- 慢启动(如JVM应用)强烈建议配。
4. 关键区别: 不过的"后果"完全不同
- readiness不过 = 摘流量(温和, 不重启, 可恢复);
- liveness不过 = 重启(激进, 杀进程);
- → 把"临时不可用(该摘流量)"错配成liveness, 就会把本可自愈的Pod反复重启 → 雪上加霜。
5. 探针参数的含义:
- initialDelaySeconds: 容器启动后多久开始探(要≥应用启动时间, 否则启动期被探死);
- periodSeconds: 多久探一次; timeoutSeconds: 单次探测超时(别太短, 留余量);
- failureThreshold: 连续失败几次才算数(liveness别设1, 容忍偶发抖动); successThreshold。
一句话: readiness管"能否接流量"(不过摘流量不重启)、liveness管"要否重启"(不过重启)、startup保护慢启动;
没配readiness会把流量打给没就绪的Pod(502), liveness太敏感会误杀健康Pod; 三探针职责不同, 要按职责正确配。
这套认知,是整个坑的根。readinessProbe(就绪探针):"现在能接收流量吗?"——不通过就把 Pod 从 Service endpoints 摘掉(停止给流量)但不重启,等它恢复;没配的后果就是容器一启动就被打流量、应用还没就绪→502。livenessProbe(存活探针):"死了/卡死了吗?要重启吗?"——不通过就重启容器;配太敏感就把健康但偶尔慢的 Pod 误判为死、无谓重启。startupProbe(启动探针):保护慢启动应用,startup 没通过前暂停 liveness/readiness,避免还没启动完就被判死重启(JVM 应用必备)。关键区别:readiness 不过=摘流量(温和、可恢复),liveness 不过=重启(激进);把临时不可用错配成 liveness 就会反复重启本可自愈的 Pod。参数:initialDelaySeconds 要≥应用启动时间、timeoutSeconds 别太短、liveness 的 failureThreshold 别设 1。一句话:readiness 管"能否接流量"(不过摘流量不重启)、liveness 管"要否重启"(不过重启)、startup 保护慢启动;没配 readiness 会把流量打给没就绪的 Pod,liveness 太敏感会误杀健康 Pod;三探针职责不同,要按职责正确配。
第二件事:正解——配好 readiness、放宽 liveness、慢启动加 startup
搞懂了原理,正解就清晰了:配上 readiness 探针(应用真正就绪才接流量)、把 liveness 探针放宽(容忍偶发抖动、别误杀)、慢启动应用加 startup 探针、readiness 和 liveness 用不同的检查端点。
# ====== 正解: readiness配上 + liveness放宽 + startup保护慢启动 ======
spec:
template:
spec:
containers:
- name: my-app
image: my-app:1.2.0
# ★ startup探针: 保护慢启动(给最多 30*10=300s 启动时间)
startupProbe:
httpGet: { path: /health/live, port: 8080 }
periodSeconds: 10
failureThreshold: 30 # startup通过前, 不跑liveness/readiness → 启动期不会被误杀
# ★ readiness探针: 应用"真正就绪"(依赖就绪、能服务)才接流量
readinessProbe:
httpGet: { path: /health/ready, port: 8080 } # ready端点: 检查DB/缓存等依赖是否就绪
initialDelaySeconds: 0 # 有startup保护, 这里可以0
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 3 # 连续3次不就绪才摘流量(不重启)
successThreshold: 1
# → 没就绪/临时过载时, 摘掉流量不打给它(但不重启), 恢复了再加回来 → 发布期不再502。
# ★ liveness探针: 放宽, 只在"真的卡死"时才重启
livenessProbe:
httpGet: { path: /health/live, port: 8080 } # live端点: 只检查进程本身是否卡死(别查外部依赖!)
periodSeconds: 10
timeoutSeconds: 3 # 给足余量, 别一次GC就超时
failureThreshold: 3 # 连续3次失败(约30s)才重启, 容忍偶发抖动
# → 只有"真的卡死了"才重启, 不再误杀偶尔慢一下的健康Pod。
# ====== 配置要点 ======
# 1. readiness 和 liveness 用【不同的检查逻辑/端点】:
# - readiness(/health/ready): 检查"能否对外服务", 可包含关键依赖(DB/缓存)是否就绪;
# - liveness(/health/live): 只检查"进程自身是否卡死", 【不要】查外部依赖!
# → 否则: DB抖一下, liveness挂了, K8s把你所有Pod重启(把"依赖问题"升级成"全部重启"灾难)。
# 2. liveness要"宽容": timeoutSeconds留余量、failureThreshold别设1(容忍GC/瞬时卡), initialDelay够长或用startup;
# 3. readiness可"敏感些": 它不过只是摘流量(温和), 用于过载保护/依赖未就绪时暂时退出;
# 4. 慢启动应用(JVM等)用startupProbe保护启动期, 避免重启循环(CrashLoopBackOff);
# 5. 配合优雅停机(同356篇): 关闭时先readiness不就绪→摘流量→处理完存量→退出。
# ====== 一个最关键的反模式 ======
# ✗ liveness探针检查了外部依赖(DB/下游): 依赖一抖, 所有Pod的liveness都挂 → 全部被重启 → 雪崩!
# → liveness只查自己"活没活", 依赖问题交给readiness(摘流量)和熔断, 而不是重启。
# 核心: readiness配好(就绪才接流量, 发布不502)、liveness放宽且只查自身(别查依赖, 别误杀)、
# 慢启动加startup; 分清"摘流量(readiness)"和"重启(liveness)"两种处置, 按职责配对探针。
修复的核心,是"配好 readiness、放宽 liveness 且只查自身、慢启动加 startup"。正解:startup 保护慢启动(通过前不跑 liveness/readiness,启动期不被误杀)、readiness 应用真正就绪(依赖就绪)才接流量(没就绪只摘流量不重启,发布期不再 502)、liveness 放宽(timeout 留余量、failureThreshold 别设 1,只在真卡死时重启)。配置要点:readiness 和 liveness 用不同端点/逻辑——readiness 可查关键依赖,liveness 只查进程自身、绝不查外部依赖(否则 DB 一抖所有 Pod 被重启、雪崩)。最关键的反模式:liveness 检查外部依赖→依赖一抖所有 Pod 被重启→雪崩;liveness 只查自己活没活,依赖问题交给 readiness 和熔断。归根结底:readiness 配好(就绪才接流量,发布不 502)、liveness 放宽且只查自身(别查依赖、别误杀)、慢启动加 startup;分清"摘流量(readiness)"和"重启(liveness)"两种处置,按职责配对探针。
第三件事:K8s 部署与健康检查的其他常见坑
排查后我把 K8s 部署、健康检查相关的其他坑也系统梳理了一遍。
K8s 部署与健康检查的其他常见坑
# 1. 探针职责配错(本文): 没readiness→502, liveness太敏感→误杀。→ 按职责正确配。
# 2. liveness查外部依赖: 依赖抖动→所有Pod重启→雪崩。→ liveness只查自身。
# 3. 慢启动没startup探针: 启动慢被liveness判死, 陷入CrashLoopBackOff。→ 配startupProbe。
# 4. 没配资源requests/limits: 调度不合理/被OOMKilled或挤占。→ 合理设requests/limits。
# 5. 优雅停机没做(同356篇): 滚动更新时正在处理的请求被强杀。→ readiness先摘流量+preStop+SIGTERM处理。
# 6. 镜像用:latest(同344篇): 不可复现/拉到意外版本。→ 用确定的版本tag/digest。
# 7. 副本数为1/无PodDisruptionBudget: 单点, 更新/驱逐时短暂全不可用。→ 多副本+PDB。
# 8. 配置/密钥硬编码进镜像: 不安全、改配置要重新打镜像。→ ConfigMap/Secret注入。
# 共同根源: K8s把"如何判断服务健康、如何调度、如何更新"交给了一堆【声明式配置】;
# 这些配置每一项都有明确语义和后果, 不理解语义就按感觉填(或不填), 就会让K8s做出你没预期的动作。
# 核心: 用K8s, 要理解每个配置项(探针/资源/更新策略)的【确切语义和它触发K8s做什么】;
# 尤其是探针——分清readiness(摘流量)和liveness(重启)的职责, 按职责配, 别让健康检查本身成为故障源。
排查让我把 K8s 部署与健康检查的其他坑也梳理清了。一、探针职责配错(本文)。二、liveness 查外部依赖→雪崩。三、慢启动没 startup→CrashLoopBackOff。四、没配 requests/limits。五、优雅停机没做。六、镜像用 :latest。七、副本数为 1/无 PDB。八、配置/密钥硬编码。它们的共同根源是:K8s 把"如何判断服务健康、如何调度、如何更新"交给了一堆声明式配置;这些配置每一项都有明确语义和后果,不理解语义就按感觉填(或不填),就会让 K8s 做出你没预期的动作。核心是:用 K8s,要理解每个配置项(探针/资源/更新策略)的确切语义和它触发 K8s 做什么;尤其是探针——分清 readiness(摘流量)和 liveness(重启)的职责,按职责配,别让健康检查本身成为故障源。下面这张图,是这次探针配置坑的成因与解法:
第四件事:readiness vs liveness 探针对比表
这次踩坑后,我把 readiness 和 liveness 两个探针的职责对比成一张表,贴在了部署清单的注释里。
| 维度 | readinessProbe(就绪) | livenessProbe(存活) |
|---|---|---|
| 回答的问题 | 能接收流量吗? | 死了/卡死要重启吗? |
| 不通过的后果 | 从 Service 摘掉(停流量) | 重启容器 |
| 是否重启 | 否(温和, 可自愈) | 是(激进) |
| 能否查外部依赖 | 可以(依赖未就绪先摘流量) | ✗ 绝不(否则雪崩) |
| 敏感度 | 可敏感些(只摘流量) | 要宽容(别误杀) |
| 没配的后果 | 流量打给没就绪 Pod(502) | 卡死的 Pod 不会被重启自愈 |
这张表把两个探针钉清了。核心是:这俩探针最本质的区别,是"不通过时的处置不同"——readiness 不通过是"暂时别给它活干(摘流量)"(温和、可逆、能自愈);liveness 不通过是"它没救了, 重启吧"(激进、杀进程);一个对应"临时不可用"(等等就好),一个对应"彻底卡死"(只能重启)——把这两种状态和处置对应错,就会出大问题。它给我的最大启发是:对一个"不健康"的事物,处置方式不该只有一种,而要根据"不健康的程度/性质"分级处置——"临时性的、可自愈的不健康"该用温和手段(隔离、限流、等待),"永久性的、需干预才能恢复的不健康"才该用激进手段(重启、替换);用"重启"(核武器)去对待一个"临时慢一下"(感冒)的 Pod,是杀鸡用牛刀且会误伤;反之,对真卡死的 Pod 只摘流量不重启,它永远好不了。这给了我一种处理"异常/降级"的清醒:设计任何"故障处置/自愈机制"时,要先区分故障的不同性质(临时 vs 永久、可自愈 vs 需干预),再为每种性质匹配恰当力度的处置——而不是"一有问题就用最重的手段(重启/熔断/全部下线)";"按故障性质分级、用匹配力度的手段处置",是构建一个既能自愈、又不会反应过激自伤的健壮系统的关键。认清 readiness 摘流量与 liveness 重启的处置之别、按故障性质分级匹配力度处置——是这个坑带给我的认知。
第五件事:这次事故暴露的"自愈机制本身成了故障源"
这次让我反思更深一层:那个本该"保障健康"的健康检查,反而成了"制造故障"的元凶。我把"配置不当的健康检查"和"配置得当的健康检查"对比成表。
| 维度 | 配置不当(本文) | 配置得当 |
|---|---|---|
| liveness 查依赖 | 依赖抖动→全部 Pod 重启→雪崩 | 只查自身, 依赖问题摘流量 |
| liveness 太敏感 | 误杀健康 Pod | 容忍抖动, 真卡死才重启 |
| 没 readiness | 流量打给没就绪 Pod(502) | 就绪才接流量 |
| 角色 | 健康检查成了故障放大器 | 健康检查保障稳定 |
| 本质 | 保护机制反噬 | 保护机制尽责 |
这张表道出了最深刻的教训。核心是:健康检查的本意,是"保障服务健康、自动剔除/恢复故障实例"——可一旦配置不当,它反而成了故障的制造者和放大器:误杀健康 Pod、把依赖抖动放大成全体重启的雪崩;那个"守护者",因为配置失当,亲手制造了它本该防范的灾难。它给我的深刻启发是:任何"自动化的保护/自愈/容错机制"(健康检查、自动重启、自动扩缩容、熔断、自动故障转移),都是一把双刃剑——配置得当,它保护系统;配置不当或考虑不周,它自己就会成为故障源,甚至比它要防范的问题更危险;因为它手握"自动采取激烈行动(重启、下线、切换)"的权力,一旦它的判断错了,就会"自动地、大规模地"造成破坏。这给了我一种对待自动化机制的敬畏:引入任何"会自动采取激烈动作"的机制时,要格外审慎地设计它的"触发条件",并问"如果它误判了, 会造成多大破坏?它会不会把小问题放大成大灾难?"——给它足够的"容错余量"(别太敏感)、限制它的"动作范围/速率"(别一次全干)、想清它和其他机制的相互作用(别和依赖故障形成正反馈雪崩);"对自动化保护机制的误判后果保持敬畏、审慎设计其触发与影响范围",是避免'守护者反成杀手'的关键。认清自愈机制配错会反噬成故障源、对自动激烈动作的误判后果保持敬畏——是这个探针坑带给我的工程态度。
第六件事:配 K8s 探针时,我现在的自检习惯
现在每当我要给一个服务配 K8s 健康检查探针,我都会先按这张图问自己:
这张图的精髓,是"readiness 管接流量、liveness 只查自身且宽容、慢启动加 startup"。先配 readiness就绪才接流量、慢启动加 startup、liveness只查自身且别太敏感,绝不让 liveness 查外部依赖。这套习惯,让我从"笼统配个 /health 探针"变成了"分清 readiness/liveness/startup 各自职责按需配"——核心始终是:readiness 管摘流量(就绪才接流量)、liveness 管重启(只查自身、要宽容、别查依赖)、慢启动用 startup 保护,别让健康检查本身成为故障源。
我立下的几条规矩
这场"发布期 502、健康 Pod 被误杀重启"的事故,换来了我配 K8s 健康检查时,刻进骨子里的几条铁律:
- readiness 管"能否接流量",不过只摘流量不重启。没配它流量会打给没就绪的 Pod(502)。
- liveness 管"要不要重启",不过就重启容器。配太敏感会误杀健康 Pod。
- liveness 绝不检查外部依赖。否则 DB 一抖所有 Pod 被重启、雪崩。
- liveness 要宽容:timeout 留余量、failureThreshold 别设 1。容忍 GC/偶发抖动。
- 慢启动应用(JVM 等)配 startupProbe。避免启动期被判死、陷入 CrashLoopBackOff。
- readiness 和 liveness 用不同端点和检查逻辑。职责不同,别混用一个。
- 自动化保护机制是双刃剑,审慎设计触发条件和影响范围。别让守护者反成杀手。
写在最后
回头看,这场由"健康检查探针配置失当"引发的、发布 502 加无故重启的事故,真正教给我的,远不止"配好 readiness、放宽 liveness"这一个技巧。它让我对"一个'笼统的概念'(健康检查), 背后往往是几个'职责截然不同的具体角色'(就绪 vs 存活); 把它们笼统地看成一回事、混为一谈, 就会用错",有了一次刻骨的体会。我栽跟头,是因为在我脑子里,"健康检查"就是笼统的一件事——"让 K8s 探一下我的服务还活着不"。我没有意识到,K8s 把这件"笼统的事"精心地拆成了两个(其实三个)职责完全不同的角色:一个管"现在能不能接客(readiness)",一个管"是不是已经病死了要重新投胎(liveness)";这两个角色,"判断的标准不同"、"判断错了的后果也截然不同"(一个摘流量、一个重启);而我用一个笼统的理解去对待它们,该配的(readiness)没配、该温柔的(liveness)又太凶,两头都错。这让我领悟到一个关于"笼统概念与具体职责"的深刻认知:很多我们习以为常的"笼统概念"(健康检查、缓存、超时、权限、日志……),在深入之后,往往会分化成几个职责不同、需要分别对待的具体维度;"把一个笼统概念当成单一的、铁板一块的东西",是误用它的常见根源——因为你会用"一套笼统的处理"去对待本应"分别精确处理"的不同维度;真正的掌握, 始于"把笼统的概念, 拆解成它内部那些职责分明的具体部分"。这给了我一种学习和使用任何概念时的深度:面对一个"用着熟、但其实没细究过"的概念时,要主动去问:"这个笼统的东西, 内部是不是其实分了好几种、各管各的?它们的区别和各自的正确用法是什么?"——把"一个模糊的整体"拆解成"若干清晰的、职责分明的部分";"拆解笼统概念、辨明其内部各部分的不同职责",是从'会用个大概'走向'精确驾驭'的关键一步。认清笼统概念背后是职责分明的具体角色、拆解概念辨明各部分职责再精确使用——这,是我用一次探针配置失当的事故,换来的、关于 K8s、也关于如何深入理解一个概念的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次配 K8s 探针时,先停下来分清 readiness 和 liveness 各管什么、别让 liveness 去查数据库,那我对着那发布期的 502 和半夜的重启告警排查的这段时间,就值了。
—— 别看了 · 2026