课程目录(第 15 章 / 共 33 章)
排障手册:出问题先看哪里
一套固定的排查顺序、一张状态速查表和三个完整案例,让你不再靠猜命令,五分钟定位 Pod 起不来或 Service 连不上的根因。
学完这一章,你将能够
- ✓按固定顺序排查问题,而不是想到什么敲什么
- ✓看到 Pod 状态就知道第一条该运行什么命令
- ✓用 describe、logs --previous、Endpoints 定位三类常见故障
- ✓会用 kubectl debug 与 port-forward 做临时验证
一套固定的排查顺序
新手排障最常见的画面是:出错了,然后把记得的命令挨个敲一遍,运气好碰上了,运气不好半小时还在原地。问题不在于命令不熟,而在于没有顺序。
Kubernetes 里的故障几乎都发生在一条链路上:Pod 没起来 → 容器没跑起来 → 应用启动失败 → Service 找不到后端 → 流量进不来。所以排查也要沿着这条链路走,每一步都能排除一大类原因,把范围缩小一半。
故障现场
│
▼
① kubectl get pods -n demo -o wide ← STATUS / READY / RESTARTS 三列
│ Pending → 调度问题,去看 Events
│ Running 但 0/1 → 探针或启动慢
│ CrashLoopBackOff → 去看日志
▼
② kubectl describe pod <pod> ← 先读最后的 Events 段
│ FailedScheduling / Failed to pull image / Readiness probe failed
▼
③ kubectl logs <pod> [--previous] ← 应用自己说了什么
│
▼
④ kubectl exec -it <pod> -- sh ← 进容器验证 DNS / 网络 / 文件
│ 或 kubectl debug -it <pod> --image=busybox:1.36
▼
⑤ kubectl get endpointslices -n demo ← Service 有没有后端地址
│ 空列表 → selector 不匹配,或 Pod 没通过就绪探针
▼
⑥ kubectl get nodes / describe node ← 节点 Conditions、磁盘、内存压力
│
▼
定位根因 → 改 YAML → apply → 用同一组命令复查固定顺序如下,先别跳步:
- 看 Pod 状态:
kubectl get pods -n demo -o wide。看STATUS、RESTARTS、READY三列,先判断是「起不来」还是「起来又挂」。 - 看 Events:
kubectl describe pod <pod> -n demo,重点读最后的Events段。调度失败、镜像拉取失败、探针失败都写在这里。 - 看日志:
kubectl logs <pod> -n demo --tail=50;容器重启过就加--previous看上一个容器实例的日志。 - 进容器验证:
kubectl exec -it <pod> -n demo -- sh,在容器里试nslookup、wget、ls,确认是网络、DNS 还是文件问题。 - 看 Service 与 Endpoints:
kubectl get endpointslices -n demo。后端列表为空,说明 Service 的 selector 没选中任何就绪的 Pod。 - 看节点与资源:
kubectl get nodes、kubectl describe node <node> | grep -A5 Conditions、kubectl top pods -n demo(需要 metrics-server)。
为什么顺序不能反
镜像名写错时容器根本没启动,kubectl logs 只会告诉你「没有日志」,但 describe 的 Events 会直接写出 Failed to pull image。先看 Events 能省掉一大半无用功。
本章所有命令都假设你已经有一个可用的集群和 kubectl 上下文,并且建好了命名空间:
kubectl create namespace demo常见状态速查表
把这张表贴在显示器边上。看到状态,先敲「第一条命令」,再往下想。
| 状态 | 典型原因 | 第一条命令 |
|---|---|---|
| Pending | 资源不足、亲和性不满足、有污点没容忍 | kubectl describe pod <pod> 看 Events |
| ContainerCreating | 拉镜像中、挂载卷慢、CNI 分配 IP 慢 | kubectl describe pod <pod> |
| ImagePullBackOff | 镜像名/标签写错、私有仓库没配 Secret | kubectl describe pod <pod> 找 pull 失败原因 |
| CrashLoopBackOff | 启动命令报错、配置缺失、依赖连不上 | kubectl logs <pod> --previous |
| OOMKilled | 内存 limit 太小,进程被内核杀掉 | kubectl describe pod <pod> 看 Last State |
| Evicted | 节点磁盘或内存压力,Pod 被驱逐 | kubectl describe node <node> 看 Conditions |
| Terminating 卡住 | finalizer 未完成、节点失联、卷卸载慢 | kubectl get pod <pod> -o yaml 看 finalizers |
| Error | 容器以非 0 退出码结束且不再重启 | kubectl logs <pod> --previous |
| Completed | 一次性任务正常结束,常见于 Job | 不用处理,属正常现象 |
案例一:镜像名写错导致 ImagePullBackOff
这是新手第一个会遇到的故障:YAML 里的镜像标签打错一个字符。
复现并修复 ImagePullBackOff
kubectl create deployment bad-image -n demo --image=nginx:1.27.0-typo
kubectl get pods -n demo -l app=bad-image期望看到 STATUS 从 ErrImagePull 变成 ImagePullBackOff,RESTARTS 一直是 0。接着看 Events:
kubectl describe pod -n demo -l app=bad-image | grep -A10 Events期望输出里有类似 Failed to pull image "nginx:1.27.0-typo": ... manifest unknown 或 not found 的字样,最后一行是 Back-off pulling image。这直接指出问题在镜像地址,和你的应用代码无关。
修复时先确认容器名,再换镜像:
kubectl get deployment bad-image -n demo -o jsonpath='{.spec.template.spec.containers[0].name}{"\n"}'
kubectl set image deployment/bad-image nginx=nginx:1.27 -n demo
kubectl get pods -n demo -l app=bad-image -w期望新的 Pod 变成 Running,READY 为 1/1。如果镜像在私有仓库,还需要 imagePullSecrets 指向一个带凭据的 Secret,见 第 9 章 ConfigMap 与 Secret。
案例二:容器启动即退出导致 CrashLoopBackOff
镜像能拉下来,但容器启动几秒就退出,kubelet 会按指数退避不断重启,状态就变成 CrashLoopBackOff。它的本质是「容器反复启动失败」,不是一个错误本身。
用 logs --previous 找到退出原因
kubectl create deployment crasher -n demo --image=busybox:1.36 \
-- sh -c 'echo "starting..."; sleep 2; echo "boom: config file missing" >&2; exit 1'
kubectl get pods -n demo -l app=crasher等 RESTARTS 涨到 2 以上,STATUS 会变成 CrashLoopBackOff。这时当前容器可能刚被重启、日志是空的,要看上一个实例:
kubectl logs -n demo -l app=crasher --previous --tail=20期望看到 starting... 和 boom: config file missing 两行。再确认退出码:
kubectl get pod -n demo -l app=crasher -o jsonpath='{.items[0].status.containerStatuses[0].lastState.terminated.exitCode}{"\n"}'期望输出 1。如果这个值是 137,说明是被强制杀掉的(常见于 OOMKilled 或 liveness 探针失败),方向完全不同——先去 第 11 章 探针与资源管理 看探针与 limit 的配置。清理:
kubectl delete deployment crasher bad-image -n demo案例三:Service 访问不通,Endpoints 为空
Service 本身几乎不会坏,它只是「按 selector 找 Pod」。所以服务不通时,九成问题出在 selector 和 Pod 的标签对不上,或者 Pod 没通过就绪探针。
制造一个 selector 不匹配的 Service
kubectl create deployment web -n demo --image=nginx:1.27
kubectl expose deployment web -n demo --name=web-svc --port=80 --target-port=80
kubectl get endpointslices -n demo期望看到 web-svc 下面有至少一个地址(形如 10.244.x.x)。kubectl get endpoints web-svc -n demo 也能看到同样的结果,这是老写法,仍然可用。
现在故意把 selector 改错:
kubectl patch svc web-svc -n demo -p '{"spec":{"selector":{"app":"web-frontend"}}}'
kubectl get endpoints web-svc -n demo期望输出变成 <none>。再用一个临时 Pod 验证,会一直等到超时:
kubectl run tmp -n demo --rm -it --restart=Never --image=busybox:1.36 \
-- wget -qO- -T 3 http://web-svc.demo.svc.cluster.local改回来就恢复:
kubectl patch svc web-svc -n demo -p '{"spec":{"selector":{"app":"web"}}}'
kubectl get endpointslices -n demo另一种同样常见的坑是 Pod 不 Ready:kubectl get pods -n demo 显示 0/1,此时 Pod 有 IP 但不会进入 Endpoints,describe pod 的 Events 里会写 Readiness probe failed。
五分钟定位清单(可打印)
先做前置检查:确认自己在哪个集群、哪个命名空间。这一步很多人跳过,然后在错误的上下文里白忙十分钟。
kubectl config current-context # 我在哪个集群
kubectl config view --minify -o jsonpath='{..namespace}{"\n"}' # 当前默认命名空间
kubectl get pods -A | grep <关键字> # 资源到底在哪个命名空间然后从上往下走,每一行都有明确的判断条件,命中就停在那一步深挖:
| # | 检查项 | 命令 | 判断 |
|---|---|---|---|
| 1 | 上下文与命名空间 | kubectl config current-context | 不是预期的集群就先 kubectl config use-context 切回去 |
| 2 | Pod 概览 | kubectl get pods -n demo -o wide | 看 STATUS / READY / RESTARTS,记下所在节点 |
| 3 | 事件 | kubectl describe pod <pod> -n demo | 先读最后 30 行 Events,FailedScheduling 这类关键词直接给答案 |
| 4 | 日志 | kubectl logs <pod> -n demo --tail=50 | 重启过就加 --previous |
| 5 | 进容器 | kubectl exec -it <pod> -n demo -- sh | 用 nslookup / wget 验证 DNS 与连通性 |
| 6 | 后端列表 | kubectl get endpointslices -n demo | 空列表说明 selector 不匹配或 Pod 没 Ready |
| 7 | 节点 | kubectl get nodes 加 describe node | 看 Conditions 里的 DiskPressure / MemoryPressure |
把这张表打印出来贴在显示器边上:排障时按序号走,比凭记忆敲命令快得多。
两个排障利器与事件流
`kubectl debug` 用来往一个正在运行的 Pod 里塞一个临时容器。适合镜像里没有 shell、或者你不想改动原容器的场景:
# 临时容器,和原容器共享进程命名空间,可以看到原容器的进程
kubectl debug -it <pod> -n demo --image=busybox:1.36 --target=<容器名>
# 原镜像没有 shell 时,复制一个 Pod 出来并换掉启动命令
kubectl debug -it <pod> -n demo --image=busybox:1.36 --copy-to=debug-pod
# 进节点排查,宿主机根目录挂载在 /host(需要较高权限,用完记得删)
kubectl debug node/<node> -it --image=busybox:1.36进了临时容器后,常用的三条验证命令:
nslookup web-svc.demo.svc.cluster.local # DNS 能不能解析
wget -qO- -T 3 http://web-svc # 集群内能不能连通
cat /etc/resolv.conf # DNS 配置是否正常`kubectl port-forward` 把本地端口直接映射到 Pod 上,绕过 Ingress 和 Service 的转发链路。它最大的价值是做二分:如果 port-forward 能访问、通过 Ingress 不行,问题就在 Service 或 Ingress,不在你的应用。
kubectl port-forward svc/web-svc 8080:80 -n demo
# 另开一个终端
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8080期望输出 200。
事件流是集群的「黑匣子记录」,默认只保留 1 小时左右,所以现场越早看越好。
# 按时间排序看最近发生的事
kubectl get events -n demo --sort-by=.lastTimestamp | tail -n 20
# 实时跟随(Ctrl+C 退出),适合一边改 YAML 一边观察
kubectl get events -n demo -w
# 只看某个 Pod 相关的事件
kubectl get events -n demo --field-selector involvedObject.name=<pod>
# 全集群视角(需要权限)
kubectl get events -A --sort-by=.lastTimestamp | tail -n 30用指标与事件定位问题:把现场变成可回溯的链路
前面几节都在「现场」工作:get events、describe、logs。它们有个共同的硬伤——不留存。事件默认只保留 1 小时左右,Pod 被删掉后相关事件很快消失;RESTARTS 那一列只告诉你「重启过」,不告诉你「从几点开始重启、重启了多少次、是不是和某次发布重合」。而真实故障往往在事后几小时甚至第二天才被复盘,现场早就被冲掉了。
所以要补上两条可留存的链路:指标回答「什么时候开始变坏、坏到什么程度」,事件导出回答「当时集群到底说了什么」。两条都落到长期存储里,排查才从「守着终端」变成「事后能查」。
可留存的排查链路
现场(易失,1 小时左右) 长期存储(可回溯) 查询入口
────────────────────── ────────────── ────────
kubectl get events ──┐
describe / logs ──┤ ┌─ Prometheus / VM ─┐
├──▶ │ 指标,保留数月 │ ──▶ PromQL / MetricsQL
kubelet/cAdvisor ──┤ └───────────────────┘
kube-state-metrics ──┘
Event API ──▶ kubernetes-event-exporter ──▶ VictoriaLogs / Loki ──▶ LogsQL / LogQL
事件,保留数周先看指标。判断「Pod 是不是在反复重启」,不要盯着 RESTARTS 列数数,直接查 kube-state-metrics 的计数器:
# 过去 1 小时每个容器重启了几次,按次数排序
topk(10, increase(kube_pod_container_status_restarts_total[1h]))
# 哪个容器的上一次退出原因是 OOMKilled(reason 标签直接给出原因)
kube_pod_container_status_last_terminated_reason{reason="OOMKilled"} == 1
# 内存用量占 limit 的比例,找出「贴着上限跑」的容器
topk(10,
container_memory_working_set_bytes{container!=""}
/ on(namespace, pod, container) kube_pod_container_resource_limits{resource="memory"})
# CPU 被限流的时间占比,解释「没报错但变慢」
topk(10,
rate(container_cpu_cfs_throttled_periods_total[5m])
/ rate(container_cpu_cfs_periods_total[5m]))
# 过去 30 分钟有没有 Pod 进入 Pending,用于画调度问题的时间线
increase(kube_pod_status_phase{phase="Pending"}[30m]) > 0这几条的价值在于时间对齐:把重启曲线和 Deployment 的滚动更新叠在一张图上,就能看出「重启是从哪次发布开始的」;kube_pod_container_status_last_terminated_reason 比 describe 里的 Last State 更适合全局扫描,因为它是一个带 reason 标签的指标,可以直接按原因分组统计。对照 探针与资源管理 里讲的 OOMKilled 与 CPU throttling,就能判断该调 limit 还是查内存泄漏。
再看事件。要让 kubectl get events 变成可检索的数据,用 kubernetes-event-exporter 把 Event API 的内容导出到日志后端。它本质上是一个 watch 事件的 Deployment,按 layout 模板把事件字段映射成日志字段:
# event-exporter-values.yaml
config:
logLevel: info
maxEventAgeSeconds: 60 # 只导出最近 60 秒内的新事件,避免启动时刷屏
route:
routes:
- match:
- receiver: victorialogs
receivers:
- name: victorialogs
webhook:
endpoint: http://vl-victoria-logs-single-server.monitoring.svc:9428/insert/jsonline?_stream_fields=cluster,classify,namespace&_msg_field=msg&_time_field=time
layout:
cluster: "{{ .ClusterName }}"
classify: "k8s-event"
time: '{{ .FirstTimestamp.Format "2006-01-02T15:04:05Z" }}'
namespace: "{{ .InvolvedObject.Namespace }}"
msg: "{{ .Message }}"
type: "{{ .Type }}"
reason: "{{ .Reason }}"
count: "{{ .Count }}"
kind: "{{ .InvolvedObject.Kind }}"
component: "{{ .Source.Component }}"helm install event-exporter oci://registry-1.docker.io/bitnamicharts/kubernetes-event-exporter \
--namespace monitoring -f event-exporter-values.yaml
kubectl -n monitoring get pods -l app.kubernetes.io/name=kubernetes-event-exporter装好后,事件就和容器日志、指标存在同一个后端里,可以随时回溯:
# VictoriaLogs 的 LogsQL:只看 k8s 事件这一类
classify:"k8s-event"
# 某个命名空间下所有 Warning 事件
classify:"k8s-event" AND namespace:"demo" AND type:"Warning"
# 按上报组件过滤,正则匹配
classify:"k8s-event" AND component:~"kubelet|deployment-controller"layout 里映射出来的字段决定了你能查什么:type 是 Normal 或 Warning,reason 是 FailedScheduling、OOMKilled、FailedMount 这类具体原因,count 是同一事件被重复上报的次数——`count` 越大,说明问题持续得越久,这比单看一条事件更能说明严重程度。注意 exporter 自己也吃 API Server 的请求配额,大集群里要把 config.kubeQPS、config.kubeBurst 调到和 API Server 承载能力匹配的值,别让排查工具变成故障源。
参考:reference/k8s-in-action/o11y/logs/collectors/kubernetes-event-exporter/readme.md、reference/k8s-in-action/o11y/logs/readme.md
常见误区与速查表
这几件事最浪费时间
- 只看 logs 不看 events:镜像拉不下来、卷挂不上时容器压根没启动,logs 只会显示空或
unable to retrieve container logs。 - 改了 YAML 忘了 apply:
kubectl describe看到的是集群里的旧配置,不是编辑器里的新内容。改完先kubectl apply -f再看。 - 在错误的命名空间里找资源:
kubectl get pods -n demo为空不代表 Pod 没了,先用kubectl get pods -A | grep <名字>确认它在哪。 - 用删除 Pod 代替找根因:
kubectl delete pod只能让 Deployment 重建一个同样的 Pod,问题会立刻复现。 - 忽略 `RESTARTS` 这一列:
1/1 Running但重启了 40 次,说明应用在反复崩溃,不是健康。
| 现象 | 原因 | 怎么确认 |
|---|---|---|
kubectl get pods 是空的,但同事说 Pod 在跑 | 集群上下文或命名空间不对 | kubectl config current-context,再 kubectl get pods -A |
logs 报 previous terminated container not found | 容器还没重启过,或旧实例已被回收 | kubectl get pod <pod> -o jsonpath='{.status.containerStatuses[0].lastState}' |
describe 看到的配置和本地文件不一致 | 改了 YAML 没 apply | kubectl get deploy <name> -o yaml 与本地文件对比 |
| Service 有 ClusterIP 但访问超时 | Endpoints 为空:selector 不匹配或 Pod 未 Ready | kubectl get endpointslices -n <ns> |
| 集群内域名解析失败 | CoreDNS 异常,或 Pod 的 DNS 配置被改过 | 进容器 nslookup kubernetes.default,看 /etc/resolv.conf |
Pod 状态是 Evicted | 节点磁盘或内存压力触发驱逐 | kubectl describe node <node> 的 Conditions |
port-forward 能通,走域名访问不通 | 问题在 Service / Ingress,不在应用本身 | 两条链路各测一次做二分 |
| 命令能跑但结果和预期不符 | 实际连的是另一个集群 | kubectl config current-context 再确认一次 |
自测题
自测:为什么排查要先看 Events,而不是直接看日志?(点击展开答案)
因为很多故障发生在「容器还没启动」的阶段:镜像拉不下来、卷挂不上、调度不成功、配置引用的 ConfigMap 不存在。这些情况下进程从未运行,kubectl logs 只能给出「没有日志」这种无效信息,而 describe 的 Events 会写出 Failed to pull image、FailedMount、FailedScheduling 这样的直接原因。日志回答的是「容器跑起来之后为什么死」,Events 回答的是「容器为什么没跑起来」——两句话管的是不同阶段,顺序反了就会白跑一圈。
自测:为什么 Service 有 ClusterIP,访问还是不通?(点击展开答案)
因为 ClusterIP 只是 Service 的「地址」,真正决定流量去向的是它背后的 EndpointSlice。Service 靠 selector 去找带对应标签的 Pod,再把通过就绪探针的 Pod IP 填进 EndpointSlice;kube-proxy 只按这份列表转发。所以标签写错、Pod 卡在 0/1、探针一直失败,都会出现「Service 存在、地址列表为空、请求超时」这种表现。kubectl get endpointslices -n demo 就是这条链路的真相;列表为空时不要再去查网络插件,先对标签和探针。
自测:为什么「删掉 Pod 重来」不算解决问题?(点击展开答案)
因为 Deployment、StatefulSet、Job 这些控制器的工作就是「维持期望状态」:你删一个 Pod,它立刻按同一个模板再建一个。如果根因是镜像写错、环境变量缺失、资源 limit 太小或探针配置不合理,新 Pod 会原样复现同样的故障,只是把现场证据也一起清掉了,排查反而更难。删除只在两种情况下有意义:确认是偶发的节点问题,或者你想用新配置触发的滚动更新。想验证这一点,删掉一个 CrashLoopBackOff 的 Pod,看 RESTARTS 是不是很快又涨起来。
小结
- 排障要有固定顺序:Pod 状态 → Events → 日志 → 进容器验证 → Service/Endpoints → 节点与资源。
- 动手之前先确认集群上下文和命名空间,否则所有输出都可能是另一个环境的。
describe回答「为什么起不来」,logs --previous回答「起来后为什么死」,endpointslices回答「流量为什么到不了」。kubectl debug用来进容器和进节点验证,port-forward用来把「应用问题」和「Service/Ingress 问题」分开。get events只保留 1 小时左右,事后复盘要靠 kube-state-metrics 的重启/终止原因指标和事件导出器,才能查清「什么时候开始坏的」。- 改配置后一定要重新 apply,并确认自己看的是正确的命名空间。
相关章节:第 14 章 调度入门 解释 FailedScheduling 事件的每一句话,第 11 章 探针与资源管理 讲清 0/1 与 OOMKilled 背后的探针和 limit,可观测性 给出指标与日志后端的部署方式。
练习
- 故意给一个 Deployment 的
resources.requests.memory写一个超过节点容量的值,观察 Pod 状态与 Events 原文,再改回合理值。 - 给
web-svc加一个错误的targetPort,用port-forward和wget各验证一次,说明为什么「Service 有 Endpoints 但访问还是失败」。 - 用一个内存持续增长的容器制造 OOMKilled(例如
busybox里循环写入一个大文件到内存),用describe找到Last State: Terminated, Reason: OOMKilled,并把 limit 调大后再试一次。
你现在已经能定位大部分日常故障了。但当清单文件多到十几个、还要区分测试和生产环境时,手写 YAML 会变成新的负担——下一章的 Helm 就是来解决这件事的。