课程目录(第 15 章 / 共 33 章)
课程/走向生产

排障手册:出问题先看哪里

15 章 / 共 33·20 分钟·入门+排障kubectl日志

一套固定的排查顺序、一张状态速查表和三个完整案例,让你不再靠猜命令,五分钟定位 Pod 起不来或 Service 连不上的根因。

学完这一章,你将能够

  • 按固定顺序排查问题,而不是想到什么敲什么
  • 看到 Pod 状态就知道第一条该运行什么命令
  • 用 describe、logs --previous、Endpoints 定位三类常见故障
  • 会用 kubectl debug 与 port-forward 做临时验证

一套固定的排查顺序

新手排障最常见的画面是:出错了,然后把记得的命令挨个敲一遍,运气好碰上了,运气不好半小时还在原地。问题不在于命令不熟,而在于没有顺序

Kubernetes 里的故障几乎都发生在一条链路上:Pod 没起来 → 容器没跑起来 → 应用启动失败 → Service 找不到后端 → 流量进不来。所以排查也要沿着这条链路走,每一步都能排除一大类原因,把范围缩小一半。

text
故障现场


① 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 → 用同一组命令复查

固定顺序如下,先别跳步:

  1. 看 Pod 状态kubectl get pods -n demo -o wide。看 STATUSRESTARTSREADY 三列,先判断是「起不来」还是「起来又挂」。
  2. 看 Eventskubectl describe pod <pod> -n demo,重点读最后的 Events 段。调度失败、镜像拉取失败、探针失败都写在这里。
  3. 看日志kubectl logs <pod> -n demo --tail=50;容器重启过就加 --previous 看上一个容器实例的日志。
  4. 进容器验证kubectl exec -it <pod> -n demo -- sh,在容器里试 nslookupwgetls,确认是网络、DNS 还是文件问题。
  5. 看 Service 与 Endpointskubectl get endpointslices -n demo。后端列表为空,说明 Service 的 selector 没选中任何就绪的 Pod。
  6. 看节点与资源kubectl get nodeskubectl describe node <node> | grep -A5 Conditionskubectl top pods -n demo(需要 metrics-server)。

为什么顺序不能反

镜像名写错时容器根本没启动,kubectl logs 只会告诉你「没有日志」,但 describe 的 Events 会直接写出 Failed to pull image。先看 Events 能省掉一大半无用功。

本章所有命令都假设你已经有一个可用的集群和 kubectl 上下文,并且建好了命名空间:

bash
kubectl create namespace demo

常见状态速查表

把这张表贴在显示器边上。看到状态,先敲「第一条命令」,再往下想。

状态典型原因第一条命令
Pending资源不足、亲和性不满足、有污点没容忍kubectl describe pod <pod> 看 Events
ContainerCreating拉镜像中、挂载卷慢、CNI 分配 IP 慢kubectl describe pod <pod>
ImagePullBackOff镜像名/标签写错、私有仓库没配 Secretkubectl 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

bash
kubectl create deployment bad-image -n demo --image=nginx:1.27.0-typo
kubectl get pods -n demo -l app=bad-image

期望看到 STATUSErrImagePull 变成 ImagePullBackOffRESTARTS 一直是 0。接着看 Events:

bash
kubectl describe pod -n demo -l app=bad-image | grep -A10 Events

期望输出里有类似 Failed to pull image "nginx:1.27.0-typo": ... manifest unknownnot found 的字样,最后一行是 Back-off pulling image。这直接指出问题在镜像地址,和你的应用代码无关。

修复时先确认容器名,再换镜像:

bash
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 变成 RunningREADY1/1。如果镜像在私有仓库,还需要 imagePullSecrets 指向一个带凭据的 Secret,见 第 9 章 ConfigMap 与 Secret

案例二:容器启动即退出导致 CrashLoopBackOff

镜像能拉下来,但容器启动几秒就退出,kubelet 会按指数退避不断重启,状态就变成 CrashLoopBackOff它的本质是「容器反复启动失败」,不是一个错误本身。

用 logs --previous 找到退出原因

bash
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。这时当前容器可能刚被重启、日志是空的,要看上一个实例:

bash
kubectl logs -n demo -l app=crasher --previous --tail=20

期望看到 starting...boom: config file missing 两行。再确认退出码:

bash
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 的配置。清理:

bash
kubectl delete deployment crasher bad-image -n demo

案例三:Service 访问不通,Endpoints 为空

Service 本身几乎不会坏,它只是「按 selector 找 Pod」。所以服务不通时,九成问题出在 selector 和 Pod 的标签对不上,或者 Pod 没通过就绪探针

制造一个 selector 不匹配的 Service

bash
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 改错:

bash
kubectl patch svc web-svc -n demo -p '{"spec":{"selector":{"app":"web-frontend"}}}'
kubectl get endpoints web-svc -n demo

期望输出变成 <none>。再用一个临时 Pod 验证,会一直等到超时:

bash
kubectl run tmp -n demo --rm -it --restart=Never --image=busybox:1.36 \
  -- wget -qO- -T 3 http://web-svc.demo.svc.cluster.local

改回来就恢复:

bash
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

五分钟定位清单(可打印)

先做前置检查:确认自己在哪个集群、哪个命名空间。这一步很多人跳过,然后在错误的上下文里白忙十分钟。

bash
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 切回去
2Pod 概览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 -- shnslookup / wget 验证 DNS 与连通性
6后端列表kubectl get endpointslices -n demo空列表说明 selector 不匹配或 Pod 没 Ready
7节点kubectl get nodesdescribe node看 Conditions 里的 DiskPressure / MemoryPressure
把这张表打印出来贴在显示器边上:排障时按序号走,比凭记忆敲命令快得多。

两个排障利器与事件流

`kubectl debug` 用来往一个正在运行的 Pod 里塞一个临时容器。适合镜像里没有 shell、或者你不想改动原容器的场景:

bash
# 临时容器,和原容器共享进程命名空间,可以看到原容器的进程
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

进了临时容器后,常用的三条验证命令:

bash
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,不在你的应用。

bash
kubectl port-forward svc/web-svc 8080:80 -n demo
# 另开一个终端
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8080

期望输出 200

事件流是集群的「黑匣子记录」,默认只保留 1 小时左右,所以现场越早看越好。

bash
# 按时间排序看最近发生的事
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 eventsdescribelogs。它们有个共同的硬伤——不留存。事件默认只保留 1 小时左右,Pod 被删掉后相关事件很快消失;RESTARTS 那一列只告诉你「重启过」,不告诉你「从几点开始重启、重启了多少次、是不是和某次发布重合」。而真实故障往往在事后几小时甚至第二天才被复盘,现场早就被冲掉了。

所以要补上两条可留存的链路:指标回答「什么时候开始变坏、坏到什么程度」,事件导出回答「当时集群到底说了什么」。两条都落到长期存储里,排查才从「守着终端」变成「事后能查」。

text
可留存的排查链路

  现场(易失,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 的计数器:

text
# 过去 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_reasondescribe 里的 Last State 更适合全局扫描,因为它是一个带 reason 标签的指标,可以直接按原因分组统计。对照 探针与资源管理 里讲的 OOMKilled 与 CPU throttling,就能判断该调 limit 还是查内存泄漏。

再看事件。要让 kubectl get events 变成可检索的数据,用 kubernetes-event-exporter 把 Event API 的内容导出到日志后端。它本质上是一个 watch 事件的 Deployment,按 layout 模板把事件字段映射成日志字段:

yaml
# 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 }}"
bash
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

装好后,事件就和容器日志、指标存在同一个后端里,可以随时回溯:

text
# 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 里映射出来的字段决定了你能查什么:typeNormalWarningreasonFailedSchedulingOOMKilledFailedMount 这类具体原因,count 是同一事件被重复上报的次数——`count` 越大,说明问题持续得越久,这比单看一条事件更能说明严重程度。注意 exporter 自己也吃 API Server 的请求配额,大集群里要把 config.kubeQPSconfig.kubeBurst 调到和 API Server 承载能力匹配的值,别让排查工具变成故障源。

参考:reference/k8s-in-action/o11y/logs/collectors/kubernetes-event-exporter/readme.mdreference/k8s-in-action/o11y/logs/readme.md

常见误区与速查表

这几件事最浪费时间

  • 只看 logs 不看 events:镜像拉不下来、卷挂不上时容器压根没启动,logs 只会显示空或 unable to retrieve container logs
  • 改了 YAML 忘了 applykubectl 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
logsprevious terminated container not found容器还没重启过,或旧实例已被回收kubectl get pod <pod> -o jsonpath='{.status.containerStatuses[0].lastState}'
describe 看到的配置和本地文件不一致改了 YAML 没 applykubectl get deploy <name> -o yaml 与本地文件对比
Service 有 ClusterIP 但访问超时Endpoints 为空:selector 不匹配或 Pod 未 Readykubectl 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 imageFailedMountFailedScheduling 这样的直接原因。日志回答的是「容器跑起来之后为什么死」,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/1OOMKilled 背后的探针和 limit,可观测性 给出指标与日志后端的部署方式。

练习

  1. 故意给一个 Deployment 的 resources.requests.memory 写一个超过节点容量的值,观察 Pod 状态与 Events 原文,再改回合理值。
  2. web-svc 加一个错误的 targetPort,用 port-forwardwget 各验证一次,说明为什么「Service 有 Endpoints 但访问还是失败」。
  3. 用一个内存持续增长的容器制造 OOMKilled(例如 busybox 里循环写入一个大文件到内存),用 describe 找到 Last State: Terminated, Reason: OOMKilled,并把 limit 调大后再试一次。

你现在已经能定位大部分日常故障了。但当清单文件多到十几个、还要区分测试和生产环境时,手写 YAML 会变成新的负担——下一章的 Helm 就是来解决这件事的。