我每次发布服务都有一两分钟大量 502、用户骂声一片,可实例明明都起来了,我对着 K8s 健康检查探针的配置排查了大半天的复盘
那是我负责的一个跑在 Kubernetes 上的服务。功能没问题,可它有一个让我头疼了很久的毛病:每次滚动发布(更新版本)的时候,都会有那么一两分钟,用户大量遇到 502/503 错误,客服那边骂声一片。等发布完成、过个一两分钟,又一切恢复正常。我一开始以为是发布本身的"必然抖动",忍了。可越想越不对:滚动发布不就是为了"不中断服务"吗?新实例不是起来了才会切流量吗?怎么还会大面积 502?我盯着发布过程看了又看,实例状态都是 Running。排查了大半天,我才真正理解了 Kubernetes 健康检查探针(probe)的门道,以及我那个"没配 readiness 探针"的致命疏忽。这篇就把这场"每次发布必抖动"的事故,从头复盘一遍。
故障现场:实例 Running,却返回 502
先看现场。发布时实例明明 Running,流量打过去却 502:
# 现象: 每次滚动发布, 都有 1-2 分钟大量 502/503
# - kubectl get pod 看: 新 Pod 状态是 Running(看起来好了)。
# - 但这期间, 打到新 Pod 的请求大量 502/503。
# - 等一两分钟(应用真正启动完), 就恢复正常。
# 我的 Deployment 配置(问题所在):
# spec:
# containers:
# - name: myapp
# image: myapp:v2
# ports:
# - containerPort: 8080
# # ✗✗ 没有配置任何健康检查探针(readiness / liveness)!
# 为什么实例 Running 却 502?
# 1. K8s 滚动发布: 启动新 Pod, 新 Pod"就绪"后, 把流量切给它、关掉老 Pod。
# 2. 关键: K8s 怎么判断新 Pod "就绪、可以接流量"了?
# - 默认情况下(没配 readiness 探针): K8s 只看"容器进程起来了没"。
# - 容器进程一启动(java/node 进程跑起来), K8s 就认为它 Running、就绪了,
# 立刻把流量切过去!
# 3. 但是: "进程起来了" ≠ "应用能处理请求了"!
# - Java/Spring 应用: 进程起来后, 还要花几十秒~一两分钟做初始化
# (加载 Spring 容器、建数据库连接池、预热缓存、注册...)。
# - 在这段"进程起来了、但应用还没初始化完"的窗口期里:
# → 应用还不能处理请求, 但 K8s 已经把流量切过来了!
# → 这些请求, 要么 502(连不上还没监听的端口)、要么报错。
# 4. 等应用真正初始化完(一两分钟后), 才能正常处理 → 恢复。
# 现象拼图:
# - 我没配 readiness 探针 → K8s 用"进程起没起来"来判断就绪。
# - "进程起来"和"应用就绪"之间有个"初始化窗口期", 这期间不能处理请求。
# - K8s 在这个窗口期就把流量切过来了 → 大量 502。
# - ★ 根因: 我没告诉 K8s"如何正确判断我的应用'真的就绪了'",
# 于是它用了一个错误的、过早的标准(进程起来=就绪)。
看清真相后,我才明白这"发布抖动"根本不是必然的,而是我配漏了。问题的根源,是我没有配置任何健康检查探针(readiness/liveness)。K8s 滚动发布时要判断"新 Pod 是否就绪、可以接流量",而在没配 readiness 探针时,它只看"容器进程起来了没"——进程一启动,K8s 就认为它就绪、立刻把流量切过去。但致命的是:"进程起来了" ≠ "应用能处理请求了"——Java/Spring 应用进程起来后,还要花几十秒到一两分钟做初始化(加载 Spring 容器、建连接池、预热缓存)。在这段"进程起来了、但应用还没初始化完"的窗口期里,应用还不能处理请求,而 K8s 已经把流量切过来了,这些请求就 502。根因是:我没告诉 K8s"如何正确判断我的应用'真的就绪了'",于是它用了一个错误的、过早的标准(进程起来=就绪)。
第一件事:搞懂 readiness、liveness、startup 三种探针
要解决它,得先彻底搞懂 K8s 的三种健康检查探针,以及它们各管什么。
K8s 三种探针: readiness / liveness / startup
# 一、Readiness Probe(就绪探针)—— "我能接流量了吗?"
# - 作用: 判断 Pod 是否"就绪、可以接收流量"。
# - 通过 → K8s 把这个 Pod 加入 Service 的负载均衡(给它发流量)。
# - 失败 → K8s 把它从负载均衡里【摘除】(不给它发流量), 但【不重启】它。
# - 用途: ★本文的关键★ 应用初始化完成前, readiness 失败 → 不给它流量,
# 等初始化完、readiness 通过, 才开始接流量 → 避免"没就绪就接流量"的502!
# - 也用于: 运行时临时不可用(如依赖的下游挂了)→ 暂时摘流量, 恢复后再接。
# 二、Liveness Probe(存活探针)—— "我还活着吗? 要不要重启我?"
# - 作用: 判断 Pod 是否"还健康、还活着"。
# - 通过 → 正常。
# - 失败 → K8s 认为它"死了/卡死了", 【重启】这个 Pod。
# - 用途: 应对"进程还在但已经卡死/假死"的情况(如死锁、内存泄漏卡住),
# 自动重启它来恢复。
# 三、Startup Probe(启动探针)—— "我启动完了吗?"
# - 作用: 专门处理"启动慢"的应用。
# - 在它通过之前, 暂时禁用 liveness/readiness 检查(给足启动时间)。
# - 用途: 避免"启动慢的应用还没起来, 就被 liveness 判定死亡而反复重启"
# 的死循环(启动太慢→被重启→又启动太慢→又被重启)。
# 关键区别:
# - readiness 失败 → 摘流量(不重启)。 管"接不接流量"。
# - liveness 失败 → 重启。 管"活没活、要不要重启"。
# - startup → 给启动慢的应用足够的启动时间, 再交给上面两个管。
# 核心: readiness管"能不能接流量"(失败摘流量不重启, 解决本文初始化窗口期接流量的502);
# liveness管"活没活"(失败重启, 应对卡死); startup给启动慢的应用足够启动时间防被误杀。
想透这三种探针,整个问题就清晰了。一、Readiness Probe(就绪探针)——"我能接流量了吗?":判断 Pod 是否就绪可接流量;通过则加入负载均衡发流量,失败则从负载均衡摘除(不给流量,但不重启);这正是本文的关键——应用初始化完成前 readiness 失败、不给它流量,等初始化完、readiness 通过才接流量,避免"没就绪就接流量"的 502;也用于运行时临时不可用(下游挂了)时暂时摘流量。二、Liveness Probe(存活探针)——"我还活着吗?要不要重启?":失败则 K8s 认为它死了/卡死、重启它;应对"进程还在但卡死/假死"(死锁、卡住)。三、Startup Probe(启动探针)——"我启动完了吗?":专门处理启动慢的应用,通过前暂时禁用 liveness/readiness;避免"启动慢的应用还没起来就被 liveness 判死反复重启"的死循环。关键区别:readiness 失败→摘流量(不重启)、liveness 失败→重启、startup→给启动慢的应用足够启动时间。
第二件事:正解——配好三种探针,各司其职
搞懂了原理,正解就清晰了:配 readiness 解决发布抖动、配 liveness 自愈卡死、配 startup 给启动慢的应用足够时间,并给应用提供专门的健康检查接口。
# ====== 正解一(解决本文的502): 配 readiness 探针 ======
# Deployment 里加上:
spec:
containers:
- name: myapp
image: myapp:v2
ports:
- containerPort: 8080
readinessProbe: # ★ 就绪探针: 应用真正能接流量才通过
httpGet:
path: /health/ready # 应用提供的"就绪"检查接口
port: 8080
initialDelaySeconds: 10 # 容器启动后等10s再开始探测
periodSeconds: 5 # 每5s探一次
failureThreshold: 3 # 连续3次失败才算未就绪
# → 应用初始化完、/health/ready 返回200, readiness 才通过,
# K8s 这时才把流量切过来 → 不再有"没就绪就接流量"的502!
# ====== 正解二: 配 liveness 探针(自愈卡死)======
livenessProbe: # 存活探针: 应用卡死了就重启
httpGet:
path: /health/live # "存活"检查接口(只检查"进程是否卡死")
port: 8080
initialDelaySeconds: 30 # 给足启动时间(或用 startupProbe)
periodSeconds: 10
failureThreshold: 3
# ⚠️ liveness 要"宽松"一点! 它失败会重启, 配太激进会误杀正常实例。
# ====== 正解三: 启动慢的应用配 startup 探针 ======
startupProbe: # 启动探针: 给启动慢的应用足够时间
httpGet:
path: /health/live
port: 8080
failureThreshold: 30 # 最多探30次
periodSeconds: 10 # 每10s一次 → 给足 300s 启动时间
# → startup 通过前, 暂停 liveness/readiness; 避免启动慢被 liveness 误杀。
# ====== 关键: readiness 和 liveness 的检查内容要不同! ======
# - readiness(/health/ready): 检查"应用能不能正常服务"——包括关键依赖
# (如数据库连接、必要的下游)是否就绪。依赖挂了 → 摘流量。
# - liveness(/health/live): 只检查"进程是不是卡死了"——【不要】检查外部依赖!
# ★ 大坑: 如果 liveness 也检查数据库, 那数据库一抖, 所有 Pod 的 liveness
# 都失败 → K8s 把所有 Pod 全重启 → 雪崩! liveness 只查"自己活没活"。
应用侧也要提供对应的健康检查接口:
# ====== 正解四: 应用提供两个不同的健康检查接口 ======
# GET /health/live (给 liveness 用):
# 只检查"进程本身是否正常运转"(如线程池没死锁、能响应)。
# ★ 不检查数据库/下游! 否则下游一挂全部 Pod 被重启 = 灾难。
# → 几乎总是返回200, 除非进程真的卡死了。
# GET /health/ready (给 readiness 用):
# 检查"应用是否真的准备好服务请求"——包括:
# - Spring 容器/初始化是否完成。
# - 数据库连接池是否就绪。
# - 必要的下游/缓存是否可用。
# → 初始化没完成、或关键依赖不可用时, 返回 503(不就绪) → 摘流量。
# Spring Boot 现成方案: spring-boot-starter-actuator
# /actuator/health/liveness 和 /actuator/health/readiness
# (开启后开箱即用, 还能自定义健康指示器)
# 核心: 配readiness(应用真就绪才接流量, 解决发布502)+ liveness(卡死才重启, 要宽松)
# + startup(给启动慢的足够时间); readiness可查依赖、liveness绝不查依赖(否则下游挂了全重启)。
修复的核心,是"配好三种探针、让 K8s 用正确的标准判断应用状态,且各司其职"。正解一(解决本文 502):配 readiness 探针——指向应用的 /health/ready 接口,应用初始化完、接口返回 200 时 readiness 才通过,K8s 这时才切流量,不再有"没就绪就接流量"的 502。正解二:配 liveness 探针(自愈卡死)——失败会重启,所以要宽松一点,配太激进会误杀正常实例。正解三:启动慢的应用配 startup 探针——给足启动时间,避免启动慢被 liveness 误杀。而最关键、最容易踩的一个坑:readiness 和 liveness 的检查内容要不同!readiness 可以检查关键依赖(数据库/下游,挂了就摘流量);但 liveness 绝不要检查外部依赖——如果 liveness 也查数据库,数据库一抖,所有 Pod 的 liveness 都失败、K8s 把所有 Pod 全重启,直接雪崩!liveness 只查"自己进程活没活"。正解四:应用提供两个不同的健康检查接口(/health/live 只查进程、/health/ready 查就绪和依赖;Spring Boot 的 actuator 开箱即用)。归根结底:配 readiness(真就绪才接流量,解决发布 502)+ liveness(卡死才重启,要宽松)+ startup(给启动慢的足够时间);readiness 可查依赖、liveness 绝不查依赖。
第三件事:健康检查配错的几种典型灾难
排查时我了解到,健康检查"配错"比"没配"还危险。我把几种典型的"配错"灾难梳理了一遍。
健康检查"配错"的典型灾难
# 灾难1: 没配 readiness(本文)
# → 发布时流量打到没就绪的实例 → 大量 502。
# 灾难2: liveness 检查了外部依赖(最危险!)
# → 数据库/下游一抖, 所有 Pod 的 liveness 都失败
# → K8s 把所有 Pod 全部重启 → 整个服务雪崩、且重启也连不上依赖、
# 无限重启 → 比"不配"严重一万倍!
# → 铁律: liveness 只检查"自己进程", 绝不检查外部依赖!
# 灾难3: liveness 配太激进(超时太短/失败阈值太低)
# → 应用偶尔慢一下(GC停顿、瞬时高负载), liveness 就失败
# → 正常的实例被反复重启 → 服务不稳定。
# → liveness 要宽松: 超时给足、failureThreshold 大一点。
# 灾难4: 启动慢但没配 startup, liveness 又配得紧
# → 应用还没启动完, liveness 就开始检查、失败、重启
# → 启动→被杀→启动→被杀 死循环, Pod 永远起不来。
# → 启动慢的应用配 startup 探针, 给足启动时间。
# 灾难5: readiness 检查太重/太慢
# → readiness 接口本身很慢/很重(如每次都查一堆东西)
# → 探测超时 → 误判未就绪 → 摘流量。readiness 接口要轻量、快。
# 灾难6: 没配优雅停机, 关 Pod 时正在处理的请求被中断
# → 配合 preStop hook + readiness 摘流量 + 优雅停机(见优雅停机篇)。
# 核心: 健康检查配错比不配更危险 —— liveness查依赖会致全体重启雪崩(最忌)、liveness太激进
# 误杀正常实例、启动慢没配startup会重启死循环; readiness要轻量、liveness只查自己且要宽松。
排查让我意识到一个反直觉的事实:健康检查"配错"比"没配"还危险。灾难一:没配 readiness(本文)——发布时流量打到没就绪的实例、大量 502。灾难二:liveness 检查了外部依赖(最危险!)——数据库/下游一抖,所有 Pod 的 liveness 都失败,K8s 把所有 Pod 全部重启、整个服务雪崩,且重启也连不上依赖、无限重启,比"不配"严重一万倍!铁律:liveness 只检查自己进程,绝不检查外部依赖。灾难三:liveness 配太激进——应用偶尔慢一下(GC、瞬时高负载)就失败,正常实例被反复重启,liveness 要宽松。灾难四:启动慢但没配 startup、liveness 又配得紧——启动→被杀→启动→被杀死循环,Pod 永远起不来。灾难五:readiness 检查太重/太慢——探测超时误判未就绪,readiness 接口要轻量快。灾难六:没配优雅停机(配合 preStop + readiness 摘流量,见优雅停机篇)。下面这张图,是这次发布 502 的成因与解法:
第四件事:三种探针对比速查
这次踩坑后,我把三种探针的区别和配置要点整理成一张表,配 K8s 时对照着来。
| 探针 | 问题 | 失败后果 | 能查外部依赖吗 |
|---|---|---|---|
| readiness 就绪 | 能接流量了吗 | 摘流量(不重启) | ✓ 可以(依赖挂就摘流量) |
| liveness 存活 | 还活着吗 | 重启 Pod | ✗ 绝不!(否则全重启雪崩) |
| startup 启动 | 启动完了吗 | 未完成就继续等 | 不适用(只判启动) |
这张表,把三种探针的本质区别钉死了。记忆诀窍:readiness 管"流量"(失败摘流量,可查依赖)、liveness 管"重启"(失败重启,绝不查依赖)、startup 管"启动时间"(给慢启动应用足够时间)。其中最该刻进脑子的铁律是:liveness 绝不能检查外部依赖——因为它失败会"重启",而"重启"是无法解决"外部依赖挂了"这个问题的(重启了依赖还是挂的),只会导致所有实例被无意义地反复重启、把局部故障放大成全局雪崩。它给我的启发是:设计任何"自动化的反应机制"(探测到 X 就自动做 Y),都必须确保"Y 这个反应,真的能解决 X 这个问题";如果"反应"解决不了"问题"(重启解决不了依赖挂),那这个自动化非但无益,反而会因为"徒劳地、反复地触发"而把事情搞得更糟。liveness 查依赖的灾难,本质就是"用一个解决不了问题的反应(重启),去应对一个它解决不了的问题(依赖故障)"。这让我领悟到一个设计自愈/自动化机制的核心原则:"检测什么"必须和"能做什么反应"匹配——只对"那个反应能解决的问题"做检测和反应;否则,自动化就会从"帮手"变成"帮倒忙的捣乱者"。
第五件事:发布相关的健壮性配置清单
这次事故让我把"平滑发布"相关的配置系统梳理了一遍,凑成一份清单。
| 配置项 | 解决的问题 | 要点 |
|---|---|---|
| readiness 探针 | 没就绪就接流量(本文) | 真就绪才接,可查依赖 |
| liveness 探针 | 卡死/假死不自愈 | 只查自己,要宽松 |
| startup 探针 | 启动慢被误杀 | 给足启动时间 |
| 优雅停机+preStop | 关Pod中断进行中的请求 | 先摘流量再等请求处理完 |
| 滚动更新策略 | 发布时实例不够扛流量 | maxSurge/maxUnavailable 调好 |
| 资源 requests/limits | 资源不足/争抢 | 设合理,requests=limits 稳定 |
| PodDisruptionBudget | 同时挂太多实例 | 保证最少可用实例数 |
这张清单,是我把"平滑发布"需要的配置凑齐后的成果。它告诉我:"不中断地发布一个服务"这件看似 K8s 自动就该搞定的事,背后其实需要一整套配置的协同(探针、优雅停机、滚动策略、资源、PDB)。它给我的最大启发是:Kubernetes 这类编排系统,给了我们"滚动发布、自愈、弹性"等强大的能力,但这些能力不是"开箱即用、自动完美"的,而是"需要你正确配置才能真正生效"的。我之前的误区,正是把 K8s 当成了"智能的、能自动搞定一切的黑盒"——以为"我只管把镜像丢给它,它就会帮我完美地滚动发布";可现实是,K8s 只是提供了"机制",而"策略"(什么算就绪、什么时候摘流量、怎么优雅停机)需要我通过配置告诉它。这让我领悟到使用一切"平台/框架"时的一个道理:平台提供的是"能力的舞台"和"机制的框架",但要让这些能力真正、正确地为你的具体应用服务,你必须理解它的工作机制、并正确地配置和适配它;"用了某个强大的平台"不等于"自动获得了它所有的好处",中间隔着的,正是"正确配置和使用"的功夫。
第六件事:部署服务到 K8s 时,我现在的检查习惯
现在每次往 K8s 部署服务,我都会按这张图把健康检查这一关把好:
这张图的精髓,是"部署前,把三种探针各司其职地配好、并验证发布平滑"。核心配置:readiness 指向 /health/ready(查初始化和关键依赖)、liveness 指向 /health/live(只查进程、绝不查外部依赖)。启动慢的配 startup 给足时间,liveness 始终配宽松(超时给足、失败阈值大);再加优雅停机(preStop + readiness 摘流量)。最后一步是我现在的硬习惯:发布前演练一次滚动发布,看有没有 502(这次的坑正是因为从没在测试环境观察过发布时的请求成功率)。这套习惯,让我部署服务时,从"丢个镜像就以为 K8s 自动搞定"变成了"配好探针并验证发布平滑"——核心始终是:K8s 靠探针判断应用状态,必须正确配好 readiness/liveness/startup,发布才平滑、卡死才自愈。
我立下的几条规矩
这场"每次发布必抖动"的事故,换来了我做 K8s 部署时,刻进骨子里的几条铁律:
- 必须配 readiness 探针。否则进程一起来 K8s 就切流量,初始化窗口期大量 502。
- readiness 查"真就绪"(含关键依赖),liveness 只查"进程活没活"。两者检查内容必须不同。
- liveness 绝不能检查外部依赖。否则依赖一抖所有 Pod 被全部重启,雪崩。
- liveness 要宽松。超时给足、失败阈值大,别因偶尔慢一下就误杀正常实例。
- 启动慢的应用配 startup 探针。给足启动时间,防"启动→被杀"死循环。
- 探针配合优雅停机。关 Pod 时先摘流量、等请求处理完,别中断进行中的请求。
- 发布前演练验证。在测试环境滚动发布,确认全程无 502 再上生产。
写在最后
回头看,这场由"没配 readiness 探针"引发的、每次发布必抖动的事故,真正教给我的,远不止"记得配健康检查"这一个技巧。它让我对"声明式系统"和"使用者的责任"之间的关系,有了更深的理解。我栽跟头,是因为我对 Kubernetes 抱有一种"它会自动把一切都处理好"的过度期待:我以为只要把应用容器化、丢给 K8s,它就会"智能地"帮我实现"不中断的滚动发布"。可这次事故让我明白:K8s 是一个"声明式"的系统——它会忠实地、强大地去实现"你声明的期望状态",但前提是"你得准确地、完整地把你的期望声明清楚"。而我,恰恰漏掉了一个关键的"声明":我没告诉 K8s"我的应用怎样才算'就绪'";于是 K8s 只能用它的默认理解(进程起来=就绪)去判断,而这个默认理解,对我的应用来说是错的。这让我领悟到一个使用声明式/自动化系统的核心道理:这类系统的强大,建立在"你提供的声明/配置"的准确和完整之上;它不会"读心"、不会"替你想到你没说的"——它只会严格执行你声明的,以及在你没声明时用它的默认值;而很多问题,恰恰出在"那些你以为系统会自动处理、但其实需要你显式声明、而你又没声明"的地方。所以,用好这类系统的关键,是清楚地知道"哪些是我必须显式声明的"(尤其是那些"跟我的应用特性强相关、系统无法自动猜对的",如就绪条件、资源需求、优雅停机),把它们完整、准确地配置好,而不是依赖一个并不存在的"全自动智能"。不把"声明式"误当"全自动"、为每一个关键的期望负起"显式声明"的责任——这,是我用一次"发布必抖动"的事故,换来的、关于 DevOps、也关于"声明式系统的使用之道"的、最朴素也最深刻的领悟。如果这篇复盘,能让你回去就给自己的服务配上 readiness 探针,那我对着那每次发布必现的一片 502 熬的这大半天,就值了。
—— 别看了 · 2026