使用 cgroupv2 (--cgroupns=private) 时,Systemd 无法在 docker 容器中运行

Ste*_*hen 10 cgroup systemd docker

我将在下面附上最小化的测试用例。但是,它是一个简单的 Dockerfile,具有以下几行:

VOLUME ["/sys/fs/cgroup"]
CMD ["/lib/systemd/systemd"]
Run Code Online (Sandbox Code Playgroud)

它是基于 Debian:buster-slim 的映像,并在容器内运行 systemd。实际上,我曾经像这样运行容器:

$ docker run  --name any --tmpfs /run \
    --tmpfs /run/lock --tmpfs /tmp \
    -v /sys/fs/cgroup:/sys/fs/cgroup:ro -it image_name
Run Code Online (Sandbox Code Playgroud)

在我升级一堆主机 Linux 软件包之前,它曾经运行良好。主机内核/systemd 现在似乎默认 cgroup v2。之前是cgroup。它停止工作。但是,如果我提供内核选项以便主机使用 cgroup,则它会再次运行。

在不提供内核选项的情况下,修复是添加--cgroupns=hostdocker run除了/sys/fs/cgroup以读写方式安装(:rw代替:ro)之外。

我想避免强迫用户提供内核选项。尽管我远非专家,但强制为 docker 容器使用主机命名空间对我来说并不合适。

我试图理解为什么会发生这种情况,并弄清楚应该做什么。我的目标是在 docker 中运行 systemd,其中主机遵循 cgroup v2。

这是我看到的错误:

$ docker run --name any --tmpfs /run --tmpfs /run/lock --tmpfs /tmp \
    -v /sys/fs/cgroup:/sys/fs/cgroup:rw -it image_name
systemd 241 running in system mode. (+PAM +AUDIT +SELINUX +IMA +APPARMOR +SMACK +SYSVINIT +UTMP +LIBCRYPTSETUP +GCRYPT +GNUTLS +ACL +XZ +LZ4 +SECCOMP +BLKID +ELFUTILS +KMOD -IDN2 +IDN -PCRE2 default-hierarchy=hybrid)
Detected virtualization docker.
Detected architecture x86-64.

Welcome to Debian GNU/Linux 10 (buster)!

Set hostname to <5e089ab33b12>.
Failed to create /init.scope control group: Read-only file system
Failed to allocate manager object: Read-only file system
[!!!!!!] Failed to allocate manager object.
Exiting PID 1...
Run Code Online (Sandbox Code Playgroud)

它看起来不对,但尤其是这条线似乎很可疑:

Failed to create /init.scope control group: Read-only file system
Run Code Online (Sandbox Code Playgroud)

似乎之前应该有什么/init.scope。这就是为什么我审查了docker run选项,并尝试了该--cgroupsns选项。如果我添加--cgroupns=host,它会起作用。如果我/sys/fs/cgroup以只读方式挂载,则会失败并显示不同的错误,相应的行如下所示:

Failed to create /system.slice/docker-0be34b8ec5806b0760093e39dea35f4305262d276ecc5047a5f0ff43871ed6d0.scope/init.scope control group: Read-only file system
Run Code Online (Sandbox Code Playgroud)

对我来说,这就像 docker daemon/engine 无法为容器配置 XXX.slice 或类似的东西。我认为 docker 可能在某种程度上负责提供命名空间,但有些事情进展不顺利。但是,我完全不能确定。问题/修复是什么?

我本次实验使用的Dockerfile如下:

FROM debian:buster-slim

ENV container docker
ENV LC_ALL C
ENV DEBIAN_FRONTEND noninteractive

USER root
WORKDIR /root

RUN set -x

RUN apt-get update -y \
    && apt-get install --no-install-recommends -y systemd \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
    && rm -f /var/run/nologin

RUN rm -f /lib/systemd/system/multi-user.target.wants/* \
    /etc/systemd/system/*.wants/* \
    /lib/systemd/system/local-fs.target.wants/* \
    /lib/systemd/system/sockets.target.wants/*udev* \
    /lib/systemd/system/sockets.target.wants/*initctl* \
    /lib/systemd/system/sysinit.target.wants/systemd-tmpfiles-setup* \
    /lib/systemd/system/systemd-update-utmp*

VOLUME [ "/sys/fs/cgroup" ]

CMD ["/lib/systemd/systemd"]
Run Code Online (Sandbox Code Playgroud)

我正在使用 Debian。docker 版本是 20.10.3 左右。Google 搜索告诉我 docker 从 20.10 开始支持 cgroup v2,但我实际上不明白“支持”是什么意思。

pin*_*een 18

太长了;博士

在我看来,这个用例尚未得到明确支持。你几乎可以让它工作,但还不够。

根本原因

当 systemd 看到一个统一的 cgroupfs时,/sys/fs/cgroup它假设它应该能够写入它,这通常应该是可能的,但这里的情况并非如此。

基础

首先,您需要为 docker 容器创建一个systemd 切片并告诉 docker 使用它 - 我当前的docker/daemon.json

{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "features": { "buildkit": true },
  "experimental": true,
  "cgroup-parent": "docker.slice"
}
Run Code Online (Sandbox Code Playgroud)

注意:并非所有这些选项都是必需的。最重要的一点是cgroup-parent。默认情况下应该cgroupdriver已经切换到“systemd”。

每个切片都有自己的嵌套 cgroup。但有一个警告:每个组可能只是一个“叶子”或“中介”。一旦某个进程取得了 cgroup 的所有权,其他进程就无法管理它。这意味着实际的容器进程需要并将其自己的私有组以systemd 作用域的形式附加到配置的组下面。

参考:请查找有关systemd 资源控制、 cgroup 命名空间处理和委托的更多信息。

注意:默认情况下,docker 守护进程应该使用这一点--cgroupns private,但无论如何您都可以强制它。

现在,新启动的容器将获得自己的组,该组应该在类似于以下的路径(取决于您的设置)中可用:

/sys/fs/cgroup/your_docker_parent.slice/your_container.scope
Run Code Online (Sandbox Code Playgroud)

这是重要的部分:您不能将卷安装到容器的/sys/fs/cgroup. 上面提到的其私有组的路径应该自动安装在那里。

目标

现在,理论上,容器应该能够几乎完全自行管理这个委托的私有组。这将允许其自己的 init 进程创建子组。

问题

问题在于/sys/fs/cgroup容器中的路径以只读方式安装。我检查了 apparmor 规则并将 seccomp 切换为 unconfined,但无济于事。

假设

我还不完全确定 - 我目前的假设是这是 docker/moby/containerd 的安全功能。如果没有私人团体,走这条路是非常有意义的ro

潜在的解决方案

我还发现,启用用户命名空间重新映射会导致私有按预期/sys/fs/cgroup安装!rw

但这远非完美 - cgroup(以及其他)挂载的所有权错误:它由真实系统根(UID0)拥有,而容器已被重新映射到完全不同的用户。一旦我手动调整了所有者,容器就能够成功启动 systemd init。

怀疑这是 docker 的用户重新映射功能的缺陷,迟早会被修复。请记住,我对此可能是错的 - 我没有确认。

讨论

用户重新映射有很多缺点,对我来说最好的方案是在rw没有它的情况下安装 cgroupfs。我仍然不知道这是故意的还是 cgroup/userns 实现的某种限制。

笔记

您的内核启用了 cgroupv2 还不够。根据 Linux 发行版,捆绑的 systemd 可能更喜欢默认使用 v1。

您可以通过内核命令行参数告诉 systemd 使用 cgroupv2:
systemd.unified_cgroup_hierarchy=1

可能还需要显式禁用混合cgroupv1 支持以避免出现问题: systemd.legacy_systemd_cgroup_controller=0

或者在内核中完全禁用 cgroupv1: cgroup_no_v1=all


小智 6

感谢 @pinkeen 的回答,这是我的 Dockerfile 和命令行,它工作正常。我希望这有帮助:

FROM debian:bullseye
# Using systemd in docker: https://systemd.io/CONTAINER_INTERFACE/
# Make sure cgroupv2 is enabled. To check this: cat /sys/fs/cgroup/cgroup.controllers
ENV container docker
STOPSIGNAL SIGRTMIN+3
VOLUME [ "/tmp", "/run", "/run/lock" ]
WORKDIR /
# Remove unnecessary units
RUN rm -f /lib/systemd/system/multi-user.target.wants/* \
  /etc/systemd/system/*.wants/* \
  /lib/systemd/system/local-fs.target.wants/* \
  /lib/systemd/system/sockets.target.wants/*udev* \
  /lib/systemd/system/sockets.target.wants/*initctl* \
  /lib/systemd/system/sysinit.target.wants/systemd-tmpfiles-setup* \
  /lib/systemd/system/systemd-update-utmp*
CMD [ "/lib/systemd/systemd", "log-level=info", "unit=sysinit.target" ]
Run Code Online (Sandbox Code Playgroud)
docker build -t systemd_test .
docker run -t --rm --name systemd_test \
  --privileged --cap-add SYS_ADMIN --security-opt seccomp=unconfined \
  --cgroup-parent=docker.slice --cgroupns private \
  --tmpfs /tmp --tmpfs /run --tmpfs /run/lock \
  systemd_test
Run Code Online (Sandbox Code Playgroud)

注意:您必须使用 Docker 20.10 或更高版本,并且您的系统启用了 cgroupv2(检查是否/sys/fs/cgroup/cgroup.controllers存在)。

  • 当我在 OpenSuSE 上的 CGroupv2 上运行时,这是我发现的唯一对我有用的解决方案。 (2认同)