ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

kubeasz部署Kubernetes集群时kubelet cgroup driver不一致的解决

kubeasz部署Kubernetes集群时kubelet cgroup driver不一致的解决

1. 项目背景:kubeasz 部署集群时的 kubelet 启动拦路虎

如果你用过 kubeasz 这个项目来部署 Kubernetes 集群,大概率对这条报错不陌生:failed to run Kubelet: misconfiguration: kubelet cgroup driver: "systemd" is different from docker cgroup driver: "cgroupfs"。我自己第一次踩到这个坑的时候,正好是在帮客户扩容一个生产环境的节点,kubeasz 脚本跑到一半,节点状态一直 NotReady,kubelet 进程反复崩溃,排查了快两个小时才定位到根因。这条报错的意思其实很直白:kubelet 的 cgroup 驱动和容器运行时的 cgroup 驱动不一致,导致 kubelet 启动时直接拒绝运行,根本不给节点任何机会加入集群。

这篇文章不像官方文档那样只告诉你“改一下参数就行了”,我会把这条报错背后的机制、kubeasz 部署链路里最容易出问题的地方、几种不同的解决思路,以及我在实际运维中踩过的坑和排错方法,全部拆开来讲清楚。适合正在用 kubeasz 部署集群的运维工程师,也适合手动部署 Kubernetes 节点时遇到类似 cgroup driver 问题的读者,甚至对 Kubernetes 基础机制感兴趣、想弄懂 cgroup 和容器运行时关系的同学也能从这里找到有用的信息。

2. 先拆报错:kubelet cgroup driver 到底在抱怨什么

2.1 cgroup 是什么?为什么要分 driver

cgroup(control group)是 Linux 内核提供的一种资源控制机制,它的作用简单来说就是可以把一组进程放在一个“容器”里,然后统一限制它们能使用的 CPU、内存、磁盘 IO 等资源。Kubernetes 里的 Pod 是有资源配额概念的,比如你在 Pod 里写了resources.limits.memory: 512Mi,这 512Mi 能不能被真正限制住,底层就是靠 cgroup 来实现的。

既然要限制资源,就得有一套方式来管理和操作这些 cgroup 目录,这套方式就是 cgroup driver。Kubernetes 生态里常见有两大阵营:一个是systemd驱动,由 systemd 这个 PID 1 进程统一接管 cgroup 的创建、删除和层级管理;另一个是cgroupfs驱动,由 kubelet 或者容器运行时直接去写/sys/fs/cgroup下的目录和文件来控制资源。在大多数现代 Linux 发行版上,systemd 已经接管了系统的 init 流程,所以理论上用systemd驱动更自然,不会出现两套逻辑打架的情况。

2.2 报错里的“misconfiguration”意味着什么

注意报错用的是misconfiguration而不是failed to connect或者timeout,这说明 kubelet 在启动前的自检阶段就已经发现了配置层面的矛盾。kubelet 启动时会检查当前 cgroup driver 的配置,同时它也会和容器运行时通信,去获取运行时正在使用的 cgroup driver。拿最常见的情况举例:kubelet 的启动参数里写了--cgroup-driver=systemd,但 Docker 的配置里默认用的还是--exec-opt native.cgroupdriver=cgroupfs,两边数据一对上,kubelet 判定这句话里的“systemd 与 cgroupfs 不一致”构成一种非法状态,干脆以启动失败收场。

这个检查并不是故意为难人,它的意义在于防止 kubelet 和运行时各管各的,导致资源限制失效甚至引发宿主机不稳定。实际生产中如果两边都是 cgroupfs,可能短期看着没事,但遇到高负载场景下 systemd 管理的 cgroup 树和 kubelet 直接操作的 cgroup 路径互相冲突,就会冒出一些连内核日志都看不明白的诡异问题。

2.3 为什么 kubeasz 部署场景特别容易触发

kubeasz 是一套基于二进制方式部署 Kubernetes 集群的自动化脚本集合,它的工作流程大致是:准备主机(安装 Docker 或 containerd)、生成证书、配置 kubelet 和 kube-apiserver 等组件、最后用 systemd 或者 supervisord 托管这些进程。kubeasz 的设计哲学是“尽可能贴近生产环境”,所以它在不少默认配置里把 kubelet 的 cgroup driver 设成了systemd——因为 etcd、kubelet 本身都是 systemd 管理的服务,用 systemd 驱动确实更符合当前主流最佳实践。

问题在于,如果安装容器运行时(比如 Docker)时它的 cgroup driver 没有跟着改成 systemd,两边配置就对不上了。你可能会问,kubeasz 难道不会自动把 Docker 也改掉吗?这就要说到版本和分支问题了。我自己遇到的情况是:自定义了部分配置文件,或者下载的 kubeasz 版本较老,它的 Docker 初始化逻辑里没有体现“必须和 kubelet 的 cgroup driver 保持一致”这个联动约束,于是 Docker 还是用默认的 cgroupfs,kubelet 用 systemd,一启动就崩。

2.4 从报错到解决方案的思维路径

面对这条报错,解决路径本质上就一条:让 kubelet 和容器运行时使用相同的 cgroup driver。但实际操作有几种不同的变体选择,取决于你的集群环境是全新部署还是已经跑了一段时间,取决于你用的是 Docker 还是 containerd,也取决于 kubeasz 版本和配置管理方式。接下来我会从排查手段和几种修复方案两个维度分别展开,看完之后你就能根据自己的现场情况对号入座。

3. 现场排查:如何确认各组件当前使用的 cgroup driver

3.1 第一步:确认报错确实来自 cgroup driver 不一致

通常在 kubeasz 执行完某个阶段之后,你会看到节点状态异常,或者在 kubelet 的 systemd 日志里有类似failed to run Kubelet的输出。很多时候 kubeasz 的日志并不会直接告诉你这个细节,所以第一步必须自己去翻日志。用下面的命令查看 kubelet 服务的状态和最近日志:

systemctl status kubelet journalctl -u kubelet -n 200 --no-pager

日志里如果出现了misconfiguration: kubelet cgroup driver: "systemd" is different from docker cgroup driver: "cgroupfs",那就直接锁定是 cgroup driver 不匹配。有些日志可能还会更详细地提示当前 kubelet 的 cgroup driver 是从某个配置文件中读取的,这在后续定位哪份配置需要改的时候非常关键。

3.2 第二步:检查容器运行时的 cgroup driver

接下来要确认容器运行时的实际驱动,这个不能靠猜,要调用运行时的接口去查。如果你用的是 Docker,执行:

docker info | grep -i cgroup

输出一般会显示Cgroup Driver: cgroupfs或者Cgroup Driver: systemd。如果你用的是 containerd 作为运行时,可以用:

crictl info | grep -i cgroup

注意这里crictl是 Kubernetes 社区的标准命令行工具,它读取的是 CRI 运行时(一般就是 containerd)的状态,信息比docker info更贴近 kubelet 视角。如果你一时找不到crictl,也可以直接查看 containerd 的配置文件,通常情况下集中在/etc/containerd/config.toml,注意里面的SystemdCgroup字段,它如果是true,表示 containerd 在用 systemd 作为 cgroup driver,如果缺失或者为false,则默认走 cgroupfs。

3.3 第三步:确认 kubelet 当前的启动参数和配置文件

kubelet 的 cgroup driver 来源有多个地方:它自己的 systemd unit 文件里的--cgroup-driver参数,或者 kubelet 的配置文件(默认路径通常是/var/lib/kubelet/config.yaml),里面的cgroupDriver字段。kubeasz 部署的 kubelet 一般是通过环境变量或者 unit 文件传入参数,所以查看:

cat /etc/systemd/system/kubelet.service cat /etc/systemd/system/kubelet.service.d/*.conf

重点看有没有--cgroup-driver=xxx,以及KUBELET_EXTRA_ARGS里有没有重复覆盖。另一个常见位置是/etc/kubernetes/kubelet.conf,但这个是 kubelet 连接 apiserver 用的证书和地址配置,里面不包含 cgroup driver 信息,别搞混了。真正决定 cgroup driver 的还是启动参数和config.yaml。

3.4 把信息汇总成一张决策表

把上面几个命令的输出汇总起来,你的问题基本就已经清晰了。我习惯把信息做成一张表,看起来更直观:

组件配置来源当前值
kubelet/etc/systemd/system/kubelet.service.d/override.confsystemd
Docker/etc/docker/daemon.jsoncgroupfs(默认)
containerd(如果用的话)/etc/containerd/config.tomlSystemdCgroup=false

看到这种对比,接下来要做的就是统一驱动。我个人强烈建议,如果是新部署的集群,直接全部切到systemd,这也是 Kubernetes 社区官方文档里推荐的默认做法。如果集群已经跑了一段时间,节点上有存量容器,切换 cgroup driver 会导致现有容器被重建,需要斟酌一下业务影响再动手。

4. 实操修复方案:三种常见路径完整演示

4.1 方案 A:改 Docker 的 cgroup driver,配合 kubelet 的 systemd

这是我在生产环境里最常用的方案。因为 kubelet 设置的驱动一般已经通过 kubeasz 或手动参数固定为systemd,改 kubelet 的风险比改 Docker 更大,所以优先去把 Docker 对齐过来。

修改 Docker 的方式是在/etc/docker/daemon.json里加入exec-opts,让 dockerd 启动时给 containerd 传递 native.cgroupdriver 参数。示例配置如下:

{ "exec-opts": ["native.cgroupdriver=systemd"], "log-driver": "json-file", "log-opts": { "max-size": "100m" } }

改完之后重启 Docker:

systemctl daemon-reload systemctl restart docker docker info | grep -i cgroup

如果看到输出变成了Cgroup Driver: systemd,就说明 Docker 这边已经对齐。接着再启动 kubelet:

systemctl restart kubelet sleep 5 systemctl status kubelet

这时候 kubelet 应该能稳定运行,节点状态也会从 NotReady 慢慢变为 Ready。

这里有个细节要注意:如果 Docker 是通过 kubeasz 的默认脚本安装的,daemon.json 可能不存在或者被其他配置覆盖,你需要先确认dockerd的启动参数里有没有--exec-opt。如果有,需要把系统服务里的参数也清掉或者一并修改,否则 daemon.json 的配置可能不生效。我个人踩过一次坑:改完 daemon.json 重启 Docker,docker info里 Cgroup Driver 还是 cgroupfs,后来发现是 systemd unit 里的ExecStart直接写了--exec-opt native.cgroupdriver=cgroupfs,和 daemon.json 冲突,去掉那个参数才正常。

4.2 方案 B:改 kubelet 的 cgroup driver,让它跟随 Docker 的 cgroupfs

如果你因为某些原因不方便动 Docker(比如 Docker 不是你负责的,或者底层镜像仓库里已经固化了 cgroupfs 缓存),也可以反过来改 kubelet。kubeasz 部署的 kubelet 通常用 systemd unit 管理,你可以先查看 unit 文件:

systemctl cat kubelet

如果是通过 drop-in 文件配置的,编辑对应的/etc/systemd/system/kubelet.service.d/10-kubeadm.conf之类的文件,把--cgroup-driver=systemd改成--cgroup-driver=cgroupfs。如果 kubelet 的 cgroup driver 是写在 yaml 配置文件里的,比如/var/lib/kubelet/config.yaml,就修改其中的cgroupDriver: cgroupfs。

修改后同样要执行:

systemctl daemon-reload systemctl restart kubelet

然后观察 kubelet 日志确认它没有再报 misconfiguration。这个方案改动面小,但如果后续引入了新的 Pod,需要确认 Pod 的资源限制依然能正常生效。我个人不推荐在全新部署时主动选 cgroupfs,因为 systemd 管理的环境里 cgroupfs 会带来双份 cgroup 树,偶尔会出现 PID 1 进程不知道某些 cgroup 目录是谁创建的尴尬情况。

4.3 方案 C:在 kubeasz 部署流程中就固定好配置,避免后期手动改

前两种方案都是“事后补救”,其实更优雅的做法是在运行 kubeasz 时就让它替你处理好。kubeasz 项目里提供了很多全局变量和 playbook,你在部署前可以修改inventory或roles下对应的参数文件。以常见情况为例,kubeasz 的 Docker 角色里会有类似docker_cgroupdriver的变量,你把它从默认值改成systemd,kubeasz 在初始化 Docker 时就会带上正确的 exec-opts 参数。

如果是新版本 kubeasz 用 containerd 作为默认运行时,同样可以在配置文件中设置containerd_systemd_cgroup: true,这样 containerd 在启动时就会将SystemdCgroup设为 true。改完配置后重新执行 kubeasz 的相关 playbook 即可,比如从kubeasz的安装目录下运行:

cd /etc/kubeasz ./ezctl setup <集群名> 07

具体哪个 phase 取决于你的安装进度,如果你卡在 node 初始化阶段,就细化到初始化节点的那个步骤重跑。这个方案的最大的好处在于所有节点一次性初始化,不会出现某个节点忘改配置的情况。

需要提醒的是,kubeasz 不同版本之间的变量名和 playbook 顺序可能不太一样,动手之前最好先看一遍 roles 目录下的vars/main.yml和任务文件,确认对应变量确实存在再改。如果你用的是很老的版本,可能根本没有docker_cgroupdriver这个变量,那就只能退回方案 A,手动去写 daemon.json。

4.4 修改后的验证流程

不管用哪种方案,修改之后都不能只看进程活了就收工。我一般会用一套标准的验证动作来确认集群真的恢复正常了:

  1. 检查 kubelet 服务状态:systemctl is-active kubelet,正常是active。
  2. 检查节点状态:kubectl get nodes,等一两分钟,看节点是否进入Ready状态。
  3. 查看节点详情里是否有任何条件异常:kubectl describe node <节点名>。
  4. 验证 Pod 能正常调度:在集群里跑一个临时 Pod,比如kubectl run busybox-test --image=busybox -- sleep 300,然后等它 Running,再删除。
  5. 检查 cgroup 相关日志是否干净:journalctl -u kubelet --since "5 minutes ago" | grep -i error。

这五步全部通过,基本可以断定问题解决。如果第四步发现 Pod 一直处于 ContainerCreating,还要继续往深处查 runtime 日志,这个后面常见问题部分再展开。

5. 说透配置原理:为什么 cgroup driver 必须统一

5.1 Kubernetes 的资源管理模型

在讲更深入的排查技巧之前,我想多花点篇幅把背后的原理说透,这样你以后再遇到类似的错误,就不会像无头苍蝇一样乱撞。Kubernetes 的 kubelet 在管理 Pod 生命周期时,会负责为每个 Pod(更准确地说,是每个 sandbox 容器和业务容器)创建对应的 cgroup 目录,并在里面写入资源限制值。这些限制值来源于 Pod spec 里的resources字段,包括requests和limits。

这些 cgroup 操作本身就是通过 cgroup driver 来完成的。kubelet 不会自己去解析/sys/fs/cgroup的挂载点和接口,它把操作委托给底层的 driver,由 driver 去管理 cgroup 的层级结构。如果 kubelet 用的是 systemd driver,它会要求 systemd 来创建和销毁 cgroup;如果用的是 cgroupfs driver,它就自己直接写文件系统。

问题的重要性在于,用户态有两个“控制者”——systemd 和容器运行时——在同一个宿主机的 cgroup 树上下发行命令,如果不统一,就会出现操作冲突。最典型的例子:systemd 以为某个 cgroup 目录是它管的,但实际上是 kubelet 直接写出来的;或者 kubelet 想在这个 cgroup 下创建子目录,结果被 systemd 的资源清理逻辑删掉了。

5.2 Docker 与 kubelet 的协作方式

Docker 作为容器运行时,并没有直接与 kubelet 通信,它通过 dockerd 内置的 containerd 来创建容器。在 Kubernetes 集成模式下,kubelet 通过 CRI(Container Runtime Interface)调用 Docker,而 Docker 的 CRI 实现会把容器和 Pod 的资源限制参数转换成对 containerd 的调用。containerd 再调用 runc 来真正启动容器进程,同时配置 cgroup。

这串链路里会涉及三处 cgroup driver 配置:kubelet 的--cgroup-driver、Docker 的native.cgroupdriver、以及底层 runc 的 cgroup 配置。Docker 在启动时接收到的native.cgroupdriver参数就决定了它传给 runc 的 cgroup 方式是 systemd 还是 cgroupfs。只要任何一环不一致,最终要么 kubelet 启动时报错,要么容器启动后资源限制静默失效。静默失效这种状态其实更危险,因为应用层看着一切正常,但内存超过 limit 时不会被 OOM killer 处理,CPU 份额也不会被正确分配。

5.3 systemd 与 cgroupfs 的底层差异

简单理解两者的差异:systemd 的 cgroup 管理是以 systemd unit 为单位的,每个 unit(比如kubelet.service、docker.service)对应一个 cgroup 切片,切片名是固定格式的,例如kubepods.slice/kubepods-burstable.slice。kubelet 在以 systemd 驱动运行时,会创建这些 slice unit,然后依赖 systemd 来管理它们的生命周期。

cgroupfs 的方式更“直给”,直接通过文件系统在/sys/fs/cgroup的对应子系统目录下创建和删除目录,不经过 systemd。这种方式在容器运行时是 Docker 或 containerd 的标准做法之一,简单粗暴,但在系统采用 systemd 作为 init 进程的现代发行版上,很容易与 systemd 的 cgroup 管理逻辑冲突,出现 cgroup 泄漏或者删除失败的问题。

Kubernetes 社区在近几个版本里对 systemd 驱动有明显偏好,也是因为在主流发行版环境下运行更稳定。所以如果你的系统里 systemd 是 PID 1(绝大多数服务器都是),我建议直接无脑选 systemd。

6. 常见问题与排查技巧实录

6.1 修改完配置后 kubelet 还是崩了,日志出现“No such file or directory”

这是一个很容易迷惑人的后续问题。有时候你明明把 cgroup driver 改成一致了,重启 kubelet 还是会报错,而且错误信息变成了failed to load config file /var/lib/kubelet/config.yaml: open /var/lib/kubelet/config.yaml: no such file or directory。

这个大概率是 kubelet 启动参数里指定了--config指向了一个不存在的文件。kubeasz 在不同版本中,有的配置会生成/var/lib/kubelet/config.yaml,有的则通过环境变量传入,不会生成这个文件。最稳妥的方法是先看 unit 文件里到底传了哪些参数,如果有--config=/var/lib/kubelet/config.yaml,就手动创建这个配置文件,或者把参数删掉。不要盲目去网上抄一段config.yaml塞进去,版本不匹配反而会引入更多问题。

6.2 cgroup driver 明明一致,但节点还是 NotReady

这种情况一般不是 cgroup driver 的问题了,要顺着链路继续排查。先看 kubelet 日志里有没有报 CNI 错误、证书过期、apiserver 连不上的问题。用 kubeasz 部署时,证书问题特别常见,尤其是节点证书有效期短或者没有正确下发。排查命令优先用:

journalctl -u kubelet -f kubectl describe node <节点名> | tail -30

然后把输出里明显的错误字段记录下来,去查对应组件。不要一看到 kubelet 挂了就认为是 cgroup driver,cgroup driver 只是启动自检的一环,真正让节点 NotReady 的原因可能遍布整个 NATS 链路。

6.3 切换 containerd 时 kubelet 的 cgroup driver 配置

如果你已经在用 containerd 而不是 Docker,cgroup driver 的对齐逻辑有点不同。containerd 通过 CRI 插件与 kubelet 对接,它的默认行为遵循一个特性:如果容器运行时启用了 systemd cgroup,它会在 CRI 请求里携带LinuxPodSandboxConfig.CgroupParent,kubelet 这边再结合自身的--cgroup-driver来决定实际路径。

常见的问题是 containerd 配置文件里SystemdCgroup = false,这种情况 containerd 以 cgroupfs 方式创建 cgroup,而 kubelet 配置又是 systemd,kubelet 启动 Sandbox 时要求 containerd 在kubepods.slice下创建 cgroup,containerd 却往/sys/fs/cgroup/kubepods下面建了一个目录,两边对不上,报错会变成failed to create containerd task: failed to create shim task: OCI runtime create failed: unable to create new parent directory。

解法也很简单:编辑/etc/containerd/config.toml,找到[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]段,设置SystemdCgroup = true,然后重启 containerd 和 kubelet。

systemctl restart containerd systemctl restart kubelet

注意 containerd 的版本差异,比较老的版本配置文件结构和字段名可能不同,可以直接用containerd config dump看当前生效的配置,再对应修改。

6.4 收官:我总结的一份排查速查表

平时维护 kubeasz 集群,我会把下面这张表打印出来贴在手边,遇到类似的 cgroup driver 报错基本不用过脑子就能逐项排查:

现象可能原因排查命令/文件处理建议
failed to run Kubelet: misconfigurationkubelet 与容器运行时 cgroup driver 不一致docker info、crictl info、/etc/systemd/system/kubelet.service统一驱动为 systemd 或 cgroupfs
kubelet 崩溃循环,但 cgroup driver 已统一配置文件缺失或路径错误systemctl cat kubelet创建对应配置或修正启动参数
节点 NotReady,kubelet 日志提示 CNI 失败网络插件未部署journalctl -u kubelet重新部署 CNI 插件,检查网络组件 Pod
节点证书过期或未下发证书签发失败journalctl -u kubelet、kubeadm init phase certs重新生成并下发证书
containerd 启动 Sandbox 失败SystemdCgroup配置与 kubelet 不一致/etc/containerd/config.toml修改配置后重启 containerd

这张表不能覆盖所有场景,但它至少能帮你把八成的问题定性到具体环节。

7. 我在实际运维中的几点心得

最后分享一个哪怕你这次没踩坑也用得上的经验:在用 kubeasz 部署集群时,其实可以把 cgroup driver 这类基础配置前置化,别等问题爆出来再修。我后来的标准操作是在跑 kubeasz 之前,先写一个预检脚本,它自动检查所有待部署节点上的 Docker/containerd 的 cgroup driver 和 kubelet 预期配置是否一致,不一致直接报错退出,从源头堵死这个隐患。

另一个容易忽略的点是,如果你在生产环境已经跑了很长时间的集群,切 cgroup driver 之前一定要先看节点上容器数量,Docker 的 cgroup driver 切换会导致 Docker 重启,所有容器全部重新创建,业务中断时间大概几分钟到几十分钟不等,得提前走变更流程。不要图省事直接在业务高峰期操作,我就见过有人在白天直接改 daemon.json 重启 Docker,整个节点上的业务 Pod 全部重建,数据库连接风暴直接把上游打挂。

至于 cgroup driver 到底选 systemd 还是 cgroupfs,我个人的意见是:只要你的宿主机用 systemd 做 init(现在几乎都是),就选 systemd。这是 Kubernetes 官方的最佳实践,也是最不折腾的选择。对于一些特殊定制的精简系统,比如某些嵌入式 Linux 环境里没有 systemd,那才考虑 cgroupfs。这两种选择本身没有绝对的对错,重点在于全链路统一,这一点是永远不变的核心原则。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进