1. 先搞清楚 Containerd 在容器栈里的位置1.1 从 Docker 拆解说起为什么现在绕不开 Containerd如果你是从 Docker 一路用过来的第一次听到 containerd 这个名字大概率是在某个 K8s 集群的报错日志里。早些年我们docker run跑得好好的装完 Kubernetes 却提示连不上容器运行时翻半天文档才发现 kubelet 要直连的是 containerd 的 socket而不是 Docker 的/var/run/docker.sock。这个变化不是谁心血来潮而是容器技术分层演进的必然结果。把容器启动这件事拆开看其实分成好几层最上面是给用户用的命令行和 API中间是负责生命周期管理、镜像管理、快照管理的运行时守护进程最底下是真正调用 Linux 内核 namespace 和 cgroup 去创建进程的 OCI 运行时。Docker 早期把这三层揉在一个大二进制里后来发现这玩意儿越来越臃肿于是把中间那层抽出来就成了 containerd。Docker 自己保留 CLI 和构建能力把容器的脏活累活交给 containerd 干。所以今天你在机器上装 Dockerps -ef | grep containerd一定能看到 containerd 在跑Docker 只是它的一个客户端而已。Kubernetes 那边更直接把内置的 dockershim 摘掉之后kubelet 通过 CRI 接口直接和 containerd 对话整条链路上没有 Docker 什么事了。这就是为什么现在装 containerd 变成了一门必修课不管你是搭 K8s 还是单纯想跑容器它都是绕不过去的一环。这篇文章我打算把 containerd 的安装过程从头到尾讲一遍包括包管理器安装和二进制安装两条路线的取舍、配置文件每一项到底干什么、装完之后为什么容器还是跑不起来以及我自己在这几年里踩过的那些坑。内容偏实操命令都是可以直接抄的环境以 Ubuntu 22.04 / Debian 12 加上 x86_64 为主CentOS 系和 ARM 平台我会在关键地方标注差异。1.2 Containerd 和 Docker、runc、kubelet 的分工边界很多人装完 containerd 之后一脸懵因为敲docker ps提示命令不存在敲containerd ps也提示没这命令。问题出在没搞清楚每个组件的职责边界。我用一张表把这条链路上常见的角色列清楚理解了这张表后面遇到的绝大多数为什么没反应都能自己推出来。组件角色定位典型交互对象Docker CLI / nerdctl面向人的命令行工具通过 socket 调用 containerd 或 dockerddockerdDocker 守护进程负责构建、网络、卷等上层能力调用 containerdcontainerd容器运行时守护进程管镜像、快照、容器生命周期调用 runc / shim被 kubelet 通过 CRI 调用containerd-shim-runc-v2每个容器的父进程负责托管容器进程调用 runc向 containerd 汇报状态runcOCI 运行时实现真正调用内核创建进程由 shim 调用执行完就退出kubelet节点代理通过 CRI 下发 Pod 指令调用 containerd 的 CRI 插件从这张表能看出一个关键点containerd 本身不带用户体验它是一个后台服务必须配一个客户端才能用。你在纯 containerd 环境里想跑个容器要么用ctr要么装nerdctl要么就交给 kubelet 去调度。理解了这一点就不会再纠结为什么装完没反应了。还有个容易混淆的地方是 CRI 插件。containerd 默认配置文件里那一大段plugins.io.containerd.grpc.v1.cri就是给 Kubernetes 用的接口实现kubelet 通过/run/containerd/containerd.sock这个 socket 连接到它。如果这段配置写错了containerd 服务本身能起来ctr也能用但 kubelet 就是连不上报connection refused或者runtime not ready。这是新手最容易掉进去的坑之一我后面会专门拆开讲。2. 安装路线怎么选包管理器还是官方二进制2.1 apt/yum 安装最省事但坑最多绝大多数人的第一反应是apt install containerd一行命令搞定看起来很美。Ubuntu 22.04 官方源里的 containerd 版本大概是 1.6 系列Debian 12 也差不多能用但有几个问题必须先知道。第一是版本滞后。官方发行版仓库里的 containerd 通常落后上游一到两个大版本。如果你要跟 Kubernetes 1.28 以上的版本配合官方明确要求 containerd 1.7 及以上这时候发行版自带的版本就不够用了kubelet 启动时会直接拒绝。第二是包名冲突Ubuntu 仓库里同时存在containerd和containerd.io两个包名前者是发行版维护的后者是上游官方仓库提供的两个包不能共存装错一个后面全是麻烦。第三点也是最恶心的一点就是自动升级。发行版的 containerd 包会跟着unattended-upgrades一起更新某天你早上来上班发现整个集群的 Pod 全部 Pending翻日志才知道半夜 containerd 被升了个小版本配置文件格式变了或者某项默认值改了。这种事故我见过不止一次尤其是生产环境一定要用apt-mark hold containerd把版本钉死。如果你只是想在一台测试机上快速体验包管理器安装完全没问题两分钟搞定。但只要是长期运行的环境我建议直接走二进制安装版本可控升级时机自己说了算出问题也好回滚。# Ubuntu 系快速安装仅建议测试环境 sudo apt update sudo apt install -y containerd sudo systemctl enable --now containerd # 把版本钉死防止半夜被升级 sudo apt-mark hold containerd2.2 官方二进制 tarball 安装可控性最好二进制安装是我个人最推荐的方式尤其是在生产环境。整个流程就是把官方发布的压缩包解压把二进制文件丢到/usr/local/bin然后写一个 systemd unit 文件托管起来。整个过程没有任何包管理器的黑魔法升级就是换个二进制重启服务回滚就是把旧二进制拷回来干净利落。下载地址在官方 GitHub Releases 页面命名规则是containerd-版本-linux-amd64.tar.gz注意别下成cri-containerd-cni-版本-linux-amd64.tar.gz那是把 CNI 插件和 systemd unit 一起打包的全家桶版本虽然省事但目录结构是写死的反而不好控制。我一般下纯 containerd 包CNI 插件单独装。版本选择上有个原则跟你的 Kubernetes 版本对齐。K8s 官方文档里有张兼容性矩阵大致是 K8s 1.26 到 1.28 配 containerd 1.6/1.7K8s 1.30 以上建议 containerd 1.7.20 或者 2.0。别盲目追新containerd 2.0 改动挺大配置文件版本从 2 升到 3一堆老配置项直接失效新手上去就是一脸懵。2.3 两条路线的横向对比为了让你一眼看清差别我把两条路线的关键维度拉出来对比一下。这张表是我自己选型时的决策依据你也可以直接拿去用。对比维度包管理器安装官方二进制安装上手难度极低一行命令中等需要手动配 systemd版本可控性差受发行版仓库限制好想装哪个装哪个升级风险高可能被自动升级低完全手动控制配置文件位置/etc/containerd/config.toml同上需手动生成卸载干净度一般配置残留高删文件即可适合场景测试机、临时验证生产环境、长期运行看到这里你应该有判断了。如果只是本地跑个 demoapt install五分钟搞定没必要折腾。但只要涉及多节点、长期运行、需要跟 K8s 配合二进制安装带来的确定性远比省下的那点时间值钱。我后面所有实操步骤都以二进制安装为主线展开包管理器安装的差异点我会顺带标注。3. 手把手实操二进制安装加 systemd 托管全流程3.1 前置检查内核模块、cgroup 版本、文件系统动手之前先做三项检查这三项任何一项不满足装完也是白装。第一项是 overlay 内核模块containerd 默认用 overlayfs 做快照存储模块没加载的话容器启动会直接失败。第二项是 br_netfilter 和网桥相关模块涉及容器网络后面装 CNI 插件时要用。第三项是 cgroup 版本v1 和 v2 的配置方式不一样得先确认清楚。# 检查 overlay 和 br_netfilter 模块是否加载 lsmod | grep -E overlay|br_netfilter # 如果没加载手动加载并写入开机自启 sudo modprobe overlay sudo modprobe br_netfilter echo -e overlay\nbr_netfilter | sudo tee /etc/modules-load.d/containerd.conf # 确认 cgroup 版本返回 cgroup2fs 说明是 v2 stat -fc %T /sys/fs/cgroup # 确认根文件系统不是 NFS 之类的网络文件系统 df -T /var/lib这里有个细节值得说不少云主机默认没加载 br_netfilter你装完 containerd、跑起 Pod发现 Pod 之间 ping 不通查半天以为是 CNI 配错了其实就是模块没加载。所以我习惯把模块加载这步前置到安装之前避免后面排查走弯路。另外df -T那一步是看/var/lib所在分区的文件系统类型overlayfs 对底层文件系统有要求放在 NFS 或者某些网络存储上会因为缺少 xattr 支持而报错这个坑我在一次云环境迁移里踩过排查了很久。3.2 下载与解压目录规划与软链接目录规划这件事看起来不起眼但影响后期维护。我习惯把 containerd 相关二进制统一放在/usr/local/bin配置文件放/etc/containerd数据目录用默认的/var/lib/containerd。数据目录千万别随便改除非你明确知道自己在干什么因为里面存着镜像层和快照路径一改所有已有镜像全部丢失。# 下载指定版本的二进制包 VER1.7.22 wget https://github.com/containerd/containerd/releases/download/v${VER}/containerd-${VER}-linux-amd64.tar.gz # 解压到 /usr/local包内目录结构是 bin/xxx sudo tar Cxzvf /usr/local containerd-${VER}-linux-amd64.tar.gz # 单独安装 runccontainerd 二进制包不含 runc wget https://github.com/opencontainers/runc/releases/download/v1.1.15/runc.amd64 sudo install -m 755 runc.amd64 /usr/local/sbin/runc # 验证版本 containerd --version runc --version这里要强调一下 runc 这个隐形依赖。containerd 二进制包里不带 runc你得单独去 opencontainers/runc 的 Release 页面下载。很多人装完 containerd 服务起来了ctr run一跑就报failed to start shim: exec: runc: executable file not found原因就在这。runc 放哪儿也有讲究放/usr/local/sbin或者/usr/bin都行只要在 PATH 里就能被 containerd 找到。注意runc 和 containerd 的版本也要大致匹配runc 版本过旧可能不支持新版 containerd 传过来的 OCI spec 字段出现容器启动后立刻退出的诡异现象。升级 containerd 时顺手确认一下 runc 版本。3.3 生成默认配置并做最小改动containerd 的配置文件不是装上就有的得手动生成一份默认配置再做修改。这一步是整个安装过程中最关键的地方配置错一行服务就起不来或者能起来但 kubelet 连不上。生成命令很简单但生成出来的文件有几百行新手一看就想关掉其实真正需要改的就那么几处。# 创建配置目录并生成默认配置 sudo mkdir -p /etc/containerd containerd config default | sudo tee /etc/containerd/config.toml # 查看配置文件的版本号确认是 version 2 还是 3 head -5 /etc/containerd/config.toml生成之后先别急着改搞清楚几个必须动的点。第一是SystemdCgroupK8s 环境下必须改成true否则 kubelet 启动时直接报 cgroup 驱动不一致。第二是sandbox_image默认指向的 pause 镜像地址在很多网络环境下拉不下来需要换成内网私有仓库的地址。第三是root和state路径如果服务器数据盘挂载点特殊需要调整到数据盘上。version 2 [plugins.io.containerd.grpc.v1.cri] # 沙箱镜像地址建议换成内网私有仓库 sandbox_image registry.internal.local/pause:3.9 # 关键开关与 kubelet 的 cgroup 驱动保持一致 [plugins.io.containerd.grpc.v1.cri.containerd.runtimes.runc.options] SystemdCgroup true上面这段里的registry.internal.local是我编的内网仓库域名你用的时候换成自己环境里实际可访问的仓库地址。这个镜像必须提前推到仓库里因为它属于沙箱容器的基础镜像kubelet 创建 Pod 的第一步就是拉它拉不到 Pod 就永远卡在ContainerCreating。3.4 systemd unit 与守护进程启动验证二进制安装的最后一个环节是写 systemd unit 文件。上游官方仓库里有个containerd.service模板直接拿来用就行注意ExecStart的路径要和你实际安装路径对上。unit 文件里几个参数值得解释一下Delegateyes是为了让 containerd 能自主管理自己的 cgroup不加的话容器资源限制会失效KillModeprocess保证重启 containerd 时不会连带杀掉所有正在运行的容器OOMScoreAdjust-999是降低被 OOM Killer 干掉的风险。# /etc/systemd/system/containerd.service [Unit] Descriptioncontainerd container runtime Documentationhttps://containerd.io Afternetwork.target local-fs.target [Service] ExecStartPre-/sbin/modprobe overlay ExecStart/usr/local/bin/containerd Typenotify Delegateyes KillModeprocess Restartalways RestartSec5 LimitNPROCinfinity LimitCOREinfinity TasksMaxinfinity OOMScoreAdjust-999 [Install] WantedBymulti-user.target写完 unit 文件之后按顺序执行重载、启动、验证三步。验证的时候别只看systemctl status显示 active还要看日志里有没有 warning以及 socket 文件是否真的生成了。sudo systemctl daemon-reload sudo systemctl enable --now containerd sudo systemctl status containerd --no-pager # 确认 socket 文件存在 ls -lh /run/containerd/containerd.sock # 查看启动日志重点关注 error 和 warn sudo journalctl -u containerd -n 50 --no-pager如果 socket 文件没生成说明服务启动过程中就崩了直接看journalctl报什么错。最常见的原因是配置文件路径不对或者 TOML 语法错误containerd 启动时会明确告诉你哪一行解析失败照着改就行。还有个小概率情况是 SELinux 或者 AppArmor 拦了dmesg | tail能看到相关拒绝记录这种就得单独处理安全策略了。4. 配置项逐条拆解哪些参数动了会出事4.1 SystemdCgroupK8s 环境下的头号开关SystemdCgroup这个参数我要单独拿出来讲因为它是新手翻车率最高的一项。它的作用是决定容器使用哪种 cgroup 驱动来管理资源取值只有true和false对应 systemd 驱动和 cgroupfs 驱动。kubelet 那边也有一个同名参数--cgroup-driver两边必须一致否则 kubelet 会直接拒绝启动日志里报failed to run kubelet: misconfiguration: cgroup driver is not the same as the cgroup driver of the container runtime。为什么推荐统一用 systemd因为在现代化的 Linux 发行版上systemd 是 cgroup 的实际管理者它会给自己管理的服务分配 cgroup 层级。如果容器运行时绕过 systemd 直接用 cgroupfs 去操作就会出现两套管理逻辑抢地盘资源限制计算错乱、容器被莫名杀掉这类问题就来了。Ubuntu 22.04、Debian 12、CentOS 8 以后这些系统默认都是 cgroup v2配合 systemd 驱动才是正解。改法很简单找到配置里 runc 运行时下面的SystemdCgroup项把默认的false改成true。但要注意不同版本的配置路径不一样1.7 里路径是plugins.io.containerd.grpc.v1.cri.containerd.runtimes.runc.options某些版本可能略有差异改之前先grep -n SystemdCgroup /etc/containerd/config.toml定位一下别凭记忆瞎改。4.2 sandbox_image 与 pause 镜像sandbox_image指向的是 Pod 的沙箱容器镜像也就是业界常说的 pause 镜像。它的作用很简单每个 Pod 启动前先跑一个几乎不消耗资源的 pause 进程把所有容器的 namespace 和网络栈挂在这个进程下面Pod 内容器重启时网络配置不用重建。这个镜像非常小一般几百 KB但它是 Pod 能跑起来的前提。问题在于默认配置里指向的是公共仓库地址很多内网环境根本拉不到Pod 就永远卡在ContainerCreating。解决办法有两个一是把镜像提前下载下来推到自己的私有仓库然后改配置指向私有仓库地址二是如果环境确实能访问公共仓库就保持默认但要确认能正常拉取。我个人的做法是统一走私有仓库所有镜像都从内网拉稳定性和速度都可控。改完配置别忘了重启 containerd 服务并且检查 kubelet 的日志确认它能正常创建沙箱。这个环节有个隐蔽的坑改了私有仓库地址但忘了在 containerd 里配置仓库认证导致拉取时 401日志里报failed to pull image ... unauthorized。私有仓库配认证的写法是在配置里加[plugins.io.containerd.grpc.v1.cri.registry.configs.registry.internal.local.auth]段把用户名密码填进去。4.3 registry 配置与私有仓库信任企业内网里的私有仓库绝大多数用的是自签证书containerd 默认会校验证书链验不过就直接拒绝连接。这时候需要在配置里针对特定仓库地址声明跳过 TLS 校验或者挂上 CA 证书。跳过校验的做法是把ca、cert、key留空然后设置insecure_skip_verify true但这只适合测试环境生产环境还是老老实实把 CA 证书挂上。[plugins.io.containerd.grpc.v1.cri.registry] [plugins.io.containerd.grpc.v1.cri.registry.mirrors] [plugins.io.containerd.grpc.v1.cri.registry.mirrors.registry.internal.local] endpoint [https://registry.internal.local] [plugins.io.containerd.grpc.v1.cri.registry.configs] [plugins.io.containerd.grpc.v1.cri.registry.configs.registry.internal.local.tls] ca_file /etc/containerd/certs.d/internal-ca.crt配置完记得把 CA 证书文件放到指定路径权限设成 644服务重启后生效。这里我踩过一个坑证书文件路径写对了但文件名拼错containerd 启动时不会报错只有实际拉镜像时才发现排查起来要翻半天日志。建议配完之后用crictl pull手动拉一次私有仓库的镜像做验证成功再往上跑业务。4.4 snapshotter 与目录规划containerd 用 snapshotter 来管理镜像层和容器可写层默认是 overlayfs。配置项在[plugins.io.containerd.snapshotter.v1.overlayfs]下面可以指定root_path来改变快照数据的存放位置。默认情况它会把数据放在 containerd 的 root 目录下也就是/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs。为什么要关心这个因为镜像层是很占空间的跑几十个镜像加上容器可写层几十 GB 就没了。如果你的系统盘容量有限数据盘又空着最好在安装前就把/var/lib/containerd整个目录挂到数据盘上而不是去改 snapshotter 的路径。直接挂载目录更简单升级也不会丢配置。具体操作就是把数据盘格式化成 ext4 或者 xfs然后写进/etc/fstab最后mount -a生效。提示如果数据盘是 xfs记得格式化时加上ftype1参数否则 overlayfs 会因为没有 d_type 支持而拒绝工作容器启动时报failed to create snapshot: not supported。这个错误信息很不直观很多人会往 CNI 或者内核版本上找原因实际就是文件系统格式化的参数问题。5. CNI 插件二进制包不自带的隐形依赖5.1 为什么 containerd 装完还是起不来 Pod这是我见过最多的困惑containerd 服务跑得好好的ctr version也正常但用 kubelet 创建 Pod 时永远卡在ContainerCreatingcrictl pods看到沙箱状态是NotReady。翻 kubelet 日志报failed to set up sandbox container network: plugin typebridge failed: failed to find plugin bridge in path [/opt/cni/bin]一眼看去好像 CNI 插件缺失但你明明装了 CNI 组件啊。问题在于 CNI 有两个东西一个是负责下发网络配置的控制面比如 Calico、Flannel 这些它们以 DaemonSet 的形式运行在集群里负责往每个节点的/etc/cni/net.d目录写配置文件另一个是负责实际执行网络操作的执行面也就是一组叫 cni-plugins 的二进制文件放在/opt/cni/bin目录下被 containerd 调用。二进制安装的 containerd 包里不带这组插件得单独装。这解释了一个很反直觉的现象你装 Calico 的时机很重要。如果先建集群再装 Calico节点上的/opt/cni/bin是空的Calico 的 DaemonSet 自己都创建不出来因为创建 Pod 需要网络需要网络又需要 CNI 插件死循环了。所以标准做法是先把 cni-plugins 二进制装好再装 Calico 这类网络组件让它们把配置写进/etc/cni/net.d。5.2 插件放置路径与配置目录cni-plugins 的安装非常直接从 GitHub Release 页面下载压缩包解压到/opt/cni/bin就行。目录权限设成 755二进制文件本身也是 755。注意不要只解压一两个插件整套一起放进去因为你不知道后面会用到哪个bridge、host-local、portmap、loopback 这几个是必用的tuning、bandwidth 之类的是可选但常用。# 下载 CNI 插件包 CNI_VER1.5.1 wget https://github.com/containernetworking/plugins/releases/download/v${CNI_VER}/cni-plugins-linux-amd64-v${CNI_VER}.tgz # 解压到标准目录 sudo mkdir -p /opt/cni/bin sudo tar Cxzvf /opt/cni/bin cni-plugins-linux-amd64-v${CNI_VER}.tgz # 验证插件是否就位 ls /opt/cni/bin | head -20/etc/cni/net.d这个目录是 CNI 配置文件的存放位置注意 containerd 是按文件名排序读取的只读第一个有效配置。这个细节很容易被忽略如果目录里同时存在 Calico 写的配置和 Flannel 写的配置containerd 只会用按字母序排在前面的那个另一个完全被忽略。排查网络问题时先ls /etc/cni/net.d看看有几个文件多出来的手动清掉。5.3 版本选择与常见冲突cni-plugins 的版本要求相对宽松1.3 到 1.5 都能用跟你的 CNI 组件比如 Calico 版本对齐就行。Calico 的文档里通常会写明推荐的 cni-plugins 版本照着装最稳。如果你的集群用了 Calico 并且开启了 eBPF 模式那对内核版本和插件版本都有额外要求这种场景建议严格按官方文档来别自己发挥。常见冲突有两个。一个是同一个节点上部署了多套 CNI 方案/etc/cni/net.d里堆了好几个配置文件前面已经说过后果了。另一个是旧版本插件残留升级时覆盖解压不完全新旧二进制混在/opt/cni/bin里可能出现调用了旧版本插件导致行为异常。我的习惯是升级前先把/opt/cni/bin清空再解压虽然粗暴但干净。注意清理/opt/cni/bin之前确认没有正在运行的 Pod 依赖其中的自定义插件。如果集群里有业务在用自定义 CNI 插件务必先备份再操作清空目录会直接导致网络中断。6. 踩坑实录那些报错信息背后的真实原因6.1 docker.io 依赖 containerd 的包冲突docker.io : depends: containerd ( 1.2.6-0ubuntu1~)这个报错在搜索热词里出现频率很高我来说说它是怎么来的。Ubuntu 官方仓库里的docker.io这个包依赖的是发行版自己维护的containerd包。当你之前手动从别处装了containerd.io这是上游官方仓库的包名或者手动编译安装了 containerd 并创建了同名文件apt 在做依赖解析时会认为系统里没有满足docker.io要求的containerd包于是拒绝安装并甩出这条依赖错误。根本原因是两套包命名体系打架。Ubuntu 系里有三种 containerd 来源发行版的containerd、官方 Docker 仓库的containerd.io、还有 containerd 项目自己 Release 的二进制。后两者都跟发行版包管理器没登记关系apt 不知道它们的存在。解决方式看你的目标如果还要用docker.io这个包就把之前装的 containerd 清干净让 apt 自己统一管理如果本来就想用官方二进制版那就别装docker.io改用官方 Docker 仓库的docker-ce或者干脆不用 Docker CLI直接用nerdctl。# 查看当前系统里所有跟 containerd 相关的包 dpkg -l | grep -i containerd # 查看某个包被谁依赖 apt-cache rdepends containerd # 彻底清除发行版包注意会停服务 sudo systemctl stop docker containerd sudo apt purge -y containerd containerd.io sudo apt autoremove -y处理这类依赖冲突有个通用思路先理清楚你要的最终状态是什么是apt 统一管理还是手动管理二进制然后按最终状态去清理别在一堆历史残留里修修补补越修越乱。我见过有人在两种方案之间反复横跳最后系统里存在三份 containerd叫什么名字的都有配置文件都不知道哪个生效了。6.2 自动升级把服务器升级成了砖这个坑我印象极深。有一批测试机装了 Ubuntu 官方源的 containerd没做版本锁定某天unattended-upgrades半夜自动升级containerd 从 1.6.x 升到了 1.7.x配置文件格式有变化服务重启后部分配置项被忽略恰好就包括了私有仓库的认证配置。第二天早上所有节点拉镜像全部 401业务大面积报错。教训是两条一是生产环境的容器运行时绝对不能允许自动升级apt-mark hold必须加上二是升级前必须先在测试环境验证配置文件格式变化这种改动光看 changelog 是看不出来的得实际启动一遍看日志。containerd 每次大版本升级配置文件版本号都会变version 2变成version 3新版本读取老配置时会有一堆 warning虽然多数能兼容但总有那么几个关键项行为变了。# 查看哪些包被 hold 了 apt-mark showhold # 锁定 containerd 相关包 sudo apt-mark hold containerd containerd.io runc # 检查自动升级配置里是否包含容器相关包 grep -r Unattended-Upgrade /etc/apt/apt.conf.d/ | head如果你用的是二进制安装这个坑自动就规避了因为没有包管理器插手。这也是我坚持二进制安装的重要原因之一升级时机完全自己掌握什么时候升、升到哪个版本都是人说了算而不是半夜被系统决定。6.3 配置文件语法错误导致服务起不来TOML 格式看着简单但有个反直觉的地方节的嵌套关系是靠路径点号表达的一旦某个节名写错整段配置就会静默失效而不是报错。比如把plugins.io.containerd.grpc.v1.cri.containerd.runtimes.runc.options里的某个层级拼错containerd 照样启动成功但你的SystemdCgroup true根本没被读到kubelet 连着连着就报 cgroup 驱动不一致。排查这个问题的利器是containerd config dump命令它会把生效后的完整配置打印出来你改的项有没有真正生效一目了然。老版本没有这个子命令那就只能用crictl info去看运行时实际加载的配置。另外 containerd 启动时会把配置解析的 warning 打到日志里养成启动后看一眼日志的习惯能省掉后面大量排查时间。提示改完配置文件别直接重启生产服务先用containerd --config /etc/containerd/config.toml前台跑一下看有没有解析报错。确认无误再systemctl restart避免服务起不来导致节点不可用。6.4 ctr 看不到镜像、crictl 连不上ctr images ls返回空但你明明用ctr images pull拉过镜像这是 namespace 的问题。containerd 内部把资源按 namespace 隔离默认 namespace 是default而 Kubernetes 用的是k8s.io。kubelet 通过 CRI 拉下来的镜像都存在k8s.io命名空间下你在default命名空间里当然看不到。# 查看所有命名空间 ctr namespaces ls # 在 k8s.io 命名空间下查看镜像 ctr -n k8s.io images ls # crictl 需要配置文件指定端点 cat /etc/crictl.yamlcrictl 连不上则是端点配置问题。它默认找的 socket 路径可能和你实际的/run/containerd/containerd.sock不一致需要显式配置。写一个/etc/crictl.yaml文件把运行时和镜像端点都指向 containerd 的 socket之后 crictl 就能正常工作了。这个文件我是每台机器必配的排查问题时用 crictl 比用 ctr 顺手得多因为它模拟的就是 kubelet 的调用路径。6.5 新版本配置格式变化引发的连锁问题containerd 升级到较新版本后CRI 插件被拆分成了运行时和镜像两个独立插件配置节的路径也跟着变了。老配置里的plugins.io.containerd.grpc.v1.cri这一段在新版本里需要拆成两个节一部分配置项挪到运行时插件下另一部分挪到镜像插件下。如果你只是升级了二进制但没更新配置文件就会出现部分配置生效、部分配置被忽略的怪现象。处理原则很简单大版本升级时不要沿用旧配置文件用containerd config default重新生成一份然后把你自定义的那几项SystemdCgroup、sandbox_image、registry 认证手动挪过去。虽然麻烦但比在一个半死不活的配置上打补丁靠谱得多。生成新配置后记得对比一下 diff看看有没有新增的默认项需要调整。7. 命令行工具三件套ctr、nerdctl、crictl 怎么选7.1 ctr自带但难用ctr是 containerd 二进制包自带的最轻量客户端功能覆盖镜像拉取、容器运行、任务管理、插件查看等但它的接口设计非常底层用起来很不顺手。比如它没有ps命令看运行中的容器得用ctr tasks ls停止容器要用ctr tasks kill配合ctr containers rm两步操作。而且它默认不指定 namespace跟 K8s 配合时每次都得手写-n k8s.io。不过 ctr 有个不可替代的价值它是离 containerd 最近的一层出问题时用它能直接看到运行时状态。比如ctr plugins ls能看到所有插件是否加载成功ctr version能看到服务端和客户端版本排查问题时这些信息很关键。我一般把 ctr 定位成调试工具而不是日常工具日常操作还是用更顺手的。7.2 crictlK8s 场景的调试主力crictl是专门为 CRI 接口设计的命令行工具它的命令设计完全贴合 Kubernetes 的调试需求crictl pods、crictl images、crictl logs、crictl inspect这些命令用起来跟kubectl有几分相似学习成本低。排查 Pod 问题时如果kubectl describe pod看不出所以然下一步就是上节点用crictl看沙箱容器的状态和日志。配置好/etc/crictl.yaml之后它就成为了解节点容器状态的窗口。比如 Pod 卡在ContainerCreating你可以crictl pods找到对应沙箱看状态是NotReady还是Ready再看crictl inspectp id的输出里有没有网络报错。这些信息比 kubelet 日志更贴近真相。7.3 nerdctl最接近 Docker 体验如果你的集群不需要 Kubernetes只是想在单机上跑容器那nerdctl是最佳选择。它的命令语法几乎和 Docker 一模一样nerdctl run、nerdctl ps、nerdctl images、nerdctl compose up从 Docker 迁移过来几乎零成本。它还能直接操作 Kubernetes 命名空间nerdctl -n k8s.io ps就能看到 K8s 里的容器比 ctr 好用太多。工具定位适用场景学习成本ctr底层调试工具插件状态、运行时问题排查中命令反直觉crictlCRI 调试工具K8s 节点容器与 Pod 排查低接近 kubectlnerdctlDocker 替代品单机容器、compose 编排极低兼容 Docker 语法三个工具不是替代关系而是各有分工。我的习惯是日常单机跑容器用 nerdctlK8s 节点排查用 crictl怀疑运行时本身有问题时用 ctr 看插件。三件套备齐遇到各种场景都不慌。8. 排查工具箱与日常维护8.1 日志与状态检查的固定套路排查 containerd 相关问题我有一套固定顺序按这个顺序走能覆盖八成以上场景。第一步看服务状态和最近日志确认守护进程本身健康第二步看 socket 文件是否存在确认能连上第三步用 crictl 看节点状态确认 CRI 插件工作正常第四步看具体 Pod 或容器的 inspect 输出定位到具体环节。这套流程坚持下来能避免东查一下西查一下浪费时间。# 第一步服务状态与近期日志 systemctl status containerd --no-pager journalctl -u containerd --since 10 min ago --no-pager # 第二步socket 文件与版本 ls -lh /run/containerd/containerd.sock containerd --version # 第三步CRI 层状态 crictl info | head -30 crictl pods # 第四步具体容器详情 crictl inspect container-id日志这块要会过滤关键信息。containerd 日志量不小直接journalctl -u containerd会刷屏加-p warning只看警告以上级别或者--since限定时间范围效率高很多。如果怀疑是某个特定操作触发的可以前台启动 containerd 开 debug 日志重现操作观察输出。8.2 常见问题速查表下面这张表是我这些年积累的高频问题清单遇到对应症状直接查表基本都能对上。报错或症状大概率原因处理方式docker.io : depends: containerd ( 1.2.6)包名体系冲突统一包管理来源或改用二进制failed to find plugin bridge in pathCNI 插件未安装解压 cni-plugins 到/opt/cni/bincgroup driver is not the sameSystemdCgroup 未开启配置改 true 并重启服务failed to pull image ... unauthorized私有仓库缺认证配置补 registry configs 认证段exec: runc: executable not foundrunc 未单独安装安装 runc 并放入 PATHPod 卡在ContainerCreatingsandbox 镜像拉不到换内网仓库并确认可拉取ctr images ls为空namespace 不对加-n k8s.io参数服务启动后 socket 未生成配置文件语法错误前台启动看解析报错容器启动后立刻退出runc 版本不匹配升级 runc 到兼容版本无法创建快照数据盘文件系统缺 d_typexfs 重格式化加ftype1速查表的价值在于缩短定位时间但要注意同一个报错可能有多个根因表里给的是最常见的那一个。如果按表处理没解决就往上一层找比如网络类报错往内核模块和 iptables 规则上查存储类报错往文件系统和 mount 选项上查。8.3 长稳运行的几点经验containerd 是个长期跑在后台的守护进程稳定性比性能重要得多。我总结了几条让它在生产环境安稳运行的经验。第一条是配置变更必须走前台验证再重启containerd --config /path/to/config.toml前台跑一遍确认没解析错误再systemctl restart这个习惯帮我避免了好几次服务起不来导致节点失联的事故。第二条是升级要有回滚方案。二进制安装的好处就是回滚简单升级前把旧版本的二进制文件和配置文件都备份一份出问题十分钟就能切回去。我习惯在/usr/local/bin旁边建一个/usr/local/bin-backup目录存放旧版本命名带上版本号一目了然。包管理器安装的话提前用apt-get download把旧版本 deb 包下载下来也能实现类似效果。第三条是监控要跟上。containerd 本身不暴露很丰富的指标但至少要做三件事监控服务进程存活、监控 socket 文件可连接、监控磁盘空间。磁盘这块尤其重要镜像和快照会不断累积/var/lib/containerd撑满系统盘导致节点不可用的事故我见过很多次。定期清理无用镜像crictl rmi --prune可以清理没被使用的镜像能回收不少空间。注意crictl rmi --prune会删除所有没有被容器引用的镜像如果集群里有节点依赖本地已有镜像快速拉起 Pod 的场景执行前评估一下影响避免删除后拉镜像慢导致扩容变慢。最后分享一个我自己用了很久的小技巧给每台节点建一个/etc/crictl.yaml加一份节点信息记录文件把 containerd 版本、runc 版本、CNI 插件版本、配置文件备份路径都写进去。节点多起来之后你不可能记住每台的版本出问题时打开这个文件就能对上下文有个快速判断比远程登录一台台查快得多。这个文件我一般放在/root/node-info.md简单粗暴但特别管用。