cri-containerd 1.7.23 离线安装、配置与排错

发布时间:2026/10/24 0:06:08
cri-containerd 1.7.23 离线安装、配置与排错

简介:配套 cri-containerd 1.7.23 Linux AMD64 的容器运行时安装包,面向云原生运维、Kubernetes 集群部署及容器运行时调试人员,主要用于离线环境下的版本对齐、CRI 接入和运行时组件替换,适合具备一定 Linux 与容器基础的中高级读者参考使用。压缩包共 19 个文件,整体约 101.22MB,主体为容器运行时及其命令行工具、CRI 测试与压力测试程序等二进制组件,同时配有 yaml 配置、systemd 服务模板、环境变量和辅助脚本,目录按系统路径组织,可覆盖安装部署、接口联调、运行验证与基础压测等环节。已有 262 人学习下载。读者能够获得与 1.7.23 版本匹配的完整运行时文件集合,便于在节点侧快速落地组件、核对配置模板、验证 CRI 连通性并开展基础性能测试;包内弃用说明也有助于提前识别后续版本迁移风险,降低组件不匹配带来的排错成本。

1. 离线节点上那个 tar 包:cri-containerd 1.7.23 到底装了什么

内网机房里最尴尬的一幕:节点出不了网,kubelet 起来了却一直报容器运行时连不上,翻遍发行版仓库才发现自带的那份 containerd 版本太老,配置项跟手上的 kubelet 对不上。这时候cri-containerd-1.7.23-linux-amd64.tar就是那颗后悔药——它是 containerd 1.7 分支专门为 CRI 场景打的静态包,解开来是几个不依赖系统库的二进制,拷进 U 盘就能带到任意一台 x86_64 机器上。

它适合两类人:一是要在离线节点上把 containerd 装起来、再交给 kubelet 用的运维和 SRE;二是想搞清楚"装一个容器运行时到底装了哪几个文件、谁在调谁"的工程师。下面按我实际交付的顺序拆:包里有什么、怎么落到 systemd、config.toml改哪几行、以及五个真把节点搞挂过的坑。

2. 解包之前先认清组件:五个二进制和一条启动链路

拿到一个 tar 包,很多人的第一反应是tar -xf完事,然后systemctl start一把梭。离线环境里这么干,出问题的时候你连从哪一层开始查都不知道。我一般会先花五分钟把包拆开看清楚,这一步省下来的时间远比它花掉的多。

2.1 tar 里到底有什么

先看清单再决定解到哪,这一步纯属保命:

# 只看清单,不解压。确认里面确实有 shim 和 runc,别拿到一个空壳包 tar -tf cri-containerd-1.7.23-linux-amd64.tar | head -n 30 # 看顶层目录结构:一般是 ./bin 和 ./etc 两层 tar -tf cri-containerd-1.7.23-linux-amd64.tar \ | awk -F/ 'NF>1 {print $2}' | sort -u # 顺手校验,离线包最容易在 U 盘拷贝、跳板机中转这两步损坏 sha256sum cri-containerd-1.7.23-linux-amd64.tar

如果清单里只有bin/、没有etc/,说明这个包不带默认配置,config.toml必须自己用containerd config default生成。另外一个高频混淆点:cri-containerd这个包不带 CNI 插件,带插件的是cri-containerd-cni那个变体。节点上如果还没有任何 CNI 配置,光装这个包,Pod 网络是通不了的,得单独补插件。

包里的二进制各管一段,缺一个就是一段链路断掉:

二进制职责缺了之后的表现
containerd守护进程本体,监听/run/containerd/containerd.sock,CRI 插件内嵌在里面服务根本起不来
containerd-shim-runc-v2每个容器一个 shim 进程,托管 runc 与容器进程的生命周期Pod 长期停在ContainerCreating
runc真正干活的 OCI 运行时,创建 namespace、cgroup、挂载根文件系统shim 起不来,日志里是no such file or directory
ctrcontainerd 自带客户端,绕过 CRI 直连 socket只是少了最底层的调试入口
crictlCRI 客户端,和 kubelet 走同一个接口少了和 K8s 视角一致的排查手段

这张表值得抄在交付文档里。后面所有排错,本质都是先判断"断在表的哪一行"。

2.2 从 kubelet 到 runc:一次 Pod 创建经过谁

理解这条链路,比背任何命令都有用。kubelet 通过 CRI 的 gRPC 接口连到containerd.sock,请求落到 containerd 内部的 CRI 插件上——注意这个插件是 containerd 进程里的一个插件(io.containerd.grpc.v1.cri),不是独立进程,所以ps里看不到第二个 containerd。插件解析请求之后,交给 runtime v2 层,由它 fork 出containerd-shim-runc-v2,shim 再调runc把容器进程创建出来。

这里有个容易忽略的点:1.7 里 CRI 插件默认是启用的,但它可以在disabled_plugins里被关掉。有些裁剪过的配置模板会把cri加进禁用列表,结果就是 containerd 明明活着,kubelet 却怎么都连不上,日志里是no CRI service这类提示。我第一次遇到时查了半小时 socket 权限,最后发现是插件被禁了,血泪经验。

另一个点是 shim 的常驻特性。容器跑起来之后,runc就退出了,真正托管容器进程的是 shim。所以ps -ef | grep shim看到的一堆进程是正常的,不要以为是泄漏。也正因为 shim 独立于 containerd 主进程,containerd 重启时容器不会跟着死——前提是你的 systemd unit 写对了,这部分在第 5 章会细说。

2.3 为什么是 1.7.23,而不是随手拿个更新的

1.7.23 是 1.7 分支后期的补丁版本,这个定位决定了它的使用场景:已经在 1.7 线上的节点做补丁升级,或者新节点要和存量节点保持同一小版本。K8s 对容器运行时的兼容窗口一般是相邻小版本,1.7 的 containerd 配 1.26 到 1.28 的 kubelet 都属于常见组合,跨度再大就要逐个配置项对一遍。

我不建议为了"版本新"直接跨到 2.x。2.x 的配置文件版本、shim 路径、sandbox 的实现方式都有变化,一份 1.7 的config.toml拿过去大概率报解析错误,而且存量节点混版本会让镜像和快照的兼容问题变得很难查。版本统一这件事,在离线环境里尤其重要——你不能指望节点出网去拉一个缺失的组件。

还有一条是和内核的关系。runc依赖内核的 cgroup 和 namespace 能力,3.10 这类老内核上跑较新的 runc,经常在 cgroup v2 相关的路径上报权限错误。装之前先确认内核版本,比装完再回滚便宜得多:

# 内核版本与 cgroup 版本,决定了 snapshotter 和 runc 的可用性 uname -r stat -fc %T /sys/fs/cgroup # 输出 cgroup2fs 表示 v2,tmpfs 表示 v1

3. 从 tar 到 systemd:一条能复现的最小安装路径

安装本身很短,短到很多人会跳过验证直接进下一步。但离线交付里翻车的,八成不是安装命令写错,而是"以为装好了"。这一章按我实际的顺序来:落盘、生成配置、拉起服务、两级验证,每一步都留一个可观测的检查点。

3.1 落盘、校验与 PATH

# 包内路径是 ./bin/xxx,解到 /usr/local 正好落成 /usr/local/bin/xxx tar -C /usr/local -xf cri-containerd-1.7.23-linux-amd64.tar # 四个关键二进制逐个确认在 PATH 里,且版本自洽 /usr/local/bin/containerd --version /usr/local/bin/runc --version /usr/local/bin/ctr --version /usr/local/bin/crictl --version

-C /usr/local这个参数是整套流程的关键。包内是相对路径./bin/containerd,解到别处就得自己建软链,而软链一旦漏建,systemd unit 里的ExecStart就会指向一个不存在的文件,报错信息还挺含糊。如果你的系统里已经有发行版仓库装的旧 containerd,先systemctl stop containerd并确认which containerd指向新路径,新旧混用是排查噩梦。

crictl默认找不到 socket,每次手动加--runtime-endpoint太难受,直接写死:

cat >/etc/crictl.yaml <<'EOF' runtime-endpoint: unix:///run/containerd/containerd.sock image-endpoint: unix:///run/containerd/containerd.sock timeout: 10 debug: false EOF

timeout设 10 秒是有讲究的:默认值偏短,在内网仓库响应慢的时候,crictl pull会莫名超时,让人误以为是网络不通。调到 10 到 30 秒之间,能把"真的不通"和"只是慢"区分开。

3.2 生成 config.toml 并改三处关键参数

mkdir -p /etc/containerd containerd config default > /etc/containerd/config.toml # 留一份原始备份,后面 diff 的时候能救命 cp /etc/containerd/config.toml /etc/containerd/config.toml.orig

config default输出的是当前这个二进制对应的默认值,所以千万别拿网上抄来的一份 1.6 模板套在 1.7 上,字段名和层级都可能变过。生成完之后改三处:

# 1) cgroup driver 对齐:kubelet 用 systemd,containerd 也必须 systemd sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml # 2) pause 镜像换内网地址,离线节点不允许它去公网拉 sed -i 's#sandbox_image = .*#sandbox_image = "harbor.example.internal/base/pause:3.9"#' \ /etc/containerd/config.toml # 3) 落盘位置确认:root 放大盘,state 放本地盘或 tmpfs grep -nE '^\s*(root|state)\s*=' /etc/containerd/config.toml

sed只是图快,正式环境我建议手改再diff一遍,sandbox_image那一行的引号和缩进很容易被改坏,导致 containerd 启动时直接解析失败。

参数的含义和调整边界:

参数默认值什么时候必须改
SystemdCgroupfalsekubelet 用 systemd 作为 cgroup driver 时必须改true,否则容器创建报 cgroup 权限错
sandbox_image指向公网 pause 镜像离线或内网环境必须换成内网仓库地址,tag 要和 kubelet 期望的一致
root/var/lib/containerd系统盘小时挪到大盘,挪之前要把已有数据迁走,不能直接改完重启
state/run/containerd一般不动,除非/run空间被别的服务挤爆
max_concurrent_downloads3仓库限流或带宽紧张时调小到 1 到 2;本地仓库可以调大到 5 到 8

3.3 systemd unit 与首次拉起

unit 文件我不从包里拿,手写一份更可控:

# /etc/systemd/system/containerd.service [Unit] Description=containerd container runtime After=network.target local-fs.target [Service] ExecStartPre=-/sbin/modprobe overlay ExecStart=/usr/local/bin/containerd Type=notify Delegate=yes KillMode=process Restart=always RestartSec=5 LimitNPROC=infinity LimitCORE=infinity TasksMax=infinity OOMScoreAdjust=-999 [Install] WantedBy=multi-user.target

三个参数值得单独说。Delegate=yes让 containerd 自己管 cgroup 子树,不写的话 Pod 数量一多就会出现 cgroup 争用和莫名其妙的 OOM。KillMode=process保证systemctl restart只杀主进程,不把 shim 和业务容器一起带走——这是很多人"改个配置重启一下,业务全没了"的直接原因。OOMScoreAdjust=-999是给 containerd 上个保险,内存紧张时优先被杀的是业务而不是运行时。

systemctl daemon-reload systemctl enable --now containerd systemctl status containerd --no-pager # 起不来就看这 50 行,配置解析错误会直接打在这里 journalctl -u containerd -n 50 --no-pager

3.4 用 ctr 和 crictl 做两级验证

这一步是整个流程里最值钱的部分。ctr走 containerd 原生接口,crictl走 CRI 接口,两个都测,就能把故障范围切成两半:ctr通、crictl不通,问题在 CRI 插件配置;两个都不通,问题在 containerd 本体或 runc。

# 第一级:绕过 CRI,验证 containerd 本体、snapshotter、镜像拉取链路 ctr version ctr -n k8s.io images pull harbor.example.internal/base/busybox:1.36 ctr -n k8s.io run --rm harbor.example.internal/base/busybox:1.36 smoke-test echo ok

-n k8s.io这个 namespace 必须带。containerd 是多租户的,kubelet 用的 namespace 就是k8s.io;不加 namespace 你会在default里操作,ctr images ls看到的是一片空白,然后误判"镜像没拉到"。

# 第二级:走 CRI,验证插件真的起来了、配置真的被读到 crictl info | head -n 20 crictl pull harbor.example.internal/base/pause:3.9 crictl images # 再确认 CRI 看到的 sandbox 配置和你写的一致 crictl info -o json | grep -i sandbox

crictl info能返回 JSON 这件事本身就是个信号——它意味着 CRI 插件在正常响应。如果这条命令卡住,基本可以断定插件没启或被禁用,直接去看config.toml里的disabled_plugins。

4. config.toml 里真正决定成败的四个配置块

config.toml几百行,绝大多数行你一辈子都不用碰。真正会让节点交付失败的,集中在四个块上。这一章把这四块拆开讲,包括每个字段改了之后的连锁反应。

4.1 sandbox_image:pause 镜像不对,Pod 就起不来

pause 容器的作用是持有 Pod 的 network namespace,其他业务容器 join 进这个 namespace。它由 containerd 而不是 kubelet 负责拉取,所以镜像地址配错时,kubelet 那边只能看到一个语焉不详的 sandbox 创建失败事件。

# 把当前配置里的 sandbox_image 抓出来,逐台节点核对 grep -n 'sandbox_image' /etc/containerd/config.toml # 别等创建 Pod 时才发现仓库里没这个 tag,提前 pull 一次 crictl pull harbor.example.internal/base/pause:3.9

tag 版本要和 kubelet 的期望对齐。版本不一致的典型现象是 sandbox 反复重建,Pod 一直在ContainerCreating和CrashLoopBackOff之间跳。另外记住一条铁律:改完sandbox_image必须重启 containerd,CRI 插件只在启动时读配置,热加载不存在,这一点坑过不少人。

4.2 registry 与 mirrors:离线环境怎么让 containerd 找到仓库

1.7 上两套写法并存:config.toml里的registry.mirrors,和/etc/containerd/certs.d/<host>/hosts.toml。前者是老写法,后者更细粒度。先看config.toml这套:

[plugins."io.containerd.grpc.v1.cri".registry] config_path = "/etc/containerd/certs.d" [plugins."io.containerd.grpc.v1.cri".registry.mirrors."harbor.example.internal"] endpoint = ["https://harbor.example.internal"] [plugins."io.containerd.grpc.v1.cri".registry.configs."harbor.example.internal".tls] insecure_skip_verify = false [plugins."io.containerd.grpc.v1.cri".registry.configs."harbor.example.internal".auth] username = "robot" password = "CHANGE_ME"

几个边界要划清。config_path一旦设置,certs.d 目录下的配置优先级更高,两套并存时非常容易出现"我明明改了 mirrors 却不生效"的玄学,我一般只保留一套,另一套注释掉。insecure_skip_verify = true只适合在自签证书且 CA 还没导入的临时阶段打开,长期开着等于关掉校验,后面换证书时会踩更大的坑。把明文密码写进配置文件也不体面,能用凭据助手或者挂载密钥就别写死。

4.3 snapshotter 与 root/state 目录

默认 snapshotter 是overlayfs,兼容性最好的是native,但空间占用大得多。overlayfs 有两个硬性前提:内核加载了 overlay 模块,且底层文件系统支持d_type——xfs 必须建卷时带ftype=1,否则会出现一堆诡异的现象,比如镜像能拉下来但容器起不来,或者层合并出错。

# 三个前提条件一次性确认 lsmod | grep overlay || modprobe overlay xfs_info /var/lib/containerd 2>/dev/null | grep -i ftype df -hT /var/lib/containerd /run

目录规划上,root放镜像和快照,是最吃空间的地方,节点上单独挂一块盘比较稳;state放 socket 和运行时状态,重启就没了,一般留在/run下。改root之前必须先把存量数据迁过去,直接改完重启会让 containerd 认为一切从零开始,已有镜像全部"消失"——不是真的丢了,是它去新目录找不到了。

4.4 改完必须回读,不能只看重启成功

systemctl restart containerd systemctl is-active containerd # 回读配置:确认 CRI 插件真的按新参数在跑 crictl info -o json | grep -iE 'sandbox|snapshotter' # 再用一次真实拉取做端到端确认 crictl pull harbor.example.internal/base/busybox:1.36

systemctl is-active返回active只说明进程活着,不说明配置生效。config.toml里某个字段名写错时,containerd 有时候会带着警告启动并忽略那一块,日志里只有一行不起眼的 warn。所以回读这一步别省。

提示:每次改config.toml前先cp一份带时间戳的备份,改完diff一眼,能省掉大量"到底改了啥"的回忆成本。

5. 排查清单:五个把节点搞挂的坑

下面五条都是我在交付现场真实遇到过的,按"现象 → 原因 → 解决"写,遇到对应的直接对号入座。

5.1 重启 containerd 之后,节点上业务容器全没了

现象:改完配置执行systemctl restart containerd,几秒后crictl ps一片空白,kubelet 开始成批报 sandbox 不存在,业务全量抖动。

原因:systemd unit 里没写KillMode=process,默认是control-group,重启时会把整个 cgroup 里的进程一起杀掉,包括 shim 和容器进程。严格说不是容器"挂了",是它们被 systemd 顺手带走了。

解决:unit 里补上KillMode=process和Delegate=yes,systemctl daemon-reload再重启。已经在跑容器的节点,改配置前先cordon+drain,把业务挪走再操作,别在高峰期赌运气。

5.2 kubelet 报连接失败,socket 明明存在

现象:kubelet 日志里是rpc error: code = Unavailable或connect: no such file or directory,但手工ls /run/containerd/containerd.sock是有文件的。

原因:路径没对齐。一种是 kubelet 的--container-runtime-endpoint还指向老运行时的 socket;另一种是 unit 里给 containerd 加了--address参数换了路径,但 kubelet 那边没同步。也有权限问题——socket 的属主和 kubelet 的运行用户不匹配。

解决:两边统一写unix:///run/containerd/containerd.sock。用ss -lntp | grep containerd确认实际监听路径,用ls -l看 socket 的权限位,再用crictl -r unix:///run/containerd/containerd.sock info从客户端侧验证一次连通性。

5.3 Pod 卡在 ContainerCreating,事件指向 pause 镜像

现象:kubectl describe pod里反复出现Failed to pull image "...pause..."或sandbox image not found,节点上其他一切正常。

原因:三种可能。sandbox_image还是默认的公网地址,离线节点出不去;地址改了但内网仓库里没有对应 tag;仓库有镜像但证书不受信,拉取时报 TLS 错误被当成拉取失败。

解决:先crictl pull手工拉一次 pause 镜像,能把三种原因快速区分开。拉取报 404 就是 tag 不存在,报x509就是证书,超时就是网络或 DNS。证书问题把 CA 放到/etc/containerd/certs.d/<host>/ca.crt,比开insecure_skip_verify干净得多。

5.4 容器创建时报 cgroup 相关错误

现象:failed to create task,后面跟着unable to apply cgroup configuration或permission denied,容器起一下就退。

原因:cgroup driver 不一致。kubelet 用 systemd,containerd 还是默认的 cgroupfs,两边对同一个 cgroup 路径的理解不同,写进去就打架。老内核上还有另一种可能:内核只支持 cgroup v1,但配置里按 v2 的路径在写。

解决:SystemdCgroup = true之后重启,用stat -fc %T /sys/fs/cgroup确认 cgroup 版本。老内核节点上,别指望靠改配置绕过所有问题,该升级内核就升级,硬扛下去只会在业务高峰期爆。

5.5 镜像拉取时快时慢,偶发超时

现象:同一个镜像,crictl pull有时几秒完成,有时卡两三分钟然后超时,重试又能成功。

原因:max_concurrent_downloads默认是 3,多个 Pod 同时启动时并发拉层,把仓库带宽或连接数打满;也可能是config.toml的 mirrors 和 certs.d 两套配置同时生效,containerd 在多个 endpoint 之间来回试。

解决:先把镜像配置收敛成一套,只留 certs.d 或只留 mirrors。并发调到 1 到 2 观察,本地仓库带宽充足时再往上调。同时把crictl.yaml里的timeout提到 30 秒,让"慢"和"不通"能区分开,别把慢当成故障处理。

6. 上线前的 dry run:certs.d 分仓库配置与自查表

多仓库场景下,我倾向于把信任关系按 host 拆开管,而不是全塞进config.toml。每个仓库一个目录,一份hosts.toml,谁的问题一眼能定位:

mkdir -p /etc/containerd/certs.d/harbor.example.internal cat >/etc/containerd/certs.d/harbor.example.internal/hosts.toml <<'EOF' server = "https://harbor.example.internal" # 主仓库:带 CA,允许 pull 和 resolve [host."https://harbor.example.internal"] capabilities = ["pull", "resolve"] ca = "/etc/containerd/certs.d/harbor.example.internal/ca.crt" # 备用镜像站:只读,主仓库不可达时兜底 [host."https://mirror.example.internal"] capabilities = ["pull", "resolve"] skip_verify = false EOF

capabilities里加不加push是有区别的:只给pull和resolve,等于从运行时层面堵死了误推的可能,构建机和生产节点用同一份配置时尤其省心。skip_verify我一般显式写false,哪怕它是默认值,写出来是为了让后来接手的人知道这里校验是开着的。

交付前我会固定跑一遍这张表,六项全过才把节点交给 kubelet:

检查项命令通过标准
二进制版本自洽containerd --version && runc --version版本与预期一致,无 "unknown"
服务状态systemctl is-active containerdactive,且journalctl无 error
cgroup 对齐grep SystemdCgroup /etc/containerd/config.toml值为true
pause 可达crictl pull <sandbox_image>拉取成功,无 TLS 报错
端到端运行ctr -n k8s.io run --rm <busybox> t echo ok输出ok且自动清理
磁盘余量df -hT /var/lib/containerd使用率低于 70%

这套流程我吃过亏才固化下来。早期交付时图快,装完systemctl start看到active就撤了,结果第二天业务上线,Pod 全卡在ContainerCreating,原因是sandbox_image还指着公网。从那以后我每台新节点交付前,都会强制走一遍crictl pull pause加ctr run一个 busybox 的双重 dry run,两个都过才允许 kubelet 接进来。这份cri-containerd-1.7.23-linux-amd64.tar本身没什么花样,真正值钱的是上面这套顺序和那份自查表——包在内网文件服务器上放一份,配置模板和检查项跟着节点一起归档,下次扩容能省掉一整天。希望帮到你。

本文还有配套的精品资源,点击获取