目标
在过早晋升的服务上盲目调大堆上限,只会让大 GC 更稀少也更巨大,恰恰背道而驰。GC trace 能告诉你身处哪个剧本:清道太频、老生代长得太快、还是碎片化。每个剧本对应一个不同的最小修复。先读曲线再动参数,并在同样的负载下保留前后两份日志作决策证据。
步骤要点
- 固定负载窗口内用 –trace-gc 记录 gc.log,统计 Scavenge 与 Mark-Compact 的次数,看最后几次大回收的暂停时长。
- 提取每次大 GC 前的堆大小画锯齿曲线:持续爬升说明短命对象被过早晋升,平稳说明活跃集就是那么大。
- 把慢请求的时间戳与 gc.log 里大 GC 的时间戳对齐,证明用户可感的卡顿确实来自 GC 暂停。
- 按诊断施加最小参数集:晋升过快就加大 semi-space,大暂停太疼就封顶 old-space 换高频小额,碎片化才考虑压缩策略。
- 同一负载重跑同款测量,把前后两组计数与暂停数字并排记录,随部署配置一起存档说明取舍理由。
排错提示
堆平稳而 RSS 仍涨说明内存在 V8 堆外,Buffer、worker 或原生插件,先看 process.memoryUsage().external 再谈堆参数;加大 semi-space 反而更糟是请求里分配了长命对象,回退参数改修分配点或封顶老生代;调参后大暂停依旧说明大头是长命对象图,给缓存加 TTL 与 LRU 上限,参数救不了代码执意持有的活跃集。
适用边界
GC 参数在暂停频率与暂停时长之间做交易,不存在同时改善两个方向的设置;trace 参数本身有开销,选定配置后要关掉 trace 复测,trace 只留给诊断期。
代码示例
gc.log before (200 rps, 5 min):
Scavenge 4,113x avg pause 1.8 ms (healthy)
Mark-Compact 22x avg pause 310 ms (users feel these)
heap before majors: 310 -> 480 -> 690 -> 905 Mb (premature promotion)
# fix: give the young generation room to let request-scoped
# objects die young
node --max-semi-space-size=64 dist/server.js
gc.log after:
Scavenge 980x avg pause 2.9 ms
Mark-Compact 6x avg pause 41 ms (7.5x shorter)
heap before majors: 320 -> 340 -> 345 Mb (stable)