K8s 节点切换容器运行时(Docker -> containerd)排错经历

主要内容:

在一次 gVisor 沙箱部署中,将 K8s 节点的容器运行时从 Docker 切换到了 Containerd。操作完成后,节点 NotReady,etcd 和 apiserver 全部 CrashLoopBackOff ------ 整个集群瘫痪。这篇文章完整记录了我从发现问题到定位根因、再到修复的全过程。

一、背景

本次记录基于Kubernetes 集群中部署 gVisor 沙箱运行时。初始集群架构为 Docker + dockershim 架构,完整调用链为:kubelet内置 dockershimdockerdcontainerdcontainerd-shim-runc-v2runc宿主机内核 (Namespace+Cgroups)

在此架构下,首先尝试创建 RuntimeClass 并启动 gVisor Pod,结果 kubelet 报错:

bash 复制代码
kubectl describe pod gvisor-test

# 关键输出如下所示:
Warning  FailedCreatePodSandBox  38m (x206 over 117m)  kubelet  
Failed to create pod sandbox: rpc error: code = Unknown 
desc = RuntimeHandler "runsc" not supported

根因 :dockershim 是 K8s v1.23 之前内置在 kubelet 中的硬编码适配器,它只认识 runc,不认识 runsc( gVisor的对外入口 )。即使 Docker 层面注册了 runsc,Kubelet 也无法把 runtimeClassName: gvisor 这个指令传达给 Docker。

因此,要在 K8s 中使用 gVisor,必须把容器运行时从 Docker(dockershim)切换到 Containerd(原生 CRI 实现)。

二、操作过程

2.1 环境信息

项目 版本
K8s 版本 v1.22.0(kubeadm 部署)
操作系统 Ubuntu 22.04
原容器运行时 Docker 28.1.1(通过 dockershim)
目标容器运行时 Containerd
节点角色 control-plane(单节点)

2.2 切换步骤

步骤一:标记节点不可调度并驱逐 Pod
bash 复制代码
NODE=$(hostname)
kubectl drain "$NODE" --ignore-daemonsets --delete-emptydir-data
步骤二:停止 Docker 与 Kubelet
bash 复制代码
systemctl stop kubelet
systemctl stop docker
systemctl disable docker
pkill -9 dockerd || true
pkill -9 containerd || true
sleep 2
步骤三:安装并配置 Containerd
bash 复制代码
apt-get update
apt-get install -y containerd.io
mkdir -p /etc/containerd
containerd config default > /etc/containerd/config.toml

修改 /etc/containerd/config.toml 核心配置:

toml 复制代码
version = 2

[plugins."io.containerd.grpc.v1.cri"]
  sandbox_image = "registry.aliyuncs.com/google_containers/pause:3.8"

  [plugins."io.containerd.grpc.v1.cri".containerd]
    snapshotter = "overlayfs"

    [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 = true

  [plugins."io.containerd.grpc.v1.cri".registry.mirrors]
    [plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
      endpoint = ["https://docker.m.daocloud.io"]
    [plugins."io.containerd.grpc.v1.cri".registry.mirrors."registry.k8s.io"]
      endpoint = ["https://registry.aliyuncs.com/google_containers"]
步骤四:修改 Kubelet 配置

备份原配置:

bash 复制代码
cp /var/lib/kubelet/kubeadm-flags.env /var/lib/kubelet/kubeadm-flags.env.bak

修改 /var/lib/kubelet/kubeadm-flags.env

bash 复制代码
KUBELET_KUBEADM_ARGS="--network-plugin=cni --pod-infra-container-image=registry.aliyuncs.com/google_containers/pause:3.8 --container-runtime=remote --container-runtime-endpoint=unix:///run/containerd/containerd.sock"
步骤五:启动服务
bash 复制代码
systemctl daemon-reload
systemctl enable containerd
systemctl start containerd
systemctl start kubelet
kubectl uncordon "$NODE"

此时集群崩溃,如下:

三、故障现象

3.1 节点状态

bash 复制代码
kubectl get nodes

输出:

text 复制代码
NAME                STATUS     ROLES           AGE   VERSION
k8s-master-shizai   NotReady   control-plane   10d   v1.22.0

3.2 控制平面 Pod 全部崩溃

bash 复制代码
kubectl get pods -n kube-system

输出:

text 复制代码
NAME                                 READY   STATUS             RESTARTS
etcd-k8s-master-shizai               0/1     CrashLoopBackOff   5
kube-apiserver-k8s-master-shizai     0/1     CrashLoopBackOff   5
kube-controller-manager-...          1/1     Running           0
kube-scheduler-...                   1/1     Running           0
coredns-...                          0/1     Pending           0

etcd 和 apiserver 双双崩溃,整个集群事实上已经瘫痪。

四、排查过程

4.1 查看 Kubelet 日志

bash 复制代码
journalctl -u kubelet --since "5m" --no-pager

关键输出片段:

text 复制代码
failed to get cgroup stats for "/kubepods/burstable/podxxx": 
failed to get container stats: no such file or directory

kubelet 找不到容器的 cgroup 统计信息, 所以这跟 cgroup 有关。

4.2 检查 Containerd 的 cgroup 配置

bash 复制代码
crictl info | grep -A 5 -B 5 systemdCgroup

输出以下内容:

text 复制代码
"systemdCgroup": false

关键点:containerd 的 cgroup 驱动是 cgroupfs,而不是 systemd

4.3 检查 Kubelet 的 cgroup 驱动

bash 复制代码
ps aux | grep kubelet | grep cgroup-driver

没有任何输出------说明 Kubelet 没有显式指定 --cgroup-driver,使用的是编译默认值。 在 K8s v1.22 中,Kubelet 的默认 cgroup 驱动是 systemd(在 systemd 系统上)。

4.4 关键发现

Kubelet 使用 systemd,containerd 使用 cgroupfs------驱动不一致!这是发生错误的原因 当 Kubelet 通过 systemd API 创建 cgroup 目录时,runc 却绕过 systemd 直接写 /sys/fs/cgroup,导致两个"管家"( cgroupfs 和 systemd )抢同一个目录,最终 systemd 为了维护一致性强制删除了 runc 创建的目录,Kubelet 再去读时发现目录消失,节点直接 NotReady。

五、为什么 cgroup 驱动不一致会导致节点崩溃?

要理解这个问题,需要先搞清三层概念:cgroup、cgroupfs、systemd

5.1 cgroup 是什么?

cgroup 是 Linux 内核自带的功能,不是软件、不是服务,而是内核里的资源管理机制。它负责限制、统计、隔离进程的 CPU、内存、磁盘 IO 等资源。

5.2 cgroupfs 是什么?

cgroupfs 是"直接操作 cgroup 的方式" 。表现就是通过文件系统直接写文件:

bash 复制代码
echo 100000 > /sys/fs/cgroup/cpu/my-pod/cpu.shares

老容器运行时(如旧版 Docker 还有默认配置的 containerd)都用这种方式,简单但容易跟 systemd 冲突。

5.3 systemd 在 cgroup 管理中扮演什么角色?

systemd 有两个角色:

  1. PID 1(服务管理器) :启动/停止 kubelet、containerd
  2. cgroup 管理者 :当设置 SystemdCgroup=true 时,runc 不再自己写 /sys/fs/cgroup,而是通过 systemd API 操作 cgroup

5.4 cgroup v1 和 v2 的核心差异

cgroup v2 最关键的变化是:强制单一委托者------整个 cgroup 树只能有一个管理者(通常是 systemd)。其他进程如果绕过 systemd 直接写文件,systemd 会认为这是"非法篡改",为了维护自身一致性,会强制删除这些目录。

特性 cgroup v1 cgroup v2
结构 多树并行 单树统一
管理 松散,允许多方直接操作 强制单一委托者(systemd)
冲突风险 较低 极高------不一致就删除

而 Ubuntu 22.04 (当前环境) 默认使用 cgroup v2,这就是问题所在。

text 复制代码
Ubuntu 22.04 系统启动
    │
    ▼
systemd 挂载 /sys/fs/cgroup
    │
    ▼
systemd 在 /sys/fs/cgroup/ 下建立自己的层级
    │
    ├──────────────────────┬─────────────────────┐
    ▼                      ▼                     ▼
kubelet 启动          containerd 启动      其他系统服务
    │                      │
    ▼                      ▼
检查 cgroup 驱动      检查 SystemdCgroup
(默认: systemd)      (默认: false)
    │                      │
    ▼                      ▼
kubelet 通过 systemd   runc 直接往
API 申请 cgroup       /sys/fs/cgroup/ 写文件
    │                      │
    └──────────┬───────────┘
               ▼
systemd 发现 /sys/fs/cgroup/ 下有它不认识的目录
               │
               ▼
systemd 为维护自身一致性,删除/覆盖了 runc 创建的目录
               │
               ▼
Pod 的 cgroup 路径被删 → Kubelet 无法获取容器状态
               │
               ▼
        节点 NotReady

六、修复方案

6.1 修改 Containerd 配置

编辑 /etc/containerd/config.toml,确保以下配置正确:

toml 复制代码
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
  SystemdCgroup = true

6.2 重启 Containerd 并验证

bash 复制代码
systemctl restart containerd
crictl info | grep systemdCgroup

输出:

text 复制代码
"systemdCgroup": true

6.3 等待节点恢复

大约 30-60 秒后:

bash 复制代码
kubectl get nodes

输出:

text 复制代码
NAME                STATUS   ROLES           AGE   VERSION
k8s-master-shizai   Ready    control-plane   10d   v1.22.0
bash 复制代码
kubectl get pods -n kube-system

所有控制平面组件恢复 Running 状态。

七、还有可能的问题

7.1 镜像拉取失败

containerd 默认从 registry.k8s.io 拉取 pause 镜像,国内访问极不稳定。配置了镜像加速后解决(上文已修改):

toml 复制代码
sandbox_image = "registry.aliyuncs.com/google_containers/pause:3.8"

[plugins."io.containerd.grpc.v1.cri".registry.mirrors]
  [plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
    endpoint = ["https://docker.m.daocloud.io"]
  [plugins."io.containerd.grpc.v1.cri".registry.mirrors."registry.k8s.io"]
    endpoint = ["https://registry.aliyuncs.com/google_containers"]

7.2 etcd 数据目录权限不匹配

Docker 启动的 etcd 容器以 root 运行,目录权限 root:root。containerd 在某些配置下尝试以 nobody 用户写入,导致权限不足。

虽然这次没有触发,但标准解决方案是切换前修改目录权限:

bash 复制代码
chown -R 65534:65534 /var/lib/etcd
chmod 700 /var/lib/etcd

八、经验教训

8.1 核心原则

在 cgroup v2 系统(如 Ubuntu 22.04+)上,Kubelet 和容器运行时(runc)必须使用相同的 cgroup 驱动,且必须是 systemd。

8.2 操作前检查清单

| 检查项 | 命令 | 期望 |
|------------|------------------------------|----------------------|-----------------|
| cgroup 版本 | stat -fc %T /sys/fs/cgroup | cgroup2fs |
| Kubelet 驱动 | `ps aux | grep kubelet` | 未显式指定 → systemd |
| 容器运行时配置 | `crictl info | grep systemdCgroup` | true |

8.3 快速修复命令

如果遇到同样的问题,执行以下命令即可恢复:

bash 复制代码
# 修改配置
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/g' /etc/containerd/config.toml
# 重启 containerd
systemctl restart containerd
# 等待节点恢复
kubectl get nodes -w

九、总结

这次故障的本质是:在 cgroup v2 系统上,容器运行时和 Kubelet 的 cgroup 驱动不一致。Kubelet 默认用 systemd,containerd 默认用 cgroupfs,两者冲突导致 systemd 强制清理目录,Kubelet 无法获取容器状态,节点 NotReady。

K8s 官方文档中有一句话: "If you use systemd as the init system, you should set the cgroup driver to systemd." 这句话我当时没在意,现在终于深刻理解了。

在 Kubernetes 节点上,默认值是最危险的东西, 越是不显式配置,越是在给未来的故障埋下伏笔

相关推荐
沉迷学习 日益消瘦3 小时前
10-Gateway API
运维·kubernetes·gateway
张忠琳6 小时前
【NVIDIA】NVIDIA k8s-device-plugin v0.19.3 配置API模块深度分析之二
云原生·容器·架构·kubernetes·nvidia
IT二叔8 小时前
Kubernetes(K8s)-02-Minikube安装
云原生·容器·kubernetes
爱莉希雅&&&10 小时前
Rocky Linux 10.1 + K8s 1.36.3 离线集群部署笔记(Kubernetes)
linux·笔记·docker·kubernetes·calico
SLD_Allen10 小时前
Kubernetes上的存算分离大数据平台
大数据·容器·kubernetes
Zhu75810 小时前
在k8s集群环境部署高可用的Apache Hadoop3.1.1定制版集群
hadoop·kubernetes
IT二叔11 小时前
Kubernetes(K8s)-08-Deployment详解
云原生·容器·kubernetes
运维大师11 小时前
【K8S 运维实战】20-证书管理cert-manager
运维·docker·kubernetes
张忠琳1 天前
【NVIDIA】NVIDIA Container Toolkit v1.19.1--07-NVCDI模块:CDI Spec生成引擎深度分析之七
云原生·容器·架构·kubernetes·nvidia