叶子组没有耗尽自己的额度,任务却仍被限制,可能是祖先资源已经用完。cgroup 的层级、控制器启用与实际节流 要连起来看;进入 cgroup namespace 只改变路径视图。
本篇以 v2 统一层级为主,v1 可使用多个控制器层级。实际挂载方式应查询当前环境。
查阅目标 阅读位置
限制来源与管理权限 层级、委派
CPU、内存与任务预算 各控制器
是否真正受阻 PSI、统计与源码入口
层级限制会沿祖先生效
子 cgroup 的配置不能绕过父级限制。父级 CPU 额度可能被多个子组共同消耗,父级内存限制也约束整个子树;只检查叶子文件会漏掉真正瓶颈。
cgroup.controllers 表示当前层级可用于子组的控制器,cgroup.subtree_control 决定向子层级启用哪些。mkdir 不意味着所有控制器文件和策略自动齐备。控制器需要沿层级自上而下启用。
普通 domain 控制器通常遵循 no-internal-process 约束:内部节点分配资源给子组时,不能同时放普通工作进程与子组竞争;根和 threaded 模式有专门规则,不能一条概括所有情况。cgroup v2
委派比直接修改系统根目录更重要
现代系统常由 systemd 等管理 cgroup 树。应用应使用获委派的子树或管理器接口,不能假定自己可重新 mount /sys/fs/cgroup 或随意改上层控制器。
在允许的目标组写 cgroup.procs 会移动整个进程的线程;threaded 模式中 cgroup.threads 有不同语义。迁移权限还涉及共同祖先等要求,不是目录可写就能拉入任意进程。进程迁移也不保证已有内存记账随之全部转移。
CPU:权重、额度与放置分别配置
接口 例子 含义
cpu.weight 200 相对同级竞争者的权重,默认 100
cpu.max 50000 100000 每 100000 微秒周期最多消耗 50000 微秒 CPU 时间
cpuset.cpus 0-3 请求可用 CPU 集合,还需看 effective 与祖先限制
weight=200 不保证获得两颗 CPU,也不保证任何时刻都比另一组快两倍;权重主要在相应任务共同竞争时体现,空闲资源可被使用。
cpu.max 的额度由组内相关任务共同消耗。两个线程可在两个 CPU 同时运行并更快用完额度,随后被节流;它不是“每颗 CPU 固定跑 50%”。观察 cpu.stat 的 nr_throttled、throttled_usec 等增量,才能判断额度是否实际成为瓶颈。
cpuset 限制放置范围不等于独占这些 CPU,分区与独占配置另有规则。cpuset.mems 也应结合有效节点与 NUMA 分配策略理解。
子组各剩 30 ms,为什么还是一起被节流
设父组 P 的 cpu.max 为 100000 100000,两个子组 A、B 都是 80000 100000。为了突出预算关系,假设周期同时从 t=0 开始,禁用额外 burst,A、B 各有一个持续可运行的普通公平调度任务,并各占一个可用 CPU;忽略调度与 runtime slice 分发的细节。
在前 50 ms 墙钟时间里,A 消耗 50 ms CPU 时间,B 也消耗 50 ms,父组的 100 ms 总额度已经用完。A、B 自己都还剩 30 ms,却不能越过父组继续运行。到下一周期补充额度之前,继续提高子组上限也没有消除这个祖先瓶颈。
两个 CPU 并行消耗同一份父组额度
任务 A
运行:消耗 50 ms CPU
父组额度耗尽,停止推进
任务 B
运行:消耗 50 ms CPU
父组额度耗尽,停止推进
0 ms
50 ms
100 ms
50 ms 墙钟 × 两个 CPU = 父组 100 ms CPU 预算
相同周期、无 burst 的理想模型;子组此时各剩 30 ms,仍受祖先约束。
def one_period ( active ) :
parent_left = 100
leaf_left = { " A" : 80 , " B" : 80 }
used = { " A" : 0 , " B" : 0 }
for _ in range ( 100 ) : for name in active :
if parent_left > 0 and leaf_left [ name ] > 0 :
parent_left -= 1
leaf_left [ name ] -= 1
used [ name ] += 1
return used , leaf_left , parent_left
assert one_period ( ( " A" , " B" ) ) == ( { " A" : 50 , " B" : 50 } , { " A" : 30 , " B" : 30 } , 0 )
assert one_period ( ( " A" , ) ) == ( { " A" : 80 , " B" : 0 } , { " A" : 0 , " B" : 80 } , 20 )
只有 A 运行时,它反而可以消耗 80 ms,之后被自己的上限限制,父组还剩 20 ms。这两个轨迹说明诊断应同时看任务竞争情况与各层 cpu.stat 增量。模型的 50 ms 是理想时间格结果,不是 Linux 对任意周期对齐、slice 缓存、调度类或机器负载的精确时序保证。CPU 控制器
权重则是另一种层级计算。若根下 P、Q 权重相同,P 下 A、B 权重相同,Q 下只有 C,在各组持续竞争同一份 CPU 容量、没有其他约束的比例模型中,P、Q 先各得一半,A、B 再分 P 的一半,最终是 A:1/4、B:1/4、C:1/2。把三个叶子的相同权重直接平摊成各 1/3,会丢掉中间层级;某组不再需要资源时,这个饱和竞争比例也就不再适用。
分别运行 A 与 B,看它们如何共同消耗祖先额度。某个子组还有余额时,祖先仍可能已被兄弟耗尽;子组配额不是预留给它的独立 CPU 资源。
A child = min ( R child , R parent )
R 是本周期剩余 CPU 时间。真实多层层级要沿所有相关祖先检查;图中只保留父子两层。
正在呈现知识画面 重新加载
子组还有额度,为什么也不能继续运行? 子组的配置额度不等于预留份额,也不能绕过祖先。可用量受 min(子组剩余, 祖先剩余) 限制,祖先被一个兄弟消耗后会影响另一个。
{"id":"cgroup-ancestor-budget","title":"子组还有额度,为什么也不能继续运行?","summary":"子组的配置额度不等于预留份额,也不能绕过祖先。可用量受 min(子组剩余, 祖先剩余) 限制,祖先被一个兄弟消耗后会影响另一个。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"parent\" data-i18n=\"parent\"></label><output for=\"parent\" id=\"parent-value\"></output><input id=\"parent\" type=\"range\" min=\"10\" max=\"100\" value=\"60\" step=\"10\"></div><div class=\"control\"><label for=\"a\" data-i18n=\"a\"></label><output for=\"a\" id=\"a-value\"></output><input id=\"a\" type=\"range\" min=\"10\" max=\"100\" value=\"50\" step=\"10\"></div><div class=\"control\"><label for=\"b\" data-i18n=\"b\"></label><output for=\"b\" id=\"b-value\"></output><input id=\"b\" type=\"range\" min=\"10\" max=\"100\" value=\"50\" step=\"10\"></div><p class=\"caption\" data-i18n=\"legend\"></p><div class=\"budget \"><header>root<span id=\"remaining-parent\"></span></header><div><i id=\"used-parent\"></i><b id=\"limit-parent\"></b></div></div><div class=\"budget child\"><header>A<span id=\"remaining-a\"></span></header><div><i id=\"used-a\"></i><b id=\"limit-a\"></b></div></div><div class=\"budget child\"><header>B<span id=\"remaining-b\"></span></header><div><i id=\"used-b\"></i><b id=\"limit-b\"></b></div></div><div class=\"actions\"><button id=\"runA\" data-i18n=\"runA\"></button><button id=\"runB\" data-i18n=\"runB\"></button></div><div class=\"metrics\"><div><span data-i18n=\"availableA\"></span><strong id=\"availableA\"></strong></div><div><span data-i18n=\"availableB\"></span><strong id=\"availableB\"></strong></div></div><div class=\"formula\" id=\"formula\"></div><div class=\"actions\"><button id=\"period\" data-i18n=\"period\"></button></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.budget{padding:10px 0;margin:12px 0}.budget.child{margin-left:18px;border-left:1px solid var(--rule);padding-left:12px}.budget header{font:12px var(--viz-font-ui);display:flex;justify-content:space-between;margin-bottom:10px}.budget>div{height:22px;background:var(--surface);position:relative}.budget i{height:100%;display:block;background:var(--accent);transition:width .2s}.budget b{width:2px;position:absolute;top:-5px;bottom:-5px;background:var(--second);transition:left .2s}.formula{min-height:5em;white-space:pre-line}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let used=[0,0];function draw(){const p=+$('#parent').value,a=+$('#a').value,b=+$('#b').value,total=used[0]+used[1],remaining=Math.max(0,p-total),aa=Math.min(a-used[0],remaining),bb=Math.min(b-used[1],remaining);for(const[id,q,u]of [['parent',p,total],['a',a,used[0]],['b',b,used[1]]]){set(id+'-value',q+' ms');set('remaining-'+id,(q-u)+' / '+q+' ms');$('#used-'+id).style.width=u+'%';$('#limit-'+id).style.left=q+'%';}set('availableA',aa);set('availableB',bb);set('formula','A: min('+(a-used[0])+', '+remaining+') = '+aa+' ms\\nB: min('+(b-used[1])+', '+remaining+') = '+bb+' ms');$('#runA').disabled=aa<10;$('#runB').disabled=bb<10;set('explanation',t('note'));}for(const[id,i]of [['runA',0],['runB',1]])$('#'+id).onclick=()=>{used[i]+=10;draw();};for(const id of ['parent','a','b'])$('#'+id).oninput=()=>{used=[0,0];draw();};$('#period').onclick=()=>{used=[0,0];draw();};draw();","audio":false,"strings":{"a":"A 额度 / ms","availableA":"A 当前可用 / ms","availableB":"B 当前可用 / ms","b":"B 额度 / ms","heading":"子组还有额度,为什么也不能继续运行?","intro":"分别消耗 A 或 B 的 CPU 时间,每次同时扣除该子组与共同祖先的额度。调整配置会开始一个新的示意周期。","legend":"实色是已消耗 CPU 时间,竖线是该级额度;同一尺度 0…100 ms。","note":"默认 A=50、B=50、祖先=60。让 A 运行 50 ms,再让 B 运行 10 ms:B 自己仍余 40 ms,但共同祖先已没有额度。","parent":"祖先额度 / ms","period":"开始下一个周期","reset":"重置","runA":"A 消耗 10 ms CPU","runB":"B 消耗 10 ms CPU","scope":"cgroup v2 两层、共同 100 ms 周期、无内部任务,无 burst。每次记账 10 ms CPU 时间,不是墙钟推进;不模拟实际运行队列、额度批量转移或精确节流统计。子组额度之和允许超过祖先,但不创造额外 CPU 预算。","scopeLabel":"模型范围与约定"}}
内存:high 是压力控制,max 是边界
memory.high 与 memory.max 的职责不同
超过 memory.high
回收与节流压力;可暂时超过
触及 memory.max
尝试限制与回收;失败可能 memcg OOM
结合 memory.events 和 memory.pressure
区分越界次数、OOM/kill 事件与实际等待时间
high 本身不触发 OOM kill;不是简单按阈值顺序执行的完整内核调用图。
接口 达到相关条件后的含义
memory.high 施加回收与节流压力,可暂时突破,本身不触发 OOM killer
memory.max 硬限制下先尝试回收等,无法满足时可能进入 memcg OOM
两者不能合并成“超过一字节马上杀进程”的单一开关。
memory.low 是最佳努力回收保护,memory.min 提供更强的保护语义,但有效保护受祖先与竞争分配影响,不是预先分配一块物理 RAM。memory.swap.max 对 swap 又有独立控制。
memory.oom.group 可要求相应 OOM 处理按组对待工作负载,不能从“high 回收失败”直接画箭头到“因为 oom.group 杀全组”。需要区分事件发生的层级与触发原因。
回收选择取决于页面状态、活跃性、可写回性、swap 和策略等,不保证固定先清所有 cache、再换匿名页。memory.current 包含相应被记账的内存,不等于某个进程 RSS。
I/O 与任务数量
io.max 按设备限制字节或 IOPS,格式中的主次设备号应来自实际目标设备;io.weight 的效果依赖块层和相应调度/策略支持。缓存写入、回写与底层设备路径会影响可见表现,不能把任意 read/write 调用速率直接当作块设备速率。
pids.max 限制内核任务数量,线程也计入。达到限制会阻止相应 fork/clone 创建,典型错误为 EAGAIN;设置更低上限或移入已有任务不等于自动杀死超额任务。应观察 pids.current 与 pids.events。
PSI 观察等待,而不只是用量
指标 解释
some 至少一个任务因该资源受阻的时间占比
full 相应范围所有非空闲任务都受阻的时间
avg10/60/300 相应平均百分比
total 累计微秒,不是百分比
先明确范围和单位,才能把等待与资源用量放在一起分析。
CPU 使用率高可能是有用工作充分执行,PSI 则帮助观察等待损失;内存使用高也可能主要是有价值缓存,不应直接等同于内存压力。系统级 CPU full 有特殊语义,不能机械套用内存 full 的解释。PSI
some 与 full:两个任务的等待区间怎样合并
以某 cgroup 的 I/O 压力为例,观察 100 ms,假设只有 A、B 两个非空闲任务,二者全程都有工作需求。A 在 [0,40) ms 等待 I/O,B 在 [20,60) ms 等待 I/O,其余时间各自能推进工作。
区间 ms A B 对压力时间的贡献
0–20 受阻 可推进 some 增加 20 ms
20–40 受阻 受阻 some、full 各增加 20 ms
40–60 可推进 受阻 some 增加 20 ms
60–100 可推进 可推进 都不增加
所以 some=60 ms,full=20 ms。把两个任务各自的 40 ms 等待相加会得到 80 ms,那是任务等待时间之和,不是 some 对墙钟区间的并集计数。
def pressure_ms ( blocked , window = 100 ) :
assert blocked and window > 0
points = { 0 , window }
for intervals in blocked :
for start , end in intervals :
assert 0 <= start <= end <= window
points . update ( ( start , end ) )
points = sorted ( points )
some = full = 0
for start , end in zip ( points , points [ 1 : ] ) :
waiting = [ any ( a <= start < b for a , b in intervals ) for intervals in blocked ]
if any ( waiting ) :
some += end - start
if all ( waiting ) :
full += end - start
return some , full
waits = [ [ ( 0 , 40 ) ] , [ ( 20 , 60 ) ] ]
assert pressure_ms ( waits ) == ( 60 , 20 )
assert pressure_ms ( waits + [ [ ] ] ) == ( 60 , 0 ) assert pressure_ms ( [ [ ( 0 , 10 ) ] , [ ( 10 , 20 ) ] ] ) == ( 20 , 0 )
assert pressure_ms ( [ [ ] , [ ] ] ) == ( 0 , 0 )
assert pressure_ms ( [ [ ( 0 , 40 ) , ( 20 , 60 ) ] ] ) == ( 60 , 60 )
代码对区间分段后计数,是固定非空闲任务集合下的教学模型。真实内核还要根据任务状态变化和统计范围记账,不能把所有创建过的线程都永久计入 full 的分母。例如一个已无工作而主动空闲的线程,不应被当成“一直在推进”来强行把 full 降为零。PSI 定义
若两次采样相隔 100000 μs,some 的 total 增加 60000 μs,得到的是这段采样窗口内约 60% 的受阻时间。它不是文件里 avg10 的重新计算公式;avg10/60/300 是内核维护的平滑平均。还应检查读取间隔、计数器重置和组的生命周期,不把不同范围的 total 相减。
只读检查当前进程
cat /proc/self/cgroup
findmnt - t cgroup2
cat /proc/pressure/cpu
cat /proc/pressure/memory
cat /proc/pressure/io
根据实际 cgroup 挂载根和进程路径定位所属组,再读取 cpu.stat、memory.events、memory.pressure、pids.current 等可用文件。容器可能只暴露一段层级,/proc 路径也受 namespace 影响,不要无条件把字符串直接拼到宿主根目录。
诊断顺序可以是:确认所属组和祖先约束,检查控制器是否启用,再比较统计增量与实际请求延迟。读取计数器不会解释全部因果,但能区分“配置了限制”和“真的被限制”。
内核阅读入口
kernel/cgroup 管核心层级与成员关系,mm/memcontrol.c 管内存记账与控制,block/blk-cgroup.c 管块层关联。controller 的 css 是子系统状态,不是所有控制器都把同一个对象挂在 request_queue 上。
从用户可见接口契约进入源码,再查看具体版本的 charge、reclaim 或调度路径,比把几十行注释伪装为通用 try_charge 调用图更可靠。隔离视图的另一半见 命名空间 。