从我第一次在生产集群的节点上用crictl ps查容器开始,我就意识到,K8s 排障的思路必须从“docker 这套命令”切换到一个更底层的视角了。当时某个工作节点上报kubelet异常,我照旧先docker ps,结果列出来的容器和kubectl get pods里看到的完全对不上。真正和 K8s 直接对话的容器运行时是 containerd,而 containerd 对外提供了一套符合 CRI 规范的 gRPC 接口。问题是,这套接口没有像docker ps那样现成的客户端工具,节点出问题的时候只能在 containerd 日志里翻,效率很低。
crictl就是补齐这个空缺的 CRI 命令行客户端。它直接通过 CRI 接口和运行时通信,让你能看到 K8s 视角下的 Pod、容器和镜像,而不是运行时内部的抽象对象。这篇文章我会从“为什么必须有这个工具”开始讲,把安装、配置、常用命令、排障实战和踩过的坑一次说清楚。适合刚接触 K8s 节点运维的同学,也适合那些在 Docker 时代依赖docker ps排查问题、现在被迫面对 containerd 环境的老手。
1. 为什么需要 crictl:K8s 与容器运行时之间的话语权交接
1.1 从 Docker 到 CRI:K8s 为什么不再直接依赖 docker
想理解crictl,得先理解 K8s 和容器运行时之间那层被大多数人忽略的“契约”。
K8s 要在一个节点上跑 Pod,实际干活的不是 API Server,也不是 kubelet 自己,而是节点上的容器运行时。Docker 时代,kubelet 通过一个叫 dockershim 的适配层去调用 Docker 的 API,这本质上是一种特事特办:Docker 不是为 K8s 设计的,K8s 想要的是“给我一个容器/沙箱/镜像”,Docker 给的是“给我一个容器”。
后来 K8s 在 1.24 版本正式移除了 dockershim,要求所有运行时必须实现 CRI(Container Runtime Interface)。CRI 是 gRPC 协议,定义了RuntimeService和ImageService两组接口,前者管容器和 Pod 沙箱的整个生命周期,后者管镜像。containerd、CRI-O 这类运行时原生实现了 CRI,kubelet 直接和它们通信,中间不再有 Docker 那一层。
问题也在这儿。containerd 本身自带的ctr命令走的是 containerd 自己的 API,里面还有命名空间的概念,直接ctr -n k8s.io image ls这种用法不仅反直觉,而且它看到的“镜像”和 K8s 理解的“镜像”不一定能对应上。CRI-O 也有自己的crictl配套,但标准生态里,K8s 官方推荐的调试客户端就是crictl。
crictl解决的核心问题:给运维人员一个“以 K8s 视角”去查看运行时状态的可执行程序。它讲的每一句话,都是 CRI 接口层面的话。
1.2 crictl、ctr、crictl.yaml:三个名字,别弄混
初学者最容易绕晕的,就是一台节点上同时出现了crictl、ctr、crictl.yaml这样几个看起来差不多的东西。
先分清ctr和crictl。ctr是 containerd 内置的调试工具,它访问的是 containerd 的底层 API,能看到 containerd 自己的命名空间(比如k8s.io、default、moby)。crictl是独立的客户端二进制,它通过 CRI 协议访问 containerd 的RuntimeService和ImageService,它不关心 containerd 内部的命名空间划分,它看到的世界就是“Pod + 容器”,和 K8s 对齐。同样的镜像,ctr -n k8s.io images ls和crictl images都能列出来,但前者输出里可能还有docker.io/library/nginx:latest这种带完整路径的版本,后者会按照 CRI 的规范显示。
再说crictl.yaml。crictl自己去连 socket,这个 socket 路径怎么走、超时多久、要不要 debug,默认值它猜不到,得靠配置文件告诉它。默认路径是/etc/crictl.yaml。你可以在里面写:
runtime-endpoint: "unix:///run/containerd/containerd.sock" image-endpoint: "unix:///run/containerd/containerd.sock" timeout: 10 debug: false pull-image-on-create: false disable-pull-on-run: false如果运行时不写配置,crictl也能执行,但默认的 endpoint 是unix:///var/run/dockershim.sock,这在现代 K8s 环境里基本是失效的,所以不配置大概率会报连接失败。
1.3 什么时候用 crictl,什么时候用 kubectl
有个很常见的误区:kubectl logs、kubectl exec已经够用了,为什么要用crictl?
kubectl是把请求发给 API Server,API Server 再通过 kubelet 的 API 去操作节点上的运行时。这条链路太长了。如果 API Server 挂了、kubelet 和 API Server 之间的网络出问题、或者 kubelet 本身体积膨胀到假死,kubectl这条路就断了。更麻烦的是,即使 API Server 正常,kubectl describe pod给的信息经过了多层过滤,它不会告诉你 containerd 这个层面的具体报错。
crictl是在节点本地直接和运行时对话,不经过 API Server,也不依赖 kubelet 的状态。只要 containerd 还活着,你就能查。
我的习惯是:kubectl看控制面状态,crictl看节点实况。控制面状态给出“是什么方向的问题”,节点实况回答“运行时到底发生了什么”。两者交叉验证,定位速度比只盯一个工具快很多。
2. 安装与配置:让 crictl 找到正确的运行时端点
2.1 安装:两种方式,按环境选
crictl是一个独立二进制,安装方式非常简单。
第一种,包管理器安装。部分发行版有直接打包,比如在 Ubuntu/Debian 系、CentOS/RHEL 系下要看软件源里有没有cri-tools这个包。装上之后,直接:
sudo apt-get install -y cri-tools或
sudo yum install -y cri-tools这个包名要记住是cri-tools,不是crictl。不同发行版版本可能偏旧,但功能足够。
第二种,直接从官方发布页下载预编译好的二进制。下载后解压到/usr/local/bin或者/usr/bin,然后:
chmod +x crictl sudo mv crictl /usr/local/bin/ crictl version版本选择上,我建议尽量选和你的 K8s 小版本差不多的cri-tools版本。原因有两个:一是 CRI 规范在演进,旧客户端打新运行时时,有些字段可能解析不出来;二是crictl本身会持续增加新命令,比如crictl runp、crictl runc这类手动调试命令,新版明显比旧版好用。
2.2 配置 /etc/crictl.yaml:让客户端找到 socket
安装完不等于能直接跑。最常见的问题就是crictl找不到运行时端点。
containerd 默认监听在unix:///run/containerd/containerd.sock。你在节点上可以用ls -l /run/containerd/或者ss -lx | grep containerd确认一下 socket 是否存在。如果用的是 CRI-O,socket 一般是unix:///var/run/crio/crio.sock。
把/etc/crictl.yaml写好:
sudo tee /etc/crictl.yaml <<EOF runtime-endpoint: unix:///run/containerd/containerd.sock image-endpoint: unix:///run/containerd/containerd.sock timeout: 10 debug: false EOF这里有两个容易出问题的点:
runtime-endpoint和image-endpoint可以分别指定。绝大多数环境用同一个 containerd socket,但如果你的运行时配置把镜像服务拆开,比如用了一个单独的镜像仓库代理服务,那就要把 image-endpoint 指过去。timeout默认 10 秒。如果在高负载节点上排障,containerd 响应可能很慢,可以临时改成 15 或 20。排障结束改回来,避免每次命令都等超时。
还可以用环境变量或命令行参数覆盖配置,命令行的优先级最高。比如:
crictl --runtime-endpoint unix:///run/containerd/containerd.sock --image-endpoint unix:///run/containerd/containerd.sock info不写配置文件,每次敲命令带着参数,适合偶尔用一次的场景。长期在节点上干活,还是写好/etc/crictl.yaml更省心。
2.3 验证安装:version、info 命令怎么用
配置完成后,先跑两个验证命令:
crictl version crictl infocrictl version输出客户端和运行时的版本信息。重点看两部分:RuntimeName是不是containerd,RuntimeVersion是不是一个你能接受的版本。如果版本输出为空的某一个字段,先考虑是不是配置文件没指对 socket。
crictl info输出整个运行时的详细状态,包括 containerd 的配置路径、存储目录、CNI 插件状态、事件数量等等。这个命令在排障中很好用,因为它会直接报出运行时内部能不能初始化成功。
如果连version都报failed to connect,别急着怀疑网络,大概率是以下几种情况:
- socket 路径写错
- containerd 没启动
- containerd 启动时 CRI 插件被禁用
第三种情况是坑。containerd 的配置文件/etc/containerd/config.toml里有一个disabled_plugins列表,如果里面误加了"io.containerd.grpc.v1.cri",CRI 插件就不会加载,socket 压根不存在。排查方法:
grep -r "disabled_plugins" /etc/containerd/config.toml systemctl status containerd journalctl -u containerd --no-pager | tail -50日志里如果看到failed to load CRI plugin或者类似字样,就去改配置,把 CRI 插件从禁用列表里删掉,然后:
systemctl restart containerd3. 命令详解:镜像、容器、Pod 三类核心操作
crictl的命令体系很规整,基本就是images、ps、pods三大类往下延伸。我对接过的运维同学里,超过一半只会在 K8s 节点上crictl ps看一眼,剩余的镜像管理命令很少用,导致排障时常漏掉很重要的信息。这一节把三类操作都过一遍。
3.1 镜像管理:pull、images、rmi,对比 docker 命令
镜像这块的语法和 docker 很接近,但细节差异值得注意。
crictl pull docker.io/library/nginx:1.25注意:crictl pull需要完整镜像名。它不帮你补全docker.io/library/,如果直接crictl pull nginx:1.25,会失败。这和 docker 的习惯不太一样,我在一个新建集群上踩过一次,镜像名少写了前缀拉不下来,后来才意识到 CRI 层面对镜像名的解析是按标准格式来的。
列出镜像:
crictl images输出类似:
IMAGE TAG IMAGE ID SIZE docker.io/library/nginx 1.25 5e2c2fcd21a1d 146MBIMAGE ID只有 12 位短 ID,但这个 ID 和docker images里显示的 ID 不是同一个命名体系。如果要用 ID 做后续操作,比如删除或 inspect,短 ID 可以正常用,不过我更推荐用完整 repo:tag 指明,避免误删。
查看单个镜像详情:
crictl inspecti docker.io/library/nginx:1.25这个命令输出 JSON 格式的镜像信息,里面有镜像的架构、标签列表、摘要、元数据等。排障时我常用来确认节点上到底拉下来的是哪个平台的镜像,比如 arm64 节点误拉 amd64 镜像时,inspecti能直接看到architecture字段。
删除镜像:
crictl rmi docker.io/library/nginx:1.25rmi支持多个镜像参数,也支持一个镜像的多个 tag 同时删。如果镜像正被某个运行中的容器使用,会删除失败,报image is being used by ...。这在清理节点镜像时常见,需要先停掉相关容器。
提示:在某生产环境,我遇到过 K8s 明明显示
ImagePullBackOff,但节点上用crictl images却能看到目标镜像的情况。原因是镜像 tag 变了但节点缓存里有旧 ID,而 kubelet 期望的 digest 匹配不上。此时手动crictl pull更新 tag、必要时crictl rmi清掉旧镜像再让 kubelet 重新拉,问题就解了。
3.2 容器操作:ps、inspect、logs、exec、stop、rm
容器操作是crictl用得最频繁的部分。
crictl ps不带任何参数时,只显示运行中的容器。这和docker ps的行为一致。区别在状态字段,crictl ps输出的STATE一般是Running、Created、Exited、Unknown。Unknown状态很烦,它通常表示 containerd 重启过,或者 CRI 元数据丢失,实际容器可能还活着,但 kubelet 已经无法确认了。
看所有容器:
crictl ps -a加上-a后,已退出、创建失败的也都会显示。我在排障时几乎总是用-a,特别是当 Pod 一直ContainerCreating时,你需要看是不是有个容器卡在Created状态没进入运行。
格式化输出:
crictl ps -a --state Running -o json crictl ps -a --state Exited -o table-o支持table、json、yaml。脚本里解析信息建议用json或yaml,但直接给人看用table更好。-q参数只输出容器 ID,配合xargs批量操作非常好用。
查看单个容器详细信息:
crictl inspect <container-id>inspect输出里信息量极大,重点关注:
info.pid:容器主进程在宿主机上的 PID,可以直接nsenter -t <pid> -n进去看网络命名空间,也可以cat /proc/<pid>/status验证容器进程是否异常。status.state:运行状态status.exitCode:如果容器退出了,看退出码status.reason:CRI 层面的退出原因,比如OOMKilled
查看日志:
crictl logs <container-id> crictl logs -f <container-id> crictl logs --tail 200 <container-id>新版crictl还支持--since、--until这类时间过滤参数。节点级日志不看 kubelet 缓冲,直接拿运行时原始输出,某些场景比kubectl logs信息更完整。不过要注意,K8s 的容器日志轮转策略由 kubelet 的containerLogMaxSize等参数控制,crictl logs读取的同样是轮转后的日志文件。
进入容器:
crictl exec -it <container-id> shexec的用法和 docker 基本一样。有一个常被忽略的点:crictl没有attach这个命令。如果你想“附着”到一个已有容器的终端看输出,只能exec一个新的进程进去,如果是排查应用为什么没往外打日志,attach缺失的影响不大,但如果你确实需要实时看主进程的 stdout,那就crictl logs -f。
停止和删除容器:
crictl stop <container-id> crictl rm <container-id>stop会发送 SIGTERM,等待一段时间后强制 SIGKILL,超时时间可以用--timeout指定。节点级清理退出容器用:
crictl rm $(crictl ps -a -q --state Exited)这个命令在一次大规模节点故障后清理僵尸容器时非常好用,但要注意:被 kubelet 管理的容器,即使你手动删了,kubelet 也会根据期望状态重新创建,所以这个操作最终是让系统自己恢复,而不是你替 kubelet 做决定。
3.3 Pod 维度:pods 与 sandbox
这是crictl区别于普通容器工具的关键维度。K8s 的 Pod 在 CRI 里被实现为一组对象:一个 Pod 沙箱(sandbox)作为网络和命名空间的锚点,内部再承载业务容器。沙箱本质上是pause容器(在 containerd 环境里叫pause镜像对应的容器)。
查看 Pod:
crictl pods列出运行中的 Pod 沙箱。看全部:
crictl pods -a输出里有个字段叫SANDBOX,那是这个 Pod 沙箱的 ID,注意它不是业务的容器 ID。业务容器用crictl ps看,沙箱用crictl pods看。
如果你想按 Pod 名过滤:
crictl pods --name <pod-name-prefix> -o jsoncrictl pods的过滤参数比crictl ps更丰富,支持--state Ready/NotReady、--label等。
查看沙箱详情:
crictl inspectp <sandbox-id>这个命令输出的 JSON 包含 Pod 级别的网络参数(DNS、IP、端口映射)、命名空间、CNI 结果等。当 Pod 一直ContainerCreating但节点上找不到业务容器时,优先查inspectp,看到 CNI 返回的错误基本就是网络插件的问题。
注意:
crictl还有runp和runc命令,可以手动启动沙箱和容器。这两个命令是给开发者调试用的,日常节点排障不建议手痒去跑。它们绕过 kubelet 直接创建对象,一旦配置错误,会在节点上留下不被 kubelet 管理的游离容器,清理起来反而麻烦。
4. 排查实战:crictl 在故障处理中的典型用法
4.1 场景一:API Server 不可达,还能看到业务容器吗
有一次某集群的控制面节点网络分区,API Server 失联,kubectl get nodes直接连接拒绝。业务侧反馈节点还在跑服务,但运维侧已经完全看不到 Pod 状态了。
这种时候,任何依赖 API Server 的工具都失效,kubectl、各种 Dashboard、Prometheus 的 kube-state-metrics 全部不可用。你唯一能直接验证业务容器是否还健康的手段,就是 SSH 到节点上用crictl ps。
我当时的排查顺序:
crictl ps -a | head -20看到所有业务容器还是Running状态,说明 kubelet 虽然没法和 API Server 通信,但本地运行时没崩,容器还在跑。接着我用:
crictl logs --tail 50 <business-container-id>直接从应用日志确认它是否还在对外提供正常响应。这一套操作完全不经过控制面,只要 SSH 能通,就能确认“节点上到底发生了什么”。
这种情况下的一个建议:集群里准备一台带crictl的跳板机,当你连不进去的节点数量变多时,从这台机器批量 SSH 过去执行crictl ps,比一个个手动登录效率高得多。
4.2 场景二:容器启动失败,kubectl 的报错太简略
kubectl describe pod给的事件常常只写Failed to create pod sandbox或者failed to generate network config,信息量很有限。但 runtime 层到底为什么失败,只有crictl看得见。
典型的排查过程是这样的:
先列出这个命名空间下失败的容器:
crictl ps -a | grep <pod-name-prefix>看到容器状态是Exited,退出码是1。再用:
crictl inspect <container-id> | jq '.status | {state, exitCode, reason, message}'reason和message字段就是 runtime 给的原始错误。有一次我看到message里写着OCI runtime create failed: unable to create new sandbox: open /run/netns/cni-xxx: no such file or directory,立刻意识到是 CNI 的 netns 清理出了问题,和业务镜像没有任何关系。如果没有inspect提供这层详情,我大概率会在镜像和启动命令上浪费时间。
再看日志:
crictl logs <container-id>拿到应用自己打印的报错,很多启动失败实际上是应用配置错误,比如连不上配置文件或环境变量里少了内容。应用日志比 runtime 报错更接近“根因”。
4.3 场景三:镜像拉取失败,不过滤 K8s 事件直接测试
ImagePullBackOff是最常见的 Pod 故障之一。控制面报错是Failed to pull image ... rpc error: code = Unknown desc = failed to pull and unpack image ...,但底层具体是网络不通、认证失败还是镜像不存在,单靠控制面消息不一定好判断。
这时候在节点上直接手动拉一次镜像,把变量隔离出来:
crictl pull <完整镜像名>如果手动拉取成功,说明节点到镜像仓库的网络和认证都没问题,问题可能出在 kubelet 的配置或 Secret 传递上。如果手动拉取也失败,错误信息会直接显示 HTTP 状态码、TLS 握手错误或者认证失败的具体位置。
有个细节:某些私有镜像仓库对并发下载有限制,kubelet 同时拉多个镜像时触发速率限制,手动单次拉取却正好成功。这种场景要用crictl images和crictl rmi结合,临时删掉一半缓存镜像,观察 kubelet 后续拉起是否正常。
如果手动拉取成功了,K8s 还是ImagePullBackOff,检查节点上 kubelet 系统日志是个方向:
journalctl -u kubelet --no-pager | grep -i "pull"通常能看到 kubelet 内部的拉取请求是在哪个步骤失败的。
4.4 场景四:Pod 卡在 Terminating,底层容器清理不掉
Pod 一直Terminating是另一个高频问题。kubectl delete pod执行后,kubelet 需要把容器停掉、沙箱删掉,但这个流程可能卡在 runtime 层。
这时候crictl的价值是让你从底层手动推进清理:
crictl ps -a | grep <pod-name-prefix>找到对应容器后:
crictl stop <container-id> crictl rm <container-id>然后再找沙箱:
crictl pods -a | grep <pod-name-prefix>crictl stopp <sandbox-id> crictl rmp <sandbox-id>stopp和rmp是操作沙箱的命令,日常接触得少,但Terminating卡住时它们是真正的“最后手段”。执行完这一套,kubelet 那边通常就能把 Pod 状态收尾了。不过要注意,这种手动清理完之后,如果 Pod 还在期望运行状态,kubelet 会重新创建它,这不是问题,是正常自愈流程。
5. 避坑清单与效率技巧:几个值得记住的细节
5.1 crictl 不是万能的:它不负责 build,也不负责 attach
crictl是节点排障工具,不是 Docker CLI 的完全替代品。它有pull、ps、exec、logs、inspect这些运行时操作,但没有build,没有port-forward,没有attach,也没有 Docker Compose 那种编排能力。它的职责管得很窄:在节点上,查看和操作已经被拉取和创建的运行时对象。
如果你在一个 K8s 节点上想临时起一个一次性容器做网络测试,crictl run可以满足基础需求,但它需要你先准备好沙箱,用起来比docker run繁琐很多。节点上做这类临时操作时,我一般会优先看系统里有没有安装nerdctl,它更接近 Docker CLI 的使用习惯,还能直接用 containerd 的镜像和网络配置。当然这是另一个话题了。
5.2 输出里的隐藏细节:-o json、-q、--truncate 怎么配合
很多人在crictl ps后对着屏幕数行,很浪费。掌握几个输出参数能让你更高效:
-q:只输出 ID。配合批量删除、批量重启很好用。-o json:输出 JSON。脚本处理首选。--truncate:默认会截断长 ID,想显示完整 ID 时用--truncate=false。--state:按状态过滤Running、Exited、Created。
我自己常用的组合:
# 找出所有 Exited 的容器 ID,并删除 crictl rm -f $(crictl ps -a -q --state Exited) # 找出所有创建失败的沙箱 crictl pods -a -o json | jq -r '.items[] | select(.status.state != "SANDBOX_READY") | .metadata.name'jq配合crictl是节点排障的黄金组合,比在-o table输出里肉眼找字段高效太多。
5.3 效率技巧:shell 别名与批量清理实战
节点上经常要敲几十次crictl,我通常会配个别名:
alias cr='crictl' alias crp='crictl ps -a' alias cri='crictl images' alias crpods='crictl pods -a'这样在排障时敲crp | grep xxx就比crictl ps -a | grep xxx少敲很多键,手速能快不少。
批量清理退出状态的容器和镜像,有需求时要小心。比如一个大版本升级失败后,节点上堆积了几十个Exited容器,还有一堆旧镜像占了几 GB 磁盘:
crictl rm $(crictl ps -a -q --state Exited) 2>/dev/null crictl rmi $(crictl images -q) 2>/dev/null第二条命令会把所有镜像全删掉,非常危险,除非你想完全清空这个节点的镜像缓存,否则不要在原样复制到生产环境。稳妥的清理方式是按关键字段过滤:
crictl images | grep "<旧版本标识>" | awk '{print $1":"$2}' | xargs -r -n1 crictl rmi这样只删带特定标识的镜像,不影响其他缓存。磁盘告警排除时,这一套比挨个删 tag 高效很多。
5.4 版本兼容性和配置一致性
最后提醒一个容易忽略的问题:crictl版本和运行时版本之间如果是比较大的跨版本,某些字段可能解析不出来。比如旧版crictl连接新版 containerd 时,crictl info输出的某些配置项可能空着,或者在crictl pods里看不到新版运行时新增的沙箱字段。
建议在所有节点上固定使用同一版本的crictl,升级时选择与 K8s 小版本基本同步的cri-tools版本。另外,如果节点很多,别在每台机器上手动改/etc/crictl.yaml,容易出乱子。建议写进配置管理里统一分发,socket 路径和超时时间保持一致,排障时就不会出现“在这台节点能查,在另一台节点连不上”的割裂状态。
我在实际使用里,印象最深的是crictl inspect里那层比kubectl describe更原始的报错信息。很多看起来像“网络插件挂了”“镜像仓库出问题”的故障,最后其实都是运行时层一个小字段没说清楚导致的方向误判。所以,下次你在节点上排查不顺时,先别急着抓包或者重启服务,用crictl inspect把reason和message看一遍,往往答案就在那里等着你。