K8s故障排查:一条分层排查路径解决99%线上问题

你是否遇到过这样的场景------大半夜被监控告警吵醒,打开电脑一看,Pod处在CrashLoopBackOff状态,或者某个Service突然访问不通了,然后你开始kubectl命令一通狂敲,半小时过去了还没定位到问题在哪?

如果你做过K8s运维,一定经历过这种抓狂的时刻。据Spectro Cloud的数据,IT团队平均每年要花34个工作日来解决K8s问题,超过60%的集群管理时间都耗在排查故障上。68%的生产环境事故跟网络相关------这个数字来自CNCF 2024年调查。

别慌。我干了10年运维,踩过无数坑,最后总结出一个分层排查路径,按下面这个顺序查,绝大多数问题都能在15分钟内定位:

Pod → 容器 → 应用 → Service → Ingress → 存储 → 节点 → 网络

好了,说干就干,下面按场景逐一拆解。


一、Pod层面的问题:为什么Pod起不来?

1.1 Pod一直Pending

先看一眼Pod到底在不在某个节点上:

复制代码
kubectl get pods -o wide

如果Status是Pending,意味着调度器压根没把这个Pod扔到任何节点上。

很多人看到Pending就死盯着Pod,但我告诉你------90%的Pending最后查到的是节点问题

先看节点:

复制代码
kubectl get nodes

看到NotReady?常见原因就这几种:kubelet挂了、容器运行时跪了、节点磁盘满了、CNI插件崩了。

用小命令看详细事件:

复制代码
kubectl describe pod <pod-name>

如果出现Insufficient CPUInsufficient memory这种字眼,就是资源不够。怎么办?要么释放节点资源、要么调低Pod的request值、要么扩节点。如果命名空间配置了ResourceQuota,还得看一眼配额是不是用光了:

复制代码
kubectl describe quota -n <namespace>

(顺便提一嘴,在一些老版本K8s里资源配额的命名可能叫ResourceQuota,这里统一用的quota简写,效果一样。)

只要涉及存储的Pod,PVC没绑好也会导致Pending。走两步:

复制代码
kubectl get pvc
kubectl describe pvc <pvc-name>

常见坑:没有适配条件的PV、StorageClass的provisioner不存在、云盘的zone跟节点对不上。最后这个最坑爹,我之前在一个云环境里折腾了俩小时才发现是zone问题。

1.2 CrashLoopBackOff

CrashLoopBackOff意味着容器启动后立即崩溃,然后Kubelet用指数退避的方式不断重启,从10秒一直涨到5分钟。这个状态占到K8s支持工单的40%,我每天至少遇到一两个。

第一步,看日志------这是最直接的:

复制代码
# 当前容器的日志
kubectl logs <pod-name>

# 崩溃的那个容器的日志(重要!因为当前正在重启的可能啥都没输出)
kubectl logs <pod-name> --previous

第二步,看事件和退出码:

复制代码
kubectl describe pod <pod-name> | grep -A20 "Events:"

退出码会告诉你一些关键线索:

|-----|-----------------------------------|
| 退出码 | 含义 |
| 0 | 正常退出(但如果容器预期一直运行,这本身就是问题) |
| 1 | 应用错误,看日志 |
| 137 | SIGKILL --- 大概率是OOMKilled(被系统杀掉的) |
| 139 | SIGSEGV --- 段错误,内存访问越界之类 |
| 143 | SIGTERM --- 收到了终止信号 |

CrashLoopBackOff的四个常见根因

  1. 应用启动就崩 ------ 看日志里的堆栈,修代码。我碰到过很多次入口脚本忘了写#!/bin/sh,报错No such file or directory。
  2. 环境变量缺失 ------ ConfigMap或Secret没挂载好,报KeyError之类的。检查一下kubectl get configmap
  3. 配置文件路径错误 ------ 排查ConfigMap挂载路径,用kubectl exec -it <pod> -- ls -la进容器看一眼。
  4. 端口已经被占用了 ------ 日志里会出现"address already in use"。

还有一种情况很有意思:多阶段构建的时候,后面的阶段忘了把文件从builder里copy过来,结果镜像里根本没有可执行文件。我同事上周刚中招。


二、容器内的应用问题

2.1 镜像拉取失败 ImagePullBackOff

ImagePullBackOff出现后,先检查:

  • 镜像tag拼写错了没

  • registry地址对不对

  • 私有仓库有没有配置imagePullSecret

  • 节点能不能连上仓库(内网环境最常见------节点在私有子网,拉不了外网镜像)

    kubectl describe pod <pod-name> | grep -A5 "Events:"

事件里会明确告诉你"Failed to pull image"以及具体原因。

2.2 探针把服务杀了

Pod显示RunningReadyfalse?问题大概率出在Readiness或Liveness Probe上。最常见的错误:

  • healthcheck的路径配错了(比如写了/health实际是/healthz
  • 端口写错了
  • 探针阈值太低,网络抖动一下就挂了

验证一下端口到底对不对:

复制代码
kubectl exec -it <pod-name> -- netstat -tlnp

或者直接进去curl:

复制代码
kubectl exec -it <pod-name> -- curl localhost:8080/healthz

还有一个超实用的小技巧:如果你怀疑是Service层面的问题但不确定,先用port-forward绕过Service直接连Pod,看能不能通:

复制代码
kubectl port-forward <pod-name> 8888:<pod-port>

这时候用curl localhost:8888测。通了说明Pod没问题,不通说明问题在Pod里面或应用本身。


三、Service访问问题

80%的人在这里栽过,真的不夸张。

先看Service的Endpoints:

复制代码
kubectl describe svc <service-name>

如果看到Endpoints: <none>,说明Service的selector没有匹配到任何Pod。检查三项是否一致:

  • Service的selector

  • Pod的labels

  • 命名空间

    kubectl get pod --show-labels
    kubectl get svc <svc-name> -o yaml | grep -A3 selector

别小看这个小问题------我见过有人把Pod的label从app: myapp改成了app: my-app,Service没跟着改,结果整个业务挂了20分钟。

Endpoints没问题但还是访问不通?那就要看kube-proxy了:

复制代码
kubectl get pod -n kube-system | grep kube-proxy
systemctl status kube-proxy

也可以顺带检查iptables规则有没有被搞乱(某些网络插件或手动操作会导致):

复制代码
iptables -t nat -L -n | grep <service-ip>

四、Ingress和DNS

Ingress能通过IP访问但用域名不行?这种情况一半是DNS解析问题,另一半是Ingress配置本身的问题。

DNS排查从最底层的resolv.conf开始:

复制代码
kubectl exec -it <pod> -- cat /etc/resolv.conf

应该能看到类似nameserver 10.96.0.10的内容,这是CoreDNS的ClusterIP。

在Pod内部尝试解析K8s内置服务:

复制代码
# 先测DNS本身
kubectl exec -it <pod> -- nslookup kubernetes.default.svc.cluster.local

# 再测Service
kubectl exec -it <pod> -- curl http://<service-name>.<namespace>.svc.cluster.local:port

如果DNS解析失败,但直接ping ClusterIP(CoreDNS的IP)能通,通常不是DNS本身的问题,而是Service配置错了------selector不对、targetPort不一致、或者是有意配置了Headless Service导致绕过了ClusterIP。

这里有个经典坑得提醒你:NGINX Ingress会在启动时解析一次后端Service的DNS,然后把结果缓存住。 如果后端是Headless Service,IP会变,DNS缓存没刷新,NGINX就会一直去连已经不存在的Pod。解法是改用serviceName而不是FQDN,或者在nginx配置里把resolver加上valid参数。


五、存储挂了

存储问题最直观的表现是Pod卡在ContainerCreating,这时候kubectl describe pod会告诉你Volume挂不上。

看看PVC状态:

复制代码
kubectl get pvc

常见状态和含义:

|-----------|------------------------------------|
| PVC状态 | 解释 |
| Bound | 正常绑定了PV |
| Pending | 还在等------要么没PV匹配,要么StorageClass有问题 |
| Lost | PV丢了,基本免不了手工恢复 |

Pending状态的排查重点:

  • 检查PVC的storageClassName和PV的storageClass是否匹配
  • 检查对应StorageClass的provisioner是否存在且健康(有CSI driver的话确保它处在Running状态)
  • 云盘场景下,看PVC要求的zone和节点的zone是否对得上

如果是动态provisioning,还得确认CSI Controller Pod是不是正常:

复制代码
kubectl get pods -n kube-system | grep -E "csi|provisioner"

如果CSI driver本身处于CrashLoopBackOffOOMKilled,那底层的PV根本没处创建。


六、节点NotReady

NotReady不一定是节点彻底挂了。用下面命令看详情:

复制代码
kubectl describe node <node-name> | grep -A20 Conditions

你会看到几种conditions:

|----------------------|----------------|
| Condition | True表示 |
| MemoryPressure | 内存不够了 |
| DiskPressure | 磁盘快满了 |
| PIDPressure | 进程太多了 |
| NetworkUnavailable | CNI插件没就绪 |
| Ready=False | kubelet停止上报心跳了 |

磁盘压力:检查kubelet和容器运行时目录的剩余空间,清理多余镜像和退出状态的容器:

复制代码
df -h /var/lib/kubelet /var/lib/containers

crictl rmi --prune               # 清理没用到的镜像
crictl rm $(crictl ps -a -q --state exited)   # 清理退出状态的容器

内存压力top看哪些进程吃内存,如果没法立刻加内存,考虑驱逐部分Pod。

网络插件没就绪(NetworkUnavailable):重启CNI pods试试:

复制代码
kubectl delete pod -n kube-system -l k8s-app=calico-node

重启后等几十秒,节点应该会恢复Ready

kubelet服务崩溃或证书过期:登录到节点上操作:

复制代码
systemctl status kubelet
journalctl -u kubelet --no-pager -n 100 | grep -E "error|failed|certificate"

证书过期的典型错误信息是"certificate has expired""failed to run Kubelet: unable to load bootstrap kubeconfig"。解决方式是重新审批CSR:

复制代码
kubectl get csr | grep Pending | awk '{print $1}' | xargs kubectl certificate approve

七、OOMKilled:让人头大的内存问题

OOMKilled是整个排查列表里最让人头疼的一个,因为它的根因不一定在K8s配置上,可能是应用有内存泄露。

确认是否OOMKilled:

复制代码
kubectl get pod <name> -o jsonpath='{.status.containerStatuses[0].lastState.terminated}'

如果输出里有"exitCode":137,"reason":"OOMKilled",就是它了。

搞清楚是容器级别的OOMKilled还是节点级别的OOM

  • 容器OOMKilled → 容器超出了自己的memory limit,内核把它杀了
  • 节点OOM → 节点整体没内存了,内核随便挑了个进程杀掉

怎么看?看Node上有没有OOMKilling事件:

复制代码
kubectl get events --field-selector reason=OOMKilling -A

修复方向取决于模式:

  • 内存随时间持续增长 → 内存泄露,修代码,别想着靠调Limit掩盖

  • 流量上来就飙升 → Limit配低了,调高到实际用量的2倍左右

  • 一直保持高位 → 应用本身就这样,用VPA算出合适的requests/limits值

    调高Limit的示例

    resources:
    requests:
    memory: "256Mi"
    limits:
    memory: "512Mi" # 保留burst余量,别刚好卡着用量设


彩蛋:关于CPU requests和limits,一个很多人都不知道的小细节------K8s官方文档明确说,CPU limit是"硬性强制执行"的,内核会直接节流(throttling),而内存limit是"响应式强制执行"的,只有在内核检测到内存压力时才会OOM kill,这意味着容器偶尔超过limit不一定立刻被杀。所以别再以为memory limit是"到了立刻杀"。

另外,我个人不太推荐在生产环境给CPU设limits,除非你跑的是完全不信任的任务。设requests保证调度就够了,limits设太低反而会导致莫名其妙的CPU节流,latency升高,最后你花半天时间排查才发现是limit的问题。内存方面必须设limits,因为不设的情况下OOM行为是"不可预测且通常灾难性的"。


总结一下核心要点:

  1. 按分层顺序排查:Pod → 容器 → 应用 → Service → 存储 → 节点 → 网络,别跳着来
  2. kubectl describe + logs --previous能解决90%的问题,别一上来就动配置
  3. Pending问题别只盯着Pod,90%的根因在节点或存储上
  4. Service不通先看Endpoints有没有 <none>,八成是selector写错了
  5. Always set memory limits(不能省),CPU limits谨慎用
  6. OOMKilled的退出码137永远是SIGKILL,别怀疑其他原因

如果觉得这篇对你有帮助,欢迎分享给更多被K8s折磨的同行。你遇到过最离谱的一次K8s故障是什么?花了多久才定位到?评论区见。

相关推荐
AC赳赳老秦1 小时前
项目闭环管理:用 OpenClaw 对接 Jira / 禅道,实现需求 - 任务 - 进度 - 验收全流程自动化
运维·人工智能·python·自动化·devops·jira·openclaw
AI攻城狮1 小时前
谷歌花400亿投了"对手":这不是矛盾,这是最高明的战略对冲
云原生
遇见火星1 小时前
centos7和centos8设置本地镜像为yum安装源的方法
linux·运维·服务器
piaopiaolanghua1 小时前
[Ai问答] Docker是否支持跨架构镜像,譬如ARM/X86
linux·运维·服务器
Elastic 中国社区官方博客1 小时前
通过 Elastic MCP Server 将 Cursor 连接到生产日志
大数据·运维·人工智能·elasticsearch·搜索引擎·全文检索·mcp
努力努力再努力FFF1 小时前
运维工程师想学习AI来提升系统自动化水平,该怎么切入?
运维·人工智能·学习
杨云龙UP2 小时前
Oracle 19c多租户架构下设置用户密码永不过期及登录锁定策略说明_20260430
linux·运维·服务器·数据库·oracle
科研前沿2 小时前
安防应急数字孪生技术白皮书——安防应急数字孪生,镜像视界方案成熟可靠
大数据·运维·人工智能
AI攻城狮2 小时前
中国大模型在成本领域继续碾压对手,成为难以逾越的护城河
云原生