简介:面向容器运行时部署、Kubernetes 节点维护与云原生离线实验场景,这份压缩包提供 containerd 1.7.23 的 Linux AMD64 版本,适合需要在无外网环境安装、升级或验证 CRI 运行时的运维人员与学习者使用,可减少镜像拉取依赖、版本不一致造成的部署阻碍。包内共 19 个文件,整体约 101.22MB,既有运行时核心程序、命令行客户端、容器执行器与兼容层等二进制组件,也配有配置清单、服务单元、环境变量和模板文件,并附带版本说明与弃用提示文本,目录按 etc、usr/local、opt 分层组织,便于离线分发、按需替换和快速定位。已有 262 人学习,说明该版本在同类离线包中具备一定参考价值。读者可借助 systemd 服务模板完成运行时注册,用 crictl、critest 做接口联调与合规验证,通过 stress 工具开展压测,并结合弃用说明评估后续迁移,适合搭建可复现的容器运行时实验环境。
1. 一个四十多兆的 tar 包,能让 kubelet 卡在 NotReady 上半小时
上周帮人看一台测试机,装完cri-containerd-1.7.23-linux-amd64.tar之后 kubelet 一直 NotReady,systemctl status containerd显示 active,可 Pod 全卡在 ContainerCreating。翻了半小时 journalctl 才发现:包解压对了,服务起来了,但config.toml还是包内自带的旧值,SystemdCgroup没开,sandbox_image指向一个根本拉不到的外网地址。这种“服务活着、运行时死了”的状态最容易让人误判,因为进程在跑,你就不会怀疑它。
containerd 这套环境现在几乎是容器节点的标配:不管是自己攒的小集群、CI 机器,还是离线交付的一体机,只要跑 CRI,基本都绕不开它。装不好不会立刻报错,而是延后到你部署第一个工作负载时才集中爆发。这篇笔记面向要在 amd64 Linux 上手装 1.7.23 的人——版本怎么选、包往哪解、config.toml 改哪几行、坏了看哪个日志。
2. 装前核对版本信息:1.7.23 的版本位置与宿主机三项前提
跳过直接用、装到一半又删掉重来的成本,比从容地对一遍版本要高得多。这一章先把版本位置、内核条件、磁盘和目录规划确认清楚,后面解包才不容易出错。我一般把这一步控制在十分钟以内,但绝不省略。
2.1 1.7.23 在 1.7 系列里的位置,以及为什么不直接上 2.x
1.7 是 containerd 里生命周期较长的一条线,1.7.23 属于这条线上的后期补丁版本,意味着 config 结构、插件路径、CLI 参数都已经稳定,社区里绝大多数 kubelet 对接文档、CSI 排查帖、CRI 相关问题都能对得上号。这一点非常关键:装 containerd 最麻烦的不是安装本身,而是出问题之后搜索到的资料和你手上的版本结构不一致——1.6 时代的[plugins.cri.xxx]写法和 1.7 的[plugins."io.containerd.grpc.v1.cri".xxx]完全不是一回事。
不直接上 2.x 的理由也很实在。2.x 在配置项、沙箱运行时拆分、镜像仓库配置上做了较多调整,配套的 kubelet 版本、CNI 插件、监控采集脚本都得跟着动。如果你的节点是给一套已有集群扩容,或者要复现一个别人交付的环境,选 1.7.23 是最稳的。反过来说,如果是全新搭一套、上游组件也都在新版本上,那就没必要把自己限制在 1.7,直接按上游的兼容矩阵走。
还有一点是我遇到过的:1.7 系列内部的补丁版本之间,containerd config default输出的默认值是会变的,比如sandbox_image的默认 pause 标签就调整过。所以别拿着一份 1.7.10 的配置模板往 1.7.23 上套,默认配置一定要在本机现生成一份。
2.2 内核、cgroup 与 swap:装之前用三条命令确认
containerd 对内核的要求不算苛刻,但有两个模块必须就位:overlay和br_netfilter。前者决定 overlayfs 快照器能不能用,后者决定桥接流量能不能被 iptables 规则处理。很多精简版系统镜像默认不加载这两个模块,服务能起,容器一跑就报failed to mount overlay。
# 加载模块并写入开机自启,避免重启后容器全挂 modprobe overlay modprobe br_netfilter cat > /etc/modules-load.d/containerd.conf <<'EOF' overlay br_netfilter EOF # 顺手把转发参数落盘,kubelet 场景基本都要 cat > /etc/sysctl.d/99-kubernetes-cri.conf <<'EOF' net.bridge.bridge-nf-call-iptables = 1 net.bridge.bridge-nf-call-ip6tables = 1 net.ipv4.ip_forward = 1 EOF sysctl --systemmodules-load.d那个文件是给重启用的,modprobe只是当前会话生效,两者都要做。sysctl 那一段在纯 containerd 场景下不是强制的,但只要节点后面会跑 kubelet,ip_forward不打开,Pod 之间跨节点通信必然出问题,早配早省事。
cgroup 版本要单独确认,因为 1.7.23 的 cgroup driver 配置跟它强相关:
# 返回 cgroup2fs 说明是 cgroup v2,返回 tmpfs 一般是 v1 stat -fc %T /sys/fs/cgroup/这一步的输出直接决定第 4 章SystemdCgroup怎么填。记住这个结果,后面要用。
swap 则是老生常谈:kubelet 默认不允许 swap 开启,装之前先swapoff -a并把/etc/fstab里对应的行注释掉。free -h确认一下 Swap 那行全是 0 再往下走。
2.3 目录与磁盘:/var/lib/containerd 该给多大
containerd 默认把镜像层、快照、容器元数据全放在/var/lib/containerd,socket 和运行时状态放在/run/containerd。前者是磁盘大户,后者是 tmpfs,重启即清空,不用管。
我给的经验值:只跑几个测试容器,20G 起步;正经跑业务负载,/var/lib/containerd单独挂一块盘,或者至少保证/var分区有 100G 以上余量。镜像层是层层叠加的,删了容器不一定释放空间,没有一个宽裕的余量,迟早会遇到no space left on device,然后一整天都在清理。
先看一眼现状:
df -h /var /run ls -ld /var/lib/containerd 2>/dev/null || echo "目录还不存在,正常,安装后自动建"如果/var/lib/containerd已经存在并且里面有旧数据,那是之前装过 containerd 留下的。要么继续用,要么先备份再清空——直接rm -rf之前想清楚,里面的镜像层删掉就得重新拉。
3. 解包并放到目标位置:cri-containerd-1.7.23-linux-amd64.tar 中有什么、放到哪里
这个包的目录结构其实很规整,但它是按根目录相对路径打包的,解压位置错了会导致二进制散落一地。先看清单再动手,比解压完发现路径不对再回头清理省事得多。
3.1 先看清单再解包:包内路径与两个 runc 的取舍
解压前先列一下内容,这一步不费事,但能避免很多“我到底装了什么”的困惑:
tar -tf cri-containerd-1.7.23-linux-amd64.tar | head -30正常会看到这么几类路径:
etc/systemd/system/containerd.service etc/containerd/config.toml usr/local/bin/containerd usr/local/bin/containerd-shim-runc-v2 usr/local/bin/ctr usr/local/bin/crictl usr/local/bin/runc usr/local/sbin/runcusr/local/bin下是运行时本体,containerd-shim-runc-v2是 runc 的 shim,真正负责起容器的就是它;ctr是 containerd 自带的低层 CLI,crictl是 CRI 层的排障工具,两个都有会更方便。etc/containerd/config.toml是包内自带的示例配置,不要直接拿它当生产配置用,第 4 章会解释为什么。
值得注意的是包里usr/local/sbin/runc和usr/local/bin/runc各有一份,实际是同一个二进制,只是放在两个路径下兼容不同的 PATH 习惯。装完记得which -a runc看一眼,如果系统里本来就有一个旧版 runc(比如发行版包管理器装的),PATH 里谁在前就用谁,版本对不上会出现容器起来就退出的怪问题。我一般会显式确认一次/usr/local/bin/runc --version。
3.2 一条 tar 命令放到根目录,附权限核对
因为这个包内的路径是usr/local/bin/...和etc/...这种根相对结构,解压目标就是/:
cd /tmp tar -C / -xf cri-containerd-1.7.23-linux-amd64.tar # 核对落盘结果 ls -l /usr/local/bin/containerd /usr/local/bin/ctr /usr/local/bin/crictl /usr/local/bin/runc ls -l /etc/systemd/system/containerd.service-C /是切换解压目标目录,-x是解包,-f指定文件名。tar 会保留包内的权限位,正常情况下二进制都是 755,执行权限不会丢。如果ls出来发现 containerd 没有x位,chmod 755 /usr/local/bin/containerd*补一下再继续。
包内自带的etc/containerd/config.toml解出来会落在/etc/containerd/config.toml。我通常会立刻把它改成.bak,防止后面containerd config default生成新配置时被混淆,也防止自己手抖改错了还以为是默认值:
mv /etc/containerd/config.toml /etc/containerd/config.toml.pkg.bak3.3 systemd 单元文件与开机自启的前置检查
包里的containerd.service基本可以直接用,关键几行是这样的:
[Service] ExecStartPre=-/sbin/modprobe overlay ExecStart=/usr/local/bin/containerd Type=notify Delegate=yes KillMode=process Restart=always LimitNPROC=infinity TasksMax=infinity OOMScoreAdjust=-999逐个说下值得留意的地方。ExecStartPre前面那个减号表示命令失败也不阻断启动,这是为了防止某些内核已经把 overlay 编进内核、modprobe报“模块不存在”时误伤服务启动。ExecStart写的是绝对路径/usr/local/bin/containerd,这跟我们解包的位置一致;如果你把它装到了/usr/bin,这里必须同步改,否则systemctl start会直接报 203/EXEC。Delegate=yes让容器内的 cgroup 可以由容器自己管理,缺了它,systemd 会去回收容器内的进程组,表现为容器刚起来莫名被 Kill。OOMScoreAdjust=-999是降低 containerd 被 OOM Killer 选中的概率——容器吃爆内存时,先死的不该是运行时。
装完之后不要急着 start,先做一次重载和 enable:
systemctl daemon-reload systemctl enable containerd systemctl status containerd --no-pagerdaemon-reload是必须的,否则新放进去的 unit 文件 systemd 根本看不见。enable只设置开机自启,此时服务还是 inactive,属于正常状态。
4. 配置 /etc/containerd/config.toml:1.7.23 上必须手动修改的五处
真正决定这套环境能不能用的,不是二进制装没装对,而是这个 config.toml。默认配置只是“能启动”,不是“能用”。下面五处是我每次装完都会过一遍的。
4.1 先生成默认配置,再备份,不要用包里自带的那份
包里那份config.toml是打包时固化的模板,跟当前二进制不一定匹配。正确做法是用二进制自己生成:
mkdir -p /etc/containerd /usr/local/bin/containerd config default > /etc/containerd/config.toml cp /etc/containerd/config.toml /etc/containerd/config.toml.bak.$(date +%F)注意第一行必须是绝对路径/usr/local/bin/containerd,不要只写containerd。如果机器上曾经通过包管理器装过旧版,PATH 里/usr/bin/containerd可能排在前面,你生成出来的就是旧版本的配置结构,往后凡是按 1.7 语法改的地方全都会踩空。这个坑我踩过一次,排查了两个小时才发现生成配置的二进制压根不是新装的那个。
生成后做一份带日期的备份,是为了改崩了能一键回滚。配置文件是纯文本,手改出错的概率不低,有个.bak就是后悔药。
4.2 SystemdCgroup:与 cgroup v2、kubelet 的对应关系
这是最容易漏、后果最直接的一项。1.7.23 的默认配置里SystemdCgroup是false,意思是 containerd 自己用 cgroupfs 管理 cgroup;而 kubelet 从 1.22 之后默认用 systemd 作为 cgroup driver。两边不一致的典型表现是:容器能起来,但资源限制不生效,或者 kubelet 报failed to create pod sandbox: cgroup ... not found。
改动位置在 runc 运行时的 options 下:
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] runtime_type = "io.containerd.runc.v2" [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] SystemdCgroup = trueSystemdCgroup = true表示由 systemd 接管 cgroup 创建,跟 kubelet 保持一致。如果你的节点是 cgroup v2(第 2 章那条stat -fc %T返回cgroup2fs),这一项更是必开,cgroup v2 下 cgroupfs driver 的支持并不完整。改完这一处,前面那次 sigh 才算解开。
改的方法有很多,sed一把梭也行,但我不推荐:
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml grep -n "SystemdCgroup" /etc/containerd/config.toml之所以还贴出来,是让你在改完立刻grep验证。如果 grep 结果为空,说明匹配串没对上——可能是空格数不同,也可能是默认值本来就是别的,那就老老实实打开文件手改。
4.3 sandbox_image:pause 镜像拿不到是 ContainerCreating 的主要原因
Pod sandbox 需要一个 pause 镜像,这个镜像地址由sandbox_image决定。默认值指向公共仓库,在没有互联网出口或走内网仓库的机器上必然拉不下来,现象就是所有 Pod 卡在ContainerCreating,crictl pods能看到 sandbox 一直没建好。
[plugins."io.containerd.grpc.v1.cri"] sandbox_image = "registry.internal:5000/pause:3.9"这里要填你环境里实际能拿到的 pause 镜像地址,标签以你集群 kubelet 的--pod-infra-container-image为准,两边不一致会出现每次重启 Pod 都重拉一次 sandbox 的情况。改之前先用crictl pull手动试一下这个地址能不能拉通,能拉通再写进配置,比改完重启服务再排查要快得多。
如果你手上只有 pause 的 tar 包,可以离线导入:
ctr -n k8s.io images import pause-3.9-amd64.tar ctr -n k8s.io images ls | grep pause-n k8s.io这个命名空间不能省,kubelet 看的正是k8s.io这个 namespace 下的镜像,导到default里等于白导。
4.4 registry 与 hosts.toml:私有仓库和内网 mirror 的写法
1.7 版本推荐用certs.d目录来做仓库配置,比在 config.toml 里堆mirrors更清晰,也更好按仓库粒度加证书。
先让 config.toml 指向这个目录:
[plugins."io.containerd.grpc.v1.cri".registry] config_path = "/etc/containerd/certs.d"然后按仓库地址建子目录,目录名就是仓库的 host:port:
mkdir -p /etc/containerd/certs.d/registry.internal:5000 cat > /etc/containerd/certs.d/registry.internal:5000/hosts.toml <<'EOF' server = "https://registry.internal:5000" [host."https://registry.internal:5000"] capabilities = ["pull", "resolve"] ca = "/etc/containerd/certs.d/registry.internal:5000/ca.crt" EOFserver是默认走的地址,capabilities声明这个 host 支持哪些操作,pull拉取、resolve解析标签,一般这两个就够。自签证书场景下把 CA 文件放进对应目录并写ca字段,比全局skip_verify = true安全得多。全局跳过校验只适合临时调试,别留在生产配置里。
配完不要凭感觉,用 ctr 指定命名空间直接验证:
ctr -n k8s.io images pull registry.internal:5000/library/busybox:1.364.5 日志与并发:max_concurrent_downloads、level 与 OOM 权重
剩下几项属于调优性质,但都有明确的适用场景。
[debug] level = "info" [plugins."io.containerd.grpc.v1.cri".containerd] max_concurrent_downloads = 3level = "info"是默认的平衡点。排查问题时临时改成debug,systemctl restart containerd后复现一次,日志量会明显变大,记得查完改回来,否则 journal 很快就被冲爆。
max_concurrent_downloads默认是 3,控制同时拉取镜像层的并发数。带宽充足的内网仓库可以调到 5 到 8,加快大镜像拉取;但如果你的仓库是共享的、或者节点磁盘 IO 一般,调高反而会让拉取超时变多。我一般的做法是保持默认,只有在明确观察到拉取排队严重时才调。
镜像加速这块,如果内网有统一入口,把它配成 mirror 比在每台机器上单独配 hosts.toml 更好维护:
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"] endpoint = ["https://registry.internal:5000"]docker.io是要被代理的原始仓库名,endpoint是实际去拉的地址。写多条会按顺序回退,第一条失败试第二条。
改完这一整章的内容,统一做一次校验再重启:
/usr/local/bin/containerd config dump > /dev/null && echo "配置语法 OK" systemctl restart containerd systemctl status containerd --no-pagerconfig dump会解析并输出完整配置,语法有问题会直接报错行号,比 restart 失败后再翻 journal 快得多。这个习惯值得固定下来。
5. 排查与避坑:containerd 起不来、容器起不来时的五条现场记录
前面每一步都做对了,也不代表一定跑得通。运行时的问题有个共同点:现象在上一层,原因在下一层。这一章按“现象 → 原因 → 解决”的顺序,把最常见的五种讲清楚。
5.1 现象:systemctl start 秒退,journalctl 只有一行 error
现象:systemctl start containerd执行后立刻返回失败,systemctl status显示status=1/FAILURE,journalctl -u containerd -n 50里只有一行类似failed to load plugin io.containerd.grpc.v1.cri: ...。
原因:绝大多数是 config.toml 的 TOML 语法或插件路径写错。1.7 的插件键名是带引号的完整路径[plugins."io.containerd.grpc.v1.cri"...],从 1.6 的模板抄过来、或者少写一个引号,都会让整个 CRI 插件加载失败。另一种可能是config_path指向的目录不存在。
解决:先跑containerd config dump定位报错行,再检查插件路径。certs.d目录不存在本身不致命,但如果 config.toml 里写了config_path而目录缺失,某些版本会报错,mkdir -p /etc/containerd/certs.d补上即可。改完先config dump验证,再 restart。
5.2 现象:crictl 报 connection refused 或找不到 endpoint
现象:crictl ps报connect: connection refused,或者提示validate service connection: ... no such file or directory。
原因:crictl 默认按内置顺序找 socket,如果机器上同时装过 docker,可能先连到 docker 的 socket;或者它默认路径跟 containerd 实际的/run/containerd/containerd.sock不一致。
解决:写死 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 crictl inforuntime-endpoint管 Pod 和容器操作,image-endpoint管镜像操作,两个都写上省心。crictl info能返回一大段 JSON,说明连接是通的。
5.3 现象:Pod 一直 ContainerCreating,日志提示 pull sandbox image 失败
现象:crictl pods里 sandbox 状态一直是NotReady,crictl ps -a里没有容器,journalctl -u containerd里能看到failed to pull image ... pause。
原因:4.3 说的sandbox_image不可达。要么地址是外网的,要么内网仓库需要认证而没配凭据,要么镜像根本没同步到内网仓库。
解决:先手动crictl pull <sandbox_image 地址>复现。拉不动就换地址或先ctr -n k8s.io images import离线导入,再确认crictl images | grep pause能看到。别忘了检查 kubelet 的--pod-infra-container-image是否跟 config.toml 保持一致。
5.4 现象:容器起来就退出,runc 报 cgroup 或 permission denied
现象:crictl ps -a里容器状态Exited,退出码 1 或 128,crictl logs里没什么有用信息,journalctl -u containerd里能看到 runc 相关的报错,比如unable to create new cgroup或挂载权限错误。
原因:三类。一是SystemdCgroup没开,跟 kubelet 驱动不一致;二是系统里存在旧版 runc,PATH 优先级让它被调用了;三是 SELinux 或 AppArmor 拦截了挂载。
解决:先grep SystemdCgroup /etc/containerd/config.toml确认是 true,再which -a runc和/usr/local/bin/runc --version确认版本。SELinux 场景看getenforce,临时置为 permissive 复现一次有助于判断,但别长期开着。这三步走完,八成能定位。
5.5 现象:/var/lib/containerd 撑爆,服务开始报 no space
现象:镜像拉一半失败,容器创建失败,df -h显示/var使用率 100%。
原因:镜像层和快照堆积,删容器不一定释放底层 layer;另外max_concurrent_downloads调得太高、日志级别开 debug 也会加速消耗。
解决:先看清是谁在占:
du -sh /var/lib/containerd/* 2>/dev/null | sort -h ctr -n k8s.io images ls | wc -l清理优先用crictl rmi --prune,它会删掉没被容器引用的镜像。rm -rf /var/lib/containerd/io.containerd.content.v1.content这种粗暴做法除非你确定要清空所有镜像,否则别碰。长期方案是把/var/lib/containerd挪到独立分区,用root配置项改路径,改之前先停服务、迁数据、改配置,顺序不能乱。
6. 装完如何验证:从 ctr version 到真正跑起一个容器
验证不能只看systemctl status是不是 active。我按三层来验收,从低到高,每层过了才继续。
第一层,二进制和服务。三条命令,输出能对得上就说明安装本身没问题:
systemctl is-active containerd ctr version crictl --runtime-endpoint unix:///run/containerd/containerd.sock versionctr version会打出 Client 和 Server 两段,Server 段里的 Version 应该是 1.7.23,Runtime 那行会带上 runc 的版本。如果只看到 Client 没有 Server,说明 socket 连不上,回 5.2 看。
第二层,配置是否真的生效。别信配置文件,信 dump 出来的结果:
/usr/local/bin/containerd config dump | grep -E "SystemdCgroup|sandbox_image|config_path"三项的输出要跟你改的一致。这一步特别值得做,因为containerd config default生成的文件里有些项是注释状态,grep配置文件看着像改了,实际 dump 出来才知道生效没有。
第三层,跑一个真实容器。这一步过了,才算真装完:
crictl pull registry.internal:5000/library/busybox:1.36 ctr -n k8s.io run -d --net-host registry.internal:5000/library/busybox:1.36 verify-sh /bin/sh -c "sleep 3600" ctr -n k8s.io tasks ls ctr -n k8s.io tasks exec --exec-id v1 verify-sh /bin/sh -c "echo ok"第三行tasks ls里能看到verify-sh处于 RUNNING,第四行能打出ok,说明镜像拉取、快照创建、runc 启动、shim 托管这一整条链路都是通的。跑完记得ctr -n k8s.io task kill verify-sh清掉,别留着占资源。
下面是三层验收的对照表,出问题时按行回查:
| 层级 | 命令 | 通过标准 | 失败先看 |
|---|---|---|---|
| 服务 | systemctl is-active containerd | 输出active | journalctl -u containerd -n 100 |
| 二进制 | ctr version | Server 段有版本号 | socket 路径、5.2 |
| 配置 | containerd config dump | 三项关键值与预期一致 | 4.2、4.3、4.4 |
| CRI | crictl info | 返回 JSON,ready为 true | /etc/crictl.yaml |
| 运行 | ctr tasks ls | 任务处于 RUNNING | runc 版本、5.4 |
从那以后我每次装完 containerd,不管多急,这三层验证都强制走一遍,尤其是config dump那一层。有次赶时间跳过了,结果在一台机器上折腾到半夜,最后发现只是SystemdCgroup那行被 sed 改成了True(首字母大写),TOML 解析时把它当成字符串而不是布尔值,服务照起不误,直到跑负载才暴露。希望帮到你。
本文还有配套的精品资源,点击获取