主要内容:
在一次 gVisor 沙箱部署中,将 K8s 节点的容器运行时从 Docker 切换到了 Containerd。操作完成后,节点 NotReady,etcd 和 apiserver 全部 CrashLoopBackOff ------ 整个集群瘫痪。这篇文章完整记录了我从发现问题到定位根因、再到修复的全过程。
一、背景
本次记录基于Kubernetes 集群中部署 gVisor 沙箱运行时。初始集群架构为 Docker + dockershim 架构,完整调用链为:kubelet → 内置 dockershim → dockerd → containerd → containerd-shim-runc-v2 → runc → 宿主机内核 (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 有两个角色:
- PID 1(服务管理器) :启动/停止 kubelet、containerd
- 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 节点上,默认值是最危险的东西, 越是不显式配置,越是在给未来的故障埋下伏笔