背景
vmpressure 是内存回收路径上的压力上报点。每次回收,它按调用者传入的 tree 标志分流。tree 为假时,上报内核态 socket 压力,供 TCP/SCTP 收紧发送窗口,这条路只在 cgroup v2 上有意义。cgroup v1 的 socket 内存另有 tcpmem 机制,读的是另一处压力值。tree 为真时,上报 cgroup v1 的用户态 eventfd 通知,经 memory.pressure_level 接口送出;cgroup v2 没有对应物,它的用户态直接读 PSI 拿回收信号,根本不碰 vmpressure。
把 cgroup 层级与 tree 取值交叉,4 种组合里只有 2 种真有人读结果:
tree 为假 tree 为真
(内核 socket 压力) (用户态通知)
v1 ✗ 结果无人读 ✓ 用户态在监听
v2 ✓ 供网络栈使用 ✗ 事件链表恒为空
现有的入口早退只挡住了 cgroup v1 且 tree 为假这个空格。对称的 cgroup v2 且 tree 为真这个空格,照常落入完整流程:加锁、累加扫描与回收计数、调度延迟上报、沿父级 cgroup 逐层遍历。可它最终要遍历的那条事件链表,在 cgroup v2 上永远是空的。事件只能经 v1 的 memory.pressure_level 接口注册,v2 的 memcg 根本触达不到。于是在只跑 cgroup v2 的机器上,这条空跑的路径成了回收线程的竞争点,它们挤在同一把全局锁上串行排队。作者用 bpftrace 在一台 176 核生产机(cgroup v2、285 个 memcg、持续回收)上实测,这条路径每分钟被命中约 16200 次。
问题
- 入口早退只挡住 cgroup v1 且 tree 为假,漏掉对称的 cgroup v2 且 tree 为真
- cgroup v2 且 tree 为真无人消费,却仍走完加锁、累加、调度与父级遍历
- 要遍历的事件链表在 cgroup v2 上恒为空,整段流程纯属空转
- 回收线程在那把全局锁上串行排队(176 核生产机每分钟约 16200 次)
方案
修复先扩开入口早退,消掉空转的运行时开销,再把只服务 cgroup v1 的通知代码隔离出去,顺带收回内存与代码体积。
扩开入口早退
判据很直接:4 种组合里只有 2 种真有人读结果,落在没人读的那 2 种上的调用都没有消费者,函数应当直接返回。原来的早退只挡 cgroup v1 且 tree 为假,现在把对称的 cgroup v2 且 tree 为真也纳入早退。入口一旦判明无人消费便立即返回,跳过后续的加锁、累加与父级遍历。
改动前:cgroup v2 的 tree 路径走完整流程
┌────────────────────────────────┐
│ 加锁累加扫描计数,调度延迟上报 │
└───────────────┬────────────────┘
▼
┌────────────────────────────────┐
│ 沿父级逐层遍历,事件链表恒空 │
└───────────────┬────────────────┘
▼
┌────────────────────────────────┐
│ 回收线程在全局锁上串行排队 │
└────────────────────────────────┘
改动后:cgroup v2 的 tree 路径入口即返
┌────────────────────────────┐
│ 入口判明无人消费,立即返回 │
└─────────────┬──────────────┘
▼
┌────────────────────────────┐
│ 跳过加锁、累加与父级遍历 │
└─────────────┬──────────────┘
▼
┌────────────────────────────┐
│ 串行竞争路径整体消失 │
└────────────────────────────┘
在只跑 cgroup v2 的机器上,那条让回收线程串行排队的路径就此消失。
把 v1 通知代码隔离
随之而来的是清理。服务 cgroup v1 用户态通知的那一整块代码(事件注册与注销、事件链表及其互斥锁、延迟上报的工作项与处理函数、沿父级逐层上报、按回收优先级上报的入口),原本和共享代码混在同一个文件里。现在它整体搬进 cgroup v1 内存控制器的专用源文件,只在启用 v1 时编译;头文件给不启用 v1 的内核留下空操作桩,调用处无须再加条件编译。vmpressure 自身只剩共享部分与 tree 为假的 socket 压力路径,主文件代码量减半。源码里剩下的条件编译,只有 vmpressure 结构体内部那几个 v1 专属字段。
这一拆是为后续把 vmpressure 最终收归 v1 专属铺的第一步。tree 为假的 socket 压力路径今天还不能删:PSI 并非 vmpressure 的等价替代,把网络 socket 回撤切到 PSI,可能在依赖 vmpressure 滞回特性的负载上拖慢网络、加重内存压力。中期计划是先引入基于 PSI 的 socket 压力路径,再让 cgroup v2 在几个内核版本里以可配置选项逐步退出,最后才砍掉 tree 为假的那条路径;届时留下的就是纯粹的 v1 子系统。
收益
测试平台:176 核生产机,cgroup v2,未启用 cgroup v1,285 个 memcg,持续回收。运行时数据由 bpftrace 实测,结构体体积由 pahole 实测。
运行时(扩开入口早退):
| 指标 | Before | After |
|---|---|---|
| tree 为真的 vmpressure 调用 | 约 16200 次/分 | 0(入口早退跳过) |
在 cgroup v2 专用主机上,回收线程在那把全局锁上串行排队的竞争路径被整体消除。
体积(v1 代码隔离,未启用 v1 时):
| 指标 | Before | After |
|---|---|---|
| struct vmpressure | 112 B | 24 B |
| struct mem_cgroup | 1664 B | 1536 B |
| vmpressure 主文件代码量 | — | 减半 |
运行时收益落在高频回收路径上(持续回收、176 核),消除了无人消费却仍要排队的那把全局锁竞争;体积收益来自把 v1 的用户态通知代码隔离进 v1 专用文件所带来的结构精简,与运行时无关。