Kubernetes 探针到底是怎么工作的

查看原文 HN 讨论

文章摘要

Sam Rose 在 ngrok 博客上发的这篇 4,197 词长文,用一连串可交互的浏览器内模拟器把 Kubernetes 三种探针讲透了。所有 demo 跑在他自己写的 webernetes 上——一个把 10 万多行 Kubernetes Go 代码移植成 TypeScript 的部分实现,能在浏览器里跑一个模拟集群;他还拿 k3s 逐一校验过 demo 的行为,过程中甚至挖出一个 Kubernetes 真实 bug。

开篇是没有探针的世界:一个 my-app:latest 容器启动后要花几秒初始化才开始监听 8080 端口,但 Kubernetes 从容器启动那一刻就认为它 Ready,于是这几秒内的请求全部失败。容器反复崩溃还会进入 CrashLoopBackOff,默认首次延迟 10 秒、每次翻倍、上限 5 分钟。

Startup 探针:httpGet 打 /startup,periodSeconds: 1、failureThreshold: 5,即给容器约 5 秒完成初始化,连续失败 5 次就杀容器。探针由每个节点上的 kubelet 发出。Kubernetes 还支持 tcpSocket、exec、grpc 三种探针类型。作者顺手澄清一个细节:Kubernetes 其实没有 NotReady 这个 condition,只有 Ready 的 True/False/Unknown,文中用 NotReady 只是为了 demo 里字短。配置陷阱是 failureThreshold 给太小(1 或 2),容器永远来不及启动,直接无限崩溃循环。

要真正避免掉请求,还得配上 ReplicaSet(replicas: 2)+ Service 做负载均衡,让 pod-b 打 service-a.default.svc.cluster.local 而不是直连 Pod IP——Kubernetes 用 Ready condition 决定是否把 Pod 纳入 Service 的负载均衡。另一半靠优雅终止:删除 Pod 时 kubelet 先发 SIGTERM,terminationGracePeriodSeconds(默认 30 秒)后才发 SIGKILL;处于 terminating 的 Pod 会被移出 Service 且不再计入 ReplicaSet 的可用副本,所以替换 Pod 会立刻被创建。启动探针 + 优雅终止两者合一,删 Pod 时可以做到零失败请求。

Readiness 探针:在 startup 探针成功之后接管,失败到阈值只是把容器标记 NotReady、从 Service 摘掉,不重启。默认 successThreshold 为 1、failureThreshold 为 3,作者建议别乱改。他还挖出一个文档写得极含糊的行为——「带外探测」(out-of-band probing):官方文档只说容器 not Ready 时探针「可能在配置的 periodSeconds 之外执行,以便让 Pod 更快就绪」。作者在 webernetes 上摸清了:容器处于 NotReady 时,几乎任何对 Pod 的更新(改 annotation、更新 status 等)都可能触发一次带外探测,而很多你意识不到的东西都在更新 Pod。有趣但不该依赖。

为什么有了 readiness 还需要 startup?作者列了几条:startup 探针会推迟 readiness 和 liveness 的启动;可以给启动阶段单独设 periodSeconds 和 failureThreshold(慢启动探得勤,稳态探得稀,减少 kubelet 和容器负载);startup 反复失败会杀容器并触发重启策略,而 readiness 失败不会——重启有时恰好能救活卡住的容器。

Liveness 探针:机制和 readiness 一样,但达到 failureThreshold 时直接杀容器,然后按 Pod 的 restartPolicy(默认 Always)重启。适用于主线程死锁、关键后台线程挂掉这类容器自己救不回来的情况。就是在做这个 demo 时他发现了 bug:容器被 liveness 杀掉重启后,在 startup 探针还没来得及发出时,一个 liveness 探针就抢先触发并失败,导致容器又被重启一次——liveness 本不该在 startup 成功前触发。他在 k3s、minikube、kind 上都复现了,撰文时最新版本 v1.36.2,问题似乎是 v1.35.0 引入的,issue 已提交且被 SIG Node 接受,标为 priority/important-soon。

liveness 的经典误用是拿它检查数据库健康:数据库抖一下就能让所有容器一起崩溃循环。作者做了个 demo,把数据库拉下线,Pod 逐个进 CrashLoopBackOff(延迟按真实 Kubernetes 的 10 秒起、逐次翻倍);再加上客户端重试,就演示出了教科书级的「惊群」(thundering herd)导致的级联故障——demo 里的容器每秒只能处理 3 个请求,超了就崩,于是数据库恢复后任何敢起来的容器都会被一束流量激光打死。探针救不了这种局面,只能靠限流慢慢放量或者让客户端加退避。结论:只有当故障是单容器局部的、重启很可能修好时才让 liveness 失败,绝不要让所有容器同时满足的条件去触发它。

最后是探针与 Deployment 的关系。示例用 replicas: 3、RollingUpdate、maxUnavailable: “25%”(floor(3×0.25)=0,即滚动期间 3 个副本必须全部可用)、maxSurge: “25%”(ceil(3×0.25)=1,最多允许 4 个副本)。滚动只能多起 1 个 Pod,且必须等它 Ready 才能杀旧 Pod,所以探针周期直接决定滚动速度:periodSeconds 为 1 时整个滚动约 11 秒,改成 5 就变成 19~20 秒;完全不配探针的话滚动飞快,但因为容器一启动就算 Ready,会掉少量请求。

文末给了设计探针端点的建议清单:startup 探针在启动慢或耗时不确定、初始化可能卡死时使用,探测要勤(降 periodSeconds 就要同步升 failureThreshold),目标是覆盖最坏启动时间加一点余量;readiness 要便宜、保守,只在「把这个 Pod 摘掉真能改善整体健康」时才失败,不要因为共享依赖(数据库、第三方 API)或 CPU/内存高而失败——服务接近满载时摘副本反而可能引发级联故障;liveness 只在很确定容器卡住且重启能修好时失败,不确定就返回成功。

HN 评论精华

这篇帖子 141 分、24 条评论,讨论几乎完全被一条反对意见带着走:探针到底该不该检查上游依赖。