Kubernetes 部署最容易让人误判的地方,是把 Pod 状态当成原因:看到 Pending 就反复重启,看到 CrashLoopBackOff 就只盯应用日志,看到流水线成功就默认服务已经可用。
更可靠的方法是先定位失败发生在哪一层。Pod 状态只是多个控制回路共同作用后的结果;真正的原因可能在工作负载类型、调度、存储、镜像身份、配置注入、进程启动或健康探针中的任何一层。
本文来自一次匿名化的测试环境恢复实践。实际名称、地址、凭据、组织和业务信息都已移除;保留下来的只有可迁移的 Kubernetes 概念、观察证据和排障顺序。
先装一个五层心智模型
Kubernetes 的核心不是“执行一串命令”,而是控制器持续比较期望状态与当前状态,再推动两者收敛。官方的控制器文档也用 Job controller 说明了这件事:控制器本身不运行容器,而是通过 API Server 创建和观察对象,让其他组件继续调度和执行。
排障时只跟踪五个杠杆:
- 控制器:该用 Deployment、StatefulSet 还是 Job?
- 调度与存储:Pod 能否选中节点,PVC 能否按 StorageClass 绑定?
- 身份与配置:镜像拉取、Secret、ServiceAccount 和外部依赖是否匹配?
- 容器启动:镜像能否启动,进程为何退出,退出码是什么?
- 可用性收敛:探针是否通过,新副本是否真正 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、可替换 Worker | Deployment | Pod 可互换,适合副本扩缩和滚动更新 |
| 数据库、队列等需要稳定存储或身份的组件 | StatefulSet | Pod 有稳定序号、网络身份和独立 PVC |
| 数据库迁移、初始化、一次性检查 | Job | 目标是成功完成后停止,而不是长期运行 |
| 每个节点都要运行的日志、网络或设备代理 | DaemonSet | 工作负载跟随节点,而不是普通副本数 |
| 定时清理、报表或备份 | CronJob | 按计划反复创建 Job |
Deployment 管理的副本应当可替换;StatefulSet 则故意保留“黏性身份”。官方 StatefulSet 文档明确说明:它适合稳定网络标识、持久存储和有序更新,删除或缩容 StatefulSet 默认不会顺便删除关联卷,这是数据安全设计,不是垃圾回收失效。
数据库迁移适合单独做成 Job。官方 Job 文档将 Job 定义为“运行到完成并停止”的一次性任务。把迁移塞进每个应用副本的启动脚本,会把副本并发、重试和结构变更绑在一起;独立 Job 更容易设置超时、观察完成状态并保留失败证据。
一张状态表,先把排障方向分开
| 表象 | 先查什么 | 常见原因 | 不要先做什么 |
|---|---|---|---|
Pending | describe pod 的 Events、PVC、节点资源 | 资源不足、亲和性冲突、PVC 未绑定 | 不要先看应用日志,容器还没运行 |
ContainerCreating | Events、Secret、卷挂载、镜像拉取 | 挂载失败、Secret 缺失、CSI 或镜像仍在准备 | 不要无脑删除 PVC |
ErrImagePull / ImagePullBackOff | 镜像名、仓库权限、imagePullSecrets | tag 不存在、凭据不在 Pod/ServiceAccount 上 | 不要把它当应用启动错误 |
CrashLoopBackOff | logs --previous、退出码、Last State | 缺配置、依赖不可达、进程异常、OOM | 不要只看当前空日志 |
Running 但 0/1 Ready | readiness/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 参与调度,再结合节点选择、亲和性和污点等条件供应卷,更适合有拓扑约束的存储。
两个现场教训很通用:
- PVC Pending 时先读 Events,不要靠猜。 如果后端拒绝过小容量,反复重建同样大小的 PVC 不会改善结果。
- 不要随意删 Bound PVC。 StatefulSet 可以重建,卷里却可能有唯一数据;只有确认 PVC 未绑定、卷未创建且没有数据,才适合清理错误声明后重建。
还有一个常见细节:新块存储格式化后,挂载根目录可能自带系统目录。某些数据库要求数据目录为空,此时应把数据目录指向挂载点下的专用子目录,例如 /data/db,而不是试图删除不认识的目录。
Secret:对象存在,不代表进程拿到了正确配置
Kubernetes Secret 文档区分了两种主要消费方式:环境变量和挂载文件。排障时需要逐项确认:
- Secret 与 Pod 是否在同一 namespace。
- Pod template 引用的 Secret 名称是否正确。
secretKeyRef的 key 是否真实存在。- 应用读取的环境变量名是否与 key 映射一致。
- 更新 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,通常应复用运行时工作负载已经验证过的:
serviceAccountNameimagePullSecrets- 镜像仓库和 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 的通用恢复顺序
下面这套顺序适合“镜像已经发布,但多个服务起不来”的场景:
- 冻结目标:列出必须恢复的 Deployment/StatefulSet,不顺手改无关服务。
- 读取状态:同时看 workload、Pod、PVC 和 Events,先确定失败层。
- 收敛配置契约:从应用 schema 得到必填键,默认值只用于安全且环境无关的项。
- 先恢复有状态依赖:让 PostgreSQL、ClickHouse、Redis 等 StatefulSet 和 PVC Ready。
- 用 Job 执行幂等迁移:设置前缀守卫,复用正确的镜像拉取身份,等待 Job Completed。
- 同步 Secret 并滚动应用:不要只改 Secret 后等待旧 Pod 自己变化。
- 逐个等待 rollout:API、Web、Worker 分别验收,保留哪个服务卡住的可见性。
- 做部署后隔离对账:数据库基线、消息命名空间、对象前缀分别核对。
这也解释了为什么 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 运维能力。
最终要记住的只有一句话:先判断哪个控制回路没有收敛,再修对应层;不要拿结果状态当根因。