我每次发布服务都有一两分钟大量 502、用户骂声一片,可实例明明都起来了,我对着 K8s 健康检查探针的配置排查了大半天的复盘

负责的一个跑在 K8s 上的服务,功能没问题,可每次滚动发布都有一两分钟大量 502/503,用户骂声一片,过一两分钟又恢复。一开始以为是发布的必然抖动,越想越不对:滚动发布不就是为了不中断吗?新实例起来才切流量啊怎么还502?排查大半天才理解 K8s 健康检查探针的门道和我没配 readiness 探针的致命疏忽:没配 readiness 时,K8s 只看进程起没起来判断就绪,进程一启动就切流量,但进程起来 ≠ 应用能处理请求(Java/Spring 还要几十秒做初始化),在这个初始化窗口期流量打过来就 502。这篇从 readiness(能接流量吗,失败摘流量不重启)/liveness(还活着吗,失败重启)/startup(启动完了吗) 三种探针、配齐三探针各司其职/readiness 查依赖、liveness 绝不查依赖的正解、健康检查配错的典型灾难(liveness 查依赖致全体重启雪崩最危险)、三探针对比、平滑发布配置清单、决策图与铁律,到附上完整的探针 YAML 和健康检查接口设计。核心领悟:声明式系统(K8s)忠实实现你声明的期望但前提是你声明准确完整,它不会读心、不会替你想到你没说的,关键且跟应用强相关的期望(就绪条件)必须显式声明;别把声明式误当全自动;自动化反应机制要确保反应真能解决问题(重启解决不了依赖挂)。

我每次发布服务都有一两分钟大量 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 部署时,刻进骨子里的几条铁律:

  1. 必须配 readiness 探针。否则进程一起来 K8s 就切流量,初始化窗口期大量 502。
  2. readiness 查"真就绪"(含关键依赖),liveness 只查"进程活没活"。两者检查内容必须不同。
  3. liveness 绝不能检查外部依赖。否则依赖一抖所有 Pod 被全部重启,雪崩。
  4. liveness 要宽松。超时给足、失败阈值大,别因偶尔慢一下就误杀正常实例。
  5. 启动慢的应用配 startup 探针。给足启动时间,防"启动→被杀"死循环。
  6. 探针配合优雅停机。关 Pod 时先摘流量、等请求处理完,别中断进行中的请求。
  7. 发布前演练验证。在测试环境滚动发布,确认全程无 502 再上生产。

写在最后

回头看,这场由"没配 readiness 探针"引发的、每次发布必抖动的事故,真正教给我的,远不止"记得配健康检查"这一个技巧。它让我对"声明式系统"和"使用者的责任"之间的关系,有了更深的理解。我栽跟头,是因为我对 Kubernetes 抱有一种"它会自动把一切都处理好"的过度期待:我以为只要把应用容器化、丢给 K8s,它就会"智能地"帮我实现"不中断的滚动发布"。可这次事故让我明白:K8s 是一个"声明式"的系统——它会忠实地、强大地去实现"你声明的期望状态",但前提是"你得准确地、完整地把你的期望声明清楚"而我,恰恰漏掉了一个关键的"声明":我没告诉 K8s"我的应用怎样才算'就绪'";于是 K8s 只能用它的默认理解(进程起来=就绪)去判断,而这个默认理解,对我的应用来说是错的这让我领悟到一个使用声明式/自动化系统的核心道理:这类系统的强大,建立在"你提供的声明/配置"的准确和完整之上;它不会"读心"、不会"替你想到你没说的"——它只会严格执行你声明的,以及在你没声明时用它的默认值;而很多问题,恰恰出在"那些你以为系统会自动处理、但其实需要你显式声明、而你又没声明"的地方所以,用好这类系统的关键,是清楚地知道"哪些是我必须显式声明的"(尤其是那些"跟我的应用特性强相关、系统无法自动猜对的",如就绪条件、资源需求、优雅停机),把它们完整、准确地配置好,而不是依赖一个并不存在的"全自动智能"。不把"声明式"误当"全自动"、为每一个关键的期望负起"显式声明"的责任——这,是我用一次"发布必抖动"的事故,换来的、关于 DevOps、也关于"声明式系统的使用之道"的、最朴素也最深刻的领悟。如果这篇复盘,能让你回去就给自己的服务配上 readiness 探针,那我对着那每次发布必现的一片 502 熬的这大半天,就值了。

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

我的服务从连接池取到的长连接其实早就"死"了、发请求全卡到超时,可连接池却以为它还活着,我对着连接假死和心跳保活排查了大半天的复盘

2026-6-2 7:46:36

技术教程

我的 RAG 知识库问答总是答非所问、要么答不全要么牛头不对马嘴,模型和向量库都没问题,我对着文档切分的 chunking 排查了大半天的复盘

2026-6-2 7:57:37

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