如果你接手过一个 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 -ioverlayfs 或 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 -20Events 最后一行是:
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 demoPod 重建后,容器顺利进入 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 failed | runc 版本不匹配/损坏 | runc --version、ctr version 比对 | 重装匹配版本 runc |
| failed to mount ... no such file or directory | hostPath 源目录不存在 | ls 对应宿主机路径 | 创建目录并设置权限 |
| permission denied / operation not permitted | SELinux 或卷权限问题 | 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 分钟内都能定位。最后再分享一个小技巧:把上面的命令清单存成一个脚本,每次接到这种工单先跑一遍,把输出存下来再开始分析,保存现场带来的安全感,等你真正用上一次就会明白。