2023 年初我们把核心订单服务迁上了 Kubernetes。迁完那阵子我特别有成就感:滚动更新、自动扩缩容、Pod 挂了自己拉起来,运维群里安静了不少。我照着教程给每个服务都配了 liveness probe——"探活嘛,容器死了 K8s 帮你重启,多省心"。直到那年的一次大促:流量上来后,订单服务明明还在正常处理请求,日志里订单一笔笔在成交,可监控上 Pod 却开始成片地 CrashLoopBackOff——被 K8s 反复 kill、反复重启。重启中的 Pod 不接流量,活着的 Pod 要扛更多压力、变得更慢,然后它们的探针也开始超时,又被 kill……整个服务在十分钟内雪崩。我盯着 kubectl describe 里那行 Liveness probe failed 看了很久才反应过来:我的 liveness 探针超时设的是 1 秒,而大促时一次 GC 停顿、一个慢查询,就足以让健康检查接口响应超过 1 秒。K8s 不知道这个容器只是"忙",它只看到探针失败,就老老实实地执行了我让它执行的动作:kill,重启。我亲手配的探针,成了压垮服务的那只手。那次之后我才认真搞懂:K8s 的探针不是"配了就有保障"的东西,它是一组你交给 K8s 的、关于"容器到底算不算健康"的指令——指令下错了,K8s 会忠实地把你的服务搞垮。这篇文章就把三种探针从头梳理一遍:它们各自管什么、怎么配、时间参数怎么调,以及那些配错了会反噬的坑。
问题背景
先把那次事故的现象和我的误判讲清楚,后面所有的配置建议,都是冲着纠正这个误判去的。
现象:订单服务部署在 K8s 上,每个 Pod 配了 liveness probe。大促流量高峰时,服务本身仍在正常处理请求,但 Pod 因 liveness 探针超时被 K8s 判定为"不健康",成片进入 CrashLoopBackOff 被反复重启;重启进一步减少了可用实例,加剧了剩余 Pod 的压力,最终雪崩。
我当时的错误认知:"liveness probe 就是探活,配上它,容器一出问题 K8s 就帮我重启,只有好处没有坏处。"
真相:liveness 探针失败的后果是重启容器,这是一个不可小看的破坏性动作。探针"失败"不等于"容器真的死了"——它也可能只是容器在忙、在 GC、在等一个慢依赖。如果你的探针把"忙"误判成"死",K8s 就会去重启一个本来好好的容器;而在高负载下,这种误判会成批发生,重启反而成了雪崩的助推器。探针配置的本质,是你要精确地告诉 K8s:什么情况算"真的不行了",以及"不行了"之后该做什么。
要把探针用对,需要几块认知:
- liveness、readiness、startup 三种探针各自管什么,失败后的动作天差地别;
- 四种检查方式(httpGet / exec / tcpSocket / grpc)分别适合什么场景;
- 探针的几个时间参数怎么调,调紧了会误杀、调松了会迟钝;
- liveness 探针为什么绝不能检查外部依赖;
- readiness 探针怎么和优雅下线、滚动更新配合。
一、三种探针:liveness、readiness、startup 各管什么
K8s 一共有三种探针,很多人(包括当年的我)只配了 liveness,这是出问题的根源。这三种探针探测的方式可以完全一样,但探测失败后 K8s 做的事情完全不同——搞混它们,后果就是上面那场雪崩。
liveness probe(存活探针):回答"这个容器还活着吗?需不需要重启?"。它失败到阈值后,K8s 会 kill 掉容器并重启。它用来兜底那些"进程还在、但已经卡死、重启才能恢复"的情况,比如死锁、线程池耗尽且无法自愈。
readiness probe(就绪探针):回答"这个容器现在能接收流量吗?"。它失败后,K8s 不会重启容器,而是把这个 Pod 从 Service 的 Endpoints 列表里摘掉——流量不再转发给它,但它继续运行。等它探针恢复成功,再自动加回去。它用来处理"容器活着,但暂时不该接流量"的情况,比如还在启动、依赖暂时不可用、正在下线。
startup probe(启动探针):回答"这个容器启动完成了吗?"。在它成功之前,liveness 和 readiness 探针都不会运行。它专门给启动慢的应用(比如冷启动要几十秒的 JVM 服务)用,避免应用还在启动就被 liveness 误杀。
下面先看一段事故现场:一个 Pod 进入 CrashLoopBackOff 后,kubectl 看到的样子。学会读这两段输出,是排查探针问题的第一步:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
order-svc-7d9f8c-abc12 0/1 CrashLoopBackOff 7 12m
$ kubectl describe pod order-svc-7d9f8c-abc12
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning Unhealthy 2m (x9 over 8m) kubelet Liveness probe failed: Get
"http://10.1.2.3:8080/healthz": context deadline exceeded
Normal Killing 2m (x3 over 8m) kubelet Container order-svc failed
liveness probe, will be restarted
Normal Pulled 1m (x4 over 12m) kubelet Container image already present
Normal Created 1m (x4 over 12m) kubelet Created container order-svc
Normal Started 1m (x4 over 12m) kubelet Started container order-svc
这段 Events 把因果链写得清清楚楚:Liveness probe failed ... context deadline exceeded(探针超时),紧接着 failed liveness probe, will be restarted(于是重启)。RESTARTS 7 和 x9 over 8m 说明这不是偶发,而是反复发生。看到这种链路,第一反应就该是:不是容器真死了,是探针判断标准出了问题。
二、四种检查方式:httpGet、exec、tcpSocket、grpc
知道了三种探针"管什么",再看它们"怎么探"。每一种探针,都可以用下面四种方式之一来做实际的检查。检查方式选得合不合适,直接影响探针准不准。
httpGet:最常用。kubelet 向容器的某个 HTTP 路径发 GET 请求,返回状态码在 200–399 之间算成功,其他算失败。适合有 HTTP 接口的 Web 服务,你需要在应用里专门写一个健康检查端点。
exec:在容器内执行一条命令,退出码为 0 算成功,非 0 算失败。适合没有 HTTP 接口的服务(比如后台 worker),或者需要检查文件、进程等容器内部状态的场景。注意 exec 每次探测都要起一个进程,开销比 httpGet 大。
tcpSocket:kubelet 尝试和容器的某个端口建立 TCP 连接,连上了算成功。它只能证明"端口在监听",不能证明"服务逻辑正常"。适合纯 TCP 服务,或作为一种最轻量的兜底检查。
grpc:K8s 1.24+ 原生支持,kubelet 调用容器实现的标准 gRPC 健康检查协议(grpc.health.v1.Health)。适合 gRPC 服务,比之前用 grpc_health_probe 二进制配 exec 要干净得多。
下面用 YAML 把这四种方式各写一遍,放在同一个容器定义里对照看(实际中一个探针只选一种):
# 方式一:httpGet —— 最常用,访问一个 HTTP 健康检查路径
livenessProbe:
httpGet:
path: /healthz
port: 8080
httpHeaders:
- name: X-Probe-Source
value: kubelet
# 方式二:exec —— 在容器内执行命令,退出码 0 算成功
livenessProbe:
exec:
command:
- sh
- -c
- "test -f /tmp/healthy" # 文件在就算健康
# 方式三:tcpSocket —— 能建立 TCP 连接就算成功(只证明端口在听)
readinessProbe:
tcpSocket:
port: 8080
# 方式四:grpc —— K8s 1.24+ 原生支持标准 gRPC 健康检查协议
livenessProbe:
grpc:
port: 50051
service: "" # 留空表示检查整个服务的健康状态
这里要强调一个最常见的误用:tcpSocket 探活的"健康"含金量很低。端口在监听,只说明进程的网络层还在,完全不代表业务逻辑正常——一个线程池耗尽、所有请求都在排队的服务,它的端口照样连得上。所以对有 HTTP 接口的服务,优先用 httpGet 配一个真正能反映业务状态的端点,而不是图省事用 tcpSocket。
三、时间参数:调紧了误杀,调松了迟钝
开头那场雪崩,直接原因就是一个时间参数(timeoutSeconds)调得太紧。探针有五个时间相关参数,它们共同决定了"多快、多敏感地"做出判断。理解它们,才能在"反应及时"和"不误杀"之间找到平衡。
initialDelaySeconds:容器启动后,等待多少秒才开始第一次探测。如果应用要 30 秒才启动完,这个值设小了,探针会在应用还没起来时就判失败。(更好的做法是用 startup probe,见第六节。)
periodSeconds:探测的间隔,默认 10 秒。多久探一次。
timeoutSeconds:单次探测的超时,默认 1 秒。这就是坑我的那个参数——默认 1 秒对很多服务都太紧了,高负载下一次正常但稍慢的响应就会超时。
failureThreshold:连续失败多少次,才最终判定探针失败,默认 3 次。它是"误杀"的最后一道缓冲——把它和 periodSeconds 一起看:failureThreshold × periodSeconds 就是"从开始不正常,到 K8s 动手"的总时间。
successThreshold:连续成功多少次才判定为成功,默认 1 次。对 readiness 探针有意义(避免服务在好坏之间抖动时频繁进出 Endpoints);liveness 和 startup 这个值必须是 1。
下面这段 YAML 把每个参数都标上注释,并算出这套配置的实际"容忍窗口":
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 20 # 容器启动后,先等 20s 再开始第一次探测
periodSeconds: 10 # 之后每 10s 探一次
timeoutSeconds: 5 # 单次探测最多等 5s(默认 1s 太紧,务必调大)
failureThreshold: 3 # 连续失败 3 次才判定失败
successThreshold: 1 # liveness 必须为 1
# 这套配置的实际效果:
# - 容器从"开始不正常"到"被 kill 重启",最坏要经过:
# timeoutSeconds + periodSeconds × (failureThreshold - 1)
# = 5 + 10 × 2 = 25 秒
# - 也就是说,一次持续不到 25 秒的卡顿,不会触发重启
# - 这个窗口要大于你服务最长的可接受 GC 停顿 / 慢请求耗时
调参的核心思路是:liveness 探针宁可"迟钝"一点,也不要"敏感"。它的失败动作是重启,代价高、还会引发连锁反应,所以它的容忍窗口(上面算出的 25 秒)应该明显大于你服务在正常压力下可能出现的最长停顿。而 readiness 探针可以相对敏感一些——它失败只是"暂时不接流量",代价小,反应快一点反而能更快把流量从有问题的实例上挪走。
四、liveness 探针的头号坑:它绝不能检查外部依赖
前面讲的都是"怎么配",这一节讲一个"配什么"的原则性错误——它是 K8s 探针领域最经典、也最致命的坑:不要让 liveness 探针去检查数据库、下游服务、缓存这些外部依赖。
想清楚为什么。假设你的 /healthz 端点里写了"先 ping 一下数据库,ping 不通就返回失败"。某一刻数据库抖动了几秒——注意,数据库是被所有 Pod 共享的。于是所有 Pod 的 liveness 探针在同一时刻一起失败。K8s 看到的是:整个 Deployment 的容器全都"死了",于是它忠实地把它们全部 kill、全部重启。可问题来了:重启一个应用容器,根本治不好数据库的抖动。容器重启完,数据库可能还没好,新起来的容器 liveness 立刻又失败,又被重启……你的整个服务陷入了一个由"外部依赖抖动"驱动的、自己永远好不了的重启风暴。本来只是数据库抖了 5 秒、服务短暂不可用,结果被你的 liveness 配置放大成了全军覆没。
正确的划分是:liveness 只回答"我这个进程自己是不是卡死了、必须重启才能恢复"——它应该只检查进程内部的状态(主循环还转不转、有没有死锁),绝不碰任何网络外部依赖。依赖能不能用,是 readiness 的职责(下一节讲)。下面是一个"正确"的 liveness 端点,它只看进程自身,一行都不碰数据库:
package main
import (
"net/http"
"sync/atomic"
"time"
)
// liveness 只回答一个问题:这个进程自己是不是卡死了?
// 它绝不检查数据库、下游服务等任何外部依赖 —— 那是 readiness 的事。
// 后台主循环每跑完一圈,就把当前时间戳写进来
var lastLoopTick atomic.Int64
// healthzHandler:liveness 端点。只看进程内部的"心跳",不碰外部依赖。
func healthzHandler(w http.ResponseWriter, r *http.Request) {
last := lastLoopTick.Load()
// 主循环超过 60s 没有更新心跳,说明它是真的卡死了 —— 这才该重启
if time.Now().Unix()-last > 60 {
w.WriteHeader(http.StatusServiceUnavailable)
w.Write([]byte("main loop stuck, restart needed"))
return
}
// 进程自身在转,就算健康 —— 哪怕此刻数据库连不上,也返回 200
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}
注意这个端点的关键:数据库连不上的时候,它照样返回 200。因为数据库连不上,重启这个容器是没用的——所以 liveness 就不该因此失败。它只在"主循环真的卡死"这种"重启确实能救"的情况下才返回失败。这就是 liveness 探针该有的样子:它的每一次失败,都应该对应一个"重启确实有用"的判断。
五、readiness 探针:把"暂时不行"和优雅下线串起来
上一节把外部依赖检查从 liveness 里赶了出来,那它该去哪?答案是 readiness。readiness 探针回答的是"我现在能不能接流量",它失败的后果只是被摘出 Endpoints、不再收新流量,但容器继续运行——所以它天生就适合处理一切"容器活着、但暂时不该接流量"的情况。
readiness 该检查什么?所有"没有它就没法正常服务"的关键依赖:数据库连不上、必需的下游服务挂了、本地缓存还没预热完——这些情况下,容器是活的,但它处理请求只会返回错误,那就不如先别给它流量。数据库抖动时,readiness 失败把 Pod 摘掉,数据库恢复后 readiness 自动成功、Pod 自动加回来,全程容器不重启,平滑得很。这正是上一节那个雪崩场景的正确解法。
readiness 还有一个极其重要、却常被忽略的用途:优雅下线。一个 Pod 要被删除时,K8s 会同时做两件事——给容器发终止信号、把 Pod 从 Endpoints 摘除——但这两件事不保证谁先谁后。如果容器先收到信号、关掉了服务,而 Endpoints 还没更新,这段时间里进来的流量就会打到一个正在关闭的容器上,直接报错。解决办法是配一个 preStop 钩子:收到终止信号后先 sleep 一小段时间,这几秒里 readiness 已经转为失败、Pod 已被摘除,新流量不再进来,老请求也处理完了,再真正关闭。下面是 readiness 端点 + 优雅下线的完整配合:
// readiness 回答:我现在能正常服务请求吗?
// 它可以、也应该检查关键依赖 —— 但失败只会被摘流量,不会重启容器。
var shuttingDown atomic.Bool // 进入优雅下线流程时置为 true
func readyzHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// 已进入下线流程:主动报未就绪,让 K8s 尽快把自己摘出 Endpoints
if shuttingDown.Load() {
w.WriteHeader(http.StatusServiceUnavailable)
w.Write([]byte("shutting down"))
return
}
// 关键依赖:数据库 ping 不通就别接流量(但容器不会因此重启)
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
w.WriteHeader(http.StatusServiceUnavailable)
w.Write([]byte("db not reachable"))
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("ready"))
}
}
应用侧准备好了,部署侧再配上 preStop 和一个相对敏感的 readiness 探针,优雅下线的闭环就成了:
# readiness 探针:周期短、失败阈值小 —— 它失败代价低,可以敏感些
readinessProbe:
httpGet:
path: /readyz
port: 8080
periodSeconds: 5 # 每 5s 探一次,变化能很快被发现
timeoutSeconds: 3
failureThreshold: 2 # 连续 2 次失败就摘流量
# preStop 钩子:收到终止信号后,先等 15s 再让进程真正退出
lifecycle:
preStop:
exec:
# 这 15s 里:Pod 已从 Endpoints 摘除 -> 没有新流量进来;
# 在途的老请求继续处理完 -> 不会被打断。
command: ["sh", "-c", "sleep 15"]
六、工程坑:慢启动、探针副作用、滚动更新、资源限流
把探针真正用稳,还有几个绕不开的工程细节。它们不属于"探针原理",但每一个都实实在在地坑过人。
坑 1:启动慢的应用,用 startup probe,别去硬调 initialDelaySeconds。一个冷启动要 60 秒的 JVM 服务,如果靠 liveness 的 initialDelaySeconds 来等,这个值要嘛设太大(启动完之后探针迟钝)、要嘛设太小(启动期间被误杀)。正确做法是配 startup probe:它成功之前,liveness 和 readiness 都不运行,等于给慢启动单独开了一个不会被误杀的"保护期"。
坑 2:探针本身不能"重",更不能有副作用。探针每隔几秒就被调一次,如果你的健康检查端点里做了复杂查询、写了日志表、甚至触发了某些业务逻辑,这些副作用会被放大几百上千倍。健康检查端点必须轻量、只读、幂等。
坑 3:滚动更新时,readiness 决定了"新 Pod 什么时候开始接流量"。K8s 滚动更新时,只有新 Pod 的 readiness 成功了,才会把流量切过去、再去删老 Pod。如果新 Pod 没配 readiness,K8s 会以为它一启动就能服务,可能在它还没就绪时就把老 Pod 删了,造成更新瞬间的请求失败。配合 maxUnavailable: 0 可以保证更新全程可用实例数不下降。
坑 4:CPU 被限流(throttling),会让探针莫名其妙超时。如果容器的 CPU limits 设得很低,在负载高时容器会被 CFS 限流,导致连健康检查这种轻量请求都响应变慢、探针超时——现象和"服务卡死"一模一样,但根因是资源配额。排查探针超时时,一定要顺带看一眼 CPU throttling 指标。
下面先把"坑 1"的 startup probe 落地——给一个慢启动服务配上启动保护期:
# 一个冷启动要 60-100 秒的 JVM 服务
startupProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 10
failureThreshold: 12 # 最多容忍 10 × 12 = 120 秒的启动时间
# startup 成功之前,下面两个探针完全不运行
livenessProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
# 不再需要靠 initialDelaySeconds 硬等 —— startup 已经兜底了启动期
把前面所有原则汇成一份完整的、可直接参考的 Deployment——三种探针各司其职、滚动更新不掉实例、带优雅下线:
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-svc
spec:
replicas: 4
strategy:
rollingUpdate:
maxUnavailable: 0 # 滚动更新全程不减少可用实例
maxSurge: 1
selector:
matchLabels: { app: order-svc }
template:
metadata:
labels: { app: order-svc }
spec:
terminationGracePeriodSeconds: 40 # 要大于 preStop sleep 时间
containers:
- name: order-svc
image: registry.local/order-svc:1.8.0
ports:
- containerPort: 8080
resources:
requests: { cpu: "500m", memory: "512Mi" }
limits: { cpu: "2", memory: "1Gi" } # 别设太低,防限流
# 探针 1) startup:给慢启动兜底,成功前另外两个探针不跑
startupProbe:
httpGet: { path: /healthz, port: 8080 }
periodSeconds: 10
failureThreshold: 12
# 探针 2) liveness:只查进程自身,窗口放宽,失败才重启
livenessProbe:
httpGet: { path: /healthz, port: 8080 }
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
# 探针 3) readiness:查依赖,失败只摘流量不重启
readinessProbe:
httpGet: { path: /readyz, port: 8080 }
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 2
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 15"]
最后是排查探针问题时最常敲的几条命令——出问题时照着这个顺序走,基本能定位:
# 1. 看 Pod 状态和重启次数:RESTARTS 高 + CrashLoopBackOff = 探针在作怪
kubectl get pods -o wide
# 2. 看 Events,探针失败的具体原因(超时?连接拒绝?)都在这里
kubectl describe pod <pod-name>
# 3. 看上一次被 kill 掉的那个容器留下的日志 —— 关键是 --previous
kubectl logs <pod-name> --previous
# 4. 进容器里手动跑一次探针请求,验证健康端点本身对不对
kubectl exec -it <pod-name> -- curl -sv http://localhost:8080/healthz
# 5. 看是不是 CPU 被限流导致探针超时(坑 4)
kubectl top pod <pod-name>
# 6. 实时观察 Pod,确认改完探针配置后是否不再反复重启
kubectl get pod <pod-name> -w
关键概念速查
| 探针 / 参数 | 说明 |
|---|---|
| liveness probe | 探活;失败到阈值则 kill 并重启容器;只该查进程自身 |
| readiness probe | 探就绪;失败则从 Endpoints 摘除、不重启;可查外部依赖 |
| startup probe | 探启动完成;成功前 liveness/readiness 都不运行 |
| httpGet | 访问 HTTP 路径,状态码 200–399 算成功,最常用 |
| exec | 容器内执行命令,退出码 0 算成功,开销较大 |
| tcpSocket | 能建立 TCP 连接算成功,只证明端口在听 |
| grpc | K8s 1.24+ 原生,调标准 gRPC 健康检查协议 |
| initialDelaySeconds | 容器启动后等多久开始第一次探测 |
| periodSeconds | 两次探测的间隔,默认 10s |
| timeoutSeconds | 单次探测超时,默认 1s——通常太紧,务必调大 |
| failureThreshold | 连续失败几次才判定失败,默认 3 |
| preStop | 容器终止前执行的钩子,配 sleep 实现优雅下线 |
避坑清单
- 探针不是"配了就有保障",它是你交给 K8s 的判断指令;指令下错了,K8s 会忠实地把你的服务搞垮。
- 三种探针失败后果完全不同:liveness 失败重启容器,readiness 失败摘流量不重启,startup 失败前另两个探针不跑。
- liveness 探针绝不能检查数据库等外部依赖——依赖一抖动会导致所有 Pod 同时失败、被集体重启、陷入重启风暴。
- liveness 只该检查进程自身状态(主循环、死锁),它的每次失败都应对应"重启确实有用"的情况。
- 外部依赖检查放进 readiness:依赖抖动时摘流量、恢复后自动加回,全程不重启,平滑无感。
- timeoutSeconds 默认 1 秒对多数服务太紧,高负载下一次稍慢的正常响应就会超时误杀,务必调大。
- liveness 的容忍窗口(timeoutSeconds + periodSeconds×(failureThreshold-1))要大于服务最长可接受停顿。
- 启动慢的应用用 startup probe,别靠硬调 initialDelaySeconds,后者要嘛迟钝要嘛误杀。
- 健康检查端点必须轻量、只读、幂等;探针每几秒调一次,任何副作用都会被放大成千上万倍。
- 配 preStop sleep + readiness 实现优雅下线;CPU limits 别设太低,否则限流会让探针莫名超时。
总结
回头看那场大促雪崩,最该记住的不是"timeoutSeconds 要调大"这个具体结论,而是我上线前那个想当然的判断——"配上 liveness 只有好处没有坏处"。探针给人的第一印象太温和了:健康检查嘛,容器坏了帮你重启,听起来纯粹是保险。但 liveness 探针手里握着的是"重启容器"这个破坏性动作,而它做判断的依据,完全是你写的那个端点、你配的那几个数字。你把判断标准写松了,它迟钝;写紧了,它误杀;让它去查不该查的东西,它就成了把局部故障放大成全局雪崩的那个开关。探针不是保险,它是一个你必须认真对待的、有杀伤力的控制器。
所以用好探针,核心是想清楚一件事:每一种探针失败时,K8s 会做什么,而那个动作到底能不能解决问题。liveness 失败 → 重启,所以它只该在"重启确实有用"时失败——也就是进程自己卡死了;readiness 失败 → 摘流量,所以一切"容器还活着、但这会儿不该接流量"的情况都归它——依赖不可用、还在启动、正在下线。把这条线想清楚,你就不会再把数据库检查塞进 liveness,也不会再指望一个探针包打天下。三种探针不是三个可选项,而是三种各司其职的角色:startup 负责"启动期别被打扰",liveness 负责"真死了才重启",readiness 负责"没准备好就别给流量"。
这篇文章的几节,其实是顺着"配一个探针要做的决定"展开的:先分清三种探针各管什么、失败动作是什么;再选检查方式(httpGet / exec / tcpSocket / grpc);再调时间参数,在"反应及时"和"不误杀"之间找平衡;然后是最关键的一条原则——liveness 不查依赖;接着把依赖检查和优雅下线交给 readiness;最后收尾几个工程坑。你会发现,探针配置和写代码里的"异常处理"特别像:你不能笼统地 catch 一切然后做同一个动作,你得分清这个异常是该重试、该降级、还是该直接抛出——探针也一样,你得分清这次"不健康"是该重启、该摘流量、还是该再等等。
最后想说,K8s 的自愈能力确实强大,但"自愈"的前提是你得准确地告诉它"什么算病、该吃什么药"。诊断标准定错了,自愈就变成了自残——一个健康的容器被反复重启,一次依赖抖动被放大成全面雪崩,都是这么来的。所以下次给服务配探针前,不妨对着每一个探针问自己三句话:它失败的时候,K8s 会做什么?那个动作,真的能解决我担心的那种故障吗?如果它在最不该失败的时候失败了(比如高峰期、依赖抖动时),会发生什么?能从容回答这三句,你的探针才是真正的保险,而不是埋在 YAML 里的一颗雷。
—— 别看了 · 2026