首页 / 资讯中心 / 文章详情

K8s节点NotReady:cri-containerd离线安装与CRI排障

K8s节点NotReady:cri-containerd离线安装与CRI排障 ★ FEATURED ARTICLE
简介面向在 Linux AMD64 环境部署 Kubernetes 容器运行时的开发者与运维人员这份 cri-containerd 1.7.23 发行包提供可直接落地的 containerd 运行时组件适合用于节点环境搭建、CRI 对接验证及版本升级对照。压缩包为 gz 格式共 19 个文件、约 101.22MB内含 yaml 配置、systemd service/template、shell 与环境变量脚本以及 runc、containerd、containerd-shim 系列、ctr、crictl、critest、containerd-stress 等二进制与测试工具覆盖服务配置、运行、调试和压测环节。目录按 etc、usr、opt 组织便于将配置与可执行文件分别放入系统对应位置附带的弃用说明也可为版本迁移提供参考。已有 262 人学习适合需要快速搭建与验证 containerd 运行环境的中高级读者查阅。1. 从 kubelet 报错说起cri-containerd-1.7.23-linux-amd64.tar 解决的是哪一段问题节点 NotReadyjournalctl -u kubelet里反复刷failed to get sandbox image而容器运行时那一侧的服务状态却是正常的——这种“两边看起来都没有明显异常但就是连不上”的情况八成问题出在 containerd 这一层没有接好。cri-containerd-1.7.23-linux-amd64.tar 就是用来补上这个缺口的它是 containerd 官方发布的一个离线 tar 包解出来是主进程、shim、以及ctr、crictl这几个命令行工具正确铺进系统之后kubelet 就能通过 CRI 接口跟它通信Pod 也就起得来。它适合两类人一是在不能连外网的机器上要把运行时铺起来的二是受够了发行版源里那个版本、想把小版本固定死、不希望某次apt upgrade把节点搞挂的。下面按我自己的装机顺序拆包里有什么、怎么铺、配置里改哪两行、以及五个把我拖到后半夜的坑。2. 解包核对bin/ 里那几件东西各自管什么拿到 tar 包后第一反应不应该是直接tar -C / -xf。之前有同事这么干过一次/bin下多了一堆不属于包管理器的二进制文件后来系统升级时跟旧版本互相覆盖查了两天才把问题定位清楚。代价是节点重装。所以我的习惯是先解到临时目录看清里面是什么再决定往哪里落。2.1 解到临时目录先把目录布局和文件类型看清楚# 建临时目录注意不要在 / 下面直接解 mkdir -p /tmp/cri-1.7.23 # -C 指定解包根目录避免把相对路径铺到当前工作目录 tar -C /tmp/cri-1.7.23 -xf cri-containerd-1.7.23-linux-amd64.tar # 只看两层这个包的结构很浅一般就是 bin/ 加可选的 etc/、opt/ find /tmp/cri-1.7.23 -maxdepth 2 | sort # 确认不是 0 字节的占位文件也确认是静态 ELF 而不是 .sh 包装脚本 ls -lh /tmp/cri-1.7.23/bin/ file /tmp/cri-1.7.23/bin/* 2/dev/null-C是关键参数它把解包的根目录钉在/tmp/cri-1.7.23包里的bin/containerd就落到/tmp/cri-1.7.23/bin/containerd而不是/bin/containerd。-maxdepth 2是防止包里带了一整棵 vendor 目录把屏幕刷爆。file那一步别省官方包里的二进制一般是静态链接的 Go 产物如果file告诉你某个文件是 shell 脚本那这个包八成被人动过手脚或者你下错了变体。不同变体比如带不带 CNI 插件、带不带 runc打包内容有差异别照抄别人文章里的清单。以你手上这个包find出来的结果为准缺什么自己补。文件角色谁调用它containerd主进程管理 content store、snapshot、metadata、对外提供 gRPCsystemdcontainerd-shim-runc-v2每个容器一个 shim容器的真正父进程containerd 按 PATH 查找后 execctrcontainerd 原生 CLI带 namespace 概念不走 CRI人工排障crictlCRI 客户端和 kubelet 走同一个入口人工排障、脚本巡检runc真正调 clone/unshare 建命名空间的执行器shim 调用多数变体包里不自带2.2 进程模型包名叫 cri-containerd但并没有 cri-containerd 这个进程这点必须讲清楚否则后面排障时你会去找一个根本不存在的服务。早期 CRI 确实是独立进程要单独起一个cri-containerd再连到containerd上后来 CRI 被做成 containerd 内置的 plugin编译进主二进制里。所以你现在systemctl status只需要看containerd一个服务ps -ef | grep cri什么都没有是正常的。进程树大致是containerd收到 kubelet 的 CRI 请求 → 准备 rootfs从 snapshotter 挂 overlay→ fork 一个containerd-shim-runc-v2→ shim 再 execrunc完成容器创建runc 建完就退出shim 留下来当容器的父进程、负责回收退出码和转发信号。这个设计的意义在于systemctl restart containerd不会杀掉正在跑的容器因为容器的父进程是 shim 不是 containerdcontainerd 重启后通过 shim 的 socket 重新接管。搞懂这条链后面“重启 containerd 会不会影响业务”这种问题你自己就能回答。验证 CRI plugin 是不是真的活了# 列出所有 plugingrep cri 看到 io.containerd.grpc.v1.cri 且状态是 ok 才算正常 ctr plugin ls | grep -i cri # 看 CRI 相关配置的实际生效值比翻配置文件靠谱 containerd config dump | sed -n /io.containerd.grpc.v1.cri/,/^\[/p | head -40ctr plugin ls输出里如果 CRI 那一行是error通常不是包的问题是配置文件写坏了或者 CNI 目录不存在先去看journalctl -u containerd -n 200。2.3 版本线为什么把 1.7.23 钉死而不是追最新选 1.7 这条线的理由很朴素它是 LTS 分支1.7.x 系列后期的小版本修的基本都是 CRI 侧的边角问题配置文件格式已经稳定在version 2registry.config_path这套按仓库分目录放证书的机制也能用。跨大版本升的时候config.toml的结构是会变的你手上这份从 1.7 生成的配置直接丢给更新的版本轻则告警重则起不来。判断要不要升我看三件事节点上 kubelet 的版本是否已经不再兼容这个运行时小版本内核和 cgroup 版本是不是 v2v2 上必须走 systemd cgroup driver两边的 driver 不一致会出各种资源统计和 cgroup 泄漏的玄学问题以及你有没有能力在出问题时半小时内回滚。三件事里有一件没底就别动。2.4 三个外部依赖runc、CNI 插件、内核 overlay包解完不等于能用这三样东西不在包里或者不一定在# runc 是否在 PATH 里版本能不能被 containerd 接受 command -v runc runc --version # CNI 插件目录bridge/host-local/portmap 这几个是 Pod 网络的最小集合 ls /opt/cni/bin 2/dev/null | head ls /etc/cni/net.d 2/dev/null # overlay 内核模块没加载的话 snapshotter 会一路报错 lsmod | grep overlay grep overlay /proc/filesystemsrunc缺失是最常见的翻车点因为 containerd 启动时不会检查它等到第一次真正要创建容器才报exec: runc: executable file not found这时候你已经以为是网络问题了。lsmod那一步同理overlay 没加载时containerd服务照样绿着创建 sandbox 才炸。3. 铺进系统从 tar 到 systemd 托管的 containerd 服务解包核对完就可以动手了。这一章是全文最需要照着敲的部分顺序别乱先落二进制、再生成配置、然后写 unit、最后启动验证。中途跳步的后果是配置和二进制版本对不上报错信息会把你带偏。3.1 二进制落位到 /usr/local/bin并确认没有旧版本抢路# 用 install 而不是 cp一次把权限和属主定死省得后面忘了 chmod install -m 0755 /tmp/cri-1.7.23/bin/containerd /usr/local/bin/ install -m 0755 /tmp/cri-1.7.23/bin/containerd-shim-runc-v2 /usr/local/bin/ install -m 0755 /tmp/cri-1.7.23/bin/ctr /usr/local/bin/ install -m 0755 /tmp/cri-1.7.23/bin/crictl /usr/local/bin/ # 关键检查系统里是不是还有一份发行版自带的 containerd which -a containerd containerd-shim-runc-v2 ctr crictl-m 0755是让文件对 root 可写、所有人可执行别用 0777。install的好处是它覆盖已有文件时会保留目标 inode 的替换语义不会像cp那样在某些情况下留下半截文件。which -a是这一步的重点。多数发行版会自带一份/usr/bin/containerd而/usr/local/bin通常在 PATH 里排前面看起来没问题——但 systemd 的 unit 里如果写的是绝对路径/usr/bin/containerd你装的新版本就完全没被用上containerd --version显示的还是旧的。血泪经验先用which -a确认再决定要不要把旧的那份挪走。3.2 生成 config.toml然后只改两个必须改的项mkdir -p /etc/containerd # 把默认配置打到文件里注意 会覆盖已有配置改之前先备份 [ -f /etc/containerd/config.toml ] cp /etc/containerd/config.toml /etc/containerd/config.toml.bak.$(date %s) containerd config default /etc/containerd/config.toml # 头三行确认是 version 2不是 1 head -3 /etc/containerd/config.toml第一项是 cgroup driver必须和 kubelet 对齐# 先数一下有几个 false正常只有 1 个runc 的 options 下面 grep -c SystemdCgroup false /etc/containerd/config.toml # 只替换第一处避免误伤别的地方 sed -i 0,/SystemdCgroup false/s//SystemdCgroup true/ /etc/containerd/config.toml grep -n SystemdCgroup /etc/containerd/config.toml为什么必须是truekubelet 在新版本里默认用 systemd 管 cgroup如果 containerd 这边还是 cgroupfs同一个节点上就有两套 cgroup 管理器同时往/sys/fs/cgroup里写短时间看不出问题跑久了会出现 cgroup 泄漏、failed to create cgroup、Pod 数上不去这类问题。这类问题最难查因为它是累积的不是一装就炸。第二项是 sandbox 镜像地址# 换成你内网仓库里实际存在的 pause 镜像别照抄默认值 sed -i s#sandbox_image .*#sandbox_image registry.example.com/pause:3.9# /etc/containerd/config.toml grep -n sandbox_image /etc/containerd/config.toml # 配置写完先让 containerd 自己解析一遍能 dump 出来说明语法没问题 containerd config dump /dev/null echo config oksandbox_image就是 pause 容器用的镜像kubelet 建每个 Pod 的 sandbox 时都要拉它。默认值指向官方地址在内网环境里必然拉不动现象是节点上所有 Pod 卡在FailedCreatePodSandBox。提前换掉省一次排查。3.3 systemd 单元Delegateyes 这一行千万别省# /etc/systemd/system/containerd.service [Unit] Descriptioncontainerd container runtime Documentationhttps://containerd.io Afternetwork.target local-fs.target [Service] # 前面的减号表示失败不阻塞启动某些内核里 overlay 是编译进去的modprobe 会返回非零 ExecStartPre-/sbin/modprobe overlay ExecStart/usr/local/bin/containerd Typenotify Delegateyes KillModeprocess Restartalways RestartSec5 LimitNPROCinfinity LimitCOREinfinity TasksMaxinfinity OOMScoreAdjust-999 # 显式声明 PATHshim 是靠 PATH 查找的别指望继承 EnvironmentPATH/usr/local/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin [Install] WantedBymulti-user.target逐条说几个容易写错的。Typenotify让 systemd 等 containerd 真正就绪再算启动成功没有它systemctl start会立刻返回你紧接着跑crictl就是 connection refused。Delegateyes是把 cgroup 子树委托给 containerd 管理kubelet 给 Pod 分配 cgroup 全靠这个漏了会出现 Pod 起不来但日志里啥也没有的情况。KillModeprocess保证systemctl stop只杀主进程不动 shim 和容器。OOMScoreAdjust-999是让内核在内存吃紧时优先杀业务进程而不是运行时本身。EnvironmentPATH...这行是我后来补的。有些精简镜像或加固过的系统会把 systemd 服务的默认 PATH 裁掉导致 containerd 找不到containerd-shim-runc-v2报错信息是exec: containerd-shim-runc-v2: executable file not found in $PATH看着像没装其实是找不到。3.4 启动并做三级验证进程、接口、实际拉镜像systemctl daemon-reload systemctl enable --now containerd systemctl status containerd --no-pager # 第一级二进制自身版本 containerd --version ctr version # 第二级socket 是否真的监听了路径是不是你以为的那个 ss -lx | grep containerd # 第三级走 CRI 接口问一次这一步过了才说明 kubelet 能连上 crictl --runtime-endpoint unix:///run/containerd/containerd.sock info三级验证对应三种失败模式进程没起来看journalctl -u containerd -n 100起来了但 socket 路径不对看ss -lxsocket 对了但 CRI 插件异常看crictl info的返回和ctr plugin ls。检查项命令期望结果服务状态systemctl is-active containerdactivesocketss -lx | grep containerd出现/run/containerd/containerd.sockCRI 就绪crictl infoJSON 里runtimeName为containerdshim 可执行command -v containerd-shim-runc-v2输出/usr/local/bin/...配置生效containerd config dump | grep -i systemdcgroupSystemdCgroup true4. 对接 kubeletCRI 端点、pause 镜像与联调参数containerd 自己跑起来只算完成一半另一半是让 kubelet 认识到它的存在。这一层出问题的表现高度雷同节点 NotReady、Pod 全挂、kubectl describe node里 Runtime 那一栏是空的或者版本号奇怪。逐项对下来九成出在端点路径和 pause 镜像这两件事上。4.1 端点对齐socket 路径只有一个真相# 先确认 containerd 实际监听在哪以这个输出为准不要以文档为准 ss -lx | grep containerdkubelet 侧有两种配置方式看你节点是怎么初始化的。用 kubeadm 起的参数在环境文件里# /var/lib/kubelet/kubeadm-flags.env KUBELET_KUBEADM_ARGS--container-runtime-endpointunix:///run/containerd/containerd.sock --image-service-endpointunix:///run/containerd/containerd.sock用 KubeletConfiguration 的字段名不带横线# /var/lib/kubelet/config.yaml 片段 containerRuntimeEndpoint: unix:///run/containerd/containerd.sock改完必须systemctl restart kubelet而且要看journalctl -u kubelet -n 50里有没有Failed to create CRI service之类的行。--image-service-endpoint建议一起写上虽然不写时 kubelet 会复用 runtime 的端点但显式写出来排障时少一层猜测。4.2 pause 镜像sandbox_image 改完还要确认真能拉到pause 容器是整个 Pod 网络命名空间的持有者Pod 里所有容器共享它的 netns。所以每个节点上必须能拉到这个镜像否则 sandbox 建不出来业务容器根本没机会启动。# 写一份 crictl 的默认配置省得每次敲 --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 # 手动拉一次 pause成功与否直接反映节点能不能建 Pod crictl pull registry.example.com/pause:3.9 # 注意 namespacekubelet 拉的镜像在 k8s.io 这个 namespace 下ctr 默认是 default ctr -n k8s.io images ls | grep pause最后那条命令里的-n k8s.io值得单独记一下。ctr有 namespace 概念ctr images ls默认看的是default命名空间而 kubelet 通过 CRI 拉下来的镜像全在k8s.io下面。很多人ctr images ls看不到镜像就以为拉失败了其实只是看错了地方——这是我最常遇到的误判之一。4.3 最小闭环从 sandbox 到容器跑通一次# 拉一个最小的测试镜像 crictl pull registry.example.com/busybox:latest # 看当前有哪些 sandbox 和容器-a 包含已退出的 crictl pods crictl ps -a # 看容器资源占用验证 cgroup 链路是否正常 crictl stats --no-stream | headcrictl pods里 STATE 为Ready的 sandbox 数量应该和节点上实际运行的 Pod 数对得上。对不上就说明 kubelet 创建的 sandbox 有一部分已经死了但没被清理这时候crictl pods --state notready能快速定位。4.4 kubelet 侧收尾确认# 节点状态和运行时版本Runtime Version 那一列应该显示 containerd 的版本 kubectl get nodes -o wide # 看节点详情里的容器运行时段落 kubectl describe node 节点名 | grep -A3 -i container runtime # kubelet 自己在启动时会把 pause 镜像拉下来看它有没有抱怨 journalctl -u kubelet --since 10 min ago | grep -i -E cri|sandbox|image到这一步节点应该已经 Ready 了。如果kubectl get nodes还是 NotReady 但 containerd 和 kubelet 都绿着去看 CNI 配置目录是不是空的——网络插件没装节点同样不会 Ready这跟运行时是两码事别混在一起查。5. 避坑排查五条把节点搞成 NotReady 的记录排障最怕的是没有顺序东看一榔头西看一棒子最后靠重启解决。我的固定顺序是先看 containerd 服务本身再看 socket再看 CRI 接口最后才看 kubelet 和网络。下面五条都是按这个顺序走下来定位到的每条都写了现象、原因和具体解法。5.1 先定位是哪一层一条命令树# 按顺序敲第一条失败的命令就是问题所在层 systemctl is-active containerd \ ss -lx | grep -q containerd.sock \ crictl info /dev/null echo runtime layer ok这条链能过说明运行时这层是干净的问题在 kubelet 或 CNI。过不了就从第一个断开的地方往下查。先定层再定因比上来就翻配置文件快得多。5.2 现象crictl 报 connection refused 或 no such file or directory原因两种。一是 containerd 根本没起来socket 文件不存在二是 kubelet 或 crictl 配的路径和实际监听的路径不一致比如配了/var/run/dockershim.sock这种旧路径或者/run/containerd/containerd.sock拼错了一个字母。解决ss -lx | grep containerd拿到真实路径然后三处对齐——/etc/crictl.yaml的runtime-endpoint、kubelet 的--container-runtime-endpoint、以及任何写死了路径的脚本。注意/var/run通常是/run的软链两者等价但软链在某些精简系统上不存在写/run更保险。5.3 现象failed to create containerd task: containerd-shim-runc-v2: executable file not found in $PATH原因只拷了containerd主二进制忘了 shim或者拷了但 systemd 服务的 PATH 不含/usr/local/bin。containerd 找 shim 是靠 PATH 查找的不是按自己所在目录找。解决command -v containerd-shim-runc-v2确认在不在在的话systemctl show containerd -p Environment看服务的 PATH没有就按 3.3 节那样显式写进 unitdaemon-reload后重启。别用软链到/usr/bin糊弄下次换版本又得来一遍。5.4 现象failed to mount ... invalid argument创建 sandbox 全失败原因/var/lib/containerd所在的文件系统不支持 overlayfs常见于把数据目录挂到了网络存储上或者运行环境本身已经在 overlay 里容器里跑容器。另一个可能是 overlay 内核模块没加载。解决lsmod | grep overlay和grep overlay /proc/filesystems先确认模块文件系统的问题就把 root 挪回本地盘改config.toml里的root字段实在只能用不支持的文件系统就切nativesnapshotter 兜底代价是磁盘占用和启动速度都会明显变差只适合临时救火。5.5 现象config.toml 改完不生效containerd config dump里还是旧值原因三种。containerd 启动时带了--config参数指向别处systemctl cat containerd能看到配置文件里version 1老格式的字段被忽略或者sed没匹配上默认配置里等号两边的空格跟你想的不一样你以为改了其实一个字符都没动。解决永远用containerd config dump | grep -i 关键字段验证生效值而不是grep配置文件本身。改完必须systemctl restart containerdreload对主配置不生效。5.6 现象私有仓库镜像拉不动但业务侧 kubelet 的 imagePullSecrets 明明配了原因两条独立的链路。kubelet 的imagePullSecrets最终是通过 CRI 传给 containerd 的而 containerd 自己还有一套 registry 配置证书、mirror。如果仓库用的是自签证书证书没放到 containerd 能读到的位置即使 secret 配对了照样报x509: certificate signed by unknown authority。解决把 CA 放到/etc/containerd/certs.d/仓库地址/ca.crt并在config.toml里打开registry.config_path具体写法在下一章。验证时用crictl pull而不是ctr images pull因为ctr走的认证链路和 CRI 不完全一样用ctr验证成功了不代表 kubelet 能拉。6. 进阶用 hosts.toml 和 snapshotter 配置把拉取和存储握在手里单机装通之后真正拉开差距的是两件事镜像从哪儿拉、容器层落在哪儿。这两件事都能在config.toml里定死不用改 kubelet 一行配置。6.1 用 config_path 按仓库分目录放证书和 mirror先打开开关这段加在config.toml末尾[plugins.io.containerd.grpc.v1.cri.registry] config_path /etc/containerd/certs.d再给每个仓库建一个同名目录目录名必须和镜像地址里的 host 完全一致带端口就得连端口一起写# /etc/containerd/certs.d/registry.example.com/hosts.toml server https://registry.example.com # 内网 mirrorcapabilities 不写的话只会被当作 push 目标 [host.https://mirror.example.com] capabilities [pull, resolve]mkdir -p /etc/containerd/certs.d/registry.example.com # 自签证书场景把 CA 放进去不需要重启 containerd下次拉取即生效 cp ca.crt /etc/containerd/certs.d/registry.example.com/ca.crt systemctl restart containerd crictl pull registry.example.com/pause:3.9证书文件放在这个目录下会被自动加载不用再去系统信任库里update-ca-certificates这是 1.7 这套机制最实用的地方。6.2 snapshotter 与磁盘规划默认overlayfsconfig.toml里[plugins.io.containerd.grpc.v1.cri.containerd] snapshotter和[plugins.io.containerd.snapshotter.v1.overlayfs]两处要一致改一处不改另一处会出现镜像拉下来但容器起不来的怪现象。数据目录单独挂盘df -h /var/lib/containerd定期看一眼镜像层撑爆根分区是节点挂掉的头号原因。6.3 留一颗后悔药我现在的固定动作是动配置之前cp config.toml config.toml.bak.$(date %s)二进制升级前把/usr/local/bin/containerd和 shim 一起备份到/opt/backup/containerd-版本/回滚就是两条install加一次daemon-reload。从那以后我每次装完 containerd都会强制走一遍crictl info、拉一次 pause 镜像、起一个最小 Pod 这三步三步全过才敢把节点加回集群——省下来的不是时间是半夜被叫起来的那种心气。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站