背景
写 memory.high、memory.max 或 memory.reclaim 会在 writer 的上下文里同步跑 reclaim,循环到用量降到目标以下(或 memory.reclaim 回收到请求量)。大 cgroup 上这一趟可能很久:swap I/O 时被设备写带宽卡住,thrashing 时几乎无界:每轮回收几页、被 workload 立即 fault 回来,循环一直在"progress"、永不收敛。
┌─────────────────────────────────────────┐
│ 写 memory.high/max/reclaim 同步 reclaim │
│ 持 active ref │
└────────────────────┬────────────────────┘
▼
┌─────────────────────────────────────────┐
│ 并发 rmdir 持 cgroup_mutex 等 kernfs │
│ drain │
└────────────────────┬────────────────────┘
▼
┌─────────────────────────────────────────┐ thrashing 下 reclaim 持续 progress
│ cgroup_mutex 被持致全系统 stall │ 不收敛
└─────────────────────────────────────────┘
这些写经 cgroup_file_write(),它不持 cgroup_mutex、也不 pin css,而是靠 kernfs 在操作期间持一个 active reference 保活。所以 reclaim 循环跑着的时候,文件上的 active reference 一直握着。此时若有另一任务并发 rmdir 同一个 cgroup:cgroup_rmdir() 拿 cgroup_mutex,然后在 kernfs_drain() 里阻塞等那个 active reference 排干。cgroup_mutex 一路被持,所有要用它的任务都堆在 remover 后面,实测整台机器停摆,连只是读 /proc/
问题
- 写 memory.high/max/reclaim 同步 reclaim 持 kernfs active reference
- 并发 rmdir 持 cgroup_mutex 阻塞 kernfs_drain 等 active ref
- cgroup_mutex 被持致全系统 stall(含无关的 /proc/pid/cgroup 读)
- thrashing 下 reclaim 持续 progress、MAX_RECLAIM_RETRIES 不触发,stall 无界
方案
cgroup 销毁在 kill_css_sync() 里置 CSS_DYING,早于 css_clear_dir() 触发的 kernfs_drain(),所以 in-flight 的 reclaim 循环保证在下一轮迭代前观测到它。本系列在所有 reclaim 循环里每轮检查 memcg_is_dying(),命中就 bail out,writer 立刻放下 active reference,remover 得以前进。
旧:reclaim 不检查 dying
┌─────────────────────────────────────────┐
│ reclaim 循环只在零进度时退出 │
└────────────────────┬────────────────────┘
▼
┌─────────────────────────────────────────┐
│ thrashing 下持续 progress 不收敛 │
└────────────────────┬────────────────────┘
▼
┌─────────────────────────────────────────┐
│ 持 active ref 阻塞 rmdir 致全系统 stall │
└─────────────────────────────────────────┘
新:dying 时 bail out
┌───────────────────────────────────────┐
│ dying 标志在 drain 前设置 │
└───────────────────┬───────────────────┘
▼
┌───────────────────────────────────────┐
│ reclaim 循环每轮检查 memcg 是否 dying │
└───────────────────┬───────────────────┘
▼
┌───────────────────────────────────────┐
│ bail out 释放 active ref 让 rmdir 前 │
│ 进 │
└───────────────────────────────────────┘
与只看零进度的 MAX_RECLAIM_RETRIES 不同,dying 检查覆盖慢 swap I/O 与 thrashing:那些场景里 reclaim 一直在"成功一点点"、循环本不会收敛。memory.reclaim 因 dying 而 bail out,意味着请求量没满足,write 返回 -EAGAIN。这套 bail out 与 O_NONBLOCK(c8e6002bd611)正交:O_NONBLOCK 让调用者事先避免同步 reclaim,本系列处理的是 reclaim 已经在跑、cgroup 开始被删的场景。dying 时 reclaim 本就无意义:cgroup 在 teardown,剩余页会 reparent 给父。
收益
作者提供 production hung task 日志(6.6.102 kernel,作者未标具体 CPU/物理机/VM):
| 指标 | 修复前 stall | 修复后 |
|---|---|---|
| cgdelete(kernfs_drain 等 active ref) | 159s | 即时释放,不再 stall |
| systemd-journal 读 /proc/pid/cgroup(等 cgroup_mutex) | 182s | 即时,cgroup_mutex 不再被持 |
修复前系统只在 reclaim 终于跑完、释放 active ref 后才恢复;而那趟 reclaim 在 dying 的 cgroup 上纯属浪费(页将 reparent)。修复后 reclaim 循环观测到 dying 即 bail out,active ref 即时释放、remover 前进、cgroup_mutex 解锁,全系统立刻恢复。