性能优化 | Go Ballast 让内存控制更加丝滑
<p>关于 Go GC 优化的手段你知道的有哪些?比较常见的是通过调整 GC 的步调,以调整 GC 的触发频率。</p><ul>
<li>设置 GOGC</li>
<li>设置 debug.SetGCPercent()</li>
</ul>
<p>这两种方式的原理和效果都是一样的,GOGC 默认值是 100,也就是下次 GC 触发的 heap 的大小是这次 GC 之后的 heap 的一倍。</p>
<p>我们都知道 GO 的 GC 是标记-清除方式,当 GC 会触发时全量遍历变量进行标记,当标记结束后执行清除,把标记为白色的对象执行垃圾回收。值得注意的是,这里的<strong>回收</strong>仅仅是标记内存可以返回给操作系统,并不是立即回收,这就是你看到 Go 应用 RSS 一直居高不下的原因。在整个垃圾回收过程中会暂停整个 Go 程序(STW),Go 垃圾回收的耗时还是主要取决于<strong>标记</strong>花费的时间的长短,清除过程是非常快的。</p>
<h2 id="设置-gogc-的弊端">设置 GOGC 的弊端</h2>
<h3 id="1-gogc-设置比率的方式不精确">1. GOGC 设置比率的方式不精确</h3>
<p>设置 GOGC 基本上我们比较常用的 Go GC 调优的方式,大部分情况下其实我们并不需要调整 GOGC 就可以,一方面是不涉及内存密集型的程序本身对内存敏感程度太低,另外就是 GOGC 这种设置比率的方式不精确,我们很难精确的控制我们想要的触发的垃圾回收的阈值。</p>
<h3 id="2-gogc-设置过小">2. GOGC 设置过小</h3>
<p>GOGC 设置的非常小,会频繁触发 GC 导致太多无效的 CPU 浪费,反应到程序的表现就会特别明显。举个例子,对于 API 接口来说,导致的结果的就是接口周期性的耗时变化。这个时候你抓取 CPU profile 来看,大部分的耗时都集中在 GC 的相关处理上。</p>
<p><img src="https://cdn.jsdelivr.net/gh/georgehao/img/gc_more.png"></p>
<p>如上图,这是一次 prometheus 的查询操作,我们看到大部分的 CPU 都消耗在 GC 的操作上。这也是生产环境遇到的,由于 GOGC 设置的过小,导致过多的消耗都耗费在 GC 上。</p>
<h3 id="3-对某些程序本身占用内存就低容易触发-gc">3. 对某些程序本身占用内存就低,容易触发 GC</h3>
<p>对 API 接口耗时比较敏感的业务,由于这种接口一般情况下内存占用都比较低,因为 API 接口变量的生命周期都比较短,这个时候 GOGC 置默认值的时候,也可能也会遇到接口的周期性的耗时波动。这是为什么呢?</p>
<p>因为这种接口本身占用内存比较低,每次 GC 之后本身占的内存比较低,如果按照上次 GC 后的 heap 的一倍的 GC 步调来设置 GOGC 的话,这个阈值其实是很容易就能够触发,于是就很容出现接口因为 GC 的触发导致额外的消耗。</p>
<h3 id="4-gogc-设置很大有的时候又容易触发-oom">4. GOGC 设置很大,有的时候又容易触发 OOM</h3>
<p>那如何调整呢?是不是把 GOGC 设置的越大越好呢?这样确实能够降低 GC 的触发频率,但是这个值需要设置特别大才有效果,GOGC 一般需要设置 2000 左右。这样带来的问题,GOGC 设置的过大,如果这些接口突然接受到一大波流量,由于长时间无法触发 GC 可能导致 OOM。</p>
<p>由此,GOGC 对于这样的场景并不是很友好,那有没有能够精确控制内存,让其在 10G 的倍数时准确控制 GC 呢?</p>
<h2 id="go-内存-ballast">GO 内存 ballast</h2>
<p>这就需要 Go ballast 出场了。什么是 Go ballast,其实很简单就是初始化一个生命周期贯穿整个 Go 应用生命周期的超大 slice。</p>
<pre><code class="language-go">func main() {
ballast := make([]byte, 10*1024*1024*1024) // 10G
// do something
runtime.KeepAlive(ballast)
}
</code></pre>
<p>上面的代码就初始化了一个 ballast,利用 runtime.KeepAlive 来保证 ballast 不会被 GC 给回收掉。</p>
<p>利用这个特性,就能保证 GC 在 10G 的一倍时才能被触发,这样就能够比较精准控制 GO GC 的触发时机。</p>
<p><strong>这里你可能有一个疑问,这里初始化一个 10G 的数组,不就占用了 10 G 的物理内存呢?</strong> 答案其实是不会的。</p>
<pre><code class="language-go">package main
import (
"runtime"
"math"
"time"
)
func main() {
ballast := make([]byte, 10*1024*1024*1024)
<-time.After(time.Duration(math.MaxInt64))
runtime.KeepAlive(ballast)
}
</code></pre>
<pre><code>$ ps -eo pmem,comm,pid,maj_flt,min_flt,rss,vsz --sort -rss | numfmt --header --to=iec --field 5 | numfmt --header --from-unit=1024 --to=iec --field 6 | column -t | egrep "est|I"
%MEMCOMMAND PID MAJFL MINFLRSS VSZ
0.1 test 128590 1.6K 344M 11530184
</code></pre>
<p>这个结果是在 CentOS Linux release 7.9 验证的,我们看到占用的 RSS 真实的物理内存只有 344M,但是 VSZ 虚拟内存确实有 10G 的占用。</p>
<p>延伸一点,当怀疑我们的接口的耗时是由于 GC 的频繁触发引起的,我们需要怎么确定呢?首先你会想到周期性的抓取 pprof 的来分析,这种方案其实也可以,但是太麻烦了。其实可以根据 GC 的触发时间绘制这个曲线图,GC 的触发时间可以利用 runtime.Memstats 的 LastGC 来获取。</p>
<h2 id="生产环境验证">生产环境验证</h2>
<ul>
<li>绿线 调整前 GOGC = 30</li>
<li>黄线 调整后 GOGC 默认值,ballast = 100G</li>
</ul>
<p><img src="https://cdn.jsdelivr.net/gh/georgehao/img/ballast_gc_time_10m.png"></p>
<p><img src="https://cdn.jsdelivr.net/gh/georgehao/img/ballast_mem.png"></p>
<p>这张图相同的流量压力下,ballast 的表现明显偏好</p>
<p><img src="https://cdn.jsdelivr.net/gh/georgehao/img/ballast_cpu.png"></p>
<h2 id="结论">结论</h2>
<p>本篇文章只是简单的阐述了 Go ballast 的使用,不过 Go ballast 是官方比较认可的方案,具体可以参见 issue 23044。很多开源程序,如 tidb,cortex 都实现了 go ballast,如果你的程序饱受 GOGC 的问题影响或者周期性的耗时不稳定,不妨尝试下 go ballast。</p>
<p>当然强烈推荐你看下twitch.tv 这篇文章,相信让你会对 GOGC 以及 ballast 的运用理解的更加透彻。</p>
</div>
<div id="MySignature" role="contentinfo">
博客文章如无特殊说明,都是作者原创,转载请在醒目的位置链接文章出处及作者,谢谢!<br><br>
来源:https://www.cnblogs.com/457220157-FTD/p/15567442.html
頁:
[1]