从 Pending 到 Ready:一套可迁移的 Kubernetes 部署排障方法

用控制器、调度与存储、容器启动、健康探针和发布收敛五层心智模型,系统判断 Pending、ImagePullBackOff、CrashLoopBackOff 与 rollout 卡住,并选择 Deployment、StatefulSet、Job 及共享基础设施隔离策略。

Kubernetes 部署最容易让人误判的地方,是把 Pod 状态当成原因:看到 Pending 就反复重启,看到 CrashLoopBackOff 就只盯应用日志,看到流水线成功就默认服务已经可用。

更可靠的方法是先定位失败发生在哪一层。Pod 状态只是多个控制回路共同作用后的结果;真正的原因可能在工作负载类型、调度、存储、镜像身份、配置注入、进程启动或健康探针中的任何一层。

本文来自一次匿名化的测试环境恢复实践。实际名称、地址、凭据、组织和业务信息都已移除;保留下来的只有可迁移的 Kubernetes 概念、观察证据和排障顺序。

先装一个五层心智模型

Kubernetes 的核心不是“执行一串命令”,而是控制器持续比较期望状态当前状态,再推动两者收敛。官方的控制器文档也用 Job controller 说明了这件事:控制器本身不运行容器,而是通过 API Server 创建和观察对象,让其他组件继续调度和执行。

排障时只跟踪五个杠杆:

  1. 控制器:该用 Deployment、StatefulSet 还是 Job?
  2. 调度与存储:Pod 能否选中节点,PVC 能否按 StorageClass 绑定?
  3. 身份与配置:镜像拉取、Secret、ServiceAccount 和外部依赖是否匹配?
  4. 容器启动:镜像能否启动,进程为何退出,退出码是什么?
  5. 可用性收敛:探针是否通过,新副本是否真正 Available,rollout 是否完成?
flowchart LR
    A[Manifest<br>期望状态] --> B[Workload Controller]
    B --> C[PodSpec]
    C --> D[Scheduler<br>节点与存储]
    D --> E[Kubelet<br>镜像与挂载]
    E --> F[Process<br>配置与依赖]
    F --> G[Probes<br>启动与可用]
    G --> H[Rollout<br>状态收敛]
    D -. Events .-> I[诊断证据]
    E -. Events .-> I
    F -. Logs .-> I
    G -. Conditions .-> I

改变一个杠杆,故障形态会可预测地变化:StorageClass 不匹配时通常停在调度或卷绑定;镜像身份缺失时停在拉取;环境变量缺失时容器往往已经启动过,只是进程退出;readiness 失败时进程可能活着,但不会进入 Service 的可用端点。

第一问不是“Pod 怎么了”,而是“这是什么工作负载”

Kubernetes 工作负载文档给出了最实用的选择边界:

场景应选对象原因
无状态 API、Web、可替换 WorkerDeploymentPod 可互换,适合副本扩缩和滚动更新
数据库、队列等需要稳定存储或身份的组件StatefulSetPod 有稳定序号、网络身份和独立 PVC
数据库迁移、初始化、一次性检查Job目标是成功完成后停止,而不是长期运行
每个节点都要运行的日志、网络或设备代理DaemonSet工作负载跟随节点,而不是普通副本数
定时清理、报表或备份CronJob按计划反复创建 Job

Deployment 管理的副本应当可替换;StatefulSet 则故意保留“黏性身份”。官方 StatefulSet 文档明确说明:它适合稳定网络标识、持久存储和有序更新,删除或缩容 StatefulSet 默认不会顺便删除关联卷,这是数据安全设计,不是垃圾回收失效。

数据库迁移适合单独做成 Job。官方 Job 文档将 Job 定义为“运行到完成并停止”的一次性任务。把迁移塞进每个应用副本的启动脚本,会把副本并发、重试和结构变更绑在一起;独立 Job 更容易设置超时、观察完成状态并保留失败证据。

一张状态表,先把排障方向分开

表象先查什么常见原因不要先做什么
Pendingdescribe pod 的 Events、PVC、节点资源资源不足、亲和性冲突、PVC 未绑定不要先看应用日志,容器还没运行
ContainerCreatingEvents、Secret、卷挂载、镜像拉取挂载失败、Secret 缺失、CSI 或镜像仍在准备不要无脑删除 PVC
ErrImagePull / ImagePullBackOff镜像名、仓库权限、imagePullSecretstag 不存在、凭据不在 Pod/ServiceAccount 上不要把它当应用启动错误
CrashLoopBackOfflogs --previous、退出码、Last State缺配置、依赖不可达、进程异常、OOM不要只看当前空日志
Running0/1 Readyreadiness/startup 探针和依赖端点错误、初始化慢、依赖未就绪不要用 liveness 替代 readiness
rollout 一直等待Deployment Conditions、新旧 ReplicaSet、Pod Events上述任一问题导致新副本不 Available不要把 set image 成功当发布成功

第一组通用命令通常已经足够把问题定位到一层:

kubectl get deploy,sts,job,pod,pvc -n <namespace>
kubectl describe pod <pod> -n <namespace>
kubectl logs <pod> -n <namespace> --all-containers --previous
kubectl get events -n <namespace> --sort-by=.lastTimestamp
kubectl rollout status deployment/<name> -n <namespace> --timeout=5m

官方的 Debug Running Pods 示例同样从 describe pod 的 Events 和容器 Last State 入手。CrashLoopBackOff 是重启退避状态,不是根因;根因通常在上一次终止状态、退出码和 previous logs 里。

Deployment:镜像更新不等于发布完成

kubectl set image 只改变 Deployment 的 Pod template。它证明“期望镜像已更新”,不证明新 Pod 已经准备好接流量。

官方 Deployment 文档把 rollout 分成 progressing、complete 和 failed to progress。readiness 失败、镜像拉取错误、权限不足和运行时配置错误都会让新 ReplicaSet 无法完成;kubectl rollout status 会等待控制器报告收敛,超过 progressDeadlineSeconds 后返回非零退出码。

所以流水线至少要区分三类动作:

  • Build:构建和推送镜像。
  • Configure/Bootstrap:同步 Secret、安装有状态依赖、执行迁移。
  • Rollout:更新镜像或 Pod template,并等待 Available。

日常代码提交可以只跑按服务拆分的 Build Job;首次安装、Secret 契约变化、数据服务变化或迁移时,必须显式运行 Configure/Bootstrap。无论哪条路径更新了 Pod,都应以 rollout 和健康检查收尾。

StatefulSet 与 PVC:先理解三层对象再动手

持久存储排障要把三个对象分开:

  • PVC 描述工作负载要什么容量和访问模式。
  • StorageClass 描述由哪个 provisioner、用什么策略提供卷。
  • PV 是最终绑定给 PVC 的实际存储资源。

动态卷供应文档说明,StorageClass 让 PVC 能按需触发卷创建,而不要求管理员预先创建每个 PV。问题是,云盘通常还有 Kubernetes YAML 看不出来的供应商限制,例如最小容量、可用区或磁盘类型;这些限制最终会出现在 PVC 或 provisioner Events 中。

volumeBindingMode 也会改变排障顺序。官方 StorageClass 文档说明:

  • Immediate 在 PVC 创建后立即绑定或供应卷,可能在不知道 Pod 调度约束时就选定拓扑。
  • WaitForFirstConsumer 等到使用 PVC 的 Pod 参与调度,再结合节点选择、亲和性和污点等条件供应卷,更适合有拓扑约束的存储。

两个现场教训很通用:

  1. PVC Pending 时先读 Events,不要靠猜。 如果后端拒绝过小容量,反复重建同样大小的 PVC 不会改善结果。
  2. 不要随意删 Bound PVC。 StatefulSet 可以重建,卷里却可能有唯一数据;只有确认 PVC 未绑定、卷未创建且没有数据,才适合清理错误声明后重建。

还有一个常见细节:新块存储格式化后,挂载根目录可能自带系统目录。某些数据库要求数据目录为空,此时应把数据目录指向挂载点下的专用子目录,例如 /data/db,而不是试图删除不认识的目录。

Secret:对象存在,不代表进程拿到了正确配置

Kubernetes Secret 文档区分了两种主要消费方式:环境变量和挂载文件。排障时需要逐项确认:

  1. Secret 与 Pod 是否在同一 namespace。
  2. Pod template 引用的 Secret 名称是否正确。
  3. secretKeyRef 的 key 是否真实存在。
  4. 应用读取的环境变量名是否与 key 映射一致。
  5. 更新 Secret 后,消费环境变量的 Pod 是否已重建。

Secret 作为 volume 时,常规挂载会以最终一致方式更新文件;作为环境变量时,值在容器启动时进入进程环境,更新 Secret 后应滚动重建 Pod。一个稳妥做法是在 Pod template 上记录配置 checksum,让 Secret/ConfigMap 变化自然触发 rollout。

Secret 也不是天然的“保险箱”。官方文档提醒,Secret 默认在 etcd 中并非自动加密;生产环境仍需静态加密、最小 RBAC、限制容器可见范围,必要时接外部 Secret Store。CI 日志则只输出键名和状态,不输出值。

ImagePullBackOff:检查 Pod 的拉取身份

有一种迁移 Job 很容易失败:它使用与应用相同的私有镜像,却没有继承应用 Deployment 的 ServiceAccount 或 imagePullSecrets。结果是长期服务能拉镜像,新建 Job 却停在 ImagePullBackOff

ServiceAccount 官方任务说明,Pod 会绑定一个 ServiceAccount;把 imagePullSecrets 配在 ServiceAccount 上后,使用它创建的新 Pod 会自动获得相应拉取配置。

因此,同一命名空间、同一私有仓库中的迁移 Job,通常应复用运行时工作负载已经验证过的:

  • serviceAccountName
  • imagePullSecrets
  • 镜像仓库和 tag/digest 规则
  • 必要的 nodeSelector、tolerations 和安全上下文

“复用身份”不等于给 Job 更大权限。它只应拿到执行迁移需要的最小 API 和外部资源权限。

探针:三种问题要用三种答案

Kubernetes 探针文档给三种探针分配了不同职责:

  • startupProbe:应用是否完成启动;适合冷启动慢、需要迁移或预热的进程。
  • readinessProbe:现在能否接流量;失败会把 Pod 从匹配 Service 的 EndpointSlice 中移除,但不会因此重启容器。
  • livenessProbe:进程是否陷入不可恢复状态;连续失败会触发重启。

把外部依赖抖动直接放进 liveness 很危险:数据库短暂超时可能让所有副本一起重启,形成级联故障。更合理的划分通常是:startup 给初始化留时间,readiness 保护流量,liveness 只检测死锁等必须重启才能恢复的问题。

一次 rollout 长时间显示“0 个新副本可用”,并不必然是失败。Worker 可能正在做真实初始化;先看 readiness、startup 和进程日志,再决定是扩大启动窗口还是修复配置。最终仍以 rollout 成功和 Ready/Available 数量为准,而不是以“等得有点久”下结论。

共享基础设施:账号可用不等于隔离成立

测试环境经常复用数据库、消息队列或对象存储。连通性测试只能证明账号能登录;隔离验证还要证明应用只能在自己的命名边界里产生状态。

共享资源最低隔离边界部署后验证
关系数据库专用 schema 最佳;否则固定表前缀、迁移表也带前缀、DDL 守卫迁移前后统计非本前缀表数量不变
消息队列独立 vhost/namespace,加交换机和队列前缀权限正则只匹配本前缀;其他 vhost 不变
对象存储独立 bucket 最佳;否则固定对象前缀生成 key 必须由受控 ID 拼接,禁止用户路径直传
缓存独立实例或独立认证边界不把共享 keyspace 当持久真源;无法证明隔离时宁可不接

一次实际验证中,迁移前共享库已有一批非本应用表;迁移后非本前缀表数量完全不变,新增表全部符合固定前缀,迁移记录表也在同一前缀下。这个“前后基线对账”比只审查 migration 文件更有说服力。

消息队列则使用独立 vhost,并把 configure/write/read 权限都限制到应用前缀。这样即使复用测试账号,AMQP 连接也落在独立逻辑空间;更严格的生产方案仍应使用独立服务账号。

对象存储可用稳定 key 模板,例如:

runs/<run-id>/tasks/<task-id>/result.bin

两个 ID 必须先做格式校验,不能接受 ../ 或任意用户路径。连通性预检只做 bucket 级只读检查;真正的上传契约测试应使用隔离前缀,并在结束后按明确目标清理测试对象。

一次从失败到 Ready 的通用恢复顺序

下面这套顺序适合“镜像已经发布,但多个服务起不来”的场景:

  1. 冻结目标:列出必须恢复的 Deployment/StatefulSet,不顺手改无关服务。
  2. 读取状态:同时看 workload、Pod、PVC 和 Events,先确定失败层。
  3. 收敛配置契约:从应用 schema 得到必填键,默认值只用于安全且环境无关的项。
  4. 先恢复有状态依赖:让 PostgreSQL、ClickHouse、Redis 等 StatefulSet 和 PVC Ready。
  5. 用 Job 执行幂等迁移:设置前缀守卫,复用正确的镜像拉取身份,等待 Job Completed。
  6. 同步 Secret 并滚动应用:不要只改 Secret 后等待旧 Pod 自己变化。
  7. 逐个等待 rollout:API、Web、Worker 分别验收,保留哪个服务卡住的可见性。
  8. 做部署后隔离对账:数据库基线、消息命名空间、对象前缀分别核对。

这也解释了为什么 CI 里把多个服务拆成独立 Job 有价值:它不是为了“看起来更细”,而是让失败归属、重试范围和 rollout 时间各自可见。反过来,所有服务塞进一个长 shell 脚本,往往只剩最后一个退出码。

最小现场检查清单

发布前:

  • 工作负载类型是否匹配状态需求?
  • StorageClass、容量、访问模式和拓扑是否被集群支持?
  • Secret 只含允许键,且未进入镜像、仓库和日志?
  • Job 是否幂等,是否有明确超时和失败证据?
  • 私有镜像 Pod 是否拥有正确的 ServiceAccount/imagePullSecrets?

发布中:

  • PVC 是否 Bound,StatefulSet 是否 Ready?
  • Job 是否 Completed,而不是仅创建成功?
  • logs --previous 是否解释了 CrashLoop?
  • readiness 与 startup 是否符合真实启动时间?
  • kubectl rollout status 是否以 0 退出?

发布后:

  • Ready 和 Available 是否达到目标副本数?
  • 真实健康请求是否成功?
  • 非本应用数据库表数量是否保持不变?
  • MQ 资源是否只存在于独立 vhost/namespace?
  • 对象 key 是否全部落在固定前缀?

Honest reminder

本文验证的是一条测试环境恢复链路,不代表所有 CSI、私有镜像仓库和托管 Kubernetes 的行为完全相同。存储最小容量、拓扑策略、Secret 管理和镜像认证都可能受云厂商或集群策略影响。

最便宜的下一步实验,是在隔离 namespace 里创建四个最小对象:一个无状态 Deployment、一个带小 PVC 的 StatefulSet、一个复用同 ServiceAccount 的一次性 Job,以及一份只含测试键的 Secret。故意制造一次错误 StorageClass、一次错误镜像凭据和一次 readiness 失败,再按本文顺序只用 Events、Conditions 和 previous logs 定位。能在 15 分钟内判断失败层,比背更多 kubectl 命令更接近真正的 Kubernetes 运维能力。

最终要记住的只有一句话:先判断哪个控制回路没有收敛,再修对应层;不要拿结果状态当根因。