cgroup v2 内存控制:四个界限的分工与五个常见误区

知识相关2分钟前发布 一方
0 00

cgroup v2 的内存控制器要解决的问题很具体:同一台机器上多个进程组抢内存时,谁先被回收、谁被限速、谁被 OOM 杀掉。它不按进程看,只按 cgroup 记账,规则落在 /sys/fs/cgroup 下的几个文件里。搞不清这几个文件的分工,就会出现“设了限制还是被杀”或“限制没到却已经开始卡”的情况。

计量口径:memory.current 里装的是什么

内核文档列了三类被统计的内存:用户态内存(页缓存与匿名内存)、内核数据结构(dentries、inodes 等)、TCP socket 缓冲区。三者都计入 memory.current,也会在 memory.stat 里分项列出。

这意味着 page cache 算在用量里。读一个大文件,memory.current 会明显上升;但只要 file 那一项占大头,这些页通常可以直接回收,不代表进程真要 OOM。

memory.stat 里 anon 与 file 是最该先看的两个键。anon 高说明进程自己申请的内存多,回收只能靠 swap;file 高说明主要是缓存,回收代价低。

还有一个常被忽略的细节:内存记在分配它的那个 cgroup 上。进程后来被移到别的 cgroup,已经占用的内存不会跟着搬。容器重组或进程迁移时误判,多半是栽在这里。

四个界限各有分工

memory.min、memory.low、memory.high、memory.max 名字接近,作用分两类:前两个是保护,后两个是限制。混着用,就会出现保护没生效、限制又太晚的局面。

memory.min 是硬保护。用量在有效边界内,这块内存在任何情况下都不被回收。memory.low 是尽力而为的保护,只有在没有其他可回收内存时才会动它。两者的有效边界都受祖先 cgroup 的同名设置限制,具体措辞见 内核 cgroup v2 文档 的内存章节。

文档对 min 还有一条明确警告:把超过实际可用总量的内存放进这层硬保护,可能导致持续 OOM。原因是保护额度一旦被占满,其他 cgroup 就没有可回收的余量,分配失败只能走 OOM 路径。硬保护不是越多越好。

memory.high 是节流线。超过后进程被限速并承受强回收压力,但内核文档写明:越过 high 从不触发 OOM killer,极端情况下可以突破。memory.max 才是硬限,用尽且回收不下时在 cgroup 内触发 OOM。

文件性质默认值超限后果systemd 设置
memory.min硬保护0不被回收;无可回收内存时触发 OOMMemoryMin=
memory.low尽力保护0无其他可回收内存时才会被回收MemoryLow=
memory.high节流线max限速与强制回收,不杀进程MemoryHigh=
memory.max硬限maxcgroup 内触发 OOM killerMemoryMax=
memory.swap.maxswap 硬限max匿名页不再换出MemorySwapMax=

默认值容易记混:min 与 low 默认 0,high 与 max 默认 max,也就是不限制。写 max 表示关闭限制,写 0 才是显式设为零,两者差别很大。

超限之后:回收、节流与 OOM 的顺序

一次内存分配要先记账。当记账会让用量越过 memory.max,内核先在当前 cgroup 内做直接回收,回收够了就继续分配。

回收不够,就进入 OOM 路径,在这个 cgroup 内挑任务杀掉。默认只杀本 cgroup 里的进程,不会波及兄弟 cgroup。文档还提到,部分类型的分配不会触发 OOM killer,例如磁盘预读可能被直接忽略。

memory.oom.group 设为 1 时,整个 cgroup 连同子孙被视为不可分割的工作负载,要么全杀要么不杀,用来避免只杀掉一半进程留下的不一致状态。oom_score_adj 为 -1000 的任务是例外,不会被选中。

memory.high 走的是另一条路:不杀进程,改成限速加强制回收。systemd 文档与内核对这条的建议一致,把 MemoryHigh 当主要控制手段,MemoryMax 只作防线。

swap 侧也有对应的一层。memory.swap.high 是节流线,文档把它描述为“不归点”:健康的工作负载不该碰到它。调低 swap 上限后,已存在的 swap 条目只会逐步回收,用量可能长时间高于新上限,这一点在观察曲线时容易被当成没生效。

事后取证靠 memory.events。high 记被节流的次数,max 记逼近上限的次数,oom 记分配即将失败的次数,oom_kill 记实际被杀进程数。这些计数是层级累计的,要分辨本层事件得看 memory.events.local。

五个容易踩的误区

下面五条在实践中反复出现,前三条最常见。

  1. 把 memory.high 当硬限。 它只节流,超了能过,代价是变慢。用它做容量规划,得到的是一段可以突破的缓冲,不是天花板。
  2. 以为容器里 free 显示的是容器可用内存。 /proc/meminfo 默认不随 cgroup 虚拟化,读到的是宿主机数字。容器运行时要靠 LXCFS 这类 FUSE 文件系统,把 meminfo、cpuinfo、pressure 等文件重映射成容器视角的值。
  3. 以为 Kubernetes 的 memory request 等于 memory.min。官方文档 的措辞是运行时“可能”把 request 当作提示去设置 memory.min 与 memory.low,并非保证。request 主要用于调度,真正落到内核的硬限来自 limit。
  4. 以为 memory.max 也管 swap。 swap 是独立的一套:memory.swap.max 限用量,memory.swap.current 记用量,zswap 另有 memory.zswap.max。只设 memory.max,容器仍可能大量换出。
  5. 把 memory.current 逼近 memory.max 当作马上要 OOM。 file 占大头时这些页可以回收。先看 memory.stat 的构成,再看 memory.pressure 的 some 与 full,比只看一个总量可靠。

排查顺序与配置建议

第一步看 memory.current 与 memory.peak。peak 记录创建以来的峰值用量,用来判断是不是刚发生过一次短时冲高,而 current 已经回落。peak 的复位有点特殊:往文件写入任意非空字符串会把峰值重置为当前用量,文档特别注明这次重置只对同一文件描述符的后续读取生效。

第二步看 memory.stat 里 anon 与 file 的比例,判断是进程自身占用还是缓存占用。这两类内存的处置方式完全不同,一个是加 swap 或减进程,另一个是等回收。

第三步看 memory.events 的 high、max、oom_kill。三个数都为 0 却仍然卡顿,问题多半不在上限,而在回收压力或磁盘 IO;内核侧 IO 路径与页缓存的关系,可以参照 io_uring 异步 IO 原理 那篇里的读路径说明。

第四步看 memory.pressure。some 表示至少有一部分任务因内存卡住,full 表示所有非空闲任务同时卡住,后者意味着 CPU 空转,接近 thrashing。两者各带 avg10、avg60、avg300 与 total,短时抖动看 total,趋势看平均值。字段定义见 内核 PSI 文档。

想主动压低用量,可以往 memory.reclaim 写字节数,例如 1G,触发目标 cgroup 的回收。文档提示内核可能多回收或少回收,回收不足会返回 EAGAIN,而且这条路径本身不代表真实内存压力。

配置上有一条经验值得先记住:生产环境里给容器设 memory.max 是必要的,但只设这一个数字,等于把限流和杀进程两件事压在同一条线上。加上 memory.high 留出一段缓冲,让压力先变成变慢而不是被杀,排查时也更容易拿到 memory.events 里的 high 计数。

这套机制适合自己运维容器或写调度策略的人。只是偶尔在笔记本上跑一个容器,记两条就够:容器里 free 不可信,看 /sys/fs/cgroup/memory.current;限制到顶先看 memory.events,不要直接猜 OOM。systemd 单元上的对应参数与重启行为,可以对照 systemd 服务反复重启的配置逻辑 一起看。


相关地址

以下为文中引用的官方文档与项目入口,参数定义与默认值以这些页面为准。

  • Linux 内核 cgroup v2 文档(内存控制器章节):https://docs.kernel.org/admin-guide/cgroup-v2.html
  • Linux 内核 PSI 压力失速信息文档:https://docs.kernel.org/accounting/psi.html
  • systemd 资源控制文档:https://www.freedesktop.org/software/systemd/man/latest/systemd.resource-control.html
  • Kubernetes 资源请求与限制文档:https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
  • LXCFS 项目仓库:https://github.com/lxc/lxcfs
© 版权声明

相关文章