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故障是什么?花了多久才定位到?评论区见。

相关推荐
abigriver7 小时前
打造 Linux 离线大模型级语音输入法:Whisper.cpp + 3090 显卡加速与 Rime 中英混输终极调优指南
linux·运维·whisper
姚不倒8 小时前
Go语言进阶:接口、错误处理与并发编程(goroutine/channel/context)
云原生·golang
charlie1145141918 小时前
嵌入式Linux驱动开发pinctrl篇(1)——从寄存器到子系统:驱动演进之路
linux·运维·驱动开发
Agent手记8 小时前
异常考勤智能预警与处理与流程优化方案 | 基于企业级Agent的超自动化实战教程
运维·人工智能·ai·自动化
cen__y9 小时前
Linux12(Git01)
linux·运维·服务器·c语言·开发语言·git
dapeng-大鹏11 小时前
KVM+LVM 零停机在线扩容 Ubuntu 根分区:从磁盘添加到逻辑卷扩展完整
linux·运维·ubuntu·磁盘空间扩展
乐维_lwops11 小时前
案例解读|运维监控助力某大型卷烟厂构建高效运维监控体系
运维·运维案例
JiaWen技术圈11 小时前
网站用户注册行为验证码方案
运维·安全
仙柒41511 小时前
Docker存储原理
运维·docker·容器
DolphinDB11 小时前
漫长人工,耗费存储?用 BackupRestore 模块一站式解决跨环境数据同步难题
运维·后端·架构