memory ballast 和 gc tuner 成为历史
memory ballast 和 auto gc tuner 是为了解决 Go 在没有充分利用内存的情况下,频繁触发 GC 导致 GC 占用 CPU 过高的优化手段。
memory ballast 通过在堆上分配一个巨大的对象(一般是几个 GB)来欺骗 GOGC,让 Go 能够尽量充分地利用堆空间来减少 GC 触发的频率。
uber 后来分享的 auto gc tuner 更智能一些,设定程序的内存占用阈值,通过 GC 期间对用户 finalizer 函数的回调来达成每次 GC 触发时都动态设置 GOGC,以使应用使用内存与目标逐渐趋近的目的。
之前在某公司工作的时候,隔壁的监控组曾经为了让 Go 写的应用不要超过 8GB 的 docker 内存配额,自己实现了一套动态 GOGC 的方案(调到 100 以下),让内存保持在 8GB 以内,不要 OOM,和 uber 的方式较为类似。
上面两种方式说到底都是 hack,因为用户无法直接修改 GC 逻辑,所以只能对应用的行为进行修改以达到欺骗 gc pacer 最终避免 OOM 或优化 GC的目的。
在 Go 1.19 中新增加的 debug.SetMemoryLimit 从根本上解决了这个问题,可以直接把 memory ballast 和 gc tuner 丢进垃圾桶了。
- OOM 的场景:SetMemoryLimit 设置内存上限即可
- GC 触发频率高的场景:SetMemoryLimit 设置内存上限,GOGC = off
怎么实现的呢?
简单来说,现在的 heap goal,也就是 Nn,是
- 用 memory limit 减去栈/以及其它非堆所占的空间
- 1+GOGC/100 * heap size + 栈大小 + 全局变量大小
两者中的较小值,如果用户设置了 memory limit,那就不会像以前一样只用 GOGC 去算 heap goal 而导致堆意外增长至 OOM。
按照官方的说法,即使 GOGC = off,memory limit 的约束也会被严格遵守,所以以前的 gc tuner 优化就只要把 GOGC 设置为 off,再给 memory limit 设置为 70%(uber 的文章里的建议值)就可以了。
除了 GC 的更新,1.19 另一个更新也比较有意思,按栈统计确定初始大小 会在 GC 期间将扫描过的 goroutine 和其栈大小求一个平均值,下次 newproc 创建 goroutine 的时候,就用这个平均值来创建新的 goroutine,而不是以前的 2KB。
基于统计的初始 goroutine 栈对于那些海量连接的 api gateway 系统应该是个利好,以后应该不会再看到那些掐着函数去算栈大小的优化了。
为了优化栈空间的内存占用去减少函数调用的嵌套层数可太累了。