K8s故障排查:Pod CrashLoopBackOff、OOMKilled、Pending的定位方法
K8s故障排查:Pod CrashLoopBackOff、OOMKilled、Pending的定位方法
适读人群:在K8s上运维Java服务的工程师 | 阅读时长:约22分钟 | 适用版本:K8s 1.20+
开篇故事
刚开始用K8s的时候,每次看到Pod处于奇怪状态,我的第一反应是重启一下试试。重启了好,重启没好,然后就不知道怎么办了,只能找平台团队。
后来被逼着系统学了K8s的故障排查方法,发现大部分问题都有清晰的排查路径,只要按步骤来,基本上都能在十分钟内定位到根因。
今天把我遇到的最频繁的三种故障状态的完整排查手册写出来:CrashLoopBackOff(应用崩溃循环重启)、OOMKilled(内存溢出被杀)、Pending(调度失败无法启动)。每种状态都有完整的排查思路、命令、以及我自己踩过的真实案例。
一、核心问题分析
Pod状态全景图
K8s Pod的生命周期中可能出现的状态,理解这些状态是故障排查的基础:
排查的通用命令工具箱
# 查看Pod状态列表
kubectl get pods -n <namespace> -o wide
# 查看Pod详细描述(事件是关键!)
kubectl describe pod <pod-name> -n <namespace>
# 查看当前日志
kubectl logs <pod-name> -n <namespace>
# 查看上一次(崩溃前)的日志
kubectl logs <pod-name> -n <namespace> --previous
# 进入Pod执行命令
kubectl exec -it <pod-name> -n <namespace> -- /bin/sh
# 查看所有Namespace的事件(按时间排序)
kubectl get events --all-namespaces --sort-by='.lastTimestamp'
# 查看Node状态
kubectl describe node <node-name>二、CrashLoopBackOff 排查手册
什么是CrashLoopBackOff
容器启动后立即退出(非0退出码),K8s重启容器,重启后再退出,形成循环。BackOff意味着每次重启的等待时间会指数增加(10s、20s、40s……最长5分钟),防止无限快速重启消耗资源。
排查步骤
第一步:看退出码
kubectl describe pod <pod-name> -n <namespace>
# 关注 "Last State" 部分的 Exit Code常见退出码含义:
- 退出码1:应用程序错误(最常见,看日志)
- 退出码137:OOM或被
kill -9(= 128 + 9) - 退出码139:段错误(Segmentation Fault)
- 退出码143:被正常终止(= 128 + 15,SIGTERM)
- 退出码2:Shell脚本语法错误或找不到命令
第二步:看崩溃前的日志
# 最关键的命令!看上一次崩溃前的日志
kubectl logs <pod-name> -n <namespace> --previous
# 如果是多容器Pod
kubectl logs <pod-name> -c <container-name> -n <namespace> --previous
# 如果崩溃得太快,日志还没来得及输出
# 尝试用tail看最后几行
kubectl logs <pod-name> --previous --tail=100第三步:分场景排查
场景一:应用启动报错(最常见)
日志里出现Java异常,通常是配置问题:
# 典型错误:数据库连接失败
com.mysql.cj.jdbc.exceptions.CommunicationsException:
Communications link failure ...
The last packet sent successfully to the server was X milliseconds ago.
# 排查:检查数据库配置
kubectl get configmap -n <namespace>
kubectl describe configmap order-service-config
# 检查Secret
kubectl get secret order-service-secret -n <namespace> -o yaml
# 临时进入一个同命名空间的Pod,测试数据库连通性
kubectl run test-conn --image=mysql:8 --rm -it \
--restart=Never \
-- mysql -h mysql-service -u root -p场景二:找不到类或jar包损坏
Error: Unable to access jarfile /app/app.jar
# 或者
java.lang.ClassNotFoundException: xxx检查镜像构建是否正确:
# 拉取镜像后检查里面的文件
docker pull <image>
docker run --rm <image> ls -la /app/场景三:JVM OOM(堆内存溢出)
java.lang.OutOfMemoryError: Java heap space
# 或者
java.lang.OutOfMemoryError: Metaspace# 检查JVM启动参数
kubectl describe pod <pod-name> | grep -A5 "JAVA_OPTS"
# 检查内存配置
kubectl get pod <pod-name> -o yaml | grep -A10 resources
# 如果想要heap dump,修改JVM参数(需要重新部署)
# 添加:-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/
# 然后kubectl cp把dump文件拷出来分析
kubectl cp <pod-name>:/tmp/java_pid1.hprof ./heapdump.hprof场景四:Init Container失败
# Pod状态是 Init:CrashLoopBackOff 时
# 查看Init Container的日志
kubectl logs <pod-name> -c <init-container-name> --previous三、OOMKilled 排查手册
什么是OOMKilled
容器内存使用超过Limit,被Linux OOM Killer强制终止。describe pod里会看到:
Last State: Terminated
Reason: OOMKilled
Exit Code: 137排查步骤
第一步:确认内存Limit设置
kubectl get pod <pod-name> -o yaml | grep -A5 limits第二步:查看历史内存使用趋势
# 查看当前内存使用
kubectl top pod <pod-name>
# 通过Prometheus查看历史峰值(如果有Prometheus)
# 查询:container_memory_working_set_bytes{pod="order-service-xxx"}第三步:区分是Java堆OOM还是容器内存OOM
两者的表现不同:
Java堆OOM:日志里有OutOfMemoryError: Java heap space,是JVM自己报的错,进程可能优雅退出或崩溃;容器内存OOM(Limit超出):进程直接被系统Kill,退出码137,没有任何Java异常日志,日志戛然而止。
# 如果日志突然截断,末尾没有Java异常,就是容器OOM
kubectl logs <pod-name> --previous | tail -50
# 检查是否有OOMKill记录
kubectl describe node <node-name> | grep -A10 "OOMKilling"
# 系统层面的OOMKill记录(在Node上执行)
dmesg | grep -i "oom kill"第四步:优化JVM内存配置
Java进程的内存组成(对于2Gi Limit的容器):
# 推荐配置:Limit=2Gi时
-XX:MaxRAMPercentage=50.0 # 堆最大1Gi
-XX:InitialRAMPercentage=25.0 # 初始堆512Mi
# 剩余1Gi给:Metaspace(~256Mi) + 线程栈(~300Mi,150线程×2Mi) + Direct Memory + JVM内部如果Metaspace溢出,增加Metaspace限制:
# 添加:-XX:MaxMetaspaceSize=256m
# 避免Metaspace无限增长(Spring Boot的默认Metaspace在100~200MB)四、Pending 排查手册
什么是Pending
Pod已创建但还没有被调度到任何Node,或者正在等待某些条件满足。
排查步骤
第一步:查看Events
kubectl describe pod <pod-name> -n <namespace>
# 重点看Events部分,通常会有明确的原因常见Pending原因和解决方法:
原因一:资源不足(Insufficient CPU/Memory)
Events:
Warning FailedScheduling 0/5 nodes are available:
5 Insufficient cpu, 5 Insufficient memory.排查:
# 查看Node资源使用情况
kubectl describe nodes | grep -A5 "Allocated resources"
# 查看所有Pod的资源请求
kubectl get pods --all-namespaces -o custom-columns=\
NAME:.metadata.name,\
CPU:.spec.containers[0].resources.requests.cpu,\
MEM:.spec.containers[0].resources.requests.memory
# 如果是Request设置太高导致无法调度
kubectl get pod <pod-name> -o yaml | grep -A5 requests原因二:PVC无法绑定
Warning FailedScheduling pod has unbound immediate PersistentVolumeClaims# 查看PVC状态
kubectl get pvc -n <namespace>
kubectl describe pvc <pvc-name> -n <namespace>
# 常见原因:StorageClass不存在或不支持
kubectl get storageclass
# 查看是否有可用的PV
kubectl get pv原因三:镜像拉取失败(ImagePullBackOff)
Warning Failed Failed to pull image:
image "registry.company.com/order-service:latest":
rpc error: code = Unknown desc = ...# 确认镜像名称和tag
kubectl get pod <pod-name> -o yaml | grep image
# 检查是否有ImagePullSecrets
kubectl get pod <pod-name> -o yaml | grep imagePullSecret
# 手动测试能否拉取镜像
docker pull registry.company.com/order-service:latest原因四:节点亲和性或污点导致无法调度
Warning FailedScheduling 0/5 nodes are available:
5 node(s) had taint {node.kubernetes.io/unschedulable: },
that the pod didn't tolerate.# 查看节点的污点
kubectl describe nodes | grep Taint
# 查看Pod的容忍配置
kubectl get pod <pod-name> -o yaml | grep -A10 tolerations
# 查看节点亲和性配置
kubectl get pod <pod-name> -o yaml | grep -A20 affinity原因五:ResourceQuota超限
# 检查namespace的quota使用情况
kubectl describe resourcequota -n <namespace>五、其他常见故障状态
ImagePullBackOff 完整排查
# 1. 确认镜像名称正确
kubectl describe pod <pod-name> | grep "image"
# 2. 确认镜像仓库的Secret存在并正确
kubectl get secret <registry-secret> -n <namespace> -o yaml
# 3. 如果是私有镜像仓库
kubectl create secret docker-registry my-registry-secret \
--docker-server=registry.company.com \
--docker-username=user \
--docker-password=password \
--docker-email=ops@company.com \
-n <namespace>
# 在Deployment里引用
# spec.template.spec.imagePullSecrets:
# - name: my-registry-secretContainerCreating卡住
kubectl describe pod <pod-name>
# 常见原因:
# 1. 挂载的Secret或ConfigMap不存在
# Error: secret "xxx" not found
# 2. PVC挂载失败
# Warning MountVolume failed ...
# 3. 网络插件初始化失败
# Warning FailedCreatePodSandBoxTerminating卡住(删不掉的Pod)
# 强制删除(谨慎使用,数据可能丢失)
kubectl delete pod <pod-name> --force --grace-period=0
# 查看是否有finalizer阻止删除
kubectl get pod <pod-name> -o yaml | grep finalizer
# 手动移除finalizer
kubectl patch pod <pod-name> \
-p '{"metadata":{"finalizers":[]}}' \
--type=merge六、故障排查速查表
# ============================================
# 故障排查一键脚本
# ============================================
#!/bin/bash
POD=$1
NS=${2:-default}
echo "=== Pod基本信息 ==="
kubectl get pod $POD -n $NS -o wide
echo -e "\n=== Pod详细描述(关注Events) ==="
kubectl describe pod $POD -n $NS
echo -e "\n=== 当前日志(最后100行) ==="
kubectl logs $POD -n $NS --tail=100 2>/dev/null || echo "无当前日志"
echo -e "\n=== 上次崩溃日志(最后100行) ==="
kubectl logs $POD -n $NS --previous --tail=100 2>/dev/null || echo "无历史日志"
echo -e "\n=== 资源使用情况 ==="
kubectl top pod $POD -n $NS 2>/dev/null || echo "metrics-server不可用"
echo -e "\n=== 所在Node信息 ==="
NODE=$(kubectl get pod $POD -n $NS -o jsonpath='{.spec.nodeName}')
kubectl describe node $NODE | grep -A10 "Conditions:\|Allocated resources:"五、踩坑实录
坑一:CrashLoopBackOff但日志为空
有个服务在K8s上反复重启,用kubectl logs --previous看到的日志完全是空的。排查了好久,最后发现是Dockerfile里的ENTRYPOINT写错了,Java命令执行就报了No such file or directory,进程在输出任何日志之前就已经退出了。
解决思路:用describe pod看Exit Code(是2),说明是Shell层面的错误,和Java应用本身无关。把镜像拉下来,用docker run本地执行同样的ENTRYPOINT命令,立刻看到了完整的错误信息。
教训:K8s层面看不到日志时,拉镜像到本地直接docker run调试,屡试不爽。
坑二:OOMKilled但Java堆内存设置了Xmx
一个服务把-Xmx2g设置好了,但容器的memory limit只有2Gi,结果频繁OOMKilled。
Java进程的内存不只是堆。堆2GB + Metaspace(~200MB) + 线程栈(~500MB,250线程×2MB) + Direct Memory + 其他 = 实际占用远超2GB。
经验:memory limit = JVM堆 × 2 是一个粗略的安全系数,不同应用需要根据实际监控数据调整。
坑三:Pending了十分钟原来是节点都处于SchedulingDisabled
有次集群维护,把几个Node标记了cordon(不可调度)之后忘了uncordon。新部署的服务Pending,describe Pod报0 nodes are available: 5 nodes are unschedulable。
# 检查Node调度状态
kubectl get nodes
# 解除不可调度状态
kubectl uncordon <node-name>维护操作一定要有checklist,cordon之后必须有uncordon的步骤。
六、总结
K8s故障排查的核心方法可以概括为:先看状态,再看描述(尤其是Events),再看日志(记得加--previous),最后看资源和配置。
三种最常见状态的特征:CrashLoopBackOff通常是应用本身的问题,看日志找异常;OOMKilled是内存配置问题,检查JVM参数和容器Limit的比例关系;Pending是调度问题,看Events找原因,通常是资源不足、PVC、镜像、亲和性之一。
养成在部署新服务后立即检查Pod状态和Events的习惯,很多问题在萌芽阶段就能发现,比等到告警来了再排查省力得多。
