1. 先搞清楚 Kubernetes 健康检查到底在检查什么1.1 健康检查不是“应用自检”而是 kubelet 的外部视角做 Kubernetes 运维这几年“健康检查”这个词我几乎每天都在碰。很多刚上手的朋友会把探针当成“容器内的一个定时器”以为只要写了 liveness 就能保证服务不出事结果生产上一个误杀就把整条链路打挂了。这篇文章不吃理论大餐直接讲 Kubernetes 健康检查的三种探针怎么配、参数怎么推导、常见翻车场景怎么排查。首先要纠正一个认知健康检查是 kubelet 从宿主机视角对容器做“外部探测”不是应用自己往控制面报告“我还活着”。你在 Pod 里写一个脚本检测自身状态那只是应用逻辑Kubernetes 是不认的。kubelet 会按照你配置的探针定期发起 HTTP 请求、TCP 连接或者 exec 命令根据结果决定这个容器的生死和流量归属。这个外部视角非常重要。容器进程存在不代表业务就绪端口监听不代表依赖可用。我见过很多案例Java 进程还在但线程池耗尽、数据库连接池打满接口全部超时这时候从容器层面看PID 是活的CPU 也正常但用户请求已经全挂在等待队列里。如果没有 Readiness 探针把这些异常实例从负载均衡端点里摘掉流量会继续灌进去雪崩就是这么一步步扩大的。健康检查解决的是两个完全不同的目标一是“要不要继续给它发流量”二是“出问题了要不要把它重启掉”。这两个目标如果混在一个探针里后果往往很严重——要么流量打到坏实例上要么一个瞬时抖动触发大规模重启。1.2 三类探针怎么分工Kubernetes 里内置了三类探针livenessProbe、readinessProbe、startupProbe。名字很直白但很多人栽在“以为字面意思就是全部”上。livenessProbe 管存活失败后 kubelet 会杀掉容器并按 restartPolicy 决定是否重启。这是自愈机制适合兜底死锁、内存泄漏导致的进程僵死。readinessProbe 管就绪失败后 Pod 会被标记为 NotReady然后从 Service 的 Endpoints 中剔除但它不会被重启。startupProbe 是后来加的保护机制专门对付启动特别慢的容器在它成功之前liveness 和 readiness 都会被暂时禁掉。用一个表格看清楚探针类型失败后果适用场景核心关键词livenessProbe容器被 kill、可能重启进程死锁、业务僵死、内存泄漏无法自愈要不要活readinessProbe从 Service Endpoints 摘除不重启依赖未就绪、流量过载、启动未完成要不要接流量startupProbe容器被 kill、可能重启老应用启动特慢、liveness 误杀启动过程等它慢慢起注意一个细节liveness 失败时kubelet 通常只重启“容器”不一定是整个 Pod。具体取决于 restartPolicy如果策略是 Always容器会在同一个 Pod 内重新拉起restartCount 加 1。如果策略是 Never容器不会被重启Pod 会进入 Failed。这个差异在排查“Pod 一直 Running 但容器不停重启”的时候很关键。我个人的理解是startupProbe 是给 livenessProbe 上的一道保险。很多老旧应用从进程启动到真正可服务需要 40 秒甚至 2 分钟如果 liveness 从容器启动那一刻就开始算极大概率在应用还没起来时就触发重启形成 CrashLoopBackOff。有了 startup 探针你只需要告诉 kubelet“先给它 XX 秒别急着杀它。” 等它起来了再把“存活判断权”交还给 liveness。2. 探针配置参数含义与“踩坑”阈值计算2.1 五个标准参数逐个拆解所有探针都有一组共同的配置参数initialDelaySeconds、periodSeconds、timeoutSeconds、failureThreshold、successThreshold。刚接触的人往往只关心 path 和 port把后面这些默认值当成空气实际上这才是最容易埋雷的地方。initialDelaySeconds 表示容器启动之后等多少秒才开始第一次探测。期间 kubelet 不执行任何探针给应用留出“进程初始化”的时间。periodSeconds 是两次探测之间的间隔默认 10 秒最小可以设成 1 秒但我不建议小于 3 秒。timeoutSeconds 是每次探测的超时时间默认只有 1 秒这个值在很多业务系统里根本不够用接口响应只要超过 1 秒就会被判定为失败这一点我后面会专门讲。failureThreshold 是连续失败多少次之后才认定探针失败。默认 3 次看的是“连续失败”只要中间成功一次计数器就会清零。successThreshold 正好相反连续成功多少次才认为探针通过默认 1 次。Readiness 探针的 successThreshold 可以设置成 2 或 3用来防止“刚 Ready 又被打掉”的抖动但 liveness 和 startup 探针的 successThreshold 必须保持 1这是硬限制。我见过的生产事故里至少有三分之一和“默认值直接上线”有关。默认 periodSeconds10、failureThreshold3意味着一次失败判定至少要经过约 20 到 30 秒窗口如果业务调用方超时只有 2 秒这个恢复速度根本不够看。反过来如果你把一个纯 liveness 探针设成 period1、failureThreshold1那就是在制造不稳定——稍微抖一次就把容器杀了。2.2 一次完整重启周期推导探针的失败判定时间是有公式的但是网上很多文章没说透。最坏情况的下持续失败到判定失败的时间大约是初始等待时间 (连续失败次数 - 1) * 探测周期也就是说如果一个探针配置是initialDelaySeconds: 10, periodSeconds: 10, failureThreshold: 3那么最坏情况下容器启动后 10 秒开始第一次探测20 秒第二次30 秒第三次第三次失败后 kubelet 认定探针失败。这里的“判定失败时刻”是 10 (3-1)*10 30 秒。举例来说一个 Java 服务进程起来大约需要 20 秒Spring 上下文加载完要 45 秒。如果你只配了 livenesslivenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 10 periodSeconds: 10 failureThreshold: 3按公式算第 3 次失败发生在 30 秒左右可应用要到 45 秒才起来这必然触发重启。容器被杀了之后又会重新启动30 秒后又杀形成一个永远起不来的循环。这就是“启动太慢导致 CrashLoopBackOff”的典型原因。正确的做法是引入 startupProbe让它来覆盖启动窗口。比如startupProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 0 periodSeconds: 5 failureThreshold: 12这个 startup 探针在容器启动后立刻开始探测每 5 秒一次连续 12 次失败才会判定失败也就是最多给容器约 55 秒的启动时间。如果应用在 45 秒就绪那 startup 就能成功随后 liveness 才正式接管。这里有个很多人都忽略的时序问题startup 探针成功之前readiness 和 liveness 都会被禁用startup 成功之后它们才开始执行。所以你在配 liveness 的时候initialDelaySeconds 不要比 startup 窗口大太多一般只要保证“在 startup 成功之后liveness 还能有至少一到两个周期的缓冲”就够了。2.3 参数设置的“宁可长一点也别过早下结论”探针参数的调优原则我一直跟团队说的是宁可长一点也别过早下结论。这个“长”是有上限的不是无脑大。如果是普通无状态 HTTP 服务我通常这样定初始值readinessinitialDelaySeconds10periodSeconds5timeoutSeconds2failureThreshold3successThreshold1livenessinitialDelaySeconds30periodSeconds10timeoutSeconds2failureThreshold3startupinitialDelaySeconds0periodSeconds5timeoutSeconds2failureThreshold12遇到老系统、依赖多、启动慢的startup 的 failureThreshold 直接给到 20 甚至 30。比如一个应用需要 90 秒才能就绪period5、failureThreshold20窗口大约是 (20-1)*5 95 秒够用了。注意不要把启动等待全压在 initialDelay 上因为 initialDelay 只影响探针开始时间不参与失败计数逻辑startup 才是更合理的“启动保护伞”。还有一点periodSeconds 不宜追求过分激进。readiness 设成 5 秒可以接受liveness 我建议至少 10 秒。因为 liveness 每次失败都可能导致重启周期太短会把一次瞬时抖动放大成全量重启而 readiness 失败只是摘流量可以更敏感一点但也要配合 failureThreshold 和 successThreshold避免频繁抖动。3. 三种探针实现的具体写法与适用场景3.1 httpGet 探针最常用但路径和状态码要盯紧绝大多数 Web 服务都用 httpGet 探针因为它最直观kubelet 定期向容器内某个 HTTP 接口发起 GET 请求只要返回的状态码在 200 到 399 之间就视为成功否则视为失败。也就是说 4xx 和 5xx 都算失败。写起来很简单livenessProbe: httpGet: path: /healthz/live port: 8080 httpHeaders: - name: X-K8s-Probe-Token value: some-static-token initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 2 failureThreshold: 3这里有几个坑必须说清楚。第一探针是从宿主机发起的不是从 Pod 内部发起的。如果应用只监听了 127.0.0.1httpGet 探针会失败因为 kubelet 访问的是 Pod IP。应用监听地址要写成 0.0.0.0 或者 Pod IP。这一点在网络插件和防火墙策略复杂的集群里特别容易踩。第二健康检查接口要轻量。别在/healthz里做全量数据库查询、清缓存、跑定时任务否则一次探针请求就可能把实例拖垮。liveness 的职责是“进程还活着”readiness 的职责是“依赖基本可用”都不适合做重型检查。第三timeoutSeconds 要大于接口的真实响应时间。很多开发同学默认把健康检查接口写成“即时返回”但实际上应用在高负载下一个健康检查请求也可能排队 1 到 2 秒。如果 timeoutSeconds 保持默认 1 秒高峰期探针就会连续失败触发误判。我一般在生产环境至少给到 2 秒复杂的给到 3 到 5 秒。第四健康检查接口如果需要登录或者鉴权要么用 httpHeaders 把 token 显式带上要么做成无需鉴权的独立端点。我见过有团队把健康检查放到需要 SSO 登录的路径后面探针全部 302 跳转最后一堆 Pod 从 Endpoints 里消失业务直接中断。3.2 tcpSocket 探针只通端口不等于业务可用tcpSocket 探针做的事情更底层kubelet 尝试与容器指定端口建立 TCP 连接只要能连上就判定成功连不上就失败。readinessProbe: tcpSocket: port: 3306 initialDelaySeconds: 5 periodSeconds: 10 timeoutSeconds: 2 failureThreshold: 3tcpSocket 的适用场景很明确非 HTTP 协议的服务比如 MySQL、Redis、消息队列、自研 TCP 网关。它不需要应用实现一个专门的健康检查协议只要端口能监听起来就行。但它的局限也非常明显端口通不等于业务可用。一个服务可能 TCP 三次握手能成功但内部线程池已经打满新的连接虽然建立了却得不到响应。另一个常见例子是代理服务监听端口正常但后端的真实服务已经挂了TCP 探针仍然显示成功。所以 tcpSocket 更适合作为“进程是否在监听”的兜底探针而不是业务质量探针。如果你的服务既能用 HTTP 探针就尽量用 HTTP 探针实在只能 TCP也要接受它的判定粒度比较粗。3.3 exec 探针适合容器内没有 HTTP 服务的场景exec 探针是在容器内执行一条命令通过退出码判断结果0 表示成功非 0 表示失败。livenessProbe: exec: command: - /bin/sh - -c - pgrep -f myapp || exit 1 initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3exec 探针最典型的用处是容器里的主程序不是 Web 服务或者不方便暴露 HTTP 健康端点。比如一个后台任务进程、一个消息消费 worker你想知道它是否还在运行可以用 pgrep 或者检查 PID 文件。但 exec 探针有三个非常容易踩的坑。第一个是性能开销。每次探测都要在容器内 fork 一个进程如果反复执行复杂 shell 脚本对节点 CPU 和容器文件系统都有压力。健康检查频率越高开销越明显。所以 exec 探针的 periodSeconds 不建议低于 10 秒command 也要尽量短。第二个是 shell 的退出码陷阱。很多同学写成/bin/sh -c pgrep myapp exit 0或者干脆写pgrep myapp但没注意到sh -c返回的是最后一个命令的退出码有时候最后一条 pipeline 成功了前面核心判断却失败了探针结果完全偏离预期。第三个是“进程在但业务死”的问题。exec 探针通常只能判断进程是否存在判断不了线程池是否可用、依赖是否健康。如果你的 worker 进程陷入了死循环但还没退出exec 探针照样显示成功。这种情况还得靠业务层自己上报心跳。3.4 探针类型怎么选先看进程在不在再看业务通不通我给团队定的选型逻辑很简单第一层确认进程在不在第二层确认业务通不通。应用类型推荐 readiness推荐 liveness推荐 startupWeb/HTTP 服务httpGet/healthz/readyhttpGet/healthz/livehttpGet/healthz/liveTCP/MySQL/Redis 类tcpSocket 端口tcpSocket 端口 或 exec根据启动速度而定后台 Worker/Consumerexec 或业务心跳接口exec 判断进程存活启动慢则加 startup短时任务/批处理不建议配 readiness通常不配 liveness一般不配 startup还有一个容易忽略的组合一个容器可以同时配置三个探针也可以只配其中两个。比如纯批处理任务跑完就退出如果你配了 liveness任务退出时 liveness 会判定失败然后容器被重启业务逻辑重新执行一遍这很可能是灾难。短时任务的容器通常不需要探针或者只需要在任务入口处做 readiness 控制让 Job 的调度逻辑来判断。4. 从零配置一套健康检查完整实战流程4.1 先确认业务健康检查点很多团队做健康检查是在部署文件里抄一段模板根本没有想清楚健康检查接口要长什么样。我的习惯是先让应用侧把健康检查端点拆分出来再写 Kubernetes 配置。一个比较稳的分法是两个端点/healthz/live只检查进程是否活着、主线程是否没有死锁。它不依赖数据库、Redis、消息队列等外部组件。这个端点给 liveness 和 startup 用。/healthz/ready检查当前实例是否具备服务能力比如数据库是否能连、缓存是否能读写、关键依赖是否可用。这个端点给 readiness 用。为什么要分开因为 liveness 失败会导致容器重启如果把数据库检查放进去数据库一抖动所有实例就可能同时被重启这是最典型的雪崩诱因。数据库恢复之后服务还得重新冷启动恢复时间反而更长。你可以把/healthz/ready设计成“容忍短时抖动”的检查依赖出问题时可以延迟判断而不是一失败立刻返回 500。例如连续 3 次从数据库读取失败才标记依赖不健康单次失败不要立刻影响 readiness。4.2 一份可以直接改的 Deployment YAML下面是一份我常用的示例配置你可以直接套用apiVersion: apps/v1 kind: Deployment metadata: name: demo-app spec: replicas: 3 selector: matchLabels: app: demo-app template: metadata: labels: app: demo-app spec: containers: - name: demo-app image: registry.example.com/demo-app:v1.0.0 ports: - containerPort: 8080 resources: requests: cpu: 100m memory: 256Mi startupProbe: httpGet: path: /healthz/live port: 8080 initialDelaySeconds: 0 periodSeconds: 5 timeoutSeconds: 2 failureThreshold: 12 readinessProbe: httpGet: path: /healthz/ready port: 8080 initialDelaySeconds: 10 periodSeconds: 5 timeoutSeconds: 2 failureThreshold: 3 successThreshold: 1 livenessProbe: httpGet: path: /healthz/live port: 8080 initialDelaySeconds: 60 periodSeconds: 10 timeoutSeconds: 2 failureThreshold: 3这份配置对应的是一个需要大约 45 秒才能完成启动、之后进入稳定状态的 Java 服务。startupProbe 的窗口是(12-1)*5 55秒默认放行窗口约 55 秒如果你的应用需要更长时间把 failureThreshold 增大比如 15 次就是 70 秒20 次就是 95 秒。readinessProbe 每隔 5 秒探测一次启动完成后如果依赖没就绪最多连续失败 3 次就把实例从 Endpoints 摘掉。livenessProbe 的 initialDelaySeconds 给了 60 秒目的是让 startup 保护窗口结束后liveness 仍然有一段缓冲期避免刚完成启动就被误杀。这里的端口不一定要写在 containerPort 里探针可以直接探测任何容器内可访问的端口但写出来更清晰也方便其他配置引用。4.3 部署后如何验证探针是否生效写完配置不是结束验证才是关键。部署之后我会按这样的顺序来检查先用kubectl apply发布然后kubectl get pods -w观察 Pod 状态。正常情况下Pod 会从 Pending 走到 ContainerCreating再走到 Running然后 Ready 列变成1/1。如果 Ready 一直是0/1说明探针已经判定失败需要进一步排查。用kubectl describe pod pod-name可以看到探针的详细配置和最近事件。比如事件里出现Unhealthy后面会跟着探针类型和失败原因如果是 HTTP 探针失败会显示HTTP probe failed with statuscode: 500如果是 exec 失败会显示退出码或错误信息。这些事件是排错的第一手资料。之后做两个主动故障演练第一个演练是“kill 主进程”。进入容器后把应用主进程杀掉比如 Java 应用ps -ef | grep java找到 PID再 kill。如果配置了 livenesskubelet 会在下一轮探测时发现进程不存在或者健康检查接口无响应然后容器被重启。你会在kubectl get pod -o yaml里看到restartCount增加。第二个演练是“让 readiness 失败”。你可以在容器内临时停掉/healthz/ready所依赖的组件或者直接改返回值让 healthz 返回 500。这时 Pod 的状态会变成 NotReady但容器不会重启。同时用kubectl get endpoints或kubectl get endpointslices观察这个 Pod 的 IP 会从 Ready 地址列表里消失。等恢复健康后它又会自动加回来。我非常建议在测试环境把这两个演练做成自动化用例每次改完探针配置都跑一遍。这样能避免“上线时探针配错了但流量少没暴露到高峰期突然流量全部打挂”的情况。4.4 滚动更新里的“串行放量”为什么要靠 readinessDeployment 滚动更新时探针的作用会被放大。默认情况下Deployment 会先创建一个新 Pod等它 Ready 之后再删除旧 Pod。如果新 Pod 一直没有 Ready滚动更新会卡住旧 Pod 不会被删新 Pod 也不会继续扩容。这其实是保护机制新版本有问题时readiness 探针会拦住它上量。但前提是你把 readiness 配对了。如果 readiness 探针只是“端口通就行”新 Pod 即使业务初始化失败也会被标记为 Ready流量照样打进去。建议给 Deployment 额外配一个minReadySeconds。它不算探针但和探针是黄金搭档。minReadySeconds表示 Pod 转为 Ready 之后至少要保持多少秒才算真正可用。比如spec: minReadySeconds: 30这个参数可以防止“新 Pod 刚 Ready进入流量后马上抖动readiness 又失败滚动更新中断”的恶性循环。配合 readinessProbe新 Pod 只有“探针通过 稳定运行 30 秒”这两个条件都满足才会被纳入流量。maxSurge 和 maxUnavailable 是控制滚动更新速率的但如果探针配置有问题调这两个参数只能事倍功半。我的经验是先把探针调稳再根据发布速度去调滚动参数。5. 常见探针翻车现场与排查套路5.1 场景一容器疯狂重启但业务日志没有报错这是最迷惑人的问题。你去看应用日志一切正常连异常堆栈都没有但 Pod 里的容器一直重启。这时候十有八九是探针把你杀了。用kubectl describe pod pod-name查看事件通常能看到类似Warning Unhealthy 3s Liveness probe failed: HTTP probe failed with statuscode: 500如果看到这条直接去访问应用的/healthz接口看看 500 是哪里来的。常见原因有三个应用监听地址绑定了 localhost探针访问不到健康检查路径写错返回 404 被当成失败健康检查接口内部依赖某个组件该组件当前抖动。还有一种情况是健康检查接口本身响应太慢超过了 timeoutSeconds。事件里可能显示context deadline exceeded或者timeout。这时把 timeoutSeconds 从 1 秒调到 2 到 3 秒多半能解决。但如果一个健康检查接口经常需要超过 2 秒才能响应那这个接口本身就太重了建议把业务逻辑拆分出去。5.2 场景二服务被 Readiness 频繁摘流量接口超时量增加某天监控里突然发现服务时不时从 Endpoints 里掉出去但容器没重启一看事件全是Readiness probe failed。先别急着调探针先想清楚这个实例是不是真的不健康。如果它只是响应慢readiness 把它摘掉是对的但如果它只是偶尔一次抖动摘掉之后流量转移到其他实例又导致其他实例过载形成连锁反应那就说明 readiness 太敏感。我的调法有几个方向一是把 successThreshold 从 1 调到 2。对于新启动的实例这个参数要求连续两次成功才加入流量可以避免“刚 Ready 又挂了”的振荡。二是把 failureThreshold 从 3 调到 5。这样单次网络抖动不会触发摘除只有持续失败一段时间才判定不健康。三是检查健康检查端点是否包含了过于严格的依赖检查。比如 readiness 接口一旦查不到 Redis 就返回 500而 Redis 恰好重启了一次那所有实例会在同一时间被摘掉这非常恐怖。四是结合 minReadySeconds。很多时候不是探针参数问题而是滚动更新太快新 Pod 一 Ready 就接流量瞬间负载上来readiness 被打掉。加 30 到 60 秒稳定期问题就会消失。5.3 场景三老应用启动特别慢一上线就 CrashLoopBackOff这种问题几乎都出在“没有配置 startupProbe 或者配置不当”。老应用启动慢、依赖多甚至第一次请求前要做一堆初始化期间健康检查接口要么不响应要么返回 503。解决办法是给启动过程一个明确的宽限窗口。比如应用需要 90 秒启动配置startupProbe: httpGet: path: /healthz/live port: 8080 initialDelaySeconds: 0 periodSeconds: 5 timeoutSeconds: 2 failureThreshold: 20这样最坏情况下有(20-1)*5 95秒的启动窗口。如果 95 秒还不够就把 failureThreshold 继续加大直到覆盖实际启动时间再留出 20% 到 30% 的余量。注意一个反面教训有人为了省事不给应用配 startupProbe而是把 livenessProbe 的 failureThreshold 调到 100并设置一个超长的 initialDelay。这个方法表面上也能让应用启动起来但代价是业务真正死锁后需要很久才能被探测出来并重启故障恢复时间被严重拉长。启动这件事就交给 startupProbeliveness 只负责运行时兜底职责边界要清晰。5.4 排查工具箱与速查表探针排错的核心工具链其实就那么几个整理成速查表现象可能原因优先检查容器不断重启应用日志正常liveness/startup 误判kubectl describe 查看 Unhealthy 事件Pod 长时间 NotReady但容器运行readiness 失败curl 健康检查端点观察 Endpoints探针事件显示 timeout接口响应慢 / timeoutSeconds 过小调大 timeoutSeconds压测响应耗时事件无探针告警容器仍重启进程本身崩溃退出kubectl logs --previous查看退出码滚动更新卡住新版本 readiness 一直不通过查看新版 Pod 的健康检查状态健康检查请求被拒绝网络插件/探针来源 IP 被安全策略拦截在宿主机手动 curl Pod IP 验证还有两条实用心得。第一Pod 的/status里会显示 containerStatuses 的 restarts 次数和 lastTerminationReason这个是确认容器是否被探针杀掉的最快途径。第二很多 CNI 安全策略可能会拦截 kubelet 到 Pod IP 的探针流量排查时要记住“探针是从宿主机发起的”不能只看容器内部。最后给一个平时不太有人提的小技巧如果你发现探针事件里频繁出现 Unhealthy但应用整体表现正常可以先看看最近有没有集群 Node 的网络或者内核抖动。kubelet 探针发出后如果短时间内收不到响应也会被判失败这种情况下应用是“被无辜牵连”的。你可以在 Node 上执行journalctl -u kubelet -f --grep probe观察是否有大量探测超时再决定是调探针参数还是优先修基础设施问题。我个人在实际操作中的体会是健康检查不是上线前最后一个想到的配置项而是服务可用性设计的一部分。每个服务从开发第一天就应该把/healthz/live和/healthz/ready两个端点定好探针参数写进发布变更单里每次业务改动都要重新过一遍启动时间和慢接口情况。你愿意在这上面多花半小时后面能少熬好几个凌晨的故障恢复夜。