☰
runc安全升级实战指南:从风险评估到回滚的容器运行时切换
2026/10/20 17:05:34 网站建设 项目流程

1. 升级之前,先摸清 RunC 的家底

干容器平台运维这行,最怕听到的一句话就是“把 runc 升一下吧”。命令就一行,但背后牵扯的东西能让人一夜白头。runc 是整个容器生态里最底层的那块砖,所有容器进程的创建、销毁、暂停、恢复,最终都要落到它身上。K8s 调用 containerd,containerd 再调用 runc,这中间只要 runc 出一点问题,集群里所有 Pod 都会跟着遭殃。

先说清楚 runc 到底是个什么角色。它是 OCI(开放容器倡议)规范的一个参考实现,负责真正去调用 Linux 内核的 namespace、cgroup、pivot_root 这些机制,把一个文件系统目录变成一个隔离的、受限的进程。你可以把它理解成“容器进程的接生婆”——Pod 能不能生出来、生出来之后能不能正常喘气,全看它。所以升级 runc,本质上是在动整个容器平台的地基。

那为什么要升?最常见的驱动因素有三个。

第一是安全漏洞。runc 历史上出过好几次高危 CVE,比如那个让容器逃逸变得异常简单的漏洞,攻击者可以借助恶意镜像在宿主机上拿到 root 权限。这类漏洞一旦被披露,几乎就是强制升级的节奏,没有讨价还价的余地。

第二是功能迭代。比如 runc 对 cgroup v2 的支持,对 idmapped 文件的挂载能力,对某些新内核特性的适配,都需要新版本才能启用。你在宿主机上把内核升到了 5.15 以上,把 systemd 切到了 cgroup v2 模式,结果 runc 版本太老,Pod 直接起不来,或者起来了但资源统计完全乱掉,这时候不升不行。

第三是兼容性收口。有些时候 upgrade 不只是为了新功能,而是为了把老版本的 debt 还掉。比如 Kubernetes 某次发布之后,官方就要求 containerd 必须搭配某个最低版本的 runc,否则稳定性不保证。这种属于平台演进带来的强制约束,躲不掉。

有人会问:runc 升级风险这么大,能不能不升?我的回答是:能拖,但不能不升。尤其当你手里的集群规模超过百台宿主机之后,安全漏洞的暴露面是乘数级的。与其等某个安全通告点燃了再做应急,不如在平常就建立起一套可控的升级机制。这篇文章就是想把我在一线实操中积累的那套评估、准备、替换、验证、回滚的完整流程写出来,给同样在维护容器平台的兄弟们一个参考。

2. 升级前的评估:动刀之前,先看清三张表

2.1 版本兼容性矩阵:从 K8s 到 runc 的依赖链

升级 runc 不是孤立动作,它牵在一条长长的依赖链上。K8s 版本决定了它支持的容器运行时版本范围,containerd 版本又决定了它能否和新版 runc 配合,最后 runc 本身的版本还要看内核特性。所以在动手之前,先画一张依赖链清单:

  • 宿主机操作系统发行版及内核版本
  • containerd 版本(或者你用的 CRI 实现)
  • K8s 组件版本(kubelet、kube-apiserver)
  • 当前 runc 版本和升级目标版本

拿我这边举例,之前维护的一套集群用的是 containerd 1.6.x 系列,K8s 是 1.24 版本。当时要把 runc 从 1.0.0 升到 1.1.4,就得先确认 containerd 1.6.x 对 runc 1.1.x 的支持情况。containerd 在 1.6 版本里实际上是把 runc 作为独立二进制调用的,只是对版本没有做硬性校验,但 containerd 内部有些逻辑会依赖 runc 的行为差异。比如 containerd 在启动容器时会用 runc 的--root参数指定数据目录,如果 runc 的存储格式变了,containerd 就可能读不到之前的容器状态。

这里有个小技巧:去读 containerd 的 CHANGELOG,专门看里面提到的 runc 版本号。containerd 官方在发新版的时候,会把它测试过的最小 runc 版本写进 release note。没有硬性约束,但那是人家验证过的组合,照着用是最稳妥的。

还有一个容易漏掉的点:runc --version里除了版本号,还会输出 runc-spec 的版本。这意味着你升级的不仅是一个二进制,还有它遵循的 OCI spec 版本。如果集群里有大量存量容器是用旧 spec 创建的,新版本 runc 在操作这些容器时可能会出现 spec 转换的兼容问题。这个问题在升级后第一次重启容器时最容易暴露。

2.2 二进制特性差异:动态链接还是静态编译

runc 的发布包里提供了两种二进制:runc和runc.static。这个差异非常重要,是很多人在升级时踩坑的重灾区。

普通的runc二进制是动态链接的,依赖 glibc 的某些版本。如果你宿主机上的 libc 版本比编译时的老,运行起来会直接报version 'GLIBC_2.28' not found这类错误。静态编译的runc.static没有这个烦恼,所有依赖都打进去了,什么环境都能跑。

我的建议是:容器平台的宿主机上,优先使用静态版本。原因很简单——这份二进制要运行的宿主机动辄几百台,操作系统发行版可能有细微差异,小版本升级也可能带来 libc 的变化,动态链接版本很容易在某个角落翻车。静态版本则像一把万能钥匙,放哪都能用。

但静态版本也不是没有坑。它开启的 build tag 可能和动态版本不同,某些特性会跟着编译选项走。比如 CGO 相关的特性,静态编译时需要设置好CGO_ENABLED=0,否则 build 出来的东西还是会偷偷依赖外部库。不过从官方 release 页面下载的静态包,这些都已经处理好了,我们不需要重新编译,直接信任官方的构建产物就行。

2.3 评估清单:动手前先过一遍这个检查表

我每次升级前都会把下面这个检查表过一遍,不是走形式,是真的能提前发现很多问题:

  • 集群总数和单批次升级范围(全量还是滚动)
  • 是否存在使用非默认 runtimeClass 的 Pod(比如 Kata、gVisor)
  • 是否存在容器逃逸类安全告警的历史记录(判断本次升级是否紧急)
  • 宿主机内核是否存在 cgroup v2 与 v1 混用的情况
  • containerd 的配置文件中是否显式指定了 runc 路径
  • K8s 集群是否有 pod security admission 相关策略(升级后需要跑一遍验证)
  • 备份工具是否就位(runc 二进制备份、containerd 状态目录备份)
  • 监控面板是否覆盖容器运行时错误指标

这个清单看起来琐碎,但每一条背后都有对应的血泪教训。比如 runtimeClass 那一条,如果你集群里有基于 runc 改造的沙箱运行时,直接用新版本 runc 替换系统默认的 /usr/bin/runc,并不会影响沙箱,因为沙箱运行时用的是独立编译的二进制。但反过来,如果你升级的时候误把沙箱组件的二进制也换成了官方 runc,沙箱直接报废。

再比如 containerd 配置中的runtime_root路径,如果 containerd 配置了自定义的 runc 数据目录,升级时要注意新旧版本的数据格式是否兼容。runc 从 1.0 到 1.1 的数据格式变化不大,但也不能完全排除。

3. 升级路径设计:三种常见套路及其取舍

3.1 二进制替换方案:最直接也最危险

最常见的升级方式就是替换宿主机上的 /usr/bin/runc 二进制。有些发行版的 containerd 包会自带的 runc 放在/usr/lib/containerd/runc目录,替换之前先确认 containerd 实际调用的是哪个路径。你可以用ss -lntp找到 containerd 进程,然后用ls -l /proc/<pid>/root/usr/bin/runc查看它当前引用的二进制。

替换流程本身不复杂:

cp /usr/bin/runc /usr/bin/runc.bak.$(date +%Y%m%d) cp runc.static /usr/bin/runc chmod +x /usr/bin/runc

但这里有一个细节经常被忽略:runc 作为 containerd 的子进程被 fork 出来,如果只是替换了磁盘上的二进制,已经运行中的容器管理流程并不会立即感知到变化。因为 containerd 每创建一个容器时,会重新从磁盘加载 runc 二进制,所以替换后新建的容器才会使用新版本,存量容器不受影响。这个特性听起来很美好,但隐藏的问题在于:如果新旧版本的规范不一致,存量容器在后续操作(比如 exec、stop、state)时,会由新版 runc 接管,可能出现状态读取不到的情况。

我在一次升级中真的遇到过:替换完二进制后,存量容器继续运行正常,但再过了一段时间之后,有人对某个三天前创建的容器执行docker exec,直接报错说找不到容器。查了半天才发现,新版本 runc 默认的数据目录格式和旧版本有细微差异,导致虽然 containerd 知道容器存在,但 runc 已经识别不了旧容器了。

所以纯二进制替换方案,只适合容器存活周期很短、随时可以全部重建的场景。如果集群里存在大量长生命周期容器,纯二进制替换就需要配合“重启存量容器”的计划一起做。

3.2 安全升级策略:先灰度,再放量

在我维护的平台上,runc 升级从来不是一把梭全量替换,而是走灰度流程。整个流程分四步走:

第一步是单机验证。挑一台负载不高的宿主机,先备份现有 runc 二进制,然后替换成新版本,创建几个临时容器,验证创建、exec、stop、rm、pause、resume 这几个核心操作全部正常。这一步的目的不是验证新版本的功能,而是验证新版本在这台宿主机上的内核、libc、cgroup 配置下能正常工作。

第二步是金丝雀集群。选一个业务影响面最小的节点池,容量在 10 台以内,业务流量不大。全量替换这些节点的 runc,观察一个完整的发版周期(至少 24 小时)。重点看两类指标:一是容器创建失败率,二是非正常重启率。我见过一个问题很隐蔽的版本,容器创建成功率正常,但运行一段几分钟后就出现个别进程被杀掉,监控指标完全正常,最后靠翻 kubelet 日志才发现是 runc 在 cgroup 资源统计上报时触发了内核 bug。

第三步是小流量放量。金丝雀没问题之后,按批次滚动替换,每批次不超过集群总量的 20%,每批次之间间隔至少 30 分钟。注意这里的 30 分钟不是拍脑袋定的,而是给监控系统留出足够的时间窗口,让问题能够在告警延迟之后还能浮出水面。如果 5 分钟内没有告警就急着推下一批,很多隐性问题是发现不了的。

第四步是全面替换。最后一批替换完成后,再观察 2 小时。如果一切平稳,就可以把备份清理掉,或者保留 72 小时再清理。备份别立刻删,有些问题会在升级后第二天才出现,到时候没有回滚余地就更被动了。

3.3 与 containerd 联动升级:避免单点升级的连带伤害

有时候 runc 升级不是孤立事件,而是配合 containerd 升级一起做的。比如 containerd 从 1.5 升到 1.6,runc 从 1.0.0 升到 1.1.4,这两个动作不建议分两步走,因为两个中间版本组合可能没有经过社区充分验证,出了问题都搞不清楚是哪个版本的问题。

联动升级的场景下,方案会更谨慎一些。建议使用 containerd 官方包自带的 runc,而不是自己单独替换。原因在于,containerd 的 RPM/DEB 包在发布时,会把它验证过的 runc 一并打包进去,路径、权限、依赖关系都是处理好的。这比自己手动替换到 /usr/bin/runc 更符合 containerd 的预期。

但这也带来一个问题:很多人安装 containerd 用的不是官方包,而是发行版自带的包。发行版自带的 runc 版本可能很老,和新装的 containerd 不匹配。所以联动升级前,先确认 containerd 是通过哪种方式安装的,这决定了 runc 应该怎么升级。

另外,联动升级时 containerd 本身的版本也和 cgroup 驱动强相关。containerd 1.5 时代很多人用 cgroupfs 驱动,1.6 之后 systemd cgroup 驱动成了主流。如果宿主机上的 systemd 版本过老,配合新版 containerd 的 systemd cgroup 驱动会有兼容性问题。这个问题虽然不属于 runc 直接职责,但最终表现是容器资源限制不生效或 Pod 杀不掉,排查时会绕到 runc 头上。

4. 核心实操:从下载二进制到完成验证的完整记录

4.1 获取并校验新版 runc

下载环节没什么多说的,但校验环节很多人会跳过,这是不该漏的。runc 官方 release 页面提供了 checksum 文件和签名文件。虽然容器平台大多在内网环境,下载可能要走私有的制品库,但校验这一步还是要做。至少算一下 sha256 值,和官方提供的 checksum 对一下。如果是安全意识更高一些的平台,还应该用 gpg 验证签名。

我用一个变量把版本号带上,方便后续脚本统一处理:

RUNC_VERSION=1.1.4 RUNC_ARCH=amd64 wget "https://github.com/opencontainers/runc/releases/download/v${RUNC_VERSION}/runc.static-${RUNC_ARCH}" wget "https://github.com/opencontainers/runc/releases/download/v${RUNC_VERSION}/sha256sum" sha256sum -c sha256sum 2>&1 | grep "runc.static-${RUNC_ARCH}"

校验通过后,先别急着替换。在升级之前,先记录旧版本的信息,方便升级后对比:

/usr/bin/runc --version

把输出保存下来,之后runc --version对比时能一眼看到差异。

4.2 备份与替换:小心驶得万年船

备份环节要区分两种情况。

第一种情况是宿主机上 runc 由软件包管理。比如通过 dpkg 或 rpm 安装的 runc,可以直接用包管理器备份。这个备份方式最干净,回滚时一条命令就能恢复。

dpkg -l | grep runc # 查看已安装的 runc 版本及包名 dpkg -P runc # 卸载 dpkg -i /path/to/runc.deb # 回滚

第二种情况是纯二进制方式安装,比如从 github releases 下载的。这种备份方式就是简单粗暴的复制:

cp /usr/bin/runc /usr/bin/runc.bak.$(date +%Y%m%d)

这里有一个容易忽略的权限问题。runc 如果被替换后没有保留可执行权限,containerd 创建容器时会直接报permission denied。所以替换完成后一定要执行:

chmod 755 /usr/bin/runc chown root:root /usr/bin/runc

替换完成后,建议执行一次runc --version确认二进制本身能正常运行。这一步不用创建容器,能跑起来说明动态链接没问题(如果用了动态版本),静态版本这一步基本必过。

4.3 创建验证容器:全生命周期操作测试

替换完成后,最直接的验证是创建一个测试容器,把核心生命周期操作都跑一遍。

先创建一个简单的 OCI bundle,或者直接用 runc 自带的 spec 命令:

mkdir -p /tmp/runc-test/rootfs runc spec --bundle /tmp/runc-test

然后拉一个最小镜像,把 rootfs 解出来:

mkdir -p /tmp/runc-test/rootfs cd /tmp/runc-test # 用一个极小的 busybox rootfs curl -L https://github.com/.../busybox-rootfs.tar.gz | tar -xz -C rootfs

接着启动容器、执行命令、停止容器,把整个流程走一遍:

runc run demo-container # 前台启动,会阻塞 runc exec demo-container /bin/echo "hello runc" runc pause demo-container runc resume demo-container runc stop demo-container runc delete demo-container

这一套操作能验证的不仅是 runc 本身能工作,还包括 cgroup 操作、namespace 创建、rootfs 挂载等核心功能。如果这些都能正常跑通,说明新版本在最基本的维度上是没问题的。

但要注意:这个测试容器没有接入 CNI 网络,所以不会验证网络插件相关的问题。runc 层面本身不关心网络——它创建容器时的 network namespace 是否被 CNI 正确配置,那是 containerd 和 CNI 插件的事。但如果你想更贴近真实场景,最好通过 containerd 的 ctr 命令拉取一个真正的镜像创建容器,这样 CNI 的链路也能被覆盖到。

4.4 用 ctr 命令验证 containerd 层

通过 runc 直接创建容器验证的是 runc 本身,但生产环境里 runc 几乎都是被 containerd 调用的,所以更关键的验证是从 containerd 的角度发起一次完整的容器生命周期操作。

ctr images pull docker.io/library/busybox:latest ctr run --rm docker.io/library/busybox:latest test-container /bin/echo "hello containerd" ctr tasks list

ctr run和ctr tasks list能验证 containerd 与 runc 的配合是否正常。如果ctr run能正常运行,说明 containerd 调起 runc 的全链路是通的。

这一步还有一个隐藏收益:能让你在 K8s 介入之前,先确认容器运行时层面的输出是否符合预期。如果连ctr run都报错,就别急着去重启 K8s 集群的节点了,先在运行时这一层把问题定位清楚。

4.5 K8s 集群层验证:调度、启动、销毁一条龙

上面两步验证通过后,才能在 K8s 集群上做真实负载验证。这一步要创建一个有特点的测试 Pod,把平时容易踩坑的配置都包含进去:

apiVersion: v1 kind: Pod metadata: name: runc-upgrade-test spec: containers: - name: nginx image: nginx:1.25 resources: limits: cpu: "1" memory: 512Mi requests: cpu: "500m" memory: 256Mi securityContext: runAsNonRoot: true allowPrivilegeEscalation: false

这个 Pod 包含了资源限制(会触发 cgroup 写入)、非 root 运行(会触发 user namespace 和 capability 操作)、nginx 长期运行(验证稳定性)。把这几个要素都覆盖到,才算是一个真实场景的验证。

部署这个 Pod 后,要做的验证动作包括:

  • Pod 是否能正常调度到目标宿主机
  • kubelet 能否正常创建容器(不出现failed to start container错误)
  • 容器日志是否能正常收集(fluent-bit 等采集端不报错)
  • 执行kubectl exec进入容器是否正常
  • 删除 Pod 后,容器是否被完整清理(/var/lib/containerd 下不残留孤儿容器)

最后一步容易被忽略。如果容器删除后残留大量未清理的 runc 容器实例,会逐步占满宿主机磁盘。runc 升级后,runc delete的清理逻辑如果有变化,可能导致孤儿容器数量增长。所以升级后的一段时间内,开个定时任务扫一下 /var/lib/containerd 的目录大小是个好习惯。

5. 常见坑位与排查实录:升级后的那些幺蛾子

5.1 容器启动报错:找不到符号

升级后如果创建容器时报类似下面的错误:

failed to create runc console: unable to find symbol \"_Z8...\": program is not dynamically linked

这个错误通常出在 runc 动态链接库的问题上。比如 containerd-shim 在启动时会尝试加载某些动态库,但它们之间的符号版本不匹配。很多情况下,这个问题的根源不是 runc 本身,而是 containerd-shim 版本太老,无法解析新版 runc 的动态符号。这种问题在静态编译的 runc 上不会出现,因为它不依赖外部动态库。

如果非要用动态版本的 runc,解决方案是把 containerd-shim 也一并升级,让 shim 和 runc 的编译基础在同一条时间线上。这也是为什么我始终建议直接用runc.static——省心。

5.2 Pod 频繁重启但运行时不报错

另一个隐蔽的问题是 Pod 创建成功,但运行一段时间后 kubelet 上报容器异常退出。kubelet 日志里可能只看到container exited with code 137,这个 exit code 表面看是 SIGKILL,但具体是被 OOM killer 杀了,还是被某种内核机制遏制了,需要进一步看内核日志:

dmesg | grep -i "killed process" journalctl -k | grep -i "oom\|killed"

如果是升级前没有这个问题、升级后频繁出现,优先怀疑是 cgroup 内存统计的问题。新版本 runc 可能启用了更严格的内存计数方式,有些之前临界超限的容器现在更早触发 OOM。这种情况不是 bug,而是行为变化,但对你来说就是故障。解决思路是重新审视业务容器的内存 request/limit 设定,把临界值放宽一些。

5.3 存量容器状态丢失的教训

这个坑我在前面提到过,值得单独拿出来讲。在一次升级后的第二天,业务方报告说某个跑了十天的容器无法 exec。报错内容是:

container not found

排查过程比较绕。因为容器还活着(业务没中断),但新 runc 看不到它的状态。最后通过runc list看实际状态时才发现,这个容器在新旧数据目录切换后,状态文件没有正确迁移。

这个问题的解法比较暴力:存量容器全部重启。重启之后状态信息会按新格式重新生成,后续就不会再出问题。但这也提醒我一个原则:如果存量容器中有关键业务且不能中断,升级 runc 前应该先评估这些容器的存活预期。存活超过一个月的容器,最好等它们自然死亡后再升级,或提前安排维护窗口让它们重启。

5.4 回滚实操:保留退路比升级本身更重要

如果升级后发现异常,回滚的要义是快、准、稳。前提是升级前备份做好。

# 停止 containerd 中受影响的服务(按平台情况而定) systemctl stop containerd # 恢复旧版本 cp /usr/bin/runc.bak.$(date +%Y%m%d) /usr/bin/runc # 重新启动 containerd systemctl start containerd

这里有两个细节。

第一,替换回旧 runc 后,新 runc 创建的容器状态可能会让旧 runc 无法识别。回滚后,已经由新 runc 创建的容器,旧版本可能接管不了。所以回滚前要先把评估范围内的测试 Pod 全部删除,避免状态混乱。

第二,如果升级过程中 containerd 已经在运行,容器状态目录在内存缓存和磁盘之间可能有差异。回滚时,建议把 containerd 停干净(stop 后确认进程退出),再动二进制。这就是为什么很多人在升级时坚持做节点排水(drain)——排水后节点上的容器全部迁走,升级后节点是干净的,任何状态兼容性问题都不会残留。

5.5 常见问题速查表

为了方便现场排查,我整理了升级过程中的常见问题及对应解法:

现象可能原因解决方向
runc 命令无法执行,报 GLIBC 版本错误动态链接版本与宿主机 libc 不匹配换用 runc.static,并确认架构正确
容器创建失败,报 device or resource busy旧容器未完全释放,device cgroup 冲突检查 /var/lib/containerd 下是否有残留容器,清理后重试
容器启动成功但网络不通CNI 插件与 runc 版本行为不兼容重新查看 CNI 的日志,必要时升级 CNI 插件
Pod 删除后磁盘空间异常增长孤儿容器或日志文件未清理扫描 /var/lib/containerd 目录,用 ctr tasks list 对比确认
存量容器 exec 报 container not found新旧版本状态数据格式不兼容存量容器重启,之后保持 runc 版本一致
runc list 显示容器状态异常runc 数据目录损坏或版本切换重建 runc 数据目录(需停止所有容器)
cgroup 路径写入权限报错cgroup 驱动从 cgroupfs 切到 systemd 后路径变化检查 containerd 配置文件中的 cgroup 驱动配置
升级后 K8s 节点 NotReadykubelet 与运行时版本不匹配查看 kubelet 日志,确认错误详情后按提示处理

这张表不是万能药,但能覆盖我在实操中遇到的大部分情况。关键是排查时思路要清晰:先判断是 runc 层问题,还是 containerd 层问题,还是 K8s 层问题。三层之间链路比较长,但每一层的日志都有特征,顺着特征排查,不走弯路。

6. 升级后的一段观察期:比升级本身更重要的是看什么

6.1 监控指标:容器运行时层面的关键信号

升级完成后,监控不能立刻关上。我是习惯在升级后保持一个专门的监控面板,观察至少 48 小时。重点看几个指标。

容器的启动时延。runc 升级后,如果新版本增加了审计逻辑或安全加固,容器的创建时延可能会上升。指标如果从原来的 50ms 变成了 200ms,不一定致命,但说明有额外开销。如果从 50ms 变成 2s,那就不正常了,需要进一步看是 runc 还是内核链路上的问题。

kubelet 的 sync 循环错误率。kubelet 每隔一段时间会和容器运行时做一次状态同步,如果同步出错率上升,说明 containerd 或 runc 的状态上报不稳定。这个指标是反映运行时健康度的晴雨表,比容器本身的监控更前置。

节点层面的 OOM 事件频次。升级前如果几乎没有 OOM,升级后频繁出现,那大概率是 cgroup 版本或资源统计口径变化导致的行为差异,需要调整资源配比。

6.2 日志特征:识别潜在的不稳定因素

日志方面我关注三个来源:kubelet 日志、containerd 日志、内核日志。

kubelet 日志中如果出现Failed to create pod sandbox这类错误,而且频率是零星出现而不是持续报错,就说明某些特定配置(比如某些 Pod 的 securityContext 设置)和新的 runc 有兼容摩擦。这种问题可能影响的是个别存量工作负载,但如果不处理,后续新发版的 Pod 也会持续踩到。

containerd 日志中的level=warning是重点观察对象。很多问题在升级后都是以 warning 形式出现,不会立刻导致故障,但如果堆积多了会导致 containerd 性能劣化。我见过一个案例是 containerd 日志里大量出现failed to load cgroupwarning,几天后节点上的 Pod 迁移变慢,最后排查发现是 cgroup 目录残留导致的。

内核日志的观察重点是 memory、cgroup 相关条目。journalctl -k | grep cgroup的输出在升级后要多看几眼。

6.3 升级后真实体验记录

这里分享一次比较典型的升级案例。某次把一个测试环境的 20 台节点从 runc 1.0.0 升到 1.1.4,前 15 台一切正常,到第 16 台开始,容器创建偶尔失败。错误信息是:

failed to write cgroup file: open /sys/fs/cgroup/...: no such file or directory

排查后发现,前 15 台节点的 cgroup 目录布局是旧版 runc 创建的,第 16 台节点由于之前经历过一次 containerd 数据清理,用的是新版目录布局。升级前没有注意到这 16 台和其他节点的历史差异,导致新旧 runc 对 cgroup 路径的预期不一致。

这个事故给我的启发是:升级 runc 前,对各节点的“历史债务”要有充分认知。不是所有节点都长一个样,有的经历过 containerd 重装,有的切换过 cgroup 驱动,有的动过内核。这些差异都会影响升级后的行为。所以升级方案里一定要包含“节点指纹采集”环节——记录每个节点的内核版本、cgroup 模式、containerd 版本、容器数量、孤儿容器数量。有了这些信息,升级过程中遇到怪异报错时才能快速定位是“普遍问题”还是“特定节点问题”。

7. 写在最后的经验:升级 runc 不只是更换一个文件

我做了这么多年容器平台运维,最深的一点体会是:runc 升级从来不是一个单纯的“下载、替换、重启”动作,而是一次对底层运行时稳定性的全面体检。它考验的是你对集群整体的掌控力——你有没有完整的清单,有没有归档好的备份,有没有验证流程,有没有兜底的回滚方案。这些都准备好了,升级只是一个机械动作。

在多次升级的实操中,我的习惯是:每次升级都会把当时的 check list、遇到的问题、排查路径记录下来,沉淀成这个环境自己的升级手册。每个集群的历史不一样,操作系统的版本不一样,内核 patch 不一样,甚至 containerd 的启动参数不一样,这些细节都会导致手册需要微调。别指望网络上的一份通用教程能覆盖你的全部场景,真正的升级手册只能靠自己在实战中磨出来。

最后分享一个小技巧。在升级 runc 之前,先手动把宿主机上的所有容器状态拍个快照:

runc list --format json > /tmp/runc-list-before-upgrade.json

升级后再次执行同样的命令,用 diff 对比两份 JSON。如果两份文件能对齐——旧容器还在,新容器能创建——那说明升级过程对存量容器的影响几乎为零。这个方法比查日志直观得多,而且能给出具体的、可控的验证依据。遇到复杂环境时,这一步能帮你快速判断是否要继续推进下一批节点。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询