1. CreateContainerError 不是“容器启动失败”:先把它卡住的阶段看清楚
有次值班,某团队反馈一个业务 Pod 卡在 ContainerCreating 半天了。我上集群第一条 kubectl describe pod,ContainerStatuses 里写着 Reason: CreateContainerError,可往下翻 Events,明明有一行 Pulled,内容是 Container image "busybox:1.36" already present on machine。现场的人很困惑:镜像都拉下来了,为什么容器还起不来?
这里先纠正一个常见的理解偏差:CreateContainerError 这个状态名非常容易让人误以为“容器没启动成功”。实际上它发生在容器真正启动之前。Kubelet 走到的位置是“创建容器对象”这一步,不是“运行容器进程”这一步。拉镜像和创建容器是两个完全独立的阶段,镜像已经出现在节点上,只代表 PullImage 成功了,容器运行时还没有把镜像实例化成可以执行的容器任务。
要理解这个错误,脑子里必须有一张完整的容器生命周期图:
- 调度成功后,Kubelet 告诉容器运行时创建 Pod 沙箱(Sandbox),也就是 pause 容器,专门负责持有一组 namespace。
- Kubelet 从镜像仓库拉取镜像,这一步失败通常表现为
ErrImagePull或ImagePullBackOff。 - Kubelet 调用容器运行时的 CRI 接口创建容器,这一步需要把镜像的 OCI 配置、Pod 里声明的环境变量、挂载卷、资源限制、安全配置全部拼装成一个完整的运行参数,并发给底层的 OCI runtime(一般是 runc)去创建。
- 容器进程真正启动由运行时内部继续完成。
CreateContainerError 就发生在第 3 步。容器运行时要拿着镜像元数据和容器参数做一大堆校验和准备工作:解析命令、准备 rootfs、合入 volume、建立 cgroup、下发 seccomp 规则等等。任何一环出错,Kubelet 拿到的都是“创建容器失败”的错,于是把状态标记成 CreateContainerError。
这也是为什么“镜像明明在节点上”这个信息,在这类问题里几乎不能说明任何事。镜像只是静态的只读层集合,容器运行时要把它变成动态进程环境,中间还隔着 OCI 配置校验、挂载点检查、权限设置、资源子系统接入。真正决定成败的不是镜像有没有,而是镜像元数据加 Pod 配置是不是能组装成一个合法的容器任务。
顺便把三个容易混淆的错误放在一起看:
| 状态 / Reason | 发生的阶段 | 典型含义 |
|---|---|---|
ErrImagePull / ImagePullBackOff |
镜像拉取阶段 | 镜像仓库访问不到、标签不存在、凭据错误 |
CreateContainerError |
容器创建阶段 | 镜像已就绪,但运行时无法基于当前配置创建容器任务 |
CrashLoopBackOff |
容器运行阶段 | 容器已经创建并启动过,只是进程一跑起来就退出 |
看明白这张表,你至少不会再犯“CreateContainerError 就去清镜像缓存”这种错。镜像拉取和容器创建是两堵墙,推倒哪堵,得先看清楚自己撞在哪堵上。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先别急着上节点:三条 kubectl 命令把现场固定住
进入节点收集日志之前,先把控制面上的现场信息固定下来。很多人一看到 CreateContainerError 就慌着登节点翻 journal,结果 Pod 被控制器重建,老的 container ID 全没了,线索断掉。我习惯先做三件事。
2.1 看 ContainerStatuses,不要被 Events 带偏
kubectl describe pod 的输出很长,先把目光放到 ContainerStatuses 这一段:
code复制Containers:
web:
Container ID:
Image: busybox:1.36
Image ID:
Port: <none>
Host Port: <none>
State: Waiting
Reason: CreateContainerError
Ready: False
注意,这一段里 Container ID 是空的。如果容器已经创建出来,这里会有一段 containerd://... 的 ID。CreateContainerError 通常意味着容器对象没有成型,所以没有 ID 可查,后面 crictl ps -a 也不一定能直接看到目标容器。
Events 里则会看到类似这样的排列:
code复制Normal Scheduled 2m default-scheduler Successfully assigned ...
Normal Pulling 2m kubelet Pulling image "busybox:1.36"
Normal Pulled 2m kubelet Container image "busybox:1.36" already present on machine
Warning Failed 20s kubelet failed to create containerd task: ...
有一种误导很常见:看到 Pulled 就认为一切正常,看到后面的 Failed 又觉得太抽象。其实 Pulled 只证明镜像拉取环节通过,真正的错误串在 Failed 那条 Event 的 Message 里,有时 Message 会截断,这时就要靠后面提到的节点日志补充。
2.2 get pod -o yaml:拿到运行时收到的“完整构造参数”
容器能不能创建成功,本质上取决于 Kubelet 拼装出来的这个容器 spec 是否合法。所以要把 Pod 的完整 YAML 存下来,重点看这几个字段:
spec.containers[].command和args:命令行是否为空;spec.containers[].image:镜像是否多架构;spec.containers[].volumeMounts:挂载目标路径是否合理;spec.containers[].securityContext:是否引用了 seccomp localhost profile;spec.containers[].resources:是否存在明显不合理的资源请求;spec.nodeName:Pod 被调度到了哪台节点,后面找容器要靠它。
bash复制kubectl -n <namespace> get pod <pod-name> -o yaml > pod.yaml
拿到这个文件之后,先不要瞪着它发呆,直接在文件里搜 command、entrypoint、seccomp、volumeMounts 这些关键词。很多根因其实一眼就能从 YAML 里看出来,比如 command 和 args 根本没有配置,比如 seccomp 里挂了个 Localhost profile。
如果 Pod 是 Deployment 管理的,建议连 Deployment 的 YAML 也一起拉出来。有些问题是因为控制器里的 template 缺字段导致的,只看运行中的 Pod YAML 也能发现问题,但多一份原始声明可以方便对比。
2.3 get events 按时间排序:把“先拉后建”的过程串起来
kubectl describe pod 里的 Events 虽然能看,但默认展示不全,事件时间顺序也容易被截断。更稳妥的方式是单独拉事件:
bash复制kubectl -n <namespace> get events --sort-by=.lastTimestamp --field-selector involvedObject.name=<pod-name>
这个命令会把和该 Pod 相关的所有事件按时间排好序。从 Scheduled 到 Pulled,再到某个 Failed,整个执行链路一目了然。如果 Failed 事件的 Message 太短,配合时间点再去节点日志里搜对应时间段,效率会高很多。
还有一个细节:Pod 如果已经处于反复重建状态,事件里可能会夹杂旧 Pod 的日志,先根据 involvedObject.name 后面跟着的 Pod 名后缀,或者根据 lastTimestamp 排除旧轮次的信息。这也是为什么我一直强调先存现场文件:重建之后,老的 POD IP、容器 ID、事件都会慢慢被挤掉。
3. 节点端命令清单:从 Kubelet 到 CRI 再到 OCI 逐层下潜
控制面信息看完了,如果还不能定位,就必须登节点。这时的目标是从 Kubelet 开始,沿着 CRI 调用链路往下钻,直到拿到那段真正的错误串。
3.1 先确认 CRI 客户端和运行时是谁
现在大多数集群已经是 containerd,但偶尔还会遇到 CRI-O,少数老环境可能还在用 docker + cri-dockerd。crictl 是连接 CRI 接口的标准客户端,先确认它能用:
bash复制crictl version
crictl info
crictl info 会输出一大段 JSON,里面有 runtimeName、cgroupDriver、snapshotter 这些信息。如果节点上装的是 docker,你习惯的 docker ps 看到的容器和 crictl ps 看到的是两套视角,排障时必须用与运行时匹配的客户端,否则会出现“明明有容器,crictl 却看不到”的困惑。
如果 crictl 不存在,检查这套环境之前是不是用 CLI 配置过的。也可以在容器运行时组件自带路径下找,比如 containerd 部署环境下有时会有单独的工具链。
3.2 crictl ps -a 与 crictl inspect:容器对象的状态与入参
拿到 Pod 所在节点后,用容器名或者镜像名过滤:
bash复制crictl ps -a | grep <pod-name或镜像名>
如果看到容器状态是 ContainerState_CONTAINER_CREATED,说明对象创建了一半,还差进程启动;如果根本没有任何输出,说明创建容器对象这一步直接失败了,错误已经回到 Kubelet。
还能找到容器的话,就用 crictl inspect 把它的详细配置拉出来:
bash复制crictl inspect -o yaml <container-id>
重点看三块:
state:是 CREATED 还是 CONTAINER_EXITED;image:运行时实际解析出的镜像 ID;mounts:每个挂载的 source 和 destination;env:环境变量是否带进了容器配置。
crictl inspect 输出的就是 Kubelet 传给 CRI 的请求落到运行时后的结果。这里面的字段和 Pod YAML 中声明的不一致时,往往是 Kubelet 和运行时之间某种转换出了问题。
3.3 crictl inspecti 与 crictl info:镜像元数据和运行时参数
有些错误表面上看是“容器起不来”,实质是“镜像在目标节点上跑不了”。这一步专门检查镜像本身:
bash复制crictl inspecti <image-id或镜像名>
看输出里的 image 字段,尤其是 architecture 和 os。如果业务容器镜像的 architecture 是 arm64,而节点 uname -m 输出是 x86_64,后面基本可以预见会出现 exec format error。
运行时参数也可以用 crictl info 提取关键配置:
bash复制crictl info | grep -i -E "cgroupDriver|snapshotter|runtimeName"
这些信息在排查 cgroup 驱动不一致、存储驱动异常时非常关键。
3.4 journalctl 抓 Kubelet 与 containerd 的原始错误
Kubernetes 的 Event Message 往往是经过包装的,真正的根因要往 kubelet 日志和容器运行时日志里找:
bash复制journalctl -u kubelet --since "-60m" | grep -i -E "CreateContainer|failed to create container" -C 10
journalctl -u containerd --since "-60m" | grep -i -E "failed to create|error" -C 10
如果 Kubelet 日志里出现类似:
code复制failed to create containerd task: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: "no command specified": unknown
这就说明错误已经沿着 Kubelet → CRI → containerd → runc 一路传到最底层了。这种情况下,重点看 containerd 日志里同一时间窗口的完整报错,runc 的错误串通常是整个链路里信息量最大的那一段。
journalctl 不太顺手的时候,也可以直接看 containerd 的重定向日志,比如 /var/log/containerd/containerd.log 或者 systemd 单元里的标准输出,但大部分发行版 journalctl 已经足够。记着给 --since 一个时间范围,避免对着上千万行日志大海捞针。
3.5 一条命令打包现场素材
我个人的习惯是,任何一次“看着像创建失败”的排障,先跑一次打包命令,把现场完整保留下来。下面这个片段可以按需调整:
bash复制TS=$(date +%s)
DIR=/tmp/k8s-debug-$TS
mkdir -p $DIR
kubectl -n <namespace> get pod <pod-name> -o yaml > $DIR/pod.yaml
kubectl -n <namespace> describe pod <pod-name> > $DIR/describe.txt
kubectl -n <namespace> get events --sort-by=.lastTimestamp --field-selector involvedObject.name=<pod-name> > $DIR/events.txt
# 以下在节点上执行
crictl ps -a > $DIR/crictl-ps.txt
crictl inspect -o yaml <container-id> > $DIR/inspect.yaml 2>/dev/null
journalctl -u kubelet --since "-60m" > $DIR/kubelet.log
journalctl -u containerd --since "-60m" > $DIR/containerd.log
这个包在 Pod 被重建之前留存,价值最高。因为 crictl ps -a 里能看到容器的 ID 和状态,一旦 Pod 被删掉,这些 CRI 层的中间对象很快会被清理,到时候再想回溯就难了。很多人排这类问题排到一半发现 Pod 已经重启好几轮,就是因为没在第一时间把现场固定住。
4. 七类高频根因,每一类都有对应的错误串和修复动作
拿到完整错误串之后,别慌。绝大多数 CreateContainerError 都能归到下面七类里。每一类都有它特有的关键词和直接的修复路径。
4.1 “no command specified”:镜像里没有入口,谁也不负责执行
这是最常见的根因之一。错误串通常长这样:
code复制OCI runtime create failed: runc create failed: unable to start container process: exec: "no command specified": unknown
原因其实很简单:容器创建时需要一个进程启动入口。Kubernetes 优先取 spec.containers[].command 和 args,如果都没配置,就依赖镜像 OCI 配置里的 Entrypoint 和 Cmd。当两者都为空时,runc 根本没有可执行的进程,只能报 “no command specified”。
容易踩进这个坑的场景包括:
- 基础镜像只把二进制文件 COPY 进去,没有写
ENTRYPOINT; - Pod YAML 只写了 image,command 和 args 留空;
- 使用了第三方精简镜像,镜像本身没有默认入口。
定位方法很直接:kubectl get pod -o yaml 里搜 command,如果没有,再用 crictl inspecti <image-id> 看镜像的 OCI 配置,确认 entrypoint 和 cmd 是否为空。
修复上,最稳妥的做法是在 Pod 里显式声明:
yaml复制containers:
- name: app
image: myapp:1.0
command: ["/bin/myapp"]
args: ["--config=/etc/app.yaml"]
如果这个镜像要给多个团队复用,走一次镜像改造,在 Dockerfile 里补上 ENTRYPOINT 更治本。镜像的发布规范里必须有一条:“非基础镜像必须显式声明 ENTRYPOINT 或 CMD,并给出可执行的验证方式。”
4.2 “exec format error”:镜像架构和节点架构不是一家人
错误串里的关键词是:
code复制OCI runtime create failed: runc create failed: unable to start container process: exec: "exec format error": unknown
“exec format error”不是文件缺失,也不是没有权限,而是内核不认这个可执行文件的格式。最常见的场景就是镜像架构和节点 CPU 架构不一致。比如 x86 节点的镜像仓库里拉到了 arm64 的镜像,或者反过来。
排查方式分两步:先在节点上看架构,再查镜像实际解析出的架构。
bash复制uname -m
crictl inspecti <image-id> | grep -E "architecture|os"
如果 uname -m 显示 x86_64,镜像 architecture 是 arm64,那基本可以拍板了。这类问题最容易出现在从某个第三方仓库拷贝镜像、或者镜像作者只发布了单架构版本的情况下。官方多架构镜像通常能正确拉取对应版本,但如果你通过 digest 拉取时锁定了单一架构 digest,就会中招。
修复分几种:
- 优先换用支持多架构的镜像仓库和 tag;
- 如果镜像必须用单架构,确保集群内所有节点的 CPU 平台一致;
- 在 CI 构建时考虑用 manifest list 发布多架构版本。
还有一个细节:有些边缘设备是 amd64 之外的特殊平台,比如 arm64 的机器跑了 arm/v7 的镜像,也会报 exec format error。所以排查时不能只看节点是“arm”还是“x86”,还要核对具体版本。
4.3 “not a directory”:volumeMounts 把容器里的“文件”当“目录”挂
这一类问题相对隐蔽,错误串通常和 mount 有关:
code复制OCI runtime create failed: runc create failed: unable to start container process: error during container init: mount /usr/bin/data: ... not a directory
容器创建时,运行时需要把 volume 挂载到容器内的 mountPath。如果 mountPath 所指向的路径在镜像里是一个普通文件,而不是目录,runc 去创建挂载点时就会报 “not a directory”。
为什么会有这种情况?镜像里很多常见路径都是文件,比如 /usr/bin/python、/etc/nginx/nginx.conf。如果 Pod 里写的是:
yaml复制volumeMounts:
- name: data
mountPath: /usr/bin/data
而镜像层里 /usr/bin/data 是一个文件,而不是目录,创建容器时就会因为挂载点类型冲突失败。
定位方法可以直接做手术式验证:把挂载先去掉,用一个最小化的 Pod 尝试启动同样的镜像。如果去掉挂载就能起来,问题基本就锁定在挂载配置或镜像路径上。再往下,可以用 crictl inspect -o yaml 查看运行时尝试的 mounts 字段,确认 source 和 destination。
修复通常是把 mountPath 换成一个镜像里明确是目录的路径,或者调整镜像里那个文件的位置,不要让它和挂载点冲突。这个案例也提醒我们,写 deploy 文件时尽量避开容器内相对重要的文件路径。
4.4 “failed to load seccomp profile”:自定义 seccomp 文件“不翼而飞”
涉及到安全配置的创建失败,错误串里通常带着 seccomp 关键词:
code复制failed to create containerd task: failed to create shim task: OCI runtime create failed: ... failed to load seccomp profile: open /var/lib/kubelet/seccomp/profiles/deny-all.json: no such file or directory
这种情况常见于 Pod 里配置了自定义 seccomp profile:
yaml复制securityContext:
seccompProfile:
type: Localhost
localhostProfile: profiles/deny-all.json
Kubelet 默认从节点上的 /var/lib/kubelet/seccomp 目录读取 localhost profile。问题在于:这个 profile 文件必须存在于 Pod 实际被调度到的那台节点上。如果你只在某台机器上放了文件,Pod 却被调度到另一台机器,容器创建就会失败。集群如果做了自动伸缩,新节点没同步 profile 文件,也会出现同样的问题。
定位方法:
bash复制ls -l /var/lib/kubelet/seccomp/profiles/
如果文件确实缺失,要么把它分发到所有节点,要么把 seccomp 策略改成 RuntimeDefault,让运行时使用默认的安全配置。很多业务场景其实不需要 custom profile,RuntimeDefault 已经足够安全。非要自定义的话,一定要把它纳入节点初始化脚本或者集群配置管理,而不是手动在单台节点上创建。
4.5 “sandbox not found”:CRI 里的沙箱对象失联
这类错误通常长这样:
code复制rpc error: code = NotFound desc = sandbox "abcdef123456" not found
Pod 的沙箱(sandbox)和业务容器是两套 CRI 对象。正常情况下,Kubelet 先确保沙箱存在,然后创建业务容器并挂到沙箱里。如果沙箱被意外删除,或者 containerd 重启后状态丢失,而 Kubelet 的期望状态还拿着旧的 sandbox ID 去创建容器,就会报 “not found”。
哪些操作容易把沙箱搞丢?
- 人工对 containerd 里的 CRI 对象做
crictl stopp、crictl rm,只清理了沙箱,没有清理关联业务容器; - 节点重启后 containerd 的持久化状态异常;
- 存储或运行时升级过程中 CRI 元数据不完整。
定位时先看本节点的沙箱列表:
bash复制crictl ps -a | grep <pod-name>
crictl inspect -o yaml <container-id> | grep podSandboxId
如果发现容器引用的 podSandboxId 在沙箱列表里根本不存在,说明两者已经失联。
修复方式不是手动去补一个沙箱,而是让控制器重新调和:
bash复制kubectl -n <namespace> delete pod <pod-name>
Pod 被重建后,Kubelet 会通过控制器重新走一遍完整的创建流程。这里要特别提醒:不要手动去 containerd 里乱删对象来“修”,这种操作很容易把还在正常工作的其他 Pod 一起搞坏。最安全的做法永远是删 Pod,让上层控制器按声明状态重建。
4.6 “no space left on device”或 snapshotter 异常:镜像在但 rootfs 造不出来
这类情况很有意思——crictl images 能看到镜像,但容器创建时就是失败。错误串里可能有:
code复制failed to create containerd task: failed to create shim task: ... no space left on device
或:
code复制failed to mount overlay: ... invalid argument
容器创建阶段需要做镜像快照,也就是把只读镜像层堆叠成可写 rootfs。这个动作依赖节点磁盘空间、inode 数量以及存储驱动的可用性。即使镜像数据已经下载到本地,如果节点磁盘或者 inode 耗尽,快照依然创建不出来。
排查时第一时间看:
bash复制df -h /var/lib/containerd
df -i /var/lib/containerd
特别要注意 inode 耗尽的情况。df -h 看着还有几个 GB,但 df -i 已经 100%,同样会报 “no space left”。这种问题往往是老镜像和停止的容器垃圾太多导致的。
如果错误串里出现 overlay 或 d_type,还要检查存储驱动是否受底层文件系统限制。比如某些网络文件系统不支持 overlayfs 需要的 d_type 特性,需要切换存储驱动。
修复路径一般是:
- 清理停止容器和废弃镜像:
crictl rmi --prune或 containerd 的垃圾回收; - 检查快照目录挂载参数和存储驱动配置;
- 对确实长期空间紧张的业务节点,扩容磁盘或者调整日志清理策略。
最重要的是,这类问题不能用“重启大法”解决。节点重启后 containerd 会花更长时间恢复,Pod 重建过程依然会卡在快照创建环节,问题只会更明显。
4.7 cgroup 驱动不一致:kubelet 说 systemd,运行时却用 cgroupfs
这类问题在集群升级、迁移或新装环境时出现频率很高。错误串通常包含 cgroup 相关路径:
code复制failed to create containerd task: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: ... failed to write ... cgroup ... no such file or directory
Kubernetes 的 kubelet 和容器运行时需要就 cgroup 驱动保持一致,业界主流是用 systemd。如果 kubelet 的 cgroupDriver 是 systemd,而 containerd 的 SystemdCgroup 没有开启或者配置相反,运行时创建 cgroup 时就会因为路径不对而失败。
检查方法:
bash复制# kubelet 视角
kubectl -n kube-system get cm -o yaml kubelet-config | grep -i cgroupDriver
# containerd 视角
containerd config default | grep -i SystemdCgroup
crictl info | grep -i cgroupDriver
修复就是要让两边对齐,通常统一为 systemd:
toml复制[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
SystemdCgroup = true
改完 containerd 配置记得重启服务:
bash复制systemctl restart containerd
再强调一次:很多升级场景里,kubelet 配置被新版本改成了 systemd,但 containerd 的配置还是老的 cgroupfs,于是一升级就大范围报 CreateContainerError。遇到“升级后集中爆发”的 CreateContainerError,第一优先级就查 cgroup driver 是否对齐。
5. 15 分钟排障路径与一套不用动脑的错误串速查表
说到这,你已经看到了七类根因。实际操作中,排障效率最大的瓶颈是“拿到错误串后还要想半天它属于哪层”。下面这张速查表可以节省很多犹豫时间。
| 错误串关键词 | 所属层 | 直接原因 | 首选排查动作 |
|---|---|---|---|
no command specified |
OCI 进程配置 | 镜像无可执行入口且 Pod 未声明 | 检查 command / 镜像 Entrypoint |
exec format error |
OCI 执行 | 架构或可执行格式不匹配 | uname -m + crictl inspecti |
not a directory |
挂载 | mountPath 在容器内是文件 | 去掉挂载二分验证 |
seccomp |
安全配置 | localhost profile 文件缺失 | ls /var/lib/kubelet/seccomp |
sandbox ... not found |
CRI 对象关系 | 沙箱丢失或元数据损坏 | crictl ps -a 对比 sandbox ID |
no space / snapshotter / overlay |
存储层 | 磁盘/inode 耗尽或驱动异常 | df -h、df -i |
cgroup / SystemdCgroup |
资源子系统 | cgroup driver 不一致 | 比较 kubelet 和 containerd 配置 |
这套速查表适用于绝大多数情况。如果你看到的错误串不在表里,还有一个通用思路:先判断错误串里有没有 OCI runtime create failed。有,就在 runc 层;没有,先在 CRI 层找。
最后给一个适用于常规场景的 15 分钟流程:
- 前 2 分钟:
kubectl describe+get events+get pod -o yaml,确认 reason 和错误串; - 第 3-4 分钟:登对应节点,
crictl ps -a拿容器 ID 和状态; - 第 5-7 分钟:
crictl inspect -o yaml+crictl inspecti,核对挂载、环境变量、镜像架构; - 第 8-10 分钟:
journalctl -u kubelet和journalctl -u containerd取原始错误串; - 第 11-13 分钟:对照速查表定位根因;
- 第 14-15 分钟:执行修复,观察 Events 变化。
这套流程我反复用过很多遍,绝大多数情况下 15 分钟足够锁定根因。如果 15 分钟还找不到,通常说明问题属于组合型,比如既有 mount 问题又有 cgroup 问题,这时不要反复试,把现场包发出去,两个层面一起看。
有些坑其实可以提前预防。镜像发布规范里强制要求非基础镜像写清 ENTRYPOINT/CMD;集群配置管理统一给所有节点同步 seccomp profile;上线前检查 containerd 的 SystemdCgroup 和 kubelet 的 cgroupDriver;给节点磁盘和 inode 用量加监控,设置 70% 报警、85% 干预。这些动作成本不高,但能直接减少一大半的 CreateContainerError。
我自己的体会是,这类问题排到最后,真正的问题往往不是 Kubernetes 本身,而是某一层配置或镜像在创建阶段埋的雷。镜像能拉下来,只代表它到了节点;能不能跑起来,是另一套完整链路的事。动手排错之前,先把“创建阶段”和“运行阶段”的分界线画清楚,你就已经赢了一半。
