当前位置:首页 > 文章列表 > 文章 > linux > Linux cgroup v2 的 memory.events.local 怎样区分本组事件

Linux cgroup v2 的 memory.events.local 怎样区分本组事件

来源:17golang原创 2026-10-09 05:56:38 0浏览 收藏

要区分 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 与 memory.events.local 的层级统计边界原创结构图
图1:memory.events 汇总当前组和后代事件,memory.events.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 增量结论
00该字段在观察窗口内没有新事件
> 00新增事件来自后代 cgroup,继续查子组 local
> 0> 0父组自身发生了事件;层级增量还可能同时包含后代事件

第三种情况尤其容易被忽略。父组 local 增量为 2、父组 events 增量为 5,并不代表另外 3 次一定都来自同一个子组,只能说明父组自身贡献了 2 次,剩余层级增量需要查看各后代的 local 计数才能进一步分配。

父组与子组内存事件差值归因原创关系图
图2:同一观察窗口内联合父组层级增量、父组本地增量和子组本地增量,才能把新增事件归到本组或后代;图中为原创静态关系图。

把差值计算写成按键对齐

下面的 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、限制值和业务日志判断是匿名内存增长、文件缓存、内核内存,还是配置过紧。

一个更可靠的排障清单

  1. 确认目标进程的 cgroup v2 路径,不要把 systemd 单元名直接当成真实目录。
  2. 确认目标目录同时存在 memory.events 和 memory.events.local。
  3. 检查 cgroup2 挂载选项中是否有 memory_localevents。
  4. 保存同一时刻的父组 events、父组 local 和候选子组 local 快照。
  5. 按键计算同一观察窗口的增量,不比较绝对值和固定行号。
  6. 先用父组 events 判断子树是否有新增事件,再用各层 local 把事件定位到具体节点。
  7. 最后联合 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 用于确认当前节点自身发生了什么。把父组层级增量、父组本地增量和子组本地增量放到同一时间窗口,才能稳定地区分“本组事件”和“子树事件”。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
用 uuid 包校验外部请求中的标识符用 uuid 包校验外部请求中的标识符
上一篇
用 uuid 包校验外部请求中的标识符
数据库中的 UUID 字节序与 Go 结果不一致怎么办
下一篇
数据库中的 UUID 字节序与 Go 结果不一致怎么办
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    386次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    466次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    474次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    411次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    239次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码