Linux cgroup v2 的 memory.events.local 怎样区分本组事件
要区分 cgroup v2 的“本组事件”和“子树事件”,最直接的判断是:默认挂载语义下,memory.events 是层级累计计数,当前 cgroup 以及后代 cgroup 发生的相关事件都可能让它增加;memory.events.local 是非层级计数,只反映当前 cgroup。排障时不要比较两个文件的绝对值,而要在同一观察窗口内比较它们的增量。
因此,当父组的 memory.events 增加而 memory.events.local 没增加时,新增事件来自后代 cgroup;两者同一字段都增加时,说明父组本身也发生了该类事件。若要继续定位到具体子组,再读取各直接子组或目标后代的 memory.events.local 增量。
Linux 内核官方文档:https://docs.kernel.org/admin-guide/cgroup-v2.html
先看懂两个文件的统计边界
Linux 内核文档明确说明,memory.events 的字段具有层级性,后代 cgroup 的事件可能向祖先汇总,并触发祖先文件的修改通知;memory.events.local 的字段只属于当前 cgroup,文件修改通知也只对应当前组。这里的“local”不是“当前主机”,而是“当前这个 cgroup 节点”。
假设层级是 service.slice/app,下面还有 worker-a 和 worker-b。读取 app/memory.events 得到的是 app 子树的观察口径;读取 app/memory.events.local 得到的是 app 节点自身口径。worker-a 的 high 增加时,app 的层级文件可能增加,但 app 的 local 文件不应因此增加。

| 文件 | 统计范围 | 适合回答的问题 |
|---|---|---|
memory.events | 当前组及后代的层级计数 | 这个子树是否出现了新的内存压力或 OOM 事件 |
memory.events.local | 仅当前 cgroup 的非层级计数 | 事件是否由这个节点自身触发 |
子组的 memory.events.local | 仅对应子组 | 究竟是哪一个后代节点出现了事件 |
确认系统确实使用默认的 cgroup v2 语义
先确认目标进程位于 cgroup v2 统一层级,并找到实际 cgroup 路径。/proc/PID/cgroup 中 v2 项通常采用 0::/路径 格式。目标目录下还应当存在 cgroup.controllers,启用内存控制器后才会看到相应的 memory.* 文件。
# 查看当前 shell 在 cgroup v2 中的相对路径 cat /proc/self/cgroup # 确认挂载点类型与挂载选项,实际挂载点不一定固定 findmnt -t cgroup2 -o TARGET,FSTYPE,OPTIONS # 把路径替换为要排查的 cgroup,确认两个事件文件都存在 CG=/sys/fs/cgroup/system.slice/example.service test -r "$CG/memory.events" && test -r "$CG/memory.events.local"
还要留意 memory_localevents 挂载选项。启用它后,memory.events 也只填充当前 cgroup 的数据,不再包含子树计数;这是系统级兼容行为,只能从初始命名空间挂载或重新挂载时设置。若该选项存在,不能再用“events 增加、local 不增加”作为后代归因公式,因为两个文件的范围已经接近。
按键读取计数,不要依赖固定行号
这两个文件都是只读的 flat-keyed 格式,一行一个“键 值”。常见键包括 low、high、max、oom、oom_kill、oom_group_kill,较新的内核还可能出现其他字段。解析器应该按键查找,而不是假设 oom_kill 永远位于第几行。
# 按字段名读取,避免内核增加新字段后行号发生变化
CG=/sys/fs/cgroup/system.slice/example.service
awk '$1 == "high" || $1 == "max" || $1 == "oom" || $1 == "oom_kill" {print $1, $2}' \
"$CG/memory.events.local"
这些值是累计事件次数,不是当前内存量。high 表示进程因超过 memory.high 而受到节流和直接回收压力的次数;max 表示内存使用即将越过 memory.max 的次数;oom 反映达到限制、分配接近失败的次数;oom_kill 统计该 cgroup 中被 OOM killer 杀死的进程数。要看当前占用,应结合 memory.current;要拆分匿名页、文件缓存和内核内存,应看 memory.stat。
用前后快照差值判断事件来源
一个常见误区是看到父组 oom_kill 3 就断言“父组刚刚杀了 3 个进程”。这可能是历史累计值,也可能来自后代。正确做法是固定一个观察窗口,分别保存父组层级文件、父组 local 文件和候选子组 local 文件的起点与终点,然后比较同名字段的增量。
#!/usr/bin/env bash set -euo pipefail # 示例只展示采样结构;请替换为真实 cgroup 路径 PARENT=/sys/fs/cgroup/system.slice/example.service CHILD="$PARENT/worker-a" OUT=/var/tmp/cgroup-event-sample mkdir -p "$OUT" # 保存观察窗口起点,事件文件是累计计数 cp "$PARENT/memory.events" "$OUT/parent.events.before" cp "$PARENT/memory.events.local" "$OUT/parent.local.before" cp "$CHILD/memory.events.local" "$OUT/child.local.before" # 在真实排障中等待业务窗口;这里不要把 sleep 时长当成固定规则 sleep 10 # 保存观察窗口终点,后续应按键计算差值 cp "$PARENT/memory.events" "$OUT/parent.events.after" cp "$PARENT/memory.events.local" "$OUT/parent.local.after" cp "$CHILD/memory.events.local" "$OUT/child.local.after"
判断逻辑可以压缩为下面三种情况:
| 父组 events 增量 | 父组 local 增量 | 结论 |
|---|---|---|
| 0 | 0 | 该字段在观察窗口内没有新事件 |
| > 0 | 0 | 新增事件来自后代 cgroup,继续查子组 local |
| > 0 | > 0 | 父组自身发生了事件;层级增量还可能同时包含后代事件 |
第三种情况尤其容易被忽略。父组 local 增量为 2、父组 events 增量为 5,并不代表另外 3 次一定都来自同一个子组,只能说明父组自身贡献了 2 次,剩余层级增量需要查看各后代的 local 计数才能进一步分配。

把差值计算写成按键对齐
下面的 awk 片段把起点文件和终点文件按键对齐,输出非零增量。它不会依赖字段顺序,也能容忍新字段出现在文件中。对父组 memory.events、父组 memory.events.local 和每个目标子组分别执行一次,就能得到可比较的数据。
# 用法:event_delta 起点文件 终点文件
event_delta() {
awk '
NR == FNR { before[$1] = $2; next }
{
# 新字段在起点不存在时按 0 处理,只输出发生变化的键
delta = $2 - (before[$1] + 0)
if (delta != 0) print $1, delta
}
' "$1" "$2"
}
# 先看父组整个子树,再看父组自身
event_delta /var/tmp/cgroup-event-sample/parent.events.before \
/var/tmp/cgroup-event-sample/parent.events.after
event_delta /var/tmp/cgroup-event-sample/parent.local.before \
/var/tmp/cgroup-event-sample/parent.local.after
生产监控还应考虑 cgroup 被删除和重建。计数与 cgroup 生命周期绑定;同名目录重建后计数会重新开始。采集器若发现 inode 或生命周期标识变化,不应把新文件的小值当成计数器回退,而应开始新的时间序列。
文件变化通知只负责唤醒,归因仍要重新读值
内核会在可通知事件变化时为事件文件生成修改通知。监控程序可以用 poll、inotify 等机制等待变化,但通知本身不会告诉你究竟哪个字段增加了多少。收到通知后仍要重新读取完整文件,并与上一份快照按键求差。
监听父组 memory.events 可以发现子树出现事件;监听父组 memory.events.local 只会因父组本地事件被唤醒。若只监听 local 文件,就会漏掉后代压力;若只监听层级文件,又无法区分来源。较稳妥的设计是:父组层级文件负责发现“子树有事”,关键叶子或直接子组的 local 文件负责归因。
把 high、max、oom、oom_kill 分开解释
- high 增加:表示触发
memory.high相关节流与直接回收,不等于发生 OOM。持续增加通常提示工作集长期压在 high 边界之上。 - max 增加:表示使用量接近超过
memory.max。直接回收若能成功,不一定随后出现 OOM。 - oom 增加:表示达到限制且分配接近失败,但某些不会考虑 OOM killer 的失败场景不会计入该事件。
- oom_kill 增加:表示该 cgroup 中有进程被 OOM killer 杀死。它应与
oom分开告警,因为一次 OOM 状态不等于一定杀进程。 - oom_group_kill 增加:表示发生组级 OOM kill,应结合
memory.oom.group的配置理解。
这些事件只说明资源控制边界发生过什么,不直接给出根因。确认是本组后,还需要联合 memory.current、memory.stat、限制值和业务日志判断是匿名内存增长、文件缓存、内核内存,还是配置过紧。
一个更可靠的排障清单
- 确认目标进程的 cgroup v2 路径,不要把 systemd 单元名直接当成真实目录。
- 确认目标目录同时存在
memory.events和memory.events.local。 - 检查 cgroup2 挂载选项中是否有
memory_localevents。 - 保存同一时刻的父组 events、父组 local 和候选子组 local 快照。
- 按键计算同一观察窗口的增量,不比较绝对值和固定行号。
- 先用父组 events 判断子树是否有新增事件,再用各层 local 把事件定位到具体节点。
- 最后联合 current、stat、high、max 与业务上下文解释原因。
常见问题
memory.events.local 会包含子 cgroup 的 oom_kill 吗?
不会。它是当前 cgroup 的非层级计数。子组的 oom_kill 会进入子组自己的 local 文件,并在默认语义下向祖先的 memory.events 汇总。
为什么 memory.events 和 memory.events.local 数值一样?
可能该 cgroup 没有后代、后代没有发生对应事件,或者系统启用了 memory_localevents 挂载选项。应结合层级结构、挂载选项和一段时间内的增量判断。
只看 oom_kill 能判断内存限制太小吗?
不能。先确认事件属于哪个 cgroup,再看 memory.max、memory.high、memory.current 和 memory.stat。限制过小、工作集突增、缓存增长或祖先边界都可能参与。
父组 events 增加 5、local 增加 2,能否断定子组增加 3?
只能断定父组自身贡献了 2,层级范围中还有额外事件。若存在多个后代,应读取各后代 local 的同字段增量,不能把差额直接归给任意一个子组。
归纳起来,memory.events 用于发现整个子树发生了什么,memory.events.local 用于确认当前节点自身发生了什么。把父组层级增量、父组本地增量和子组本地增量放到同一时间窗口,才能稳定地区分“本组事件”和“子树事件”。
用 uuid 包校验外部请求中的标识符
- 上一篇
- 用 uuid 包校验外部请求中的标识符
- 下一篇
- 数据库中的 UUID 字节序与 Go 结果不一致怎么办
-
- 文章 · linux | 3小时前 | Linux · 性能监控 · Pressure Stall Information poll Linux PSI POLLPRI 资源压力
- Linux PSI 触发器如何在压力超过阈值时通知进程
- 331浏览 收藏
-
- 文章 · linux | 5小时前 |
- systemd socket 的 Accept=yes 如何启动实例化服务
- 386浏览 收藏
-
- 文章 · linux | 7小时前 | Linux · 最小权限 StateDirectory systemd 服务 systemd DynamicUser 动态用户
- systemd DynamicUser 如何运行无固定账号的服务
- 261浏览 收藏
-
- 文章 · linux | 11小时前 | cgroup v2 memory.max Linux OOM OOM Killer oom_score
- OOM Killer 选择了哪个进程:分数、限制与证据收集
- 233浏览 收藏
-
- 文章 · linux | 13小时前 |
- ext4 与 XFS 在线扩容前后分别要核对什么
- 179浏览 收藏
-
- 文章 · linux | 15小时前 | 容器 · Linux · 容器隔离 mount namespace PID namespace linux namespace network namespace
- 用 namespace 理解容器进程、网络与挂载隔离
- 268浏览 收藏
-
- 文章 · linux | 18小时前 | Linux · 文件描述符 ·
- 日志轮转后服务仍写旧文件,文件描述符发生了什么
- 490浏览 收藏
-
- 文章 · linux | 20小时前 | 运维 · SSH Linux安全 sshd_config 服务加固 密钥登录
- SSH 只允许密钥登录后还要收紧哪些服务边界
- 297浏览 收藏
-
- 文章 · linux | 1天前 | Linux ·
- Linux 负载高但 CPU 空闲,怎样区分 I/O 等待与锁等待
- 398浏览 收藏
-
- 文章 · linux | 1天前 | 磁盘配额 Linux磁盘空间 No space left on device inode耗尽 ext4保留块
- 磁盘空间没满却无法写入:inode、配额与保留块检查
- 326浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 386次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 466次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 474次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 411次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 239次使用
-
- golang进程内存控制避免docker内oom
- 2022-12-22 160浏览
-
- golang进程在docker中OOM后hang住问题解析
- 2022-12-22 105浏览
-
- 详解golang执行Linuxshell命令完整场景下的使用方法
- 2023-01-07 426浏览
-
- go程序部署到linux上运行的实现方法
- 2023-01-02 387浏览
-
- Linux系统下Go语言开发环境搭建
- 2023-01-07 242浏览

