主题
K8s Work 节点上 Pod 无法启动或启动时间过长的排查手册
问题现象
某个 work 节点被调度过去的 Pod 副本出现以下一种或多种情况:
- Pod 长时间处于
Pending、ContainerCreating或CrashLoopBackOff状态 - Pod 启动时间明显变长(几分钟甚至几十分钟)
- 相同副本调度到其他节点正常,仅该节点异常
bash
kubectl get pods -o wide | grep <节点名>输出示例:
text
NAME READY STATUS RESTARTS AGE NODE
app-7d9c4b5f6-x1y2z 0/1 ContainerCreating 0 12m worker-03
app-7d9c4b5f6-a3b4c 1/1 Running 0 3m worker-01排查思路总览
按"从外到内"的顺序排查,命中率从高到低:
- Pod 事件:
kubectl describe pod最先看,80% 的问题直接有线索 - 节点状态:磁盘压力、内存压力、PID 压力、NotReady 闪断
- 镜像拉取:大镜像、私有仓库慢、拉取失败重试
- 节点存储:磁盘满、inode 耗尽、容器运行时垃圾堆积
- kubelet / 容器运行时:服务异常、CNI 插件问题
- 容器自身:探针配置、应用初始化慢
排查过程
1. 查看 Pod 事件(第一步必做)
bash
kubectl describe pod <pod-name> -n <namespace>重点关注 Events 部分,常见关键字:
| 事件关键字 | 指向问题 |
|---|---|
Failed to pull image / ImagePullBackOff | 镜像拉取问题(见第 4 步) |
Failed to create pod sandbox | CNI / 容器运行时问题(见第 6 步) |
failed to assign an IP address | CNI IP 地址耗尽或网络插件异常 |
MountVolume.SetUp failed | 存储卷挂载问题(见第 5 步) |
0/5 nodes are available / 无事件一直 Pending | 调度问题:资源不足、污点、亲和性 |
Evicted | 节点资源压力驱逐(见第 3 步) |
Unhealthy / Liveness probe failed | 探针失败(见第 7 步) |
同时查看具体卡在哪个阶段:
bash
kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.status.containerStatuses[*].state}'waiting+reason: ContainerCreating→ 卡在镜像拉取或 sandbox 创建waiting+reason: CrashLoopBackOff→ 容器反复启动失败running但READY为 0 → 探针未通过
2. 检查节点状态与资源压力
bash
kubectl describe node <节点名>重点看 Conditions 部分,以下任一为 True 都会导致调度/启动异常:
text
Conditions:
Type Status Reason
---- ------ ------
Ready True KubeletReady
MemoryPressure False KubeletHasSufficientMemory
DiskPressure True KubeletHasDiskPressure <-- 异常!
PIDPressure False KubeletHasSufficientPIDDiskPressure=True:磁盘使用率超过阈值(默认 85%),kubelet 会拒绝新 Pod 并驱逐旧 PodMemoryPressure=True:内存不足,Pod 可能被驱逐PIDPressure=True:进程数耗尽
再看 Allocated resources 确认节点资源是否已被占满(requests 总和接近容量上限会导致新 Pod 一直 Pending)。
3. 登录节点检查磁盘与系统资源
bash
# 磁盘使用率(重点关注 /var/lib/containerd 或 /var/lib/docker 所在分区)
df -h
# inode 耗尽(df -h 显示未满但写入报 No space left)
df -i
# 内存与负载
free -h
uptime
# 是否有大量僵尸进程 / PID 耗尽
ps aux | wc -l
cat /proc/sys/kernel/pid_max清理容器运行时磁盘垃圾(高危,确认后再执行):
bash
# containerd
crictl rmi --prune # 清理无用镜像
# 查看各镜像占用
crictl images --digests4. 排查镜像拉取问题
最常见的"启动变慢"原因:大镜像首次拉取、私有仓库网络慢、拉取并发受限。
bash
# 在节点上直接拉镜像测试速度
crictl pull <镜像名>
# 查看是否已有该镜像(没有则需完整拉取)
crictl images | grep <镜像名>检查要点:
镜像大小:数 GB 的镜像首次拉取必然慢,考虑镜像分层优化或预热
私有仓库连通性:
bashcurl -k https://<私有仓库地址>/v2/镜像拉取串行限制:kubelet 默认串行拉取(
--serialize-image-pulls=true),多个大镜像 Pod 同时调度到同一节点会排队。可在 kubelet 配置中关闭:yamlserializeImagePulls: false maxParallelImagePulls: 5containerd 拉取配置:检查
/etc/containerd/config.toml或 RKE2 的/etc/rancher/rke2/registries.yaml中 mirror 配置是否正确
5. 排查存储卷挂载问题
如果 Pod 卡在 ContainerCreating 且事件提示 MountVolume.SetUp failed:
bash
# 查看挂载错误详情
kubectl describe pod <pod-name> -n <namespace> | grep -A 20 Events
# 检查 PVC 状态
kubectl get pvc -n <namespace>
# 节点上检查挂载进程(NFS 场景常见卡死)
mount | grep <volume>
ls /var/lib/kubelet/pods/<pod-uid>/volumes/常见原因:
- NFS 服务端不可达或挂载点残留(需清理残留的挂载)
- CSI 插件 driver 容器异常:
kubectl get pods -n <csi-namespace> -o wide | grep <节点名> - 云盘未从旧节点卸载完成(Multi-Attach 错误)
6. 检查 kubelet 与容器运行时日志
bash
# kubelet 日志(搜索该 Pod 相关报错)
journalctl -u kubelet --since "30 min ago" | grep <pod-name>
# RKE2 agent 节点
journalctl -u rke2-agent --since "30 min ago" | grep <pod-name>
# containerd 日志
journalctl -u containerd --since "30 min ago" | grep -iE "error|fail"典型报错及含义:
text
failed to create pod sandbox: rpc error: ... failed to setup network
→ CNI 插件问题,检查该节点 CNI agent Pod 是否正常
ImageGCFailed: failed to garbage collect required amount of images
→ 镜像垃圾回收失败,通常伴随磁盘问题
failed to get imageFs info: non-existent label "crio-images"
→ 容器运行时统计异常,可能需重启 kubelet检查 CNI 插件在该节点的状态:
bash
kubectl get pods -n kube-system -o wide | grep <节点名> | grep -iE "canal|calico|flannel|cilium"必要时重启服务(会造成该节点 Pod 短暂重建网络):
bash
systemctl restart containerd
systemctl restart kubelet # 或 rke2-agent7. 排查容器自身启动慢
如果容器已 Running 但 READY 迟迟不为 1/1:
bash
# 查看容器日志
kubectl logs <pod-name> -n <namespace> --previous # 查看上一次崩溃前的日志
kubectl logs <pod-name> -n <namespace> -f
# 查看探针配置是否过严
kubectl get pod <pod-name> -n <namespace> -o yaml | grep -A 10 Probe常见原因:
JVM 类应用冷启动慢:
initialDelaySeconds配置过小导致探针过早失败重启,恶性循环启动时依赖外部服务(数据库、配置中心)连接超时
initContainer 卡住:
bashkubectl get pod <pod-name> -n <namespace> -o jsonpath='{.status.initContainerStatuses[*].state}'节点 CPU 被其他 Pod 打满:应用启动慢是资源争抢的结果,回到第 3 步检查负载
8. 对比法定位节点特异性问题
相同副本在其他节点正常、仅该节点异常时:
bash
# 将该节点标记为不可调度,观察新副本在其他节点是否正常
kubectl cordon <节点名>
# 驱逐该节点上的 Pod(会触发重建调度)
kubectl drain <节点名> --ignore-daemonsets --delete-emptydir-data若驱逐后新节点一切正常,则问题锁定在原节点本身(磁盘、运行时、CNI、内核),重点复查第 3、6 步;若问题跟随 Pod 走,则问题在应用/镜像/配置本身。
常见根因速查表
| 现象 | 最可能根因 |
|---|---|
| ContainerCreating 数分钟 | 大镜像首次拉取 / 私有仓库慢 / 串行拉取排队 |
| ContainerCreating + sandbox 报错 | CNI 插件异常 / IP 地址耗尽 |
| Pod 被 Evicted | 节点 DiskPressure / MemoryPressure |
| 拉镜像失败重试 | 私有仓库不通 / 认证过期 / 镜像 tag 不存在 |
| Running 但一直 NotReady | 探针配置不合理 / 应用依赖服务不通 / CPU 争抢 |
| MountVolume 超时 | NFS 卡死 / CSI 异常 / 云盘 Multi-Attach |
| 整节点所有 Pod 都慢 | 磁盘 IO 瓶颈 / inode 耗尽 / kubelet 异常 |
附:定位节点高 CPU 应用并迁移
当节点 CPU 争抢导致 Pod 启动慢或运行卡顿时,需要找出占用 CPU 高的具体应用并迁移。整体链路为:top 找 PID → 通过 cgroup 找容器 ID → 通过容器找 Pod。
1. 用 top 找出高 CPU 进程
bash
top
# 按 P 按 CPU 排序,按 c 显示完整命令行
# 或非交互方式直接看前 10
top -b -n 1 -o %CPU | head -20
ps aux --sort=-%cpu | head -15输出示例:
text
PID USER %CPU %MEM COMMAND
15234 root 215.0 8.2 java -jar app-server.jar记录高 CPU 进程的 PID(如 15234)。
2. 通过 PID 找到容器 ID
K8s 中每个容器进程都属于一个独立的 cgroup,从进程的 cgroup 信息可以提取容器 ID:
bash
cat /proc/15234/cgroupcontainerd 运行时输出示例:
text
0::/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod2a3b4c.slice/cri-containerd-8f3a2b1c9d4e.scopedocker 运行时(老集群)输出示例:
text
1:name=systemd:/kubepods/burstable/pod2a3b4c-5d6e-4f7a-8b9c-0d1e2f3a4b5c/8f3a2b1c9d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a其中 cri-containerd-8f3a2b1c9d4e / 末段长十六进制串即为容器 ID。老集群如果 cgroup 里看不出运行时特征,可先确认:
bash
# 查看节点使用的容器运行时
kubectl get node <节点名> -o jsonpath='{.status.nodeInfo.containerRuntimeVersion}'
# 输出 docker://20.10.x 即为 docker 运行时,containerd:// 则为 containerd也可以反向操作,按 cgroup 树直接看各容器整体占用:
bash
# 按 cgroup 聚合显示 CPU 使用,可直观看哪个容器消耗大
systemd-cgtop -b -n 1 | grep -E "kubepods|cri-containerd" | head -203. 通过容器 ID 找到 Pod
containerd 运行时:
bash
# 用容器 ID 查容器信息
crictl ps --id 8f3a2b1c9d4e
# 查看详情,可看到 Pod 名称、namespace、标签
crictl inspect 8f3a2b1c9d4e | grep -E '"podName|namespace|io.kubernetes' | head -10输出中的 io.kubernetes.pod.name、io.kubernetes.pod.namespace 即对应的 Pod。
docker 运行时(老集群,无 crictl):
bash
# 用容器 ID 查容器信息(ID 可只给前几位)
docker ps --no-trunc | grep 8f3a2b1c9d4e
# K8s 管理的容器命名格式为:k8s_<容器名>_<Pod名>_<namespace>_<uid>_<重启次数>
# 直接从 NAMES 列即可读出 Pod 名和 namespace
docker ps --format '{{.ID}} {{.Names}}' | grep 8f3a2b1c输出示例:
text
8f3a2b1c9d4e k8s_app-server_app-7d9c4b5f6-x1y2z_default_2a3b4c5d-..._0
└─容器名 └─Pod 名 └─namespace也可用 inspect 看标签:
bash
docker inspect 8f3a2b1c | grep -E 'io.kubernetes.pod.(name|namespace)'更简便的方式:跳过手动映射,直接看各容器 CPU 统计,或用 kubectl 从集群视角看:
bash
# containerd:节点上直接看容器资源占用(需 kubelet 暴露 metrics)
crictl stats -o table
# docker:等效命令,实时显示各容器 CPU/内存/网络/磁盘
docker stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}' | sort -k2 -h -r | head -20
# 集群视角看该节点上 CPU 最高的 Pod(需 metrics-server)
kubectl top pods -A --sort-by=cpu | head -20
kubectl top node <节点名>4. docker 运行时老集群的快捷定位
docker 老集群中容器名本身包含 Pod 信息,可以跳过 PID 映射,一条命令直接按 CPU 排序找出对应 Pod:
bash
# 按 CPU 使用率排序,容器名即 Pod 信息(去掉 % 号再排序更稳定)
docker stats --no-stream --format '{{.CPUPerc}}\t{{.Name}}' | tr -d '%' \
| grep 'k8s_' | sort -k1 -n -r | head -15注意排除 pause 容器(k8s_POD_... 是 Pod 沙箱,几乎不耗 CPU):
bash
docker stats --no-stream --format '{{.CPUPerc}}\t{{.Name}}' | tr -d '%' \
| grep 'k8s_' | grep -v 'k8s_POD' | sort -k1 -n -r | head -15清理磁盘等维护操作的 docker 版本对应命令:
bash
# 清理无用镜像(对应 crictl rmi --prune)
docker image prune -a
# 查看镜像占用(对应 crictl images)
docker images
# 磁盘满时 docker 日志也常见占用大户,检查容器日志大小
du -sh /var/lib/docker/containers/*/*-json.log 2>/dev/null | sort -h -r | head5. 迁移高 CPU 应用
确认目标 Pod 后,将其调度到其他节点:
bash
# 方式一:单个 Pod 直接删除,由 Deployment/StatefulSet 重建并重新调度
kubectl delete pod <pod-name> -n <namespace>
# 方式二:该节点整体压力大时,驱逐全部业务 Pod
kubectl cordon <节点名>
kubectl drain <节点名> --ignore-daemonsets --delete-emptydir-data注意事项:
- 迁移前确认其他节点有足够资源(
kubectl describe node看Allocated resources),否则新 Pod 会 Pending - 有状态应用(挂 PVC、StatefulSet)迁移前先确认存储支持在其他节点挂载
- 想避免高 CPU 应用被调度回原节点,可在 drain 前执行
kubectl cordon,恢复时用kubectl uncordon - 根治手段是为容器配置合理的
resources.requests/limits,避免单个应用打满节点 CPU - docker 运行时老集群中,kubelet 驱逐/清理机制与 containerd 一致,但排查命令需用
docker替代crictl;若集群计划升级,建议迁移到 containerd 运行时(K8s 1.24+ 已移除 dockershim)
经验总结
- 先看 Events 再登录节点:
kubectl describe pod能解决大部分问题,避免盲目操作。 - 磁盘是第一嫌疑:work 节点异常优先考虑磁盘满、inode 耗尽、镜像垃圾堆积,日常应配置磁盘监控告警和镜像清理策略。
- 串行拉取是大镜像场景的隐形杀手:多副本同时调度到同一节点时镜像排队拉取,建议关闭
serializeImagePulls并对大镜像做节点预热。 - 探针参数要匹配应用实际启动时间:Java 类慢启动应用应合理设置
initialDelaySeconds或启用startupProbe。 - 善用 cordon/drain 对比法:快速区分"节点问题"还是"应用问题",同时也是恢复服务的应急手段。