
从我第一次在生产集群的节点上用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. 为什么需要 crictlK8s 与容器运行时之间的话语权交接1.1 从 Docker 到 CRIK8s 为什么不再直接依赖 docker想理解crictl得先理解 K8s 和容器运行时之间那层被大多数人忽略的“契约”。K8s 要在一个节点上跑 Pod实际干活的不是 API Server也不是 kubelet 自己而是节点上的容器运行时。Docker 时代kubelet 通过一个叫 dockershim 的适配层去调用 Docker 的 API这本质上是一种特事特办Docker 不是为 K8s 设计的K8s 想要的是“给我一个容器/沙箱/镜像”Docker 给的是“给我一个容器”。后来 K8s 在 1.24 版本正式移除了 dockershim要求所有运行时必须实现 CRIContainer Runtime Interface。CRI 是 gRPC 协议定义了RuntimeService和ImageService两组接口前者管容器和 Pod 沙箱的整个生命周期后者管镜像。containerd、CRI-O 这类运行时原生实现了 CRIkubelet 直接和它们通信中间不再有 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已经够用了为什么要用crictlkubectl是把请求发给 API ServerAPI 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-Osocket 一般是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是不是containerdRuntimeVersion是不是一个你能接受的版本。如果版本输出为空的某一个字段先考虑是不是配置文件没指对 socket。crictl info输出整个运行时的详细状态包括 containerd 的配置路径、存储目录、CNI 插件状态、事件数量等等。这个命令在排障中很好用因为它会直接报出运行时内部能不能初始化成功。如果连version都报failed to connect别急着怀疑网络大概率是以下几种情况socket 路径写错containerd 没启动containerd 启动时 CRI 插件被禁用第三种情况是坑。containerd 的配置文件/etc/containerd/config.toml里有一个disabled_plugins列表如果里面误加了io.containerd.grpc.v1.criCRI 插件就不会加载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-idinspect输出里信息量极大重点关注info.pid容器主进程在宿主机上的 PID可以直接nsenter -t pid -n进去看网络命名空间也可以cat /proc/pid/status验证容器进程是否异常。status.state运行状态status.exitCode如果容器退出了看退出码status.reasonCRI 层面的退出原因比如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-idstop会发送 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镜像对应的容器。查看 Podcrictl 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-prefixcrictl stopp sandbox-id crictl rmp sandbox-idstopp和rmp是操作沙箱的命令日常接触得少但Terminating卡住时它们是真正的“最后手段”。执行完这一套kubelet 那边通常就能把 Pod 状态收尾了。不过要注意这种手动清理完之后如果 Pod 还在期望运行状态kubelet 会重新创建它这不是问题是正常自愈流程。5. 避坑清单与效率技巧几个值得记住的细节5.1 crictl 不是万能的它不负责 build也不负责 attachcrictl是节点排障工具不是 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 时用--truncatefalse。--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.namejq配合crictl是节点排障的黄金组合比在-o table输出里肉眼找字段高效太多。5.3 效率技巧shell 别名与批量清理实战节点上经常要敲几十次crictl我通常会配个别名alias crcrictl alias crpcrictl ps -a alias cricrictl images alias crpodscrictl 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看一遍往往答案就在那里等着你。