Skip to content

K8s Work 节点上 Pod 无法启动或启动时间过长的排查手册

问题现象

某个 work 节点被调度过去的 Pod 副本出现以下一种或多种情况:

  • Pod 长时间处于 PendingContainerCreatingCrashLoopBackOff 状态
  • 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

排查思路总览

按"从外到内"的顺序排查,命中率从高到低:

  1. Pod 事件kubectl describe pod 最先看,80% 的问题直接有线索
  2. 节点状态:磁盘压力、内存压力、PID 压力、NotReady 闪断
  3. 镜像拉取:大镜像、私有仓库慢、拉取失败重试
  4. 节点存储:磁盘满、inode 耗尽、容器运行时垃圾堆积
  5. kubelet / 容器运行时:服务异常、CNI 插件问题
  6. 容器自身:探针配置、应用初始化慢

排查过程

1. 查看 Pod 事件(第一步必做)

bash
kubectl describe pod <pod-name> -n <namespace>

重点关注 Events 部分,常见关键字:

事件关键字指向问题
Failed to pull image / ImagePullBackOff镜像拉取问题(见第 4 步)
Failed to create pod sandboxCNI / 容器运行时问题(见第 6 步)
failed to assign an IP addressCNI 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 → 容器反复启动失败
  • runningREADY 为 0 → 探针未通过

2. 检查节点状态与资源压力

bash
kubectl describe node <节点>

重点看 Conditions 部分,以下任一为 True 都会导致调度/启动异常:

text
Conditions:
  Type             Status  Reason
  ----             ------  ------
  Ready            True    KubeletReady
  MemoryPressure   False   KubeletHasSufficientMemory
  DiskPressure     True    KubeletHasDiskPressure    <-- 异常!
  PIDPressure      False   KubeletHasSufficientPID
  • DiskPressure=True:磁盘使用率超过阈值(默认 85%),kubelet 会拒绝新 Pod 并驱逐旧 Pod
  • MemoryPressure=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 --digests

4. 排查镜像拉取问题

最常见的"启动变慢"原因:大镜像首次拉取、私有仓库网络慢、拉取并发受限。

bash
# 在节点上直接拉镜像测试速度
crictl pull <镜像>

# 查看是否已有该镜像(没有则需完整拉取)
crictl images | grep <镜像>

检查要点:

  • 镜像大小:数 GB 的镜像首次拉取必然慢,考虑镜像分层优化或预热

  • 私有仓库连通性

    bash
    curl -k https://<私有仓库地>/v2/
  • 镜像拉取串行限制:kubelet 默认串行拉取(--serialize-image-pulls=true),多个大镜像 Pod 同时调度到同一节点会排队。可在 kubelet 配置中关闭:

    yaml
    serializeImagePulls: false
    maxParallelImagePulls: 5
  • containerd 拉取配置:检查 /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-agent

7. 排查容器自身启动慢

如果容器已 RunningREADY 迟迟不为 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 卡住

    bash
    kubectl 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/cgroup

containerd 运行时输出示例:

text
0::/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod2a3b4c.slice/cri-containerd-8f3a2b1c9d4e.scope

docker 运行时(老集群)输出示例:

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 -20

3. 通过容器 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.nameio.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 | head

5. 迁移高 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 nodeAllocated 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)

经验总结

  1. 先看 Events 再登录节点kubectl describe pod 能解决大部分问题,避免盲目操作。
  2. 磁盘是第一嫌疑:work 节点异常优先考虑磁盘满、inode 耗尽、镜像垃圾堆积,日常应配置磁盘监控告警和镜像清理策略。
  3. 串行拉取是大镜像场景的隐形杀手:多副本同时调度到同一节点时镜像排队拉取,建议关闭 serializeImagePulls 并对大镜像做节点预热。
  4. 探针参数要匹配应用实际启动时间:Java 类慢启动应用应合理设置 initialDelaySeconds 或启用 startupProbe
  5. 善用 cordon/drain 对比法:快速区分"节点问题"还是"应用问题",同时也是恢复服务的应急手段。