背景
memcg 的统计分两种口径:层级值包含整棵子树,v2 的 memory.stat 读的是它;非层级值只算 memcg 自己,读者只有 v1 的 memory.stat、memory.numa_stat,以及 memcg offline 时向父级的 reparenting。memcg_vmstats 为此备了两份数组,state 与 events 存层级值,state_local 与 events_local 存非层级值。
每次 rstat flush,mem_cgroup_stat_aggregate() 遍历全部 77 个 memcg 范围统计项(57 项 state、20 项 events),把各 CPU 上的差值累加进层级数组,同一份差值再对非层级数组做一次读改写。
而 v1 有两种不可用的形态:内核编译时不启用 CONFIG_MEMCG_V1,读取函数 memcg_page_state_local() 等直接编译消失;或者编译启用 v1,但启动参数 cgroup_no_v1={memory,all} 把 memory 控制器挡在 v1 之外,v1 挂载被拒绝,legacy 接口根本不会出现。两种形态下非层级数组都没有读者,flush 却照样逐次写入。这些数组占的 cacheline 与聚合循环已在遍历的不相邻,纯粹为了写而拉入。
非层级数组只写不读
┌──────────────────────────────────────┐
│ rstat flush 把 per-cpu 差值累加进层 │
│ 级与非层级两份数组 │
└──────────────────┬───────────────────┘
▼
┌──────────────────────────────────────┐
│ 层级数组由 v2 接口读取,非层级数组只 │
│ 有 v1 接口读取 │
└──────────────────┬───────────────────┘
▼
┌──────────────────────────────────────┐
│ v1 不可用时非层级数组没有读者,flush │
│ 仍逐次写入,白白拉入 cacheline │
└──────────────────────────────────────┘
问题
- 非层级统计数组随每次 rstat flush 更新,读者却只有 v1 接口
- CONFIG_MEMCG_V1=n 时读取函数已编译移除,数组照样被写
- cgroup_no_v1= 封锁 memory 后 v1 无法挂载,数组同样只写不读
- 这些 cacheline 与 flush 遍历路径不相邻,拉入只为写入
方案
按两种形态分两层处理,让非层级数组不再被更新。
编译期一层:CONFIG_MEMCG_V1=n 时把 state_local 与 events_local 用 #ifdef 从 memcg_vmstats 中移除,数组不占内存,也无从写入。
运行期一层:CONFIG_MEMCG_V1=y 但启动参数封锁了 memory 时,flush 不再直接取数组地址,改经新增的 memcg_state_local_array() 获取:
static long *memcg_state_local_array(struct mem_cgroup *memcg)
{
#ifdef CONFIG_MEMCG_V1
if (!cgroup1_ssid_disabled(memory_cgrp_id))
return memcg->vmstats->state_local;
#endif
return NULL;
}
cgroup1_ssid_disabled() 查的是启动参数 cgroup_no_v1= 是否封锁了对应控制器,被封锁就返回空指针。聚合参数里的 local 为空时,mem_cgroup_stat_aggregate() 跳过 local[i] 的读改写。flush_nmi_stats() 对 state_local 的几处写入同样先判空再写,events 一侧由 memcg_events_local_array() 对称处理。
空指针作为跳过信号
┌──────────────────────────────────────┐
│ flush 组装聚合参数,取非层级数组指针 │
└──────────────────┬───────────────────┘
▼
┌──────────────────────────────────────┐
│ 编译未启用 v1 或 cgroup_no_v1= 封锁 │
│ memory 时返回空指针 │
└──────────────────┬───────────────────┘
▼
┌──────────────────────────────────────┐
│ 聚合循环见到空指针,跳过非层级数组的 │
│ 读改写,层级数组照常聚合 │
└──────────────────────────────────────┘
收益
80 核 x86_64 机器,500 个 cgroup 各自持续更动 anon、file、dirty/writeback、slab、kmem、mlock、reclaim 各类计数,内核内以 TSC 统计单次 flush 耗时,取 70 个样本的中位数:
| 指标 | Before | After | 改善 |
|---|---|---|---|
| memcg 范围聚合(TSC tick) | 1231 | 1180 | 4.1% |
| flush 函数整体(TSC tick) | 2452 | 2397 | 2.2% |
基准循环读 memory.stat 维持高频 flush,每次 flush 中位数只有 1.5 个统计项有非零差值,相当于每次少拉入一至两条 cacheline。若 flush 只由 2 秒一次的周期性刷新触发,两次 flush 间积累的差值更多,跳过的 cacheline 也更多,单次 flush 的收益会更大。