普通进程00 的 /health=200、/ready=503已说明“活着”与“就绪”不同。移到 Kubernetes 后又多了几道门:Pod phase、容器当前状态、探针结果、Pod Ready condition、EndpointSlice 后端集合,以及一次真正的 Service 请求。kubectl get pods 显示 Running 时,有时应用还在加载;显示 Ready 时,业务查询也可能失败。真正的问题不是“该把 readiness 调短多少”,而是要能在同一份时间线上指出请求被允许进入后端的条件和失败发生在哪个环节。

Running 是一个阶段,不是一个端到端结果

Kubernetes 的 Pod phase 是对象状态的粗粒度概览。Pending 可包含等待调度、下载镜像或启动 init 容器;Running 表示 Pod 已绑定节点、容器已创建,至少有一个容器正在运行、启动或重启中,并不承诺所有容器对请求负责。CrashLoopBackOff 通常是某个容器处于重启退避的等待原因,不是第六种 Pod phase;不能把 kubectl 显示栏中熟悉的词当作底层状态机的完整定义。26 的 UID/容器 ID 链是把事件归给同一次执行尝试的必要前提。

当前 Pod 对象的 status.containerStatuses[].ready 与 conditions 比 phase 更接近流量门槛,但仍不等于一条业务请求成功。探针可能只 GET 一个很轻的 HTTP 路径,不覆盖应用依赖的数据库写入、身份校验或跨节点网络。即便所有 Pod Ready,Service selector 可能不匹配,DNS 可能失败,客户端可能请求了不同端口;28 只把探针与 EndpointSlice 的关联讲清楚,29 才展开 Service 转发和 NetworkPolicy 的数据面。采样时应把“直接向 Pod 发请求”“向 Service 发请求”分开,不能用一个 curl localhost 200 同时验收两条路径。

Pod 名称与容器列表也可能在滚动更新中同时变化。Deployment 新建的 ReplicaSet 与旧 ReplicaSet 各有不同的 Pod UID;用共同的应用标签聚合它们固然方便,但不便定位一次 503 是旧版本还是新版本。在每轮记录里保存 Deployment revision、ReplicaSet 名、Pod UID、容器 ID、镜像 digest、探针事件和请求 UTC 时间。若仅保留最后的 kubectl rollout status 退出码,不但看不到中间是否有失败请求,甚至无法知道请求被哪个版本处理。

取证也不应该把 kubectl get pod 的 READY 1/1 当成长期属性。ready 是某次采样里的状态,Pod UID 才能把两次采样绑定到同一对象;容器重启可能让 ready 先真后假再真,而 Pod 名并未变化。一个已被终止但尚在 API 中短暂可见的 Pod,还会携带它之前的容器状态和删除时间。查询 status.conditions 时记下 lastTransitionTime、取样时间与 metadata.deletionTimestamp;如果服务请求恰好在交叠窗口失败,不得事后把最终状态倒填成请求发生时的状态。缺这条时间线,就无法分辨真正的就绪抖动与一次重建造成的身份切换。

三种探针不是三个等价的“健康”开关

startupProbe 给启动阶段设置一个容忍窗口:配置后,在它成功以前,readiness/liveness 不按普通进度工作;超过允许失败阈值仍未通过时,容器可能被 kubelet 重启。这适用于初始化慢、但初始化后行为正常的进程,不应用来永久掩盖应用从未就绪。readinessProbe 决定该容器当前能否被视为接收流量的候选:失败时通常不会因为这一项单独重启,而是从 Ready 候选中摘出;成功后可能重新加入。livenessProbe 判断是否需要重启容器,用会在后台依赖短时故障时失败的业务深检查做 liveness,可能把一时的不就绪变成反复重启。

本系列的 probe_app.py 用 /health 返回活着,/ready 在 --ready-delay 10 的初始时间窗内返回 503,随后返回 200。Deployment 与 Service 清单让 startup/liveness GET /health,readiness GET /ready,并固定请求/限制与一副本滚动更新策略。这样可在同一镜像摘要下观察:应用 Python 进程已监听、Pod 可能 Running,但 readiness 暂时不通过;到延迟期结束才有机会 Ready。什么时候被第一次探测、什么时候 EndpointSlice 更新、何时 Service 的客户端真正收到 200,由节点与控制面实际时间决定,不能从 YAML 计算出精确“第十秒生效”。

启动探针在这份清单里不负责等待 10 秒就绪:/health 在服务启动后立即成功,之后仍须由 /ready 独立判断初始加载时间。若把两个路径都指向 /ready,不仅混淆了控制目标,还可能造成不必要的重启。失败计数、探测周期和超时在不同探针上有独立语义;periodSeconds: 2 不意味着探测一定恰好落在墙钟偶数秒,也不意味着控制面、kube-proxy 和客户端在同一瞬间观察到状态变化。要解释边界时,保存 Pod condition 的转折点与真实请求的时间戳,而不是把默认值描述成确定延迟。

探针请求的源位置也不同于业务客户端。kubelet 从节点侧检查 Pod IP/指定端口,不能用应用进程在容器内 urllib.request.urlopen("http://127.0.0.1:18080/ready") 成功代表 kubelet 一定能访问 Pod IP;反过来,只有集群内 Service 客户端失败而直接 Pod 请求成功,优先回到 endpoint selector、DNS、Service 路径,不要第一时间缩短探针周期。同一条 /ready 503,业务客户端通过 Service 未必能收到这份 503:若该 Pod 尚未成为 ready 后端,客户端可能收到无后端错误或连接失败。观察对象的不同决定日志里应分别记录 Pod 路径与 Service 路径。

HTTP 探针还涉及端口、scheme、path、host 和超时。探针结果失败时,先确认当前 Pod 最终 spec 的这些字段指向容器真正监听的地址与端口,再读 kubelet 事件中返回的是 404、503、连接拒绝还是超时;四者分别提示路径错误、应用主动未就绪、未监听/网络失败与响应太慢。本文 probe-app 只定义 /health、/ready 等固定路径,错误路径返回 404,因此坏配置应先在对应 Pod 内对比直接请求结果,再检查 kubelet 是否把这只容器标为 not ready。即使临时打开 kubectl port-forward 得到 200,它走的诊断路径与 kubelet探针并不相同,不能据此取消节点侧的失败事件。

init 容器和 sidecar 改变了哪个阶段

正常 init 容器需要先完成,主应用才进入启动流程。本篇清单复用同一个固定镜像摘要,init 仅用 Python 休眠两秒,为“应用进程还不存在”的窗口制造有限的教学输入;它不构造网络、迁移数据或接管探针。此时检查 initContainerStatuses 与应用 containerStatuses 的差异,比盯着 Pod phase 更能定位为何 /ready 尚无响应。若 init 失败或镜像拉取失败,主应用连启动都没有,不能把它写成 readiness 503;该状态的第一条证据是 init 的终止原因和事件。

Kubernetes 的原生 sidecar 是一种可重启的 init 容器语义,而不是在清单里随意多写一个名为 sidecar 的普通容器。特定版本支持的 initContainers[].restartPolicy: Always 让该容器继续运行并与应用共存,启动、就绪/终止排序需对照本机版本与特性状态;带 readinessProbe 的 sidecar 还可能参与 Pod 就绪判定。本篇固定源码阅读定点为 Kubernetes v1.33.4,但尚未在目标 VM 安装,也没有提供可以证明 sidecar 顺序的运行清单。当前 28 的正反案例覆盖普通 init,不把未执行的原生 sidecar 行为标成已验证;后续若扩展,应先固定真实 API 版本、增加有界 sidecar 附件,再把 Sidecar/主容器状态分开取证。

迁移到含代理的真实 Pod 时,不能把“sidecar 已启动”直接读作“应用依赖已初始化完成”。一个持久 sidecar 可能先于主程序准备日志管道或证书,但若它自身还没 Ready、或它提供的端口没在同一个 Pod 网络空间监听,业务请求仍会失败。要分析它影响了谁,应保留 init/sidecar 的 name、restartPolicy、各自的 containerID 和状态,确认自己的就绪条件是否纳入 Pod Ready,并从主容器读取对该依赖的真实请求结果。本篇不去改共享 probe-app 为其模拟 sidecar 服务;没有同一 Pod 的双容器原始证据,不能在 STATUS 将这项先修直接划为通过。

同样,Pod 还可能有额外的 readiness gates;它们未通过时,即便应用容器自身 ready=true,Pod Ready condition 也可以保持 false。这里没有配置自定义 gate,因此不应把此机制写成本次实验的原因。读者换到带准入注入 sidecar 或服务网格代理的集群,要先检查 API 最终 Pod spec 和所有容器的 readiness/termination 状态,再决定是应用未就绪还是注入组件挡住了就绪。把副本和所有容器都压扁为一个布尔值,很容易误把代理故障归咎于业务进程。

从 Ready 到 EndpointSlice,仍有传播窗口

Service 根据 selector 指向一组 Pod;EndpointSlice 记录后端地址和 ready 等条件。配置没有 publishNotReadyAddresses 的正常场景里,readiness 影响后端能否接新流量,但控制器更新 EndpointSlice、节点实际转发和客户端 DNS/连接复用之间不是一笔原子事务。即使在连续采样中先看到 Pod Ready、后看到 EndpointSlice 条件更新,也不能以一个 API 对象的观察时刻推导所有节点已同时完成转发表切换。至少要分别保存 Pod JSON、EndpointSlice JSON、Service selector/端口和集群内客户端的请求结果。

EndpointSlice 自身也不能只数地址。新旧 Pod 地址在更新过程中可能同时列出,conditions.ready=false 的地址并非可接新流量的健康后端,终止期的 conditions.terminating/serving 又不能直接翻译为同一个 ready 布尔值。将每条 endpoint 的 targetRef.uid 与 26 保存的 Pod UID 对齐,才能把“控制器仍看见两个 Pod”与“Service 实际把请求发给哪只 Pod”分开。若运行时网络插件或代理不能在本次环境读到这些条件,报告这个观测缺口,不要把 kubectl get endpointslices 中的一行地址数量解释成已验证的请求分布。

正例流程先固定镜像 manifest digest,在独占 kind VM 创建本篇命名空间及 Deployment/Service;从创建开始,每两秒保存 Pod UID、init 容器状态、phase、容器 ready、Pod Ready、EndpointSlice conditions.ready,并通过同一 digest 的短命客户端对 Service 发最多 15 次、有上限的 /identity 请求。比较 init 阶段、应用进程运行但 readiness 为 false、随后 readiness 通过的真实客户端结果。这里的 10 秒仅为应用内单调时钟的延迟参数,和 kubectl 检查时的 UTC、探针第一次调度时刻都不是同一个起点。若镜像不可拉、CNI 无法 ADD 或 DNS 不通,记录这些先行错误,不伪称“探针导致 Service 无后端”。

一份可靠的正例报告还应说明首次失败是什么失败:客户端解析不到 c28-probe、解析到 ClusterIP 但连接失败、Pod 内 /ready 明确答 503、或者 Service 请求已进入应用却被业务返回 4xx,这些都不能被压缩成“前十秒访问失败”。分别保存 Kubernetes API 上的 Service/EndpointSlice、客户端的错误类型和对应时刻的应用日志,检查 Service 的端口 18080 与容器监听一致。若客户端 Pod 本身 Pending/拉取镜像失败,也不把它的零请求成功数算作后端不可用。在正例还没跑通之前,坏配置负例的假设条件不成立,应先解决环境问题而非制造更多变更。

单次 /identity 响应含应用视角 PID、namespace 与 cgroup,可以与 26 的 UID/容器 ID 取证关联。Service 有后端但返回 404/500 时,应检查请求路径、应用日志和控制器选择,而不是见到“有 endpoint”就宣称业务完成;没有 ready 后端也不证明 Pod 没启动。有效的判别顺序是先确认 API 对象和最终清单,再读容器进程/探针,最后核对 EndpointSlice 与业务请求。记录各步原始返回码和失败数,而非只显示成功请求截图;若请求走经代理,还应记录代理的 upstream 标识,避免把重试后的最终 200 当成第一次后端选择也成功。

坏就绪配置下滚动更新会怎样

负例并不偷换源码:坏就绪补丁只把新 Pod 模板的 readiness 路径改成探针应用不存在的 /__c28_not_ready__,镜像 digest、应用端口、CPU/内存配置保持不变。这是坏配置而非一个新的二进制版本。Deployment replicas: 1、maxSurge: 1、maxUnavailable: 0 允许在节点有额外容量时先起一只新 Pod,等新 Pod Ready 再让旧 Pod 退出;若新 Pod 虽 Running 却一直答 404,可能长期卡在滚动更新阶段。要判定原因必须同时保存两个 ReplicaSet 的 revision、两个 Pod UID、就绪事件和新 Pod 的 /ready 实际结果。

策略约束也有限:节点资源不足、镜像拉取失败或 PodDisruptionBudget/其它控制器干预,都可能使新旧副本数不符合教学预期;这时首先看调度和事件,而不是把 rollout status --timeout=60s 的非零结果直接翻译为“readiness 一直 404”。CLI 的 60 秒是本次观察窗口,Deployment 自身的 progressDeadline 是另一项配置;两者未必同时到期。恢复实验只把本篇 Deployment 新模板 readiness 改回 /ready,等修复后的 ReplicaSet/Pod Ready,再检查 EndpointSlice 和真实请求,不能通过删掉所有旧 Pod 来“迫使”旧版本退出。

用相同 digest 而只改 readiness 路径,恰好可以检验“新版本应用其实正常,只是被接入条件拦住”的边界:直接 Pod IP 请求 /identity 可能返回正常 JSON,同时 Pod Ready 为 false、EndpointSlice 的对应后端未就绪、Service 继续把请求交给旧 Pod。若发现新 Pod 的 liveness 也不通过,则它可能不断重启,必须先检查启动命令或镜像而非把责任单独归给 readiness。若旧 Pod 在回退前本身被删或故障,可能没有可用后端,但这不推翻“maxUnavailable 配置了 0”,只说明控制器策略不能修复先于更新发生的节点/应用故障。

旧 Pod 留着也不等于一次请求零失败。初始 10 秒的就绪窗口、客户端 TCP 连接复用、终止过程中连接处理、单机 kind 的容器重调度都会影响结果。因此必须从客户端真正请求的时间线里分别算成功、连接错误和应用返回的 HTTP 错误;kubectl rollout status 只能补充 Deployment 进展。即便所有请求看上去成功,也只对本次入口、并发和观察窗口有效,不能泛化为“maxUnavailable=0 保证任何在途请求不丢”。

终止时 Ready 变 false,不等于在途请求被处理完

Pod 被删除后有终止宽限期和相应的 kubelet 信号/钩子路径;EndpointSlice 在现代 Kubernetes 中可提供 terminating、serving 等状态,消费者是否使用及何时停止发新请求还要看具体转发实现。观察到 Pod Ready 很快转 false,只说明控制面的某些新请求候选可能被撤下,不代表已经建立的连接消失,也不证明应用收到了 TERM、更不证明一笔慢请求被处理完。应用是否实现了终止处理、关闭监听与等待在途任务,是服务代码自己的契约,不是 Deployment YAML 自动赋予的能力。

终止期也有明确的身份陷阱。删除带 owner 的 Pod 可能由 Deployment 重新补一个同名标签的新 Pod,因此 kubectl get pods -l app=c28-probe 总数没变,不说明被删除的 UID 还在;Service 后端条件也可能短暂包含正在终止与新建的两个 UID。抓包或请求记录要能说明客户端请求发向哪个端点,至少用响应中的应用身份和同时刻 EndpointSlice/Pod UID 做近似关联;如果代理隐藏了这个身份,结论就局限于“某次请求成功/失败”,不能说“旧 Pod 已停止承接”。只看 delete 命令退出 0 不足以验收 drain。

本系列的 probe_app.py 用单线程 HTTPServer 处理请求,服务循环运行在工作线程。应用已注册 SIGTERM/SIGINT 处理器:收到信号后设置停止标志,主线程先启动三秒定时器,再调用 server.shutdown();到期时仍有连接则执行 socket shutdown,提前结束则取消定时器。这是有上限的终止处理,并非固定等待三秒;连接本身也有三秒超时。当前没有一个可在请求中保持两秒并与 TERM 对齐的 /work,普通短请求无法充分检验在途处理。21 的 signal_probe可以观察 TERM 到达,但它没有 HTTP;把其信号日志与另一次 probe-app 请求拼接,不能形成“同一进程安全完成在途请求”的证据。本篇 RUN 限制操作范围、提供有界短请求与 EndpointSlice 观察;真正的在途成功/失败和 drain 时长仍缺专项应用实现、镜像 digest 以及专用 kind 实验,应维持 NOT_RUN,不能写模拟的零失败率。

若后续在共用 probe-app 加受控慢请求,应限定最大延迟、请求体和并发,在同一容器中记录请求开始、TERM 收到、响应写出/客户端读到与进程退出的顺序;节点与客户端时钟也要校准。先验证变更不会破坏 00–27 的既有探针契约,再重建每篇需要同步的源码包/校验值。仅在 VM 上对本篇独占 Deployment/Service 执行删除与观察;任何公共入口没有 drain 能力时,不允许借生产流量试探来“补在途实证”。

甚至在具备慢请求的版本里,等待的起止点也要写清楚:发起客户端连接时刻、服务端处理开始、应用收到 TERM、结束生成响应、客户端实际收到完整响应,这是五个可能不同的时间。客户端超时后的服务端日志仍可能显示处理完成,而服务端回写成功也可能在网络中间节点被关闭时丢失。不能把“收到了 SIGTERM”与“完成了用户请求”合并成一个测试结果,更不能用不同次运行的信号输出和 HTTP 截图拼接成一条伪造时间线。终止宽限期还受 preStop 等钩子占用,本篇清单没有配置额外钩子,未来添加后须重算有效处理预算。

迁移判断:哪个成功标志对应哪一层

遇到“Running 但访问不通”,先问应用容器是否创建并已监听;若 init 未完成或容器反复重启,去查 kubelet/runtime,不要先动 Service。容器存在后,看 startup/readiness/liveness 的实际路径、成功门槛与 reason;Pod Ready 已真但 Service 客户端仍失败,去查 selector、EndpointSlice、DNS 和转发。Service 能回 200 仍需按业务写读约定验证正确性;滚动更新完成还需查旧连接与请求日志。条件顺序比一个“Pod 状态绿了”的面板更容易迁移到别的实现:替换 Ingress、Service Mesh 或网关时,新增的只是要核对的前置与转发层,不是让 Pod Ready 语义变成应用正确性保证。

面对混合版本的真实集群,这条判断法还需要按节点细化。新旧 Pod 可能被调度到运行不同 kubelet/CNI/代理配置的节点,控制面的 readiness 条件一致,并不保证各节点对 terminating endpoint 的处理同步一致。先从本次请求反查 EndpointSlice 的 UID 与节点,按实际节点版本确认它怎样处理终止状态、健康检查和连接复用;无法取得数据面证据时,最多说“控制面已改变候选后端”,不能说“客户端已经不可能再命中旧 Pod”。这也是为什么本文把页面构建、API 取证和真实服务响应分列验收,而不是做成一个绿色勾号。

有界集群取证及清理、同篇 YAML 校验值 和 共用应用源码 可独立取得。本站静态构建只能验证这些文件及链接,不会产生实际 Pod UID、Service 请求、终止信号或在途统计。当前云端没有 kind/CRI,探针、init、滚动更新、终止全部 **`NOT_RUN`**;原生 sidecar 清单与慢请求处理仍为明确的专项附件缺口。阶段性正文不能因此标记为完整验收。
现象 首先观察 不能由此推出
init 未完成 initContainerStatuses 和镜像事件 应用 readiness 返回 503
Running、/ready 503 应用状态、探针与 Pod Ready 进程 create 一定失败
Pod Ready、Service 请求失败 EndpointSlice、selector、DNS 与请求路径 探针配置必然无效
更新卡住 两代 ReplicaSet/Pod UID 与实际事件 所有错误都来自坏版本
终止时 ready=false EndpointSlice 条件、TERM 和客户端时间线 已在途请求都已写回

练习

  1. 先预测 --ready-delay 10 与普通 init 休眠 2 秒叠加时,Pod phase、容器 Running/Ready、EndpointSlice ready 与 Service 请求会依什么顺序变化;在专用 kind 集群实际每两秒采样并指出哪些状态不能仅按配置参数算出墙钟时间。
  2. 预测坏就绪路径更新后在 maxUnavailable=0 的条件下旧 Pod 是否一定仍处理请求,再给出哪种节点容量/镜像错误会破坏你的预测;保存两代 UID/容器 ID、请求和修复后的结果。若要再断言在途请求安全完成,指出本篇还缺哪个同进程的慢请求与 TERM 证据。

上一篇:27:资源;下一篇:29:Pod IP 与 Service。

系列总目录:从进程隔离到运行时与编排。

参考资料