☰
镜像已拉取却报CreateContainerError?一份Kubernetes排障命令清单
2026/10/12 2:46:32 网站建设 项目流程

如果你接手过一个 Pod 状态卡在 ContainerCreating、kubectl describe里刷出好几条CreateContainerError的老问题,你应该懂那种感觉:镜像明明已经拉下来了(连crictl images都显示它在本地),容器就是起不来,事件里反反复复就一句话。这篇我就把这几年排查CreateContainerError的经验沉淀成一份能直接抄的命令清单,帮你把“镜像已拉取却起不来”这个经典场景彻底打透。适合正在值班救火的人,也适合想系统补齐 Kubernetes 排障能力的新手。

1. 先把错误链路打穿:CreateContainerError 到底发生在哪一环

1.1 从 Pod 生命周期看这个错误的位置

在正式敲命令之前,我建议先花 30 秒理解一下这个错误在 Kubernetes 整个流程里的位置。一个 Pod 从提交到运行,大致会经过 Pending → ContainerCreating → Running 三个阶段;而CreateContainerError就藏在 ContainerCreating 这个阶段里,它不是一个独立的 Pod 状态,而是容器状态里的一个waiting.reason。

所以你在kubectl get pod里看不到“CreateContainerError”这种状态,你只会看到 Pod 一直卡在 ContainerCreating,或者像抽风一样的状态反复变换;真正明确的信息在kubectl describe pod的 Events 尾部,以及 Pod YAML 的containerStatuses[].state.waiting里。

这一步如果不清不楚,后面排查很容易南辕北辙。我见过不少人把CreateContainerError和CrashLoopBackOff混成一谈,其实这俩区别极大:CrashLoopBackOff是容器已经创建成功、甚至启动过又退出了;而CreateContainerError是容器从头到尾就没创建出来,kubelet 在调用容器运行时的创建接口时就失败了。

为了让你一眼就能分清这几个状态,我把常见“起不来”的场景列了个对照表:

事件 / 状态含义简单判断
ImagePullBackOff / ErrImagePull镜像拉取失败本地没镜像,事件里能看到拉取错误
CreateContainerError容器创建失败镜像已经在本地,但运行环境没准备好
CrashLoopBackOff容器已创建但不断重启容器状态能看到 Running / Terminated 交替
CreateContainerConfigError配置解析失败通常是 configmap、env 引用有问题
RunContainerError容器启动后运行失败更偏进程启动后的崩溃

注意最后一行RunContainerError,它和CreateContainerError在描述上非常接近,但排查方向完全不同:前者多半是运行时启动容器进程失败(进程级),后者多半卡在创建容器运行环境(环境级)。这一步判断错了,命令清单抄了也没用。

1.2 kubelet、CRI、containerd、runc 四层链路解剖

要定位CreateContainerError,必须先明白它是在哪层产生的,以及错误是怎么一步步传上来的。现在的 Kubernetes 节点上,一条创建容器的完整调用链大致是这样:

kubelet 通过 CRI(Container Runtime Interface)调用容器运行时创建容器。以默认的 containerd 运行时为例,kubelet 调的是 containerd 里的 CRI plugin;containerd 收到请求后,先把镜像挂载成本地 rootfs,然后通过内部 API 让 containerd-shim 拉起一个 OCI runtime 进程,这个进程通常就是 runc。

CreateContainerError是 kubelet 包装后的结果。底层任意一环报错,都会逐层往上传递:runc 失败 → containerd 返回错误 → CRI plugin 再包装 → kubelet 收到后写进 Pod 事件,生成一条CreateContainerError。所以你在 Pod 事件里看到的永远是最笼统的那句话,真正的细节藏在 kubelet 日志和 containerd 日志里。

这就是为什么很多人拿到CreateContainerError后觉得无从下手——因为 Pod 事件只是个“结果”,不是“原因”。想看到原因,必须顺藤摸瓜往日志和运行时状态里挖。

1.3 镜像已拉取为什么还会栽在创建环境上

很多人的第一反应是:镜像都拉下来了,怎么还报错?这里有个关键认知误区。镜像拉取成功,只代表镜像仓库的压缩包已经下载并解包到节点本地;而“创建容器”是另一套完全不同的逻辑,它要做的事包括:把镜像里的 rootfs 挂载为可写的容器文件系统、创建和挂载各种卷、配置 /dev/shm、/proc、/sys 等特殊目录、初始化 cgroup 资源限制、装配好 OCI runtime spec 再让 runc 把进程拉起来。

你可以把镜像想象成一张安装光盘,拉镜像只是把光盘拷进光驱,而CreateContainerError是告诉你说“光盘放进去读不出来”。常见问题可能出在光驱本身(runc 损坏)、光盘文件系统(镜像配置异常)、也可能出在读取环境(挂载失败、磁盘满、权限不足)。

把这一层想明白,后续排查就有了主线:镜像在本地 → 创建环境失败 → 去查节点上跟“创建环境”有关的每一环。

2. 排查工具箱:一份能直接抄的命令清单

2.1 第一板斧:kubectl describe 先把事件捞出来

所有CreateContainerError排查,我建议都从这条命令开始:

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

重点不是去看那些资源配额描述,而是末尾的 Events 部分。你要在 Events 里找到最后几条CreateContainerError,以及它们前面的 Warning 信息。有时候信息量足够,比如failed to create containerd task: failed to create shim task: OCI runtime create failed: ...,后面这串冒号后面的内容就已经指向根因了。

如果 describe 输出里事件被截断,或者你只看到一段很笼统的报错,那就直接看 Pod 的 YAML 原始状态:

kubectl get pod <pod-name> -n <namespace> -o yaml

重点看:

containerStatuses: - name: xxx state: waiting: reason: CreateContainerError message: "这里会有更详细的错误说明"

这个message字段往往比 Events 更完整,它其实是 kubelet 拿到的 CRI 错误原始文本。我把-o yaml当作第一板斧的补充手段,很多 describe 显示不全的细节都能从这里捞出来。

顺手记录一下拿到的事件时间,方便后面在日志里定位同一时刻。注意时区问题,一般用 KST/UTC 对齐,不要只看 Pod 里的时间戳。

2.2 第二板斧:kubelet 与 containerd 日志双通道

describe 拿到的是结论,日志拿到的才是过程。CreateContainerError的核心日志主要分布在两个地方:kubelet 和 containerd。

先看 kubelet,它负责调用 CRI、包装错误成事件,是离 Pod 最近的一层:

journalctl -u kubelet -f journalctl -u kubelet --since "10 min ago" --no-pager journalctl -u kubelet -n 500 --no-pager | grep -i "failed to create"

在 kubelet 日志里,你最想看到的关键句式是:

failed to create containerd task: failed to create shim task: ... failed to create container: ...

这句后面所有内容,就是 containerd 返回给它的原始错误。说实话,大部分CreateContainerError的真相都藏在这条日志里,只要能抓到这一句,根因基本已经锁定一半。

接着看 containerd 日志,这里面是更底层的错误细节:

journalctl -u containerd -f journalctl -u containerd --since "10 min ago" --no-pager journalctl -u containerd -n 500 --no-pager | grep -i "create.*task\|shim\|mount\|runc"

如果节点上 journald 没有把服务日志收到系统日志里,也可以去 /var/log/containerd.log、/var/log/pods/ 目录下找容器相关日志。既然错误发生在创建阶段,日志里大概率能看到failed to mount、permission denied、exec format error之类的字眼,这些就是根因方向。

提示:kubelet 和 containerd 的日志级别可能需要调整。必要时可以重启 kubelet 加启动参数提升日志级别,但生产环境不建议随便重启,先用--v=3或--v=4重新复现一次再采集日志更稳妥。

2.3 第三板斧:crictl / ctr / nerdctl 直接摸运行时

日志毕竟是间接的,想确认“运行时的实际状态”,绕不开手里这三件工具。先明确一个概念:crictl是 CRI 视角,ctr是 containerd 原生视角,nerdctl是命令风格更接近 docker 的 containerd 客户端。日常排障,我一律优先crictl,因为它能看到 kubelet 眼中的容器状态。

先列出节点上当前所有容器(包括已退出的):

crictl ps -a

找到目标容器 ID 后,直接从 CRI 视角看它的完整状态:

crictl inspect <container-id>

inspect输出里重点看status.state、status.reason,以及最底下有没有error字段。很多情况下,错误信息会原样出现在这里。

进一步确认镜像是否真的在本地:

crictl images crictl images | grep <你的镜像名>

如果crictl拉到的信息不够用,再用ctr直接问 containerd:

ctr -n k8s.io c list ctr -n k8s.io images list ctr -n k8s.io tasks list

注意-n k8s.io这个命名空间参数,这是 containerd 里存放 Kubernetes 容器的默认命名空间,不带它你会看到一片空。

nerdctl用得少一点,但有些场景很顺手,因为它支持类似nerdctl --namespace k8s.io ps -a的命令,输出格式更像 docker,适合已经习惯 docker 的人快速过渡。

2.4 为什么不用 docker 命令排查这个场景

这里多提醒一句:如果你的节点用的是 containerd,那 docker 命令在这个场景下基本没意义。

kubelet 通过 CRI 调用 containerd,容器是 containerd 创建并管理的;docker 管理的是 dockerd 自己的容器和镜像,两者数据隔离。很多人在节点上顺手敲了一个docker ps -a,发现啥都没有,就误以为容器不存在,白白浪费时间。

只有在确认节点运行时是 docker 时,才用docker ps -a和docker logs。判断方法很简单:

crictl info | grep runtime

或者kubectl get node <节点名> -o yaml | grep -i runtime。现在主流节点基本上全是 containerd,为了效率,遇到CreateContainerError直接默认走 containerd 的三件套,别在 docker 上绕弯。

3. 根因拆解:从表象到本质的定位路径

3.1 runC 异常:最常见的“沉默凶手”

先讲讲我最常遇到的一类:runC 本身出问题。runC 是 containerd 默认用的 OCI runtime,负责真正去创建和运行容器进程。如果 runc 二进制损坏、版本和 containerd 不匹配、或者容器运行时配置指向了错误的 runtime,就会出现一种“镜像正常、容器创建失败”的尴尬局面。

这类问题的典型症状是 describe 或 kubelet 日志里出现:

OCI runtime create failed: runc create failed: unable to start container process: ...

也可能是:

runc: symbol lookup error: runc: undefined symbol: ...

我遇到过最典型的一次,节点在做完二进制升级后,runc 和 containerd 版本出现错位,containerd 还在用旧版的调用方式,runc 已经换了版本,导致每次创建容器都失败,而镜像拉取完全正常,表面看起来就是标准的CreateContainerError。

定位方法并不复杂:

crictl info | grep -A 5 runtime runc --version ctr version

比对ctr version里列出的 runc 版本和系统实际runc --version的版本,不一致就大概率是这个问题。另外可以检查一下 /run/containerd/runc/ 目录下的容器状态文件:

ls -la /run/containerd/runc/ cat /run/containerd/runc/<容器ID>/state.json | head

处理方式一般就是重新安装与 containerd 匹配的 runc,或者改 containerd 配置里的 runtime 类型。修复完再手工复现一次,确认不再报错。

3.2 挂载与文件权限:镜像没问题,环境不给面子

第二个高频根因是挂载和权限。这类问题最容易让人心态崩,因为镜像本身完全是好的,错误纯粹来自宿主机环境。

举个实际例子:Pod 里挂了一个 hostPath 类型的卷,路径指向宿主机上某个目录。如果这个目录不存在,或者目录权限不对,容器创建时的挂载动作就会失败。kubelet 日志大概率会出现:

failed to mount "/var/lib/kubelet/pods/xxx/volumes/kubernetes.io~hostpath/xxx" ... no such file or directory permission denied

另外一类是 SELinux 拦路。很多发行版默认开了 SELinux,宿主机的挂载卷目录上下文不对时,明明权限是 0755 或 0777,容器就是挂不上去,日志里也是“permission denied”这种极难区分的错误。

排查命令要成组使用:

findmnt | grep kubelet ls -la /var/lib/kubelet/pods/<pod-uid>/volumes/ getenforce dmesg | grep -i selinux ausearch -m avc -ts recent

解决路径通常是:先把 hostPath 源目录补齐、检查目录属主属组;SELinux 的问题可以通过给 Pod 加合适的安全上下文,或者在节点上调整目录的 SELinux 标签。如果确实是环境比较特殊,临时用securityContext加privileged验证能跑,再逐步细化权限,也是一种常用的缩小范围手段。

3.3 镜像与应用配置:别再怀疑墙,先看 command

第三种根因在镜像本身,但这里的“错误”不在镜像拉取环节,而在镜像配置的启动命令或入口文件上。

常见报错是:

exec: "xxx": executable file not found in $PATH exec format error

解释起来就是:镜像里配置的ENTRYPOINT或CMD里指定的命令不存在;或者镜像架构和节点架构不匹配——比如你拉了个 amd64 的镜像跑到 arm64 的节点上,启动时就会报 exec format error。

还有一种比较隐蔽的情况:镜像里做的是多阶段构建,最终镜像里根本没有那个二进制文件,但镜像的 ENTRYPOINT 还保留着旧路径。容器创建时,runc 去尝试执行那个路径,结果找不到,只能抛CreateContainerError回来。

这个方向怎么查?直接用 crictl 看容器的配置:

crictl inspect <container-id> | grep -A 10 "command\|entrypoint"

或者用 ctr 拉出来看:

ctr -n k8s.io image inspect <镜像名>:<tag>

检查镜像的entrypoint、cmd、working_dir、user是否合理。处理办法很简单:在 Deployment / Pod 的 YAML 里显式覆盖command和args,或者重建镜像修正 ENTRYPOINT。确认架构问题就用crictl images -v看镜像架构,重拉对应平台的镜像。

3.4 节点资源与存储驱动:被忽略的底层

最后一类根因,往往藏得更深,属于“底层环境”问题。我把它归成三个子类:磁盘空间、inode 耗尽、cgroup 和快照器不匹配。

磁盘满带来的报错非常典型:

failed to create task for container: ... no space left on device

但这里有个非常容易踩的坑:df -h看根分区可能还有空间,但df -i一看 inode 已经 100% 了。节点上堆积了大量小文件、临时容器、日志切片,inode 耗尽后,任何新文件都创建不了,容器自然起不来。检查命令:

df -h df -i

overlayfs 或 snapshotter 的问题也比较典型。containerd 默认用 overlayfs 作为快照器,如果节点的内核或文件系统不支持某些 overlay 特性,就可能出现:

failed to mount overlay: operation not permitted

排查命令:

mount | grep overlay cat /proc/filesystems | grep overlay crictl info | grep snapshotter

而 cgroup 的问题在 systemd 托管节点的场景里尤其常见。kubelet 和 containerd 的 cgroup 驱动如果不一致——一个 systemd 一个 cgroupfs——轻则报错重则整个节点资源管理错乱。检查方式:

cat /etc/containerd/config.toml | grep -A 5 cgroup ps aux | grep kubelet | grep -o 'cgroup-driver=[a-z]*'

这些底层问题解决起来往往不复杂:清理多余镜像和日志、调整 snapshotter 配置、统一 cgroup 驱动,但要命的是它们在表面上看全是同一种CreateContainerError,不往底层挖根本发现不了。

4. 实操复盘:一次镜像已拉取、容器死活起不来的完整定位

4.1 六步定位法(先给框架)

前面把原理和命令都铺开了,这一节我用一个虚构案例把整个排查流程串起来。我习惯把这套流程固定成六步,值班时候照着走基本不会乱:

第一步:kubectl get / describe 确认现象 第二步:journalctl 查 kubelet 日志,找 failed to create 关键句 第三步:journalctl 查 containerd 日志,锁定底层错误方向 第四步:crictl / ctr 核对容器与镜像的运行时状态 第五步:按错误主线深入节点底层(mount、runc、cgroup、磁盘) 第六步:用 ctr run --rm 最小化复现,再修复并验证

这套流程的核心原则很简单:从最外层的事件,一层一层往底层剥,每层都用命令去验证,而不是靠猜。很多人排障慢,就是卡在第三步之后不知道该干什么,或者直接从第一步跳到猜想。

4.2 从日志到根因:一条真实链路的拆解过程

举个例子。某次同事说有一个服务 Pod 一直 ContainerCreating,让我过去看。我先执行了第一步:

kubectl get pod web-xxxx -n demo

状态确实一直 ContainerCreating。然后 describe:

kubectl describe pod web-xxxx -n demo | tail -20

Events 最后一行是:

CreateContainerError: failed to create containerd task: failed to create shim task: OCI runtime create failed: unable to start container process: error during container init: ...

其实到这里还看不出太多,因为后面被截断了。我没有停在 describe 上,直接进节点查 kubelet 日志:

journalctl -u kubelet -n 200 --no-pager | grep -i "web-xxxx" | tail -30

抓到了一句完整的关键日志:

failed to create containerd task: failed to create shim task: OCI runtime create failed: runc create failed: unable to create new container: [error] creating container mount rootfs: mount /var/lib/kubelet/pods/<pod-uid>/volumes/kubernetes.io~hostpath/config -> /var/lib/containerd/... : no such file or directory

到这里,方向已经非常明确了:挂载 hostPath 卷时找不到宿主机的源目录。于是我再执行第四步和第五步,上节点确认这个路径是否存在:

ls -la /var/lib/kubelet/pods/<pod-uid>/volumes/kubernetes.io~hostpath/config

结果是最外层目录存在,里面的 config 子目录不存在,因为当时有人只创建了父目录,部署的 ConfigMap 却挂在更深的子路径上。修复只需要在宿主机补齐目录并给足权限,然后:

kubectl delete pod web-xxxx -n demo

Pod 重建后,容器顺利进入 Running。整个过程从接手到修复,十分钟左右。

4.3 最小化复现与修复验证

挂载这类环境问题,最怕的不是找根因,而是修完不知道有没有修彻底。所以我一般会多做一步“最小化复现”,确保问题确实在主因上,而不是碰巧好起来。

最小化复现的命令是直接用 ctr 拉镜像、起临时容器:

ctr -n k8s.io run --rm --mount type=bind,src=/var/lib/kubelet/pods/<pod-uid>/volumes/kubernetes.io~hostpath/config,dst=/config docker.io/library/nginx:latest test-container

如果这条命令能在宿主机层面复现同样的错误,说明根因就在文件系统或挂载路径;如果它报的错和 Pod 里的事件一模一样,那修复方向就稳了。修完后再跑一遍这条命令,不报错就说明环境层已经 OK,然后再重建 Pod 做最终验证。

这个习惯帮我过滤掉了很多“看似修好、实际仍坏”的情况。特别是 hostPath 权限、SELinux 这类问题,如果不做最小化复现,经常会出现 Pod 重建后换了调度节点,问题暂时消失,过几天又随机复现的假象。

5. 常见问题速查与避坑经验实录

5.1 常见问题速查表

把我在实际排查中高频遇到的CreateContainerError场景整理成一张速查表,适合贴在笔记本边上应急用:

表象 / 日志关键词可能根因快速定位命令处理建议
exec: "xxx": executable file not found镜像 command 路径错误或镜像损坏crictl inspect 查看 command/entrypoint修正 YAML command/args 或重建镜像
exec format error镜像架构和节点不一致crictl images -v 看架构改镜像 tag 到匹配架构
OCI runtime create failed: runc create failedrunc 版本不匹配/损坏runc --version、ctr version 比对重装匹配版本 runc
failed to mount ... no such file or directoryhostPath 源目录不存在ls 对应宿主机路径创建目录并设置权限
permission denied / operation not permittedSELinux 或卷权限问题getenforce、ausearch -m avc调整 SELinux 标签或安全上下文
no space left on device磁盘满或 inode 耗尽df -h、df -i清理镜像、日志、临时文件
failed to mount overlay快照器/内核 overlay 异常mount、cat /proc/filesystems调整 containerd snapshotter
cgroup 相关报错kubelet 与 containerd 驱动不一致比对 cgroup-driver 配置统一为 systemd 或 cgroupfs

这张表不是万能的,但足够覆盖我遇到过的九成情况。使用的时候注意顺序,先根据 kubelet 日志里的关键词匹配表格,匹配不到再往日志更深处挖。

5.2 实操中我踩过的三个坑

第一个坑:只盯着 describe 看,浪费了大量时间。describe 里的 Events 是被截断和包装过的人类可读信息,与其盯着那几行反复看,不如直接kubectl get pod -o yaml看containerStatuses[].state.waiting.message,或者直接上节点查 kubelet 日志。信息量完全不是一个量级。

第二个坑:一看到 ContainerCreating 就怀疑网络插件或 CNI。其实网络类问题通常会报Failed to create pod sandbox、CreateContainerSandboxError,和CreateContainerError是两码事。如果事件里明确写的是 CreateContainer,基本不用先碰 CNI,优先查运行时创建链路更高效。

第三个坑:盲目重启节点或 kubelet。有一次我手贱重启了 containerd,然后 Pod 全部被重建,现场日志全被刷掉,原本几分钟能定位的问题变成从头再来。正确的姿势是先保存现场:把 describe 输出、kubelet 日志、containerd 日志全部拷贝到本地或对象存储,再动手做任何重启操作。重启是最后的兜底手段,不是排查手段。

注意:碰到 DaemonSet 或调度在多个节点上的工作负载,不要只盯着一个节点查,要每个节点都确认 kubelet 日志。同一个CreateContainerError,在十个节点上可能对应六种不同的根因,这个细节很容易被忽略。

我个人在实际操作中最深的体会是:CreateContainerError不是什么玄学,它就是 kubelet 把底层的真实错误抛到你面前的一个窗口。镜像拉取成功只是第一关,rootfs 挂载、卷权限、runc 状态、cgroup 配置,任何一环出问题都会在这里暴露。只要你按着事件 → kubelet 日志 → containerd 日志 → 运行时状态 → 节点底层这条线一层层剥,绝大多数问题 10 分钟内都能定位。最后再分享一个小技巧:把上面的命令清单存成一个脚本,每次接到这种工单先跑一遍,把输出存下来再开始分析,保存现场带来的安全感,等你真正用上一次就会明白。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询