05 的身份映射不会阻止一个进程消耗 CPU。隔离“看见什么”、规定“谁是谁”和限制“能用多少”是三个独立问题。cgroup v2 的层级与控制器负责第三个问题;套用 v2 的文件名到本机的 v1 只读层级,会得到看似合理但根本没生效的配置。

带宽、竞争与亲和性

cpu.max 是带宽额度和周期:例如在支持该接口的 cgroup 中写 50000 100000 表示每 100000 微秒最多使用 50000 微秒配额,具体跨多个 CPU 的行为还要看周期和并发。达到额度会节流,读取 cpu.stat 的 nr_throttled、throttled_usec 观察变化;延迟增加却不一定是因为节流,还可能是排队、存储或网络。cpu.weight 是父层级内的相对权重,主要在同一竞争条件下分配份额,空闲时并不等于硬上限。cpuset.cpus.effective 表示可运行 CPU 集,CPU 集缩小不等于改变权重;还需比较宿主拓扑与亲和性。

控制器的可见性与委派也有层级条件。只有父层启用对应的 cgroup.subtree_control、将可写子层委派给实验用户并满足内核规则,才有资格写子组配置。不能在共享根 cgroup 创建压力任务;不能把 cgroup namespace 的路径隐藏能力当成资源限额。官方滚动文档所列字段需按专用 VM 的内核版本再次核对。

层级是测量值的一部分

cgroup v2 的资源限制不是给 PID 贴一个永久标签,而是把进程加入一棵有父子关系的组树,由控制器沿树施加约束。/proc/<pid>/cgroup 告诉观察者目标进程位于什么组;它不保证该组有 CPU 限额,也不保证当前用户能修改父组。在 v2 文件系统里,父组的 cgroup.controllers 列出可供启用的控制器,父组 cgroup.subtree_control 决定哪些控制器向子组开放;要写某个子组的 cpu.max,须先确定父层启用了 cpu 且子组确实属于预期委派。普通域组还受“内部无进程”规则约束,不能随意在载有任务的父组启用域控制器,再把失败说成内核不支持 CPU 控制。

比较两个任务之前,应把它们放在同一个受控父组的不同子组,并留证所有层级的有效配置:某个祖先 cpu.max 已经限流,子组再写较宽配额不会让整体突破祖先约束;把两个任务放在同一个子组则谈不上比较兄弟组的 weight。迁移 PID 也有权限前提和时间窗口,脚本写入 cgroup.procs 后应重新读取目标进程的位置,不能仅凭 echo 成功猜测在整段测试中始终在该组。实验应由管理员委派一棵独立的可写子树并明确删除责任;共享机器上的根 cgroup 和其他任务的子组都不属于教学实验资源。

额度的分子分母分别是什么

Linux 5.10 文档把 cpu.max 写为 $MAX $PERIOD,单位均为微秒;max 100000 表示没有带宽上限。配置 50000 100000 意味着每个 100 毫秒周期可累计使用至多 50 毫秒 CPU 时间,通常可近似理解为长期平均半个 CPU 的额度,而不是“每个可用核都能跑一半时间”。两个计算线程同时在两个可运行核心上执行,可能更快花完同一个组的 50 毫秒 CPU 时间,随后在周期余下的墙上时间等待。一次几毫秒的请求未必碰到限额;两个线程同时推进时单次请求的抖动又可能高于单线程对照,必须记录并发与实际运行时长。

cpu.stat 中 usage_usec、user_usec、system_usec 是使用量;在 cpu 控制器启用的条件下还报告 nr_periods、nr_throttled 和 throttled_usec。先读取基线,再对有限时负载读取结束值,求增量而非直接拿累计值比较不同机器。nr_throttled 增加支持某些周期受节流,但不说明哪一次业务请求一定因节流失败;throttled_usec 也不能被当作应用所有线程等待时间的简单总和。零增量则应核对负载是否持续到触达限额、CPU 控制器是否启用、负载是否迁入该组及采样窗口是否足够,而不是把零写成“没有限制”。本机 cgroup v1、只读,没有 cpu.max 可以写,这些都是 VM 补跑预期而非当前结果。

相对权重为什么需要对手

cpu.weight 在同一父组的活跃子组之间分配 CPU 机会,默认值与允许范围以目标内核的接口文件为准。若两组分别设为 100 与 200、同时持续计算且都可在相同核心集上运行,后者可以在竞争时获得较高份额;这不是对每次迭代严格保证的 1:2 吞吐比。Python 循环执行速度还受解释器、调度噪声、CPU 频率和其他工作负载影响,不能用一轮哈希次数按比例校验内核是否正确。负载只有一组时,另一组的权重不构成竞争,改成 200 并不会把处理器频率提升一倍;把这一次“没变快”说成权重失效,是把模型适用条件省掉了。

对照应先跑同一计算探针在单组无竞争场景,再开启第二组相同上限的负载,保持两者输入和时间窗一致,多轮交替顺序并记录每组的 CPU 时间、完成的迭代数和宿主背景负载。若存在硬额度,先使额度足以不触发节流,才能把差异重点归到竞争权重;否则更窄的 cpu.max 会先截断权重带来的机会。即便多轮结果倾向某组,还要检查两组是否位于同一个父组、各自实际 cpu.weight 是否生效。这样从“配置值 → 所在层级 → 竞争条件 → 计数器”走通,才是可迁移的判断方法。

cpuset 是另一条约束轴

cpuset.cpus.effective 给出当前组实际可使用的 CPU 列表,可能受上层配置或热插拔影响,不能只读本组希望设置的 cpuset.cpus。把可运行核心缩成一个,不会自动把 cpu.max=50000 100000 改成别的值;同样是累计 50 毫秒 CPU 时间,但单核组与双核组消耗额度的墙钟形态和并行度可能不同。若线程还设有自己的 CPU affinity,实际可运行位置还要取交集;性能下降但 nr_throttled 不变时,优先检查可运行核心、排队、主机负载,而不是臆造节流。

一个具体的反例:容器里报告“可见 32 核”,但工作进程的有效 cpuset 只有 2 核,且 cpu.max 允许平均半个 CPU;此时仅以 /proc/cpuinfo 的行数预测并发吞吐会错两次。反过来,cpuset.cpus.effective 显示 2 核不等于 CPU 限额一定是 2 核,也不保证这两个核整段时间都是空闲的。先看进程所在组、有效核心、额度、权重和统计增量,再结合业务耗时判断;CPU topology、频率和宿主调度条件始终是解释吞吐差异的必要背景。

正例、反例与当前证据缺口

专用 VM 的正例应把同一计算程序各运行一次:一组受 cpu.max 限额,一组不设硬限;记录程序耗时、限额、运行前后的 cpu.stat,再加第二组并发负载对照 cpu.weight。反例是没有其他竞争者时提高 weight 却看不到吞吐提升;这不能推翻权重语义,因为比较条件已变。清理先停止自己的任务、核对 cgroup.procs 为空,后删除子 cgroup,不能写全局控制器开关。

本云端的00 基线显示 cgroup v1 且只读,不存在 cpu.max、cgroup.controllers;因而上述正反例均 NOT_RUN。附件给出检查条件与补跑指令;共用最小源码包不含任何伪造的节流输出。静态阅读 v2 文档是 DOC_VERIFIED,不是本机 LAB_VERIFIED。

现有共用 bounded_load.py 的 --seconds 限为 0–2、--memory-mib 限为 0–32;用它在自建委派组中分别计算时,也须保留实际迭代数、采样前后时间和进程退出码。脚本输出一次 iterations 只能说明它在那段墙钟时间做了多少次哈希,无法证明写 cpu.max 成功;必须与该组的实时文件值和 cpu.stat 增量相互印证。每一支实验都应逐条记录命令、开始结束 UTC、内核及 cgroup 文件系统版本、写入的值、预期与实际差异,最后停止所有自己启动的负载、检查子组进程数归零并移除子组。若在委派 VM 上仍看到 EACCES,应先记录目录属主和委派配置,不能申请对共享根组的写权限来凑一次通过。

用一个有界输入练习归因

设定两个同级自有子组 A、B,各有一份最多计算两秒的程序。先都不设硬配额:单独运行 A 时记录 usage_usec 差值、迭代数和墙钟;再让 A、B 同时跑且两个组都有相同有效 cpuset,分别比较 cpu.weight=100 与 200。这一步只能说明在特定竞争条件下的相对变化,主机还有其他任务时记录它们的负载而不试图改动其组。最后只给 A 设置 cpu.max=50000 100000,将 weight 恢复相同,读取 A 的 nr_periods 与 nr_throttled 增量。若出现节流,解释 A 的工作完成数为什么可能下降;若没有节流,先检查 A 是否真的持续占满 CPU 以及组配置是否写入成功。

配额的心算可以先于实验:单线程持续跑满一个核心,理想化模型下每周期能用 50 毫秒、等待大约 50 毫秒;两条线程分别跑在不同核心上,合计消耗 50 毫秒的理想化墙钟时间可短于 50 毫秒。现实中核间调度、频率波动、程序本身等待与 quota 周期边界会使观测不等于这个简单比例,因而不能用演算数当“实际采样时间”。需要把理想值、程序观察值和 cpu.stat 的增量分别放在实验记录的三列。

错误写入也应作为单独的失败情形,而不是偷偷跳过:若父组未开放 cpu 控制器,子组对应接口不可用;若没有写入权限,内核可能返回权限错误;若给 cpu.max 写了不符合接口要求的值则是输入错误。每种情况分别保存 cgroup.controllers、cgroup.subtree_control、对应文件可读性及确切 stderr/退出码,才能决定是换到管理员已委派的 VM、修正参数,还是补齐前置配置。当前云端没有满足先决条件,所以这三类失败也未在本机逐一触发;将推演与 NOT_RUN 保持分离,是比一张漂亮吞吐图更可靠的实验结论。

nr_throttled 对应组级计数,不能用进程 ID 的变化推断统计已重置:若先后将两份负载放在同一组,计数器持续累计;正确做法是每轮保存运行前和结束后的原始值,按同组做差,并记录进程是否从其他组迁入。退出后先确认所有实验进程不在该组,再删除自建组;删除失败时保存仍留在组内的 PID 与命令,不在共享宿主随意清理未知进程。

现象 核心证据 前提
CPU 变慢 cpu.stat 前后、任务耗时 对比相同工作负载
改 weight 无变化 竞争组及父子层级 无竞争时无硬额度
配额写入失败 版本、控制器、委派 本机 v1 只读无法运行

练习

  1. 先预测一组满载、另一组空闲时 weight=100 与 weight=200 的差异,再在专用 VM 实际对照;写出预期的竞争前提。
  2. 预测 CPU 从两个可用核心收缩为一个后 cpu.max 的解释是否改变;分别查看 cpuset.cpus.effective 与节流计数,不要用单次耗时归因。

上一篇:05:user namespace;下一篇:07:内存、pids 与 I/O。

系列总目录:从进程隔离到运行时与编排。

参考资料