服务器入坑指北:cgroup v2 内存上限与 OOM 取证
进程被杀了,却查不到是谁杀的;明明设了内存上限,服务还是被 OOM;软限也设了,可一点反应都没有;同一个控制组里的进程一起死掉,说不清为什么。
顺序是:先把压力算成比例 → 再看"杀过没有"的判据键 → 分清软限与硬限 → 最后看是谁占的。跳过前三步,你会把"压力大"直接当成"被杀过"。
这篇给三段可直接粘贴的内容:第一段只读(算压力与软硬限对照)、第二段只读(取证:判据键与两类内存)、第三段含写操作(复现与读回,只在测试单元上做、都标回滚点)。取值用占位符,文件名、字段名与配置键照写。
压力是一个比例,不是绝对值
压力比例 = 当前用量 / 硬上限。
只看分子没有意义——同一个用量在不同上限下意义完全不同:memory.current 给的是当前用量,memory.max 给的是硬上限,而达到硬上限且内存无法回收时才触发 OOM。
算一遍:假设当前用量 3 GiB、硬上限 4 GiB,则 3 / 4 = 75%;同一个 3 GiB,硬上限是 3 GiB 时就是 3 / 3 = 100%(已经在门槛上),硬上限是 8 GiB 时只有 3 / 8 = 37.5%。分子没变,含义完全变了。
现读值 → 算式 → 结论(走一遍):
- 现读
memory.current得<用量>、memory.max得<上限>; - 算式:
<用量>/<上限>= 压力比例; - 现读
memory.events里oom_kill的计数<计数>; - 结论:比例高、
oom_kill不变 ⇒ 只是压力,不是故障;比例顶到 1、且oom_kill在递增 ⇒ 确实被 OOM 杀过。
第 4 步之所以成立,是因为比例与判据键回答的是两个问题:一个说"挤不挤",一个说"有没有被杀"。两个都读,才敢下结论。
算压力块
cat /sys/fs/cgroup/<路径>/memory.current # 分子:当前用量
cat /sys/fs/cgroup/<路径>/memory.max # 分母:硬上限(达此值且无法回收才 OOM)
cat /sys/fs/cgroup/<路径>/memory.high # 软限:超它只加大回收压力,不触发 OOM
cat /sys/fs/cgroup/<路径>/memory.swap.max # 值为 0 表示本层禁用交换:压力无处可去
systemctl show <单元> -p MemoryMax # 上层写法对应的现读生效值(不看配置文件原文)
判据:三个值分别意味着什么状态——
- 用量低于软限:正常区间;
- 用量超过软限但没到硬限:在节流,回收压力加大,不会杀进程;
- 用量顶到硬限且无法回收:这才走到 OOM 那一步。
memory.swap.max读出来是0,说明这一层禁用了交换——内存压力没有任何缓冲,顶到硬限就是硬碰硬。
例证:软限与硬限的分界,用同一份数据看两遍
在你去设上限之前,先把这条分界看清:超过软限只节流,不杀进程;只有到硬限且无法回收才触发 OOM。
拿同一份数据看两遍(设软限 3 GiB、硬限 4 GiB):
| 当前用量 | 与软限 3 GiB 比 |
与硬限 4 GiB 比 |
会发生什么 |
|---|---|---|---|
2 GiB |
没到 | 没到 | 正常,2 / 3 ≈ 67%、2 / 4 = 50% |
3.5 GiB |
超了 | 没到 | 只节流:回收压力加大,不杀进程——3.5 / 4 = 87.5% |
4 GiB |
超了 | 到了 | 先尝试回收;回收不到才 OOM |
| 判据:设了软限还指望它防 OOM,方向就错了——软限是"提前施压",硬限才是"最后一道"。 |
取证块:判据键只有一项
cat /sys/fs/cgroup/<路径>/memory.events # 判据键:oom_kill 计数递增即发生过 OOM
cat /sys/fs/cgroup/<路径>/memory.events.local # 本层专用入口:只看本层、不含子层级
cat /sys/fs/cgroup/<路径>/memory.stat # 判据列:anon 与 file 两项看"谁占的"
systemd-cgtop # 宿主级视角:按 cgroup 排出的内存占用列
dmesg -T # 判据行:oom-kill 一类文本与时间点是否对齐
判据:
- 只看
oom_kill这一项是否递增——其余字段不参与这个判定;只看dmesg是不够的(它可能没记全),两者对齐才算同一次事件; - 两个入口回答不同问题:
memory.events含子层级,memory.events.local只看本层——"我会不会被我自己吃掉"用后者,"我这棵子树整体被杀了多少次"用前者,混用会把别人的账算到自己头上; - 谁占的看
memory.stat里anon与file的差值:匿名内存高说明是应用自己吃掉的,页缓存高说明是缓存撑起来的——这是"该加内存还是该调缓存"的分水岭。
复现与读回块
复现实验必须在测试单元上做,不在生产上做——这一点写在动作之前。
systemd-run --unit=<名字> -p MemoryMax=<值> <命令> # 在测试单元里把压力推到上限附近(非生产)
cat /sys/fs/cgroup/<路径>/memory.events # 读回:oom_kill 是否递增
cat /sys/fs/cgroup/<路径>/memory.oom.group # 现读"整组一起杀"开关
cat /sys/fs/cgroup/<路径>/memory.current # 读回:用量回到什么位置
"整组一起杀"是取舍:memory.oom.group 置 1 时,该控制组作为一个整体被处理(整组进程一起被杀)——好处是不会留下半个残废的服务(同组里有的活着、有的死了,状态最难收拾);代价是同组里无辜的进程一起被带走。两种取向下"用户看到什么"完全不同:一种是"服务整体重启",另一种是"服务半死不活"。
上层写法与内核文件同义:MemoryMax=(硬限)、MemoryHigh=(软限)、MemorySwapMax=(交换上限)、MemoryAccounting=(记账开关)。记账不开,上面这些读数全都拿不到——那时你会以为"没有上限",其实是"没有数"。
读回是独立的一步,不许省:改完要重读 memory.events 的判据键与 memory.current,确认比例下降、oom_kill 不再递增。改上限与改开关都属高风险写操作,需回滚点。
现象到动作的判定表
| 现象 | 结论 | 动作 |
|---|---|---|
| 进程被杀但查不到谁杀的 | 没看判据键 | 读 memory.events 的 oom_kill,再与 dmesg -T 对齐时间 |
| 设了软限仍被 OOM | 把软限当硬限用了 | 记住分界:memory.high 只节流,memory.max 才是最后一道 |
| 比例高但事件键不动 | 只是压力,不是故障 | 继续观察,别按"被杀过"处理 |
| 同组一起死 | memory.oom.group 置了 1 |
明确这个取舍是否符合预期,再决定留不留 |
| 读不到数 | 记账没开 | 看 MemoryAccounting= 的现读值 |
改完怎么验证:每项一条判据
- 压力比例下降到目标(
memory.current / memory.max); oom_kill不再递增;- 软限与硬限的取值关系符合预期(软限低于硬限);
- 两类内存的构成能解释"谁占的"(
anon与file); - 有回滚形态与原值记录。
这篇不解决什么
- 讲内存泄漏的观察与处置顺序的那条线管泄漏的观察;本篇管上限与 OOM 取证——一个管"涨不涨"、一个管"什么时候被杀"。
- 讲内存口径打架时信谁的那篇是多个内存口径之间的取舍;本篇是单个控制组内的压力与上限。
- 讲容器退出后的现场保留的那篇提到容器侧的退出原因字段;本篇是宿主级视角的取证——两篇互相指路。
踩坑清单
- 只看用量不看上限:没有分母,比例无从谈起。
- 把软限当硬限用:它不杀进程,只提前施压。
- 只看
dmesg不看事件计数:可能没记全。 - 把"含子层级"和"只看本层"混用:把别人的账算到自己头上。
- 不分匿名内存与页缓存就断定"应用泄漏":可能是缓存撑起来的。
- 记账没开却以为"没有上限":其实是读不到数。
- 在生产上做复现实验:验证动作自己成了事故。
- 打开整组被杀却没预期无辜进程被带走。
- 只看配置文件原文不看现读生效值。
- 改前不记原值:回滚没有依据。
- 把
memory.high设得比memory.max还高:分界形同虚设。 - 只调上限不管交换上限:
memory.swap.max为 0 时压力全压在内存上。
- 点赞
- 收藏
- 关注作者
评论(0)