一、引言
做 Kubernetes 一线运维的人都有同感:真正花在"写 YAML"上的时间,远少于花在"为什么没按我写的来"上的时间。 声明式系统的好处是"你只管说想要什么",代价是"出问题时你得去猜它卡在哪一步"。
Kubernetes 排错有两大难点:
- 组件多、链路长:一个 Pod 从 kubectl apply 到真正跑起来,要经过 API Server(认证/鉴权/准入)→ etcd(持久化)→ Scheduler(决策)→ kubelet(执行)→ 容器运行时(CRI)→ 网络插件(CNI)→ kube-proxy/DNS。任何一环出问题,现象都可能在最末端才暴露。
- 信息分散:错误不会集中弹在你脸上,而是散落在 Pod status、Events、kubelet 日志、容器日志、审计日志 里。
本文的目标不是罗列"XXX 报错的解决办法"(那种列表你记不住也搜得到),而是给你一套可复用的方法论——先理解 K8s 是"怎么失败的",再带你把最高频的几类报错逐一复现、定位、修复,最后沉淀一个排错工具箱和决策树。
排错的底层逻辑只有一句话:找到"期望状态"与"实际状态"之间的偏差,并定位产生偏差的那一层。
二、原理解析:先搞懂 K8s 是怎么"失败"的
不把机制讲清楚,排错就只能靠"试"。这一章是后面所有场景的"语法基础"。
2.1 声明式系统的排错本质
K8s 是声明式的:kubectl apply 写进去的是 Spec(期望状态),各控制器不断做 Reconcile(调和),把 Status(实际状态) 向 Spec 靠拢。
你写的 Spec ──▶ API Server ──▶ etcd ──▶ 各 Controller/Scheduler/kubelet
│
└──▶ 持续 Reconcile ──▶ 实际 Status
所以"出错了"通常意味着 某一段 Reconcile 卡住了或拒绝了。排错的第一步,永远是判断:这次失败,卡在哪一环? 后续每一类报错,本质都对应一条特定的卡点。
2.2 Pod 生命周期状态机
Pod 的状态(来自 kubectl get pod 的 STATUS 列)不是随便显示的,它来自 Pod 真实的状态机。把这张图刻在脑子里,读状态就不再靠猜:
kubectl apply
│
▼
┌──────────┐ 调度成功 ┌──────────────────┐
│ Pending │─────────────▶│ ContainerCreating│
│ │ └────────┬─────────┘
│ (等待调度/ │ │ 镜像拉取+启动
│ 资源/PVC)│ ▼
└──────────┘ ┌──────────────────┐
│ │ PodInitializing │
│ (调度失败) └────────┬─────────┘
▼ │
┌──────────────────┐ ▼
│ 长期 Pending │ ┌────────────────┐
│ (需排查调度) │ │ Running │◀───┐
└──────────────────┘ └───┬────────┬───┘ │
│ │ │ 重启
探针失败/退出│ │存活 │
▼ ▼ │
┌────────────┐ ┌─────────┐ │
│ Succeeded │ │CrashLoop│─┘
│ (正常结束) │ │ BackOff │
└────────────┘ └─────────┘
│
▼
┌────────────────┐
│ ImagePullBackOff│ (镜像拉取失败)
│ OOMKilled/Error │ (运行期崩溃)
└────────────────┘
常见状态的一句话成因速查:
状态
含义
通常卡在
Pending
已接受但还没调度/启动
调度层:资源、污点、PVC
ContainerCreating
调度成功,正在拉镜像/建沙箱
节点/运行时层:镜像、volume、CNI
CrashLoopBackOff
容器反复启动即退出
应用层:启动报错、配置、依赖、OOM
ImagePullBackOff
镜像拉不下来
仓库认证、镜像名/Tag、网络
OOMKilled
内存超限被 kill(属于 Error 子状态)
内存 limits 过低 / 泄漏
Terminating
正在删除但卡住
finalizer、node 失联、volume 卸载
Unknown
节点失联,状态上报不上来
节点/网络层
Running 但没流量
探针未就绪
探针配置错误或应用未 listen
关键认知:Running 不等于"能用"。 只有 readinessProbe 通过,Pod 才会进 Service 的 Endpoint 拿到流量;Running 但 readiness 不通过,是最容易被忽略的一类"假活"。
2.3 三道防线决定了错误"长什么样"
按失败发生的时间顺序,可以粗略分成三层,错误现象天然不同:
① 准入层(API Server 内):PSA / Kyverno / RBAC
│ 失败特征:kubectl apply 直接报错被拒,Pod 根本没创建
▼
② 调度层(Scheduler):资源、污点、亲和、PVC
│ 失败特征:Pod 处于 Pending,Events 里有 FailedScheduling
▼
③ 节点/运行时/网络层(kubelet + CRI + CNI)
失败特征:ContainerCreating / CrashLoop / Running但不通
这条线也正好呼应本仓库前面几篇:PSA 拒绝见《Pod 安全》,镜像验签拒绝见《镜像供应链》,RBAC 403 见《RBAC》,网络不通见《NetworkPolicy》,Pending 见《调度器剖析》。本文后面会把这些"被拒/阻断"类问题统一收口为场景 6。
2.4 Events 与 Probe:你的两个望远镜
(1)Events——排错的第一现场
Events 是集群内各组件向 API Server 打的"带时间戳的流水账",几乎每个异常都会在这里留下线索。
- 谁产生:Scheduler、kubelet、控制器、准入插件。
- 看什么:kubectl describe pod 底部的 Events: 段。
- 坑:Events 有存活窗口(默认约 1 小时)且只保留最近若干条,老问题要趁早看,或用 kubectl get events --sort-by=.lastTimestamp 全局捞。
- 级别:Warning 才是异常,Normal 是例行通知(别被 Normal 干扰)。
(2)探针——决定"活没活、通不通" - startupProbe:启动慢的应用先靠它,通过前不触发 liveness/readiness。
- livenessProbe:失败 → 杀容器重启(杀错了会雪崩)。
- readinessProbe:失败 → 摘流量但不重启(不通过就不进 Endpoint)。
探针配置示例(节选),三个探头各司其职
livenessProbe:
httpGet: { path: /healthz, port: 8080 }
initialDelaySeconds: 10 # 给应用留足启动时间
periodSeconds: 10
failureThreshold: 3 # 连续 3 次失败才杀
readinessProbe:
httpGet: { path: /ready, port: 8080 }
periodSeconds: 5
startupProbe: # 启动慢的服务必配,避免被 liveness 误杀
httpGet: { path: /healthz, port: 8080 }
failureThreshold: 30
periodSeconds: 5 # 允许最长 150s 启动
经典翻车:initialDelaySeconds 设太短 + livenessProbe 太敏感 → 应用还没起来就被杀,进入 CrashLoopBackOff 的死循环。这就是"探针误杀"。
三、实战演示:高频报错场景逐一复现与修复
下面 6 个场景覆盖了日常 90% 的排错诉求。每个都按 现象 → 定位 → 修复 → 验证 展开。
场景 1:ImagePullBackOff / ErrImagePull(镜像拉不下来)
现象
kubectl get pod
NAME READY STATUS RESTARTS AGE
web-0 0/1 ImagePullBackOff 0 2m
定位 —— 直接看 Events,它几乎总会告诉你原因:
kubectl describe pod web-0 | sed -n ‘/Events:/,$p’
Failed to pull image “registry.local/app:v2”: rpc error: code = NotFound …
or: pulling image … unauthorized: authentication required
or: dial tcp: i/o timeout
失败原因通常就四类:
- 镜像名 / Tag 拼错(NotFound)
- 私有仓库未配 imagePullSecrets(unauthorized)
- 仓库网络不通 / 域名解析失败(timeout / no such host)
- 节点上已有该镜像但 digest 不匹配(少见,清本地缓存可解)
修复 —— 以私有仓库为例:
1) 先建拉取 secret
kubectl create secret docker-registry regcred
–docker-server=registry.local
–docker-username=ci --docker-password=‘****’
–docker-email=ci@corp.local -n shop
2) Pod/Deployment 里引用
spec:
imagePullSecrets:
- name: regcred
containers:
- name: web
image: registry.local/app:v2 # 确认 tag 真实存在
验证
kubectl get pod web-0 -w # 状态从 ImagePullBackOff → Running 即成功
排错口诀:看到 ImagePullBackOff 先 describe 看 Events 的 Failed 原因,别上来就改 YAML——80% 的情况下原因就在那一行。
场景 2:CrashLoopBackOff(容器反复崩溃)
CrashLoopBackOff 是个"包罗万象"的状态,它本身不是病根,只是说"容器启动后很快退出,kubelet 在退避重试"。真正的病根在容器退出码和日志里。
定位步骤
1) 看退出码与最近重启次数
kubectl get pod app-0
STATUS CrashLoopBackOff;RESTARTS 在快速增长
2) 看当前/上一次容器日志(–previous 看崩溃那次的)
kubectl logs app-0 --previous
常见:配置文件缺失、连不上数据库、端口被占用、panic stack
3) 看 Events 里的退出原因(是否 OOM)
kubectl describe pod app-0 | grep -A3 -i ‘OOMKilled|Back-off|Error’
子场景 A:应用启动即报错(配置/依赖)
- 日志里通常直接打印堆栈或 “connection refused”。
- 修复:修正 ConfigMap/Secret、确认依赖服务就绪(注意启动顺序,必要时用 initContainers 等依赖)、修正环境变量。
子场景 B:OOMKilled(内存超限)
kubectl describe pod app-0 | grep -i oom
Reason: OOMKilled;Exit Code: 137
- 说明容器用了超过 memory limit 的内存,被 cgroup 杀掉。
- 排查真实用量:
进节点看该容器实际 RSS(需节点权限)
kubectl debug node/ -it --image=busybox – chroot /host crictl stats
- 修复:
resources:
requests: { memory: “256Mi”, cpu: “100m” }
limits: { memory: “512Mi”, cpu: “500m” } # 内存 limit 调到真实峰值之上
重要:OOMKilled 调的是 limits.memory,不是 requests。 只加 requests 不顶用。若调大 limit 后仍频繁 OOM,多半是应用内存泄漏,要从代码层定位(heap profile),而非一味加内存。
子场景 C:探针误杀(liveness 太严) - 表现为:容器其实能跑,但 RESTARTS 持续增加,日志显示业务正常却被重启。
- 看 describe 的 Last State / Events 里 Unhealthy: Liveness probe failed。
- 修复:放宽 initialDelaySeconds、failureThreshold,或改用 startupProbe 兜住慢启动。
验证
kubectl get pod app-0 -w # RESTARTS 停止增长、STATUS 变 Running
kubectl logs app-0 # 业务日志正常输出
场景 3:Pod 一直 Pending(调度不出去)
这一场景在《调度器剖析》里讲透了机制,这里给最短排错路径。
kubectl describe pod stuck-0 | grep -A20 ‘Events:’
0/5 nodes are available: 3 Insufficient cpu, 2 node(s) had taint …
常见卡点对应表:
Events 关键字
含义
修复方向
Insufficient cpu/memory
节点资源不足以满足 requests
加节点 / 降 requests / 删闲置负载
node(s) had taint … that … did not tolerate
节点有污点不容忍
加 tolerations 或换节点
node(s) didn’t match Pod’s nodeSelector
nodeSelector 无匹配
放宽 selector 或打标签
persistentvolumeclaim “x” not found/bound
PVC 未绑定
先修 StorageClass / PV
1 node(s) had volume node affinity conflict
本地卷地域不匹配
调整拓扑 / 用延迟绑定
快速佐证
kubectl get nodes -o custom-columns=NODE:.metadata.name,CPU:.status.allocatable.cpu,MEM:.status.allocatable.memory
kubectl describe node | sed -n ‘/Allocated resources/,/Events/p’
场景 4:Running 但访问不通(Service / 网络)
Pod 是 Running 且 READY 1/1,但业务访问不到。这类问题最容易让人抓狂,按下面顺序查:
1) Service 的 Endpoint 是否为空?空 = selector 没匹配到 Pod
kubectl get endpoints web-svc
NAME ENDPOINTS AGE
web-svc 10.244.1.23:8080 5m ← 有值才正常; 就是问题
2) 对比 Service 的 selector 和 Pod 的 labels(大小写/连字符最易错)
kubectl get svc web-svc -o jsonpath=‘{.spec.selector}’
kubectl get pods --show-labels
3) 端口是否对得上:svc.port.targetPort 必须指向容器真正 listen 的端口
kubectl get svc web-svc -o jsonpath=‘{.spec.ports[0].targetPort}’
4) 临时从集群内另起一个 Pod 直接打 Pod IP,绕过 Service 验证应用本身
kubectl run netshoot --rm -it --image=nicolaka/netshoot – curl -s 10.244.1.23:8080
三类典型成因:
- selector 标签错配:最常见,Endpoint 为空。核对 Service selector 与 Pod labels 完全一致(K8s label 不认大小写模糊)。
- targetPort 写错:Service 转发到错误端口,连接被拒。
- 网络策略阻断(呼应《NetworkPolicy》):Pod 虽 Running,但 Ingress 被默认拒绝策略挡掉。查:
kubectl get networkpolicy -n shop
若启用了 default-deny,需显式放通来自同 ns 或特定前端的流量
排错顺序建议:先确认 Endpoint 非空 → 再确认端口对得上 → 再用 netshoot 直连 Pod IP 排除应用本身 → 最后查 NetworkPolicy/DNS。这样能快速把"网络问题"和"应用问题"切开。
场景 5:节点 NotReady 与节点级排错
当 kubectl get nodes 出现 NotReady,该节点上 Pod 会失联、状态变 Unknown,调度也会避开它。
kubectl get nodes
node-a NotReady 120d v1.28.x
看节点事件与条件
kubectl describe node node-a | sed -n ‘/Conditions:/,/Addresses/p’
节点级排查(需 SSH / 节点权限):
kubelet 是否活着(systemd 托管时)
journalctl -u kubelet -n 100 --no-pager
systemctl status kubelet
容器运行时是否健康(containerd 为例)
crictl info | head -n 20
systemctl status containerd
节点资源是否耗尽(磁盘满会导致镜像/日志写入失败)
df -h /var/lib/containerd /var/log
常见成因:kubelet 进程挂掉/配置错、容器运行时崩溃、节点磁盘/内存/inode 耗尽、节点与控制面网络分区、kubelet 证书过期。
场景 6:准入被拒(收口前面安全系列)
这类问题特征最明显:kubectl apply 时直接报错,Pod 根本没创建出来——说明卡在 2.3 节的"准入层"。
(a)PSA(Pod Security Admission)拒绝 —— 呼应《Pod 安全》
kubectl apply -f bad.yaml
Error from server (Forbidden):
violates PodSecurity “restricted:latest”:
allowPrivilegeEscalation != false (container “app” must set …)
unrestricted capabilities (container “app” must drop ALL)
修复:按提示补齐 securityContext(drop ALL、runAsNonRoot、allowPrivilegeEscalation=false 等)。
(b)镜像未签名被 Kyverno 拒 —— 呼应《镜像供应链》
kubectl apply -f unverified.yaml
Error from server (Forbidden):
policy “require-signed-images” validation failure:
image registry.local/app:v2 is not signed
修复:先 cosign sign 再用 cosign verify 确认,或用 audit 模式临时放通。
(c)RBAC 403 —— 呼应《RBAC》
kubectl get secrets -n kube-system
Error from server (Forbidden):
User “system:serviceaccount:shop:default” cannot list resource “secrets” …
修复:用 kubectl auth can-i --as=… list secrets -n kube-system 验证,按需补 Role/RoleBinding。
通用定位技巧 —— 想看完整的准入拒绝响应,加 verbosity:
kubectl apply -f x.yaml -v=8 # 在请求/响应里能看到 Webhook 返回的完整拒绝理由
四、排错工具箱与通用方法论(重点)
光记场景不够,真正值钱的是一套固定动作。下面是我建议的"肌肉记忆"。
4.1 kubectl 四件套(任何异常都从这四个开始)
① 看状态(加 -o wide 看节点/IP,-w 看变化)
kubectl get pod -o wide -w
② 看详情(Events 是金矿,永远先看这里)
kubectl describe pod
③ 看日志(-p 看上一次崩溃的;-c 指定多容器)
kubectl logs --previous -c
④ 进容器(确认文件/配置/网络真的那样)
kubectl exec -it – sh
顺序口诀:get → describe → logs → exec。先看整体状态,再看 Events 定位大方向,再看日志看应用层面,最后进容器实证。
4.2 kubectl debug:没有 shell 也能排错
很多生产镜像为了瘦身没有 sh/bash,传统 exec 进不去。kubectl debug 用 Ephemeral Containers(临时容器)注入一个排错镜像到运行中的 Pod:
给运行中的 Pod 注入一个排错容器(共享同一 PID/网络命名空间)
kubectl debug -it --image=busybox --target=
进去后可直接看目标容器的进程、/proc、网络栈
节点级排错:以特权容器"寄生"到节点上
kubectl debug node/ -it --image=busybox – chroot /host
之后可查 /host/var/log、/host/etc/kubernetes 等
4.3 全局 Events 与时间线
单 Pod 的 Events 不够用时,捞全局并按时间排序,定位"同一时刻一大批 Pod 出问题"的系统性故障:
kubectl get events --sort-by=.lastTimestamp -A | tail -n 40
kubectl get events -A --field-selector type=Warning # 只看 Warning
4.4 排错决策树(一图速查)
Pod 状态?
├─ Pending ──────────────▶ describe 看 FailedScheduling ─▶ 场景3(资源/污点/PVC)
├─ ImagePullBackOff ─────▶ describe 看 Failed 拉取原因 ──▶ 场景1(secret/Tag/网络)
├─ ContainerCreating ────▶ describe 看 mount/CNI 报错 ──▶ volume/CNI/secret 挂载
├─ CrashLoopBackOff ─────▶ logs --previous 看退出码 ────▶ 场景2(应用/OOM/探针)
│ └─ OOMKilled? ──▶ 调大 limits.memory
├─ Running 但 READY 0/1 ─▶ 查 readinessProbe / 应用 listen ─▶ 探针未通过
├─ Running 但访问不通 ───▶ endpoints 是否空 → 端口 → netshoot 直连 → NP ─▶ 场景4
├─ apply 直接被拒 ───────▶ 看 Forbidden 提示(PSA/Policy/RBAC) ─▶ 场景6
└─ Unknown/节点 NotReady ▶ 查节点 kubelet/运行时/资源 ─────▶ 场景5
4.5 几个能救命的好习惯
- 先 describe 再改 YAML:80% 的错误原因在 Events 里明写着,别盲改。
- 区分"起不来"和"起来但不对":前者看调度/镜像/启动,后者看探针/配置/网络。
- 改完用 -v=8 看原始响应:特别是准入类拒绝,完整理由只在 verbose 日志里。
- 排错前先 kubectl get -o wide:一眼看到节点、IP、重启次数,往往省掉后面三步。
- Events 会过期:问题发现后尽早看,别等一小时后再去翻。
五、总结
Kubernetes 排错,表面上是在跟各种 BackOff、Forbidden、Pending 搏斗,本质上是在跟一个声明式系统"期望 vs 实际"的偏差搏斗。本文给你两样东西:
- 一份机制地图:从状态机(§2.2)到三道防线(§2.3),让你读到任何异常状态都能立刻判断"卡在哪一环"。
- 一套固定动作 + 场景库:get → describe → logs → exec 四件套(§4.1)是永远的起点,6 个高频场景(§三)覆盖了日常九成诉求,决策树(§4.4)把两者串成可执行的路径。
也正因为 K8s 的错误是"分层暴露"的,本文刻意把前面几篇的排错线索收口为场景 6(准入被拒):当你看到 kubectl apply 直接 Forbidden,那不是 Pod 的问题,而是 PSA(Pod 安全)/ Kyverno 验签(镜像供应链)/ RBAC(授权) 在准入层把不合规的负载挡在了门外——对应本仓库的《Pod 安全》《镜像供应链》《RBAC》三篇。把这几篇和本文连起来看,就是一张从"提交清单"到"Pod 真正健康运行"的完整排错全景图。
最后一句忠告:不要去背特定错误的解决办法,要去练"定位偏差在哪一层"的肌肉记忆。 方法对了,新错误自然也能解。
附录:本文与系列其他文章的索引关系
现象
本文场景
关联深度文章
镜像拉取失败
场景 1
—
容器崩溃 / OOM
场景 2
—
长期 Pending
场景 3
《调度器全链路剖析》
服务访问不通
场景 4
《NetworkPolicy 网络微隔离》
节点失联
场景 5
—
PSA 拒绝
场景 6
《Pod 安全上下文与 PSA》
镜像验签拒绝
场景 6
《镜像供应链安全》
RBAC 403
场景 6
《RBAC 最小权限》
完全跑不通第一个应用
—
《从零跑通第一个应用》