1. 为什么需要三种探针
1.1 从一次“假死”事故说起
先讲一个我踩过的坑:某次给服务做滚动发布,Kubernetes 显示所有 Pod 都处于 Running 状态,但真是流量一打进来,网关大量 502。当时第一反应是查Service 和 Endpoint,发现后端 Pod 的 IP 都在,但请求就是不正常。后来手动进到容器里 curl 本地端口,发现应用进程已经没法响应了——进程还在,线程池全部卡死,端口虽然监听中,但一接连接就超时。这就是典型的“进程活着,但服务已经死了”。
这种场景就是探针(Probe)要解决的问题。Kubernetes 创建了容器,但它无法感知容器内部的业务状态,只能看到进程是否启动。而一个应用真正的状态,往往比“进程有没有启动”复杂得多:依赖的数据库是不是可用、缓存是否连接上、接口能不能在超时时间内返回、线程池是否还有空闲……这些都是 Kubelet 无法通过“看”容器进程来确认的。探针就是让 Kubelet 替你去问应用:“你到底能不能接客?”
所以探针不是锦上添花,而是生产环境的基础配置。没有探针的 Deployment,在 Pod 场景下基本属于裸奔。
1.2 三种探针各自管哪一段
Kubernetes 给 Pod 内的每个容器都提供了三种可选的探针:readinessProbe、livenessProbe、startupProbe。它们对应的其实是容器生命周期的三个阶段。
startupProbe 管的是“启动期间”,专门给那些初始化很慢的应用用的。很多应用启动需要加载模型、预热缓存或者等依赖就绪,如果直接用 livenessProbe 去做检查,很可能在应用还没起来时就被判定失败,然后被 Kubelet 杀掉重启,形成无限重启循环。startupProbe 给应用留一个“缓冲期”,它成功后会立即把控制权交给 livenessProbe。
readinessProbe 管的是“是否就绪”,决定这个 Pod 要不要被加进 Service 的 Endpoints。如果探测失败,Kubernetes 会把 Pod 从 Endpoint 列表里摘除,流量就不再打进来,但容器不会被杀。简单说:readiness 只管“要不要分给我流量”,不管“要不要弄死我”。
livenessProbe 管的是“是否活着”,决定 Kubelet 要不要把这个容器杀掉并重启。如果探测失败,Kubelet 会依据 restartPolicy 杀掉容器,然后重启一个新的。它保护的是那种“进程卡死、死锁、假死”的场景。
这三种探针叠加起来,覆盖了一个容器从启动到运行中的全部状态判断。我把它们比作门卫:startupProbe 是“安检口”,你还在慢慢排队时,它盯着你不放弃;readinessProbe 是“营业状态牌”,写着“营业中”才放顾客进来;livenessProbe 是“保安巡逻”,发现你在地上躺尸了,直接拖出去换人。
2. 探针配置核心参数与工作原理
2.1 三个最常见的探测方式
每个探针都需要告诉 Kubelet 用什么样的方式去判断成功还是失败,Kubernetes 支持三种:HTTP 请求、TCP 连接、命令行执行。
HTTP 请求是最常用的。Kubelet 会定期向容器内某个端口发起 GET 请求,你可以指定路径、端口和协议(http/https)。当响应的状态码在 200~399 之间,就认为探测成功。这个方式适合有 HTTP 服务的应用,比如 Java 的 health 接口、Go 的 /healthz 端点。注意,如果你没有单独的 health 接口,直接用主页 / 做探针,一旦首页包含动态渲染逻辑或者依赖外部系统,探针就会非常敏感,容易误判。
TCP 探测是让 Kubelet 去尝试连接容器里的指定端口。只要 TCP 连接建立成功,就认为探测成功。这个方式比较粗糙,因为它只能判断端口能不能连上,不能判断业务逻辑是否健康。很多端口监听是靠一个后台线程完成的,线程池卡死时,端口照样能连上。所以 TCP 探针一般用于那些没有 HTTP 接口的老旧服务,属于“比没有好”的级别。
Exec 探测是在容器内执行一个命令,命令退出码为 0 表示成功,非 0 表示失败。这个方式最灵活,因为你可以写一个脚本,做完整的多项检查——比如检查一个临时文件是否存在、查一下进程数、甚至跑一条 SQL。但代价是每次探测都会启动一个进程,对性能有消耗;同时脚本本身也是一个维护成本。
2.2 参数如何影响探测结果
探针的参数不多,但每一个都值得认真推敲。initialDelaySeconds 表示在容器启动后多久才开始第一次探测,用来给应用留出初始化时间。periodSeconds 是探测周期,默认 10 秒。timeoutSeconds 是单次探测的超时时间,默认 1 秒,这个很多新手没注意——默认值只有 1 秒;如果应用在 1 秒内没返回,就会记一次失败。successThreshold 是几次连续成功后,才把探针状态标记为成功,默认 1;failureThreshold 是几次连续失败后,才把探针状态标记为失败,默认 3。
这里有个关键点:一旦探针被标记为失败,之后需要连续 successThreshold 次成功才会恢复;否则会一直失败。而 livenessProbe 累计失败达到 failureThreshold,容器就会被杀掉。
还有一个很多人忽略的细节:initialDelaySeconds 与 startupProbe 的关系。如果在容器里配置了 startupProbe,那么在 startupProbe 成功之前,其他探针(liveness 和 readiness)都会暂停执行,不会干扰启动过程。也就是说 startupProbe 实际上起到了“延迟一切探针”的作用。所以即使你没设 initialDelaySeconds,只要配了 startupProbe 并且它处于失败中,其他探针就不会启动。
参数之间的关系,我建议用一个表来表达清楚:
| 参数 | 默认值 | 作用 | 建议 |
|---|---|---|---|
| initialDelaySeconds | 0 | 容器启动后到首次探测的等待时间 | 根据应用启动时长设置,宁大勿小 |
| periodSeconds | 10 | 两次探测间隔 | 业务敏感度越高,间隔越短,但会增大开销 |
| timeoutSeconds | 1 | 单次探测超时时间 | 如果应用响应轻微缓慢,设置 2~3 秒 |
| successThreshold | 1 | 连续多少次成功才标记为就绪/存活 | 一般保持 1 |
| failureThreshold | 3 | 连续多少次失败才标记为失败 | liveness 一般 2~3,readiness 可以 3~5 |
2.3 kubelet 内部到底怎么执行探针
探针是 kubelet 以“kubelet 进程”的身份发起的,不是从 Pod 网络发起的。如果是 HTTP 探测,kubelet 会直接访问容器的 IP 加端口,绕过了 Service 和 kube-proxy 的转发。这一点对排查问题非常重要——很多人在 Node 上 ping 不通 Pod IP,但不能说明探针也会失败;反过来,有些人通过 Service NodePort 去访问服务,发现通的,但这也不能代表探针一定成功。
每次探测其实是 kubelet 并发执行的。如果一个 Node 上运行了很多 Pod,并且每个 Pod 的探针周期很短,kubelet 会产生大量并发请求。HTTP 探针的响应超时和 tcp 连接超时都以 timeoutSeconds 为准。有一个容易踩的坑:如果探针的 timeoutSeconds 设为 1 秒,而应用的 GC 停顿超过了 1 秒,探测就会失败一次;偶尔一次失败没事,但如果连续 3 次失败,liveness 就会触发重启。所以对于响应时间本身就有波动性的服务,timeout 一定要预留余量。
另外,kubelet 探测容器时使用的是“容器网络命名空间中的 localhost”?这里有个误区:如果探针配置的是 httpGet host 默认值,它用的是 Pod 的 IP,所以不是访问容器自己的 localhost 或 127.0.0.1,而是访问该 Pod IP 下的端口。这个差别在某些极端场景下会体现出来,比如容器里跑了多个网络命名空间、或者使用了 networkMode: host。我后面实操部分会细说。
3. 实操:为一个 Web 服务配置齐全的三个探针
3.1 先写一个基础 Deployment YAML
我以一个典型的 Java Spring Boot 应用和 Node.js 应用混合环境来举例。先看一个最简的 YAML 结构:
apiVersion: apps/v1 kind: Deployment metadata: name: demo-api spec: replicas: 3 selector: matchLabels: app: demo-api template: metadata: labels: app: demo-api spec: containers: - name: demo-api image: registry.example.com/demo-api:v1.2.3 ports: - containerPort: 8080 startupProbe: httpGet: path: /health/startup port: 8080 initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 2 failureThreshold: 30 readinessProbe: httpGet: path: /health/ready port: 8080 initialDelaySeconds: 0 periodSeconds: 10 timeoutSeconds: 2 successThreshold: 1 failureThreshold: 3 livenessProbe: httpGet: path: /health/live port: 8080 initialDelaySeconds: 0 periodSeconds: 15 timeoutSeconds: 2 failureThreshold: 3这个配置里,我给启动探针设置了 30 次失败上限,配合 5 秒周期,意味着 startupProbe 最多允许 150 秒的启动时间。这个数值不是拍脑袋定的,而是观察了应用在冷启动时从进程创建到 HTTP 端口可响应的平均耗时,再乘上一个 1.5 到 2 的余量系数。
为什么 startupProbe 和 livenessProbe 要拆成两个不同路径?因为我故意让 /health/live 这个接口只检查进程状态和线程池是否阻塞,不检查数据库连接;而 /health/ready 则要检查数据库、缓存、消息队列等依赖。这是两种探针的典型分工:liveness 用来探测“进程还活着吗”,readiness 用来探测“依赖是否可用、流量能不能打进来”。如果 liveness 探针直接去检查数据库,数据库一抖动,Pod 就会被不断重启,这会让问题从“部分不可用”升级成“大面积重启”,更加混乱。
3.2 各字段的“为什么”:参数取值背后的依据
关于 startupProbe,我给它设了 initialDelaySeconds: 5,原因是应用启动的前 5 秒内,连 HTTP 服务都没监听,探针跑去请求端口只会得到连接拒绝。虽然 failureThreshold 可以容忍很多次失败,但与其让探针白打几十次无意义的请求,不如让 initialDelaySeconds 稍等一下。
periodSeconds 决定探测频率。startupProbe 周期设置为 5 秒,是为了缩短“从应用启动到探针探测成功”的观察间隔,尽快把控制权交给 livenessProbe;如果周期太长(比如 30 秒),那 startupProbe 可能要等上接近 1 分钟才能完成首次判断,拖慢了后续启动节奏。
readinessProbe 的 initialDelaySeconds 我设为 0,这里有个容易误会的点:在 startupProbe 成功之前,readinessProbe 根本不会执行。因为 K8s 的机制是:只要容器配置了 startupProbe,就会先等 startupProbe 成功,然后才会开始执行 readinessProbe 和 livenessProbe。所以 readiness 的 initialDelaySeconds 反而可以设小一些;如果你在 readiness 里也设置一个很大的初始延迟,那 startupProbe 成功之后,又会额外多等一段“空白期”,Pod 虽然已经就绪了,但 K8s 迟迟不把它加入 Endpoints,白白浪费资源。
livenessProbe 的周期我设置为 15 秒,比 readiness 更长。原因很简单:liveness 是用来发现“假死”的,这种状态一般不会在几秒内恢复,探测频率高反而会浪费一次 HTTP 请求,也容易因为偶发超时而误杀。readiness 则是为了快速感知依赖变化——比如 Redis 连接断开,如果 10 秒内能恢复,readiness 可以自动放回去,流量几乎不中断;如果依赖长时间不可用,readiness 失败后 Pod 会被摘除,但不会杀掉重启,应用还能继续等依赖恢复。
这里的取舍逻辑是:判断周期越短,发现故障越快,但误判风险也越高,成本也越高。你需要根据业务对“不可用”时间的容忍度去权衡。
3.3 探针路径要区分“活”“就绪”“启动”
三种探针如果全用一个路径,比如全是 /health,那等于没区分。我强烈建议项目里为不同的探测目的设计不同的端点,哪怕它们在实现上只是几个子检查的排列组合。
一个比较通用的设计模式是,在应用内部提供三个端点:
- /health/startup:只检查进程初始化状态,比如配置文件加载是否完成、核心对象是否初始化;一旦启动完成,这个接口永远返回 200。
- /health/live:检查进程是否存活,比如主循环是否在跑、goroutine 或线程池的数量是否低于阈值、有没有发生致命死锁。
- /health/ready:检查所有下游依赖是否可用,比如数据库连接池是否可用、Redis 能否 Ping 通、队列连接是否建立。
为什么要这样拆?因为不同类型的故障有着不同的恢复策略。依赖数据库不可用时,我们希望 Pod 先“不接流量”,等数据库恢复了再自动重新接流,而不是重启容器;重启容器也是一样的效果,但要浪费启动时间,还会清空内存状态。相反,如果进程内部发生了死锁,重启是唯一的选择,这时候 liveness 就要发挥作用。
如果项目来不及做三个独立的接口,那至少也要把 liveness 和 readiness 分开。liveness 端点不要检查外部依赖,readiness 可以。我见过不少团队因为偷懒,两个探针共用同一个依赖数据库的接口,结果某次数据库做 HA 切换,数据库连接中断了几分钟,整个服务的 Pod 全被 Kill 掉,最后数据库恢复后所有 Pod 要从头启动,服务中断时间被拉长到十几分钟。
4. 探针被误杀?这些经典排障场景我基本都遇到过
4.1 探针配置文件都有“坑”:不要把 Pod 配置搞成假不变
先看一个最常见的配置错误:把探针的端口配成了 Service 端口而不是容器端口。比如容器监听 8080,Service 对外端口是 80,你在 httpGet 里写了 port: 80。在大多数网络模式下,Pod 的 80 端口根本没有进程监听,探针永远失败。这种情况在 YAML 里还特别难排查,因为 Service 访问正常,只是 Pod 的 status 里 readiness 一直是 false。遇到这种问题,第一动作是kubectl describe pod看 Events,里面会直接告诉你探针探测失败,比如Readiness probe failed: HTTP probe failed with statuscode: 503,如果没有错误信息,则看一下端口是否匹配。
第二个坑是探针依赖了镜像里的命令。Exec 探针运行的命令必须存在于容器镜像中,如果你用的是一个精简镜像,比如 distroless 或 alpine 里没有 bash 只有 sh,但你在探针里写的是bash -c,那结果就是找不到命令,探针必然失败。还有一个隐蔽的坑:Exec 探针的执行环境是容器的 PID namespace,但这个命令的退出码会被直接当作探测结果,如果脚本本身因为权限问题,比如读不了 /tmp 下的某个文件,退出码非 0,也会被判定失败。
第三个坑是把 initialDelaySeconds 设置的非常大,比如给 300 秒。这样做虽然避免了启动误杀,但带来了一个副作用:如果应用启动崩溃了,startupProbe 还没到第一次探测时间,Kubelet 永远不会发现它已经死掉。虽然在 restartPolicy=Always 的情况下,容器退出后仍然会被重新拉起来,但探针在这个过程中没有起到加速发现故障的作用。更合理的做法是使用 startupProbe 的 failureThreshold * periodSeconds 来控制启动窗口,而不是用一个过长的 initialDelaySeconds。
4.2 HTTP 探针返回状态码的“欺骗性”
HTTP 探针的判断逻辑是“状态码在 200~399 之间都算成功”。这是官方定义的,但生产环境里要小心 4xx 和 3xx。302 重定向会算成功,因为 302 在 200~399 范围内。但很多应用中定义 /health 接口时,如果用户未登录就返回 302,那探针会一直成功,但实际业务完全不可用。反过来,如果应用框架在发生异常时返回 200 但 body 里写错了内容,探针也会误判为成功。这就引出一个经验:HTTP 探针只能判断“接口能返回一个响应”,不能验证响应内容是否符合预期。如果你需要验证响应体里的某个字段,就得用 Exec 探针,在容器里执行 curl 加 grep 才能做到。
另外注意默认的 HTTP 探针并不会跟随重定向。如果你在探针的 path 中写了一个会重定向到其他地址的路径,Kubelet 不会像浏览器那样跟着重定向走,它只会拿到第一个响应。如果第一个响应是 302,判定为成功;如果是 301,也成功。如果你希望探针请求的路径永远不要有重定向,就应该在应用的设计中保证 health 接口不依赖任何认证和重定向逻辑。
经验里还有一条,关于 livenessProbe 与 discovery 服务的关系:如果你的服务依赖某个注册中心,每次 readiness 检查都会请求注册中心 API,那当注册中心整体 crash 时,readiness 会失败,但 liveness 还活着,Pod 会被摘流量,但不会重启。这种设计是合理的,因为服务本身还能运行,等注册中心恢复后可以自动接回流量;但如果你把同一接口也配给了 liveness,那注册中心 crash 后所有 Pod 会一起重启,反而可能触发注册中心雪崩。这个案例在我维护的集群里真实发生过,后来我把两者的路径分开,稳定性明显改善了。
4.3 滚动更新时 readinessProbe 如何影响发布
滚动更新中 readinessProbe 的作用经常被人忽略。Deployment 默认的 maxUnavailable 是 25%,意味着滚动过程中最多允许 25% 的旧副本不可用。新 Pod 的 readiness 如果一直失败,那么滚动更新就会卡住:新副本永远不能变成可用,旧副本也不会被删除。很多人以为这表示“发布停滞”,其实这是 Kubernetes 在保护你的集群,说明应用在更新后没有正常起来。
我自己遇到过一次非常诡异的问题:每次发布新版本后,新副本 readiness 失败,看日志发现应用基本启动成功,但 readiness 一直返回 503。最后发现是应用里用于“预热”的定时任务出了问题——它会在启动后尝试连接一个后台服务,而那个服务被我又改了网关策略,导致新版本 Pod 的网络不能访问那个后台服务,但旧版本还能访问。排查到最后,发现是 NetworkPolicy 只应用到了新 Pod 的标签上,新旧 Pod 的标签不一致,导致新 Pod 访问外部依赖被拦截。这提醒我,当滚动更新卡在 readiness 时,先别着急看探针的参数,要检查新 Pod 的标签和 NetworkPolicy 的匹配关系、服务是否真正可访问。
另一个更常见的现象是:新版服务依赖了某个数据库的新表或新字段,但数据库迁移没有先做,应用启动时没有直接报错,但所有请求都失败,导致 readiness 探针反复失败。这种问题需要在发布流程中提前进行 schema 迁移,而不是靠探针来发现。探针确实能拦截住“带病上线”,但最好的办法还是让 CI/CD 流程提前检查好依赖。
4.4 探针日志里看不到失败原因怎么办
有时候你发现 Pod 处于 Running,但探针一直失败,kubectl get pod里 READY 显示 0/1,然后去看kubectl describe pod,Events 里写了Readiness probe failed: HTTP probe failed with statuscode: 503。OK,状态码是 503,但你看不到响应体。这时候如果要解决问题,一般的做法是手动进容器执行:
kubectl exec -it <pod-name> -- curl -v http://localhost:8080/health/ready如果容器里没有 curl,也可以先kubectl exec到一个同网段的工具 Pod,然后访问目标 Pod IP 的端口。不过要记住,Kubelet 探针是从 Node 上发起的,和你从其他 Pod 里发起访问,路径完全相同吗?不完全一样,但大多数情况下,网络策略和本地回环差异会带来干扰。
另一个排查技巧是临时把探针的 failureThreshold 调大(比如改成 30),让 Pod 保留更多失败日志,然后通过kubectl logs --previous看被杀容器在死前留下什么日志。如果 liveness 失败,Kubelet 杀掉容器后,容器原本的 stdout/stderr 日志可以用kubectl logs --previous查看。这些日志经常包含“健康检查线程超时”之类的信息,能帮你快速定位问题出在哪一层。
还有一点,很多人不知道kubelet在探针执行失败时,默认不会把详细错误写入到容器日志,而是只写到 Event 里。如果要看到更多细节,可以调整 kubelet 的日志级别,但生产上不建议这么做。更实用的是,利用探针的 HTTP 路径,在应用里把健康检查接口的日志单独输出到一个文件或 stdout,这样每次探测都会记录,方便追踪。
5. 进阶实战:把探针与生命周期、定制策略配合起来
5.1 startupProbe 与 initContainer 的配合
很多需要等待依赖的 Pod,会把“等待数据库初始化”的任务放在 initContainer 里。initContainer 执行完成后,主容器才会启动。这确实能解决一些依赖顺序问题,但你永远无法保证主容器启动后,数据库连接立刻可用。比如数据库刚做完一次 failover,主容器启动时可能拿到了旧的连接信息,初始化连接池失败,但进程没有退出,线程池可能只是处于“正在重试”的状态。此时 startupProbe 可以帮上忙:如果设置了 startupProbe,并且它检测到连接不可用,就会一直失败,Pod 不会进入 Ready,K8s 也不会把流量打进来。但注意,startupProbe 只会在失败达到阈值后把 Pod 标记为不健康,并不会主动重启容器(除非同时配置了 livenessProbe 且等待 startup 成功后才会执行)。所以如果你想在 startupProbe 失败后重启容器,必须同时配置 livenessProbe,并且让 startupProbe 成功后再接管。通常推荐:启动阶段用 startupProbe 探测,如果 startup 失败(说明应用初始化出问题),Kubelet 会先看 liveness,如果 liveness 未配置,那也不会重试,Pod 会一直处于不安全状态,只会被标记为不健康,但不会重启。
这里的分工要理清:initContainer 适合做“一次性准备”,比如下载文件、迁移数据、等待第三方初始化完成;startupProbe 更适合“主容器内部状态是否初始化完成”的判断。两者不是替代关系,而是各有侧重。举个例子,我维护的某个服务,主容器启动时会加载一个几千 MB 的规则文件,加载耗时超过 2 分钟。initContainer 只需要检查文件是否存在,无法知道加载进程是否成功;以前的方案是让主容器启动脚本去 sleep 10 分钟,整个流程非常浪费。后来改成了 startupProbe,把路径设为 /health/startup,该接口在加载完成之前始终返回 503,加载完成后返回 200。这样一来,Pod 不会被误杀,流量也不会提前进入,整个发布过程可观测性也提高了。
5.2 探针执行阶段与 Pod 生命周期终止的重叠问题
当 Pod 要被删除时(比如滚动更新、手动 delete、节点驱逐),Kubernetes 会先进入 terminating 状态,同时向容器发送 SIGTERM 信号。从收到 SIGTERM 到进程真正退出,会有一小段“优雅终止”期(terminationGracePeriodSeconds,默认 30 秒)。在这段时间内,readinessProbe 是否还会继续执行?答案是:不,不会继续执行。
从 K8s 1.x 的某些版本开始,当 Pod 处于 terminating 时,readinessProbe 的结果会被忽略,因为 Endpoint 控制器在收到 Pod 删除事件后会立即把该 Pod 从 Service Endpoints 中移除(无论 readiness 如何)。这意味着,如果应用在被删除时优雅退出动作很慢,比如还在排空连接、退出前还需要完成一些请求,那么流量已经在入口处被摘除了,剩下的是存量连接如何处理的问题。这里有个实践建议:在应用的 shutdown hook 里,可以先主动调用一组“标记不健康”接口(如果有的话),让某些外部的负载均衡器提前摘除这个节点。虽然 K8s 的 Service 会自动摘除,但如果你还依赖了云厂商的 LB 或自建的 nginx Ingress,它们可能有一定的缓存时间,主动标记总能减少误差窗口。
另外,livenessProbe 在 Pod terminating 期间也会停止执行。这避免了一个问题:如果容器收到了 SIGTERM 后已经开始关闭 HTTP 服务,探针此时失败,Kubelet 又杀掉它,会造成不必要的强制 SIGKILL。更好的做法是:应用在 SIGTERM 后要尽快让探针路径失效,比如返回 503,这样如果某些调度器还要重新判断健康状态,也能正确拿到“正在关闭”的信息。
5.3 多容器 Pod 的探针隔离细节
一个 Pod 里有多个容器时,每个容器需要独立配置自己的探针。一个常见的困惑是:Pod 的 READY 状态是所有容器的 readiness 都通过才算 Ready。只要有一个容器的 readiness 失败,整个 Pod 的 READY 就是 0/1。liveness 则不同,每个容器如果 liveness 失败,会被各自重启,不会直接导致整个 Pod 失败。
多容器场景下的另一个坑是探针端口的冲突。如果两个容器使用共享的网络命名空间(即没有设置独立 network namespace),但容器各自监听不同的端口,探针指定端口必须精确。如果误写了另一个容器的端口,探测结果会变得不可预测。还有一个场景,比如 Envoy sidecar 写日志转发,它会启动一个 admin 端口。如果你给主容器配了 readiness 探针探测 8080,而 sidecar 的探针忘了配置,那么主容器健康,但 sidecar 不可用,Pod READY 依然不通过。这种问题有点隐蔽,因为主容器的日志一切正常,需要看kubectl get events找到是哪个容器的探针失败。
因此建议:每个容器的探针都要加上配置;如果 sidecar 只是做日志或监控,不需要提供流量,可以给它禁用 readiness(不配置),但要给它配置 liveness 防止它挂掉后一直不重启?其实不配置 liveness,sidecar 退出后 Kubelet 会根据 restartPolicy 重启容器,这也够用。具体要看你的 restartPolicy 是否允许重启。
5.4 探针与 HPA 的联动:不要忽略流量突增导致的探针抖动
最后一个很多人想不到的问题:探针失败和 HPA 扩容存在微妙的关系。当 Pod 的 CPU 使用率超过阈值,HPA 会扩容,新拉起的 Pod 需要经过 startupProbe 和 readinessProbe 才能加入 Endpoints 接收流量。如果 startupProbe 配置得太严格(比如 failureThreshold 很小),在流量高峰期,负载较高的新 Pod 启动速度变慢,startupProbe 失败几次后,Kubelet 会直接重启容器,导致它迟迟不能 Ready,而 HPA 又继续往更多 Pod 扩容,集群中的 Pod 数量可能被短暂放大,浪费资源。
另一方面,如果 existing Pod 因为流量突增而变得响应变慢,readinessProbe 可能偶尔超时,但还没到 failureThreshold 的 3 次,它依然是 Ready。因为 HTTP 探针的超时只代表那一次请求没返回,不代表服务不可用。如果你觉得自己的服务本来就是高延迟负载型,建议把 timeoutSeconds 调大到 3~5 秒,而不是把 failureThreshold 调很大。调大 failureThreshold 只会让你更晚感知问题,而调大 timeout可以更好地适应响应波动。
我最后再分享一个经验:无论配置了哪种探针,把探针的检查结果暴露成监控指标,是一个极好的习惯。比如在你的业务代码里统计 /health/live 接口的调用次数和延时,再把这个指标接到 Prometheus 上。这样当探针反复失败时,不是只看 K8s 事件,而是能从指标趋势上判断是瞬时抖动还是渐进劣化。有一次,我们的 readiness 探针总是在每天凌晨 2 点左右出现零星失败,看 Pod 描述找不到原因,打开指标的 p99 之后发现,凌晨有一个定时任务会让健康检查接口的延迟飙到 3 秒,而我们的 timeout 刚好是 3 秒,最终把 timeout 调到 5 秒问题就消失了。
这类细节很多,但本质就是一句话:探针配置不能抄模板,要理解你的应用在什么状态下才是“活的”、什么状态下才是“能接客的”,再把这些判断转化为 K8s 能理解的探测方式。多花一点时间设计探针路径和参数,比在事故现场手忙脚乱要值得多。