一个看板服务同时连着三样东西:存沙箱记录的 PostgreSQL、存历史指标的 ClickHouse、读实时状态的 Kubernetes API。三样都会挂。我把
/healthz写成只回答「进程还能应答」,一个依赖都不查;依赖状态另开一个/readyz,并且不接给 kubelet。我的判断是:存活探针查依赖,等于把下游故障翻译成自己的重启,而重启对一个连不上的库毫无作用。
三个探针指着同一个一行端点
/healthz 的实现是一行,返回 {"status": "healthy"}。部署清单里三个探针全指它:启动探针 2 秒一次最多 30 次,给启动期 60 秒;就绪探针 10 秒一次;存活探针 20 秒一次,连续 3 次失败才算数。
把存活探针改成查 PG 会怎样。PG 挂了,探针开始失败,3 次之后 kubelet 重启容器。重启完 PG 还是挂的,探针继续失败,再重启。CrashLoop。这个服务不但没好转,连本来还干得了的活也停了。
重启能修的是进程自己的状态:内存泄漏、死锁、句柄耗尽。依赖不可用不在这个集合里。探针查什么,就是在声明「这件事重启能解决」。
换成就绪探针同样不行,而且坏的方式不一样。就绪失败不重启容器,但 Pod 会从 Service 的端点里摘掉,流量不再进来。PG 挂掉时这个看板还能给出 Kubernetes 的实时快照,摘了流量,连这部分也没了。
降级要真的拿得出东西
「不重启、不摘流量」成立的前提,是降级之后确实有东西可给。
PG 挂掉时 /api/pods 仍然返回 200,响应里 degraded: true:沙箱列表还在,但只能按 pod 名筛,framework、owner 这些业务维度没有了。
这里有个坑,踩中就是静默错误。降级分支的判据必须是「PG 可不可用」,而不能是「查到几行」。筛选条件真的没匹配时 rows 同样为空,按空走降级会丢掉全部筛选条件返回全量沙箱,而 degraded 还标着 false——调用方筛的是一个框架,拿到的是所有沙箱,并且这是最贵的一条查询路径。
PG 和 ClickHouse 同时挂,再退一档:直接列 Kubernetes 当前的 Pod。身份、状态、Request/Limit、重启次数都在;CPU 和内存的真实用量标成 metrics_available: false,不拿 Request 冒充用量;live_source: kubernetes 说明这批数据是哪来的。
这条路测过。单测把 ClickHouse 换成必抛异常的替身、把 PG 连接池置空,断言 degraded 为 true、live_source 是 kubernetes,非沙箱的那个 Pod 被滤掉。集群侧那条要显式开环境变量才跑:停掉 ClickHouse 容器,断言 /readyz 里它为 false、指标接口返回 5xx、错误体里不出现内部地址和 SQL。默认 skip,因为它会动环境。
依赖状态仍然要报,只是报给人看。/readyz 返回三个依赖各自的布尔值,不接任何探针。它是未鉴权端点,所以探测这一步刻意不占查询并发的名额——同时最多 4 个查询在跑,这个上限是用来保护重型聚合的;探测也去占一个的话,任何能连到端口的人反复打 /readyz 就能占满,已鉴权的看板查询全部排队到超时。探测本身是一次 SELECT 1。
测试自己也读这个口:一个 fixture 打 /readyz 看 PG 在不在,不在就把业务维度筛选的用例 skip 掉,而不是判失败。本机开发连不上集群内网的 PG 是常态,那本来就是设计里的降级路径。
不查依赖的代价,和它仍然抓得到的事
代价是真的:后台死了,探针不知道。
采集循环是进程内的一个 task,每 30 秒扫一遍 Pod 列表写进 PG。Kubernetes API 调用如果不设超时,一条被静默丢弃的 TCP 连接会让 await 永久挂住——什么都没抛,循环里的 except 接不到,数据从此停止更新,而 /healthz 照样 200,kubelet 没有任何理由重启它。
这个洞不归探针补,归超时:每次 API 调用 30 秒上限(全集群 LIST 实测 4.2 秒、7.5 MB,余量够)。探针负责「进程还能应答」,超时负责「调用不会永远挂着」。
反过来,不查依赖不等于永远返回 200。另一个评测服务的存活探针也只打 /healthz,30 秒一次、超时 10 秒、连续 6 次失败。它从 9 月 1 日起重启过 8 次,全在交互式评测期间。根因是一段同步的模型调用没放进线程池,直接跑在事件循环上,单次 20 到 45 秒,整个循环被堵住 60 到 190 秒。这期间 /healthz 一次都答不上来,6 次探测全部超时,kubelet 重启了容器——它判得没错,那个进程当时确实无法响应任何请求。
存活探针该抓的就是这个:进程还在,但答不了。依赖挂掉不属于这一类。
探针和防火墙共用一条网络路径
另一个服务上,探针撞的是安全策略。
那个沙箱服务要堵一条越权路径:同一台机器上的程序能不经授权连到沙箱里执行代理的端口,换掉它的访问凭据。NetworkPolicy 管不到,宿主访问本机 Pod 走的是节点本地路由。办法是每台机器加一条防火墙规则,挡住宿主和与宿主共用网络的 Pod 发起的连接。实测加上之后,从宿主连那个端口立即被拒,95% 在 0.04 毫秒内,普通 Pod 不受影响。来历我写在以为有网络隔离的集群,实查 0 条规则。
kubelet 就跑在宿主上。TCP 就绪探针从宿主发起,正好落在被挡的那一侧:Pod 起来了,端口在听,探针连不上,就绪判定一直不过。只能换成 exec probe,它在 Pod 的命名空间里 fork 一个进程,不经过 Pod 的网络接口。
这不是等价替换,代价我在那篇里写了一半:TCP 探针验的是「端口在 accept」,exec probe 验的是你写的那条命令,判据得重挑,挑错了就是拿另一件事冒充就绪;每次探测还多一次进程启动。另一半当时没看到。
现在沙箱 Pod 一个探针都没有,实查 125 个在跑的 Pod,0 个配了就绪探针,所以 ready 就等于「容器在跑」,容器退出才归 0。沙箱服务判断一个沙箱是不是已经没了,正是建在这个事实上:过去 24 小时 33959 个沙箱里,ready 没有一次 1→0→1 的反复,147 个 1→0 之后再没回来,全是真死。于是「Running 过又回到未就绪」可以直接判死,不担心假阳性。加上 exec probe,这个前提就没了——探针会因为命令写得不好、容器一时负载高而抖,这套判定得跟着重做。
防火墙那条规则实测过,换 exec probe 这步到现在只定了方向,没在集群上跑过。