Linux 内核中 cpuset cgroup 继承语义有什么“问题”?

Linux 内核中 cpuset cgroup 继承语义有什么“问题”?

引用2013 systemd 宣布新的控制组接口(添加强调):

请注意,当前作为单元属性公开的 cgroup 属性的数量是有限的。随着它们的内核接口被清理,这将在稍后扩展。例如由于内核逻辑的继承语义被破坏,cpuset 或 freeze 目前根本没有公开。此外,不支持在运行时将单元迁移到不同的切片(即更改运行单元的 Slice= 属性),因为内核当前缺少原子 cgroup 子树移动。

那么,内核逻辑的继承语义有什么问题cpuset(以及这种问题如何不适用于其他 cgroup 控制器,例如cpu)?

RedHat 网站上的一篇文章为如何在 RHEL 7 中使用 cgroup cpuset 提供了未经验证的解决方案,尽管它们缺乏易于管理的 systemd 单元属性的支持......但这是否是一个好主意?上面的粗体引文令人担忧。

换句话说,使用此处引用的 cgroup v1 cpuset 可能存在哪些“陷阱”(陷阱)?


我正在为此开始悬赏。

回答此问题的可能信息来源(排名不分先后)包括:

  1. cgroup v1 文档;
  2. 内核源代码;
  3. 检测结果;
  4. 真实世界的经验。

上面引用中粗体线的一个可能含义是,当一个新进程被分叉时,它不会保留在与其父进程相同的 cpuset cgroup 中,或者它位于同一个 cgroup 中,但处于某种“未强制”状态它实际上可能运行在 cgroup 允许的不同 CPU 上。然而,这纯粹是我的猜测我需要一个明确的答案。

答案1

此处的内核错误跟踪器记录了至少一个明确且未解决的 cpuset 问题:

错误 42789- cpuset cgroup:当一个CPU离线时,它会从所有cgroup的cpuset.cpus中删除,但当它上线时,它只会恢复到根cpuset.cpus

去引用一条评论从票证中(我将超链接添加到实际提交,并删除 IBM 电子邮件地址,以防出现垃圾邮件机器人):

这是由 Prashanth Nageshappa 独立报告的……并在提交中修复8f2f748b0656257153bcf0941df8d6060acc5ca6,但随后被 Linus 恢复为提交4293f20c19f44ca66e5ac836b411d25e14b9f185。根据他的承诺,该修复导致其他地方出现回归。

修复提交(后来被恢复)很好地描述了这个问题:

目前,在 CPU 热插拔期间,cpuset 回调会修改 cpuset 以反映系统状态,并且此处理是不对称的。也就是说,当 CPU 离线时,该 CPU 将从所有 cpuset 中删除。然而,当它重新上线时,它仅被放回根 cpuset。

这在挂起/恢复期间引起了严重的问题。在挂起期间,我们将所有非启动 cpu 脱机,并在恢复期间将它们重新联机。这意味着,恢复后,所有 cpuset(根 cpuset 除外)将仅限于一个 CPU(启动 cpu)。但挂起/恢复的全部目的是将系统恢复到尽可能接近挂起之前的状态。


描述了相同的非对称热插拔问题,并进一步深入了解了它与继承的关系,如下:

错误 188101- cgroup 的 cpuset 中的进程调度无法正常工作。

引用那张票:

当容器的 cpuset(docker/lxc 都使用底层 cgroup)变空(由于 hotplug/hotunplug)时,在该容器中运行的进程可以调度到其最近的非空祖先的 cpuset 中的任何 cpu 上。

但是,当正在运行的容器(docker / lxc)的cpuset通过更新正在运行的容器的cpuset(通过使用echo方法)从空状态变为非空(将cpu添加到空cpuset)时,在该容器中运行的进程仍然使用与其最近的非空祖先相同的 cpuset。


虽然 cpuset 可能存在其他问题,但以上内容足以理解和理解 systemd 不会公开或利用 cpuset“由于内核逻辑的继承语义被破坏”的说法。

从这两个错误报告来看,CPU 不仅在恢复后不会添加回 cpuset,而且即使它们(手动)添加后,该 cgroup 中的进程仍将在 cpuset 可能不允许的 CPU 上运行。


我发现来自伦纳特·珀特林的消息这直接证实了这一点的原因(加粗):

Lennart Poettering 在 2016 年 8 月 3 日星期三 16:56 +0200 写道:

周三,2016 年 8 月 3 日 14:46,Werner Fink 博士(werner at suse.de)写道:

v228 的问题(我猜这也是后来来自当前 git 日志的 AFAICS)重复 CPU 热插拔事件(离线/在线)。根本原因是 cpuset.cpus 无法通过机械加工恢复。请注意,libvirt 无法执行此操作,因为不允许这样做。

这是内核 cpuset 接口的限制,并且这是我们现在不在 systemd 中公开 cpuset 的原因之一。值得庆幸的是,还有一个 cpuset 的替代方案,即通过 systemd 中的 CPUAffinity= 公开的 CPU 亲和力控制,它的作用与此基本相同,但语义较少。

我们希望直接在 systemd 中支持 cpuset,但只要内核接口像它们一样无聊,我们就不会这样做。例如,当前,当系统经历挂起/恢复周期时,CPU 集会被完全刷新。

答案2

我对 cgroups 的了解还不够多,无法给出明确的答案(而且我当然没有 2013 年使用 cgroups 的经验!),但在普通的 Ubuntu 16.04 cgroups v1 上似乎可以协同工作:

我设计了一个小测试,强制分叉为不同的用户,使用一个sudo /bin/bash与 root 分离的子进程&- 该-H标志是额外的偏执,强制sudo在 root 的家庭环境中执行。

cat <(whoami) /proc/self/cgroup >me.cgroup && \
sudo -H /bin/bash -c 'cat <(whoami) /proc/self/cgroup >you.cgroup' & \
sleep 2 && diff me.cgroup you.cgroup

这产生:

1c1
< admlocal
---
> root

作为参考,这是我的系统上 cgroup 挂载的结构:

$ mount | grep group
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
lxcfs on /var/lib/lxcfs type fuse.lxcfs (rw,nosuid,nodev,relatime,user_id=0,group_id=0,allow_other)
$

答案3

Linux 内核中 cpuset cgroup 继承语义有什么“问题”?

“请注意,当前作为单元属性公开的 cgroup 属性的数量是有限的。随着它们的内核接口被清理,这将在稍后扩展。例如由于内核逻辑的继承语义被破坏,cpuset 或 freeze 目前根本没有公开。此外,不支持在运行时将单元迁移到不同的切片(即更改运行单元的 Slice= 属性),因为内核当前缺少原子 cgroup 子树移动。”

那么,cpuset 的内核逻辑的继承语义有什么问题(以及这种问题如何不适用于其他 cgroup 控制器,例如 cpu)?

上面的粗体引文令人担忧。换句话说,使用此处引用的 cgroup v1 cpuset 可能存在哪些“陷阱”(陷阱)?

非常简短的答案:代码不能很好地进行多进程处理,不同的进程使用并释放 PID,在其子进程的 PID 终止之前将它们返回到池中 - 让上游相信 PID 的子进程是活动的,因此跳过该 PID,但该 PID在终止孩子之前不应该重新发布。简而言之,锁很差。

服务、范围和切片可以由管理员自由创建或由程序动态创建。这可能会干扰操作系统在启动期间设置的默认切片。

使用 Cgroup 时,进程及其所有子进程都会从包含组中获取资源。

还有更多...导致一个冗长的答案...

不少人表达了他们的担忧:

  1. Linux 控制组不是工作“(2016)乔纳森·德博因·波拉德:

    提供“作业”抽象的操作系统内核提供了一种取消/终止整个“作业”的方法。见证 Win32终止作业对象()机制,例如。

    当 systemd 终止 cgroup 中的所有进程时,它不会发出单个“终止作业”系统调用。没有这样的事。而是它位于应用程序模式代码中的循环中重复扫描 cgroup 中的所有进程 ID(通过重新读取充满 PID 号的文件)并向以前未见过的新进程发送信号。这有几个问题。

    • systemd 可能比在进程组中获取子进程的进程慢,导致终止信号被发送到完全错误的进程:在 systemd 读取 cgroup 的进程列表文件和它实际上开始向进程列表发送信号。 ...

    ...

    • 一个在 cgroup 内足够快地分叉新进程的程序可以使 systemd 保持运行很长一段时间,理论上只要合适的“天气”占上风,就可以无限期地运行,因为在每次循环迭代时都会有一个进程需要终止。请注意,这不一定是叉子炸弹。它只需派生足够多的分叉,以便 systemd 每次运行循环时都能在 cgroup 中至少看到一个新的进程 ID。

    • systemd 将已发出信号的进程的进程 ID 保存在一组中,以了解哪些进程不会再次尝试向其发送信号。 ID 为 N 的进程可能会被收割者/父进程发出信号、终止并从进程表中清除;然后 cgroup 中的某些东西再次分叉出一个新进程,并再次为其分配相同的进程 ID N。 systemd将重新读取cgroup的进程ID列表,认为它已经向新进程发出信号,并且根本不向它发出信号。

     

    这些都是通过真正的“工作”机制来解决的。但 cgroup 并非如此。 cgroup 旨在改进传统的 Unix 资源限制机制,解决一些长期存在且众所周知的设计缺陷。它们并不是被设计成相当于 VMS 或Windows NT 作业对象

    不,冰箱不是答案。 systemd不仅不使用冷冻室,而且systemd人们明确地将其描述为“内核逻辑的继承语义被破坏“。你必须问他们这是什么意思,但对于他们来说,冷冻机也没有神奇地将 cgroup 变成一种工作机制。

    此外:更不用说 Docker 和其他人会为了自己的目的而操纵控制组的冻结状态,并且没有真正的无竞争机制来在多个所有者之间共享此设置,例如原子读取和更新为了它。
    特此授予以原始、未经修改的形式复制和分发本网页的权限,只要保留其最后修改日期戳即可。

    • 终止作业对象()功能

      Terminates all processes currently associated with the job. If the  
      job is nested, this function terminates all processes currently  
      associated with the job and all of its child jobs in the hierarchy. 
      
    • Windows NT 作业对象

      A job object allows groups of processes to be managed as a unit.  
      Job objects are namable, securable, sharable objects that control  
      attributes of the processes associated with them. Operations  
      performed on a job object affect all processes associated with the  
      job object. Examples include enforcing limits such as working set   
      size and process priority or terminating all processes associated 
      with a job.
      

    回答 提供乔纳森的解释是:

    systemd 的资源控制概念

    ...

    服务、范围和切片单元直接映射到 cgroup 树中的对象。当这些单元被激活时,它们各自直接映射(对某些字符转义取模)到根据单元名称构建的 cgroup 路径。例如,分片 foobar-waldo.slice 中的服务 quux.service 可在 cgroup foobar.slice/foobar-waldo.slice/quux.service/ 中找到。

    服务、范围和切片可以由管理员自由创建或由程序动态创建。然而,默认情况下,操作系统定义了许多启动系统所需的内置服务。另外,默认定义了四个切片:首先是根切片 -.slice(如上所述),还有 system.slice、machine.slice、user.slice。默认情况下,所有系统服务都放置在第一个切片中,所有虚拟机和容器放置在第二个切片中,用户会话放置在第三个切片中。然而,这只是默认值,管理员可以自由定义新的切片并为其分配服务和范围。另请注意,所有登录会话都会自动放置在单独的范围单元中,VM 和容器进程也是如此。最后,所有登录的用户还将获得自己的隐式切片,其中放置所有会话范围

    ...

    正如您所看到的,服务和作用域包含流程并放置在切片中,而切片不包含自己的流程。另请注意,特殊的“-.slice”未显示,因为它隐式地标识为整个树的根。

    可以以相同的方式对服务、范围和切片设置资源限制。 ...

请点击上面的链接以获得完整的解释。

  1. Cgroups v2:第二次资源管理做得更糟“(2016 年 10 月 14 日),作者:davmac:

    ...

    您可以创建嵌套层次结构,以便在其他组中存在组,并且嵌套组共享其父组的资源(并且可能会受到进一步限制)。通过将进程的 PID 写入组的控制文件之一,可以将进程移动到组中。因此,一个组可能包含进程和子组。

    您可能想要限制的两个明显资源是内存和 CPU 时间,每个资源都有一个“控制器”,但可能还有其他资源(例如 I/O 带宽),并且某些 Cgroup 控制器并不真正管理资源利用率如此(例如“冷冻机”控制器/子系统)。 Cgroups v1 接口允许创建多个层次结构,并附加不同的控制器(这一点的价值是可疑的,但可能性是存在的)。

    重要的是,进程从其父进程继承其 cgroup 成员身份,并且不能将自己移出(或移入)cgroup,除非它们具有适当的权限,这意味着进程无法逃脱通过分叉施加的任何限制。将此与 setrlimit 的使用进行比较,其中可以使用 RLIMIT_AS(地址空间)限制来限制进程对内存的使用(例如),但该进程可以分叉,并且其子进程可以消耗额外的内存,而无需从原始资源中提取过程。另一方面,对于 Cgroup,进程及其所有子进程从包含组中获取资源。

    ...

    cgroup 控制器实现了许多永远不会被接受为公共 API 的旋钮,因为它们只是向系统管理伪文件系统添加控制旋钮。 cgroup 最终得到了没有正确抽象或细化的接口旋钮,并且直接揭示了内核内部细节。

    这些旋钮通过定义不明确的委托机制暴露给各个应用程序,有效地滥用 cgroup 作为实现公共 API 的捷径,而无需经过所需的审查。

    ...

    cgroup v1 允许线程位于任何 cgroup 中,这产生了一个有趣的问题,即属于父 cgroup 及其子 cgroup 的线程竞争资源。这是令人讨厌的,因为两种不同类型的实体之间存在竞争,并且没有明显的方法来解决它。不同的控制器做了不同的事情。

  2. 另请参阅 cgroup v2 文档:“v1 的问题和 v2 的基本原理”:

    多重层次结构

    cgroup v1 允许任意数量的层次结构,每个层次结构可以托管任意数量的控制器。虽然这似乎提供了高度的灵活性,但在实践中并没有什么用处。

    例如,由于每个控制器只有一个实例,因此可在所有层次结构中使用的诸如冰箱之类的实用型控制器只能在一个层次结构中使用。一旦填充了层次结构,控制器就无法移动到另一个层次结构,这一事实加剧了这个问题。另一个问题是绑定到层次结构的所有控制器都被迫具有完全相同的层次结构视图。无法根据特定控制器来改变粒度。

    实际上,这些问题严重限制了哪些控制器可以放置在同一层次结构中,并且大多数配置都诉诸于将每个控制器放置在其自己的层次结构中。只有紧密相关的控制器(例如 cpu 和 cpuacct 控制器)才有意义放在同一层次结构中。这通常意味着用户空间最终会管理多个相似的层次结构,每当需要层次结构管理操作时,就会在每个层次结构上重复相同的步骤。

    此外,对多个层次结构的支持需要付出高昂的代价。它极大地复杂了 cgroup 核心实现,但更重要的是,对多个层次结构的支持限制了 cgroup 的一般使用方式以及控制器能够执行的操作。

    可能存在的层次结构数量没有限制,这意味着线程的 cgroup 成员身份无法以有限长度进行描述。密钥可能包含任意数量的条目并且长度不受限制,这使得操作非常困难,并导致添加仅用于识别成员身份的控制器,这反过来又加剧了层次结构数量激增的原始问题。

    此外,由于控制器不能对其他控制器可能所在的层次结构的拓扑有任何期望,因此每个控制器必须假设所有其他控制器都附加到完全正交的层次结构。这使得控制器之间的协作变得不可能,或者至少非常麻烦。

    在大多数用例中,没有必要将控制器放在彼此完全正交的层次结构上。通常需要的是能够根据特定的控制器具有不同级别的粒度。换句话说,当从特定控制器查看时,层次结构可能会从叶向根折叠。例如,给定的配置可能不关心超出特定级别的内存如何分配,但仍希望控制 CPU 周期的分配方式。

请参阅第 3 部分的链接以获取更多信息。

  1. Lennart Poettering(systemd 开发人员)和 Daniel P. Berrange(Redhat)于 2016 年 7 月 20 日星期三 12:53 之间的通信,检索自systemd-devel 档案标题为:“[systemd-devel] 通过 cpuset 控制器将所有进程限制到 CPU/RAM”:

    2016 年 7 月 20 日星期三 12:53,Daniel P. Berrange(berrange at redhat.com)写道:

    对于虚拟化主机,通常希望将所有主机操作系统进程限制到 CPU/RAM 节点的子集,而其余部分可供 QEMU/KVM 独占使用。历史上人们使用“isolcpus”内核参数来做到这一点,但去年它的语义发生了变化,因此其中列出的任何 CPU 也会被调度程序排除在负载平衡之外,这使得它在一般的非实时用例中毫无用处您仍然希望 QEMU 线程在 CPU 之间实现负载平衡。

    所以唯一的选择是使用 cpuset cgroup 控制器来限制进程。 AFAIK,systemd 目前没有对 cpuset 控制器的明确支持,所以我正在尝试找出“最佳”方法来在 systemd 背后实现这一点,同时最大限度地减少未来 systemd 版本破坏事物的风险。

    2016 年 7 月 20 日星期三下午 03:29:30 +0200,Lennart Poettering 回复:

    是的,我们目前不支持这一点,但我们愿意。但问题是,它的内核接口现在非常无聊,在这个问题没有得到解决之前,我们不太可能在 systemd 中支持这一点。 (据我了解 Tejun,cpuset 中的 mem 与 cpu 的关系可能也不会保持原样)。

    下一条消息

    2016 年 7 月 20 日星期三 14:49,Daniel P. Berrange(berrange at redhat.com)写道:

    一旦发行版切换,cgroupsv2 可能会破坏很多东西,所以我认为这不会在小更新中完成 - 只有一个主要的新发行版版本,所以,不那么令人担忧。

我希望这能澄清事情。

相关内容