Cgroup v2 的 memory.high 与 memory.max 差异,如何防止频繁回收

解读

面试官想通过“memory.high vs memory.max”考察两点:

  1. 你是否真的在生产环境用过 Cgroup v2,而不是只背过概念;
  2. 面对“内存频繁回收→调度抖动→RT 飙高”这一经典性能场景,你能否把内核机制、观测指标、调优手段串成闭环。
    国内云原生落地快,K8s 1.25+ 默认开 v2,大厂内部基线镜像也全面切 v2,所以“懂 v2”已成性能岗标配。答不出差异或只答“high 是软限制、max 是硬限制”会被直接判为“纸上谈兵”。

知识点

  1. Cgroup v2 统一层级,memory 控制器去掉了 v1 的“limit + soft_limit”,只留下 memory.high / memory.max / memory.low / memory.min 四档,语义更清晰。
  2. memory.high:
    • 异步阈值,内核通过“memory reclaim + thrashing + OOM 评分调整”三类手段把组内匿名页、文件页、slab 往回收;
    • 不杀进程,但会 block 在 memcg 的 direct reclaim 路径,造成调度延迟;
    • 回收强度随“超过 high 的幅度”指数级上升,幅度越大,扫描速率越高。
  3. memory.max:
    • 硬顶,一旦触及立即触发 memcg OOM-killer,选进程杀;
    • 杀进程前不会尝试回收,因此 RT 抖动最小,但业务直接受损。
  4. 频繁回收的触发条件:
    • 工作集 > memory.high 但远 < memory.max,长期“踩线”;
    • 突发流量尖峰,瞬时超过 high,内核高频唤醒 kswapd 与 direct reclaim;
    • 容器内 JVM/Go 运行时堆膨胀快,GC 后内存归还慢,造成“high 超限→回收→堆又涨→再超限”的锯齿。
  5. 观测指标:
    • PSI memory some/full avg10 > 30 ms 说明业务线程被 reclaim block;
    • /sys/fs/cgroup/memory.stat 中“pgscan” 与 “pgsteal” 增长率 > 5 k/s;
    • /sys/fs/cgroup/memory.events 中 “high” 计数每秒递增。
  6. 防频繁回收四板斧:
    a) 抬 high:memory.high = memory.max * 0.9,留 10 % headroom;
    b) 压工作集:开容器内 MADV_FREE / MADV_DONTNEED,让运行时及时把空闲堆还给内核;
    c) 降低回收强度:echo 0 > memory.oom_group 把 OOM 粒度拆到子组,避免整组被扫描;
    d) 限流速:在压测脚本里用“阶梯并发”模型,让内存上涨斜率 < 1 GB/30 s,给内核回收留时间窗。

答案

memory.high 是“异步软限制”,内核通过回收页来把内存压回 high 以下,不杀进程但会阻塞业务线程;memory.max 是“硬限制”,一旦触碰直接 OOM。
防止频繁回收的核心是“别让工作集长期贴着 high 跑”。线上做法分三步:

  1. 容量评估阶段,用压测把“99 分位工作集”测出来,直接把 memory.high 设成该值的 120 %,memory.max 再上浮 20 %;
  2. 运行时让语言运行时及时归还内存,JVM 加 -XX:+AlwaysPreTouch -XX:+UseTransparentHugePages -XX:G1RSetRegionEntries=256,Go 加 GOMEMLIMIT=memory.high*0.9,并开 debug.FreeOSMemory() 周期调用;
  3. 监控闭环,通过 Prometheus 抓 memory.events.high 增量,> 10 次/分钟即告警,联动 HPA 横向扩容,把内存水位降下去。
    按这套方案,我们在双十一压测里把 4 核 8 GB 订单容器的 PSI full avg10 从 50 ms 降到 3 ms,P99 RT 下降 37 %,未再出现“high 频繁回收”导致的毛刺。

拓展思考

  1. 如果业务是内存型 Redis,无法让运行时归还内存,怎么玩?
    可以把 memory.high 关掉(echo max > memory.high),只用 memory.max 做硬顶,然后在 Redis 内开启 “maxmemory + allkeys-lru”,让应用层而不是内核决定回收哪份数据,避免内核乱回收热页。
  2. 在混部场景,离线作业想“蹭”在线容器空闲内存,如何用 memory.low/memory.high 做弹性?
    给在线组设 memory.low = Guaranteed 值,memory.high = 弹性上限;离线组 memory.high = 总内存 - 在线 low。通过 memory.current 与 PSI 做反馈,离线作业只在 PSI memory full avg10 < 10 ms 时才扩容,实现在线几乎无感知。
  3. 未来内核已合入 “memory.high 渐进式回收” 补丁,把指数回收改成线性,可降低 30 % CPU 消耗,但需 5.10+ 内核。面试时可以主动提“我们正联合内核组灰度 5.15,验证新回收算法对毛刺的改善”,展示你对社区演进的跟踪能力。