性能优化 | Go Ballast 让内存控制更加丝滑
关于 Go GC 优化的手段你知道的有哪些?比较常见的是通过调整 GC 的步调,以调整 GC 的触发频率。
- 设置 GOGC
- 设置 debug.SetGCPercent()
这两种方式的原理和效果都是一样的,GOGC 默认值是 100,也就是下次 GC 触发的 heap 的大小是这次 GC 之后的 heap 的一倍。
我们都知道 GO 的 GC 是标记-清除方式,当 GC 会触发时全量遍历变量进行标记,当标记结束后执行清除,把标记为白色的对象执行垃圾回收。值得注意的是,这里的回收仅仅是标记内存可以返回给操作系统,并不是立即回收,这就是你看到 Go 应用 RSS 一直居高不下的原因。在整个垃圾回收过程中会暂停整个 Go 程序(STW),Go 垃圾回收的耗时还是主要取决于标记花费的时间的长短,清除过程是非常快的。
设置 GOGC 的弊端
1. GOGC 设置比率的方式不精确
设置 GOGC 基本上我们比较常用的 Go GC 调优的方式,大部分情况下其实我们并不需要调整 GOGC 就可以,一方面是不涉及内存密集型的程序本身对内存敏感程度太低,另外就是 GOGC 这种设置比率的方式不精确,我们很难精确的控制我们想要的触发的垃圾回收的阈值。
2. GOGC 设置过小
GOGC 设置的非常小,会频繁触发 GC 导致太多无效的 CPU 浪费,反应到程序的表现就会特别明显。举个例子,对于 API 接口来说,导致的结果的就是接口周期性的耗时变化。这个时候你抓取 CPU profile 来看,大部分的耗时都集中在 GC 的相关处理上。

如上图,这是一次 prometheus 的查询操作,我们看到大部分的 CPU 都消耗在 GC 的操作上。这也是生产环境遇到的,由于 GOGC 设置的过小,导致过多的消耗都耗费在 GC 上。
3. 对某些程序本身占用内存就低,容易触发 GC
对 API 接口耗时比较敏感的业务,由于这种接口一般情况下内存占用都比较低,因为 API 接口变量的生命周期都比较短,这个时候 GOGC 置默认值的时候,也可能也会遇到接口的周期性的耗时波动。这是为什么呢?
因为这种接口本身占用内存比较低,每次 GC 之后本身占的内存比较低,如果按照上次 GC 后的 heap 的一倍的 GC 步调来设置 GOGC 的话,这个阈值其实是很容易就能够触发,于是就很容出现接口因为 GC 的触发导致额外的消耗。
4. GOGC 设置很大,有的时候又容易触发 OOM
那如何调整呢?是不是把 GOGC 设置的越大越好呢?这样确实能够降低 GC 的触发频率,但是这个值需要设置特别大才有效果,GOGC 一般需要设置 2000 左右。这样带来的问题,GOGC 设置的过大,如果这些接口突然接受到一大波流量,由于长时间无法触发 GC 可能导致 OOM。
由此,GOGC 对于这样的场景并不是很友好,那有没有能够精确控制内存,让其在 10G 的倍数时准确控制 GC 呢?
GO 内存 ballast
这就需要 Go ballast 出场了。什么是 Go ballast,其实很简单就是初始化一个生命周期贯穿整个 Go 应用生命周期的超大 slice。
func main() {
ballast := make([]byte, 10*1024*1024*1024) // 10G
// do something
runtime.KeepAlive(ballast)
}
上面的代码就初始化了一个 ballast,利用 runtime.KeepAlive 来保证 ballast 不会被 GC 给回收掉。
利用这个特性,就能保证 GC 在 10G 的一倍时才能被触发,这样就能够比较精准控制 GO GC 的触发时机。
这里你可能有一个疑问,这里初始化一个 10G 的数组,不就占用了 10 G 的物理内存呢? 答案其实是不会的。
package main
import (
"runtime"
"math"
"time"
)
func main() {
ballast := make([]byte, 10*1024*1024*1024)
<-time.After(time.Duration(math.MaxInt64))
runtime.KeepAlive(ballast)
}
$ 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 "[t]est|[P]I"
%MEM COMMAND PID MAJFL MINFL RSS VSZ
0.1 test 12859 0 1.6K 344M 11530184
这个结果是在 CentOS Linux release 7.9 验证的,我们看到占用的 RSS 真实的物理内存只有 344M,但是 VSZ 虚拟内存确实有 10G 的占用。
延伸一点,当怀疑我们的接口的耗时是由于 GC 的频繁触发引起的,我们需要怎么确定呢?首先你会想到周期性的抓取 pprof 的来分析,这种方案其实也可以,但是太麻烦了。其实可以根据 GC 的触发时间绘制这个曲线图,GC 的触发时间可以利用 runtime.Memstats 的 LastGC 来获取。
生产环境验证
- 绿线 调整前 GOGC = 30
- 黄线 调整后 GOGC 默认值,ballast = 100G


这张图相同的流量压力下,ballast 的表现明显偏好

结论
本篇文章只是简单的阐述了 Go ballast 的使用,不过 Go ballast 是官方比较认可的方案,具体可以参见 issue 23044。很多开源程序,如 tidb,cortex 都实现了 go ballast,如果你的程序饱受 GOGC 的问题影响或者周期性的耗时不稳定,不妨尝试下 go ballast。
当然强烈推荐你看下twitch.tv 这篇文章,相信让你会对 GOGC 以及 ballast 的运用理解的更加透彻。
性能优化 | Go Ballast 让内存控制更加丝滑的更多相关文章
- JVM性能优化系列-(1) Java内存区域
1. Java内存区域 1.1 运行时数据区 Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域.主要包括:程序计数器.虚拟机栈.本地方法栈.Java堆.方法区(运 ...
- android app性能优化大汇总(内存性能优化)
转载请注明本文出自大苞米的博客(http://blog.csdn.net/a396901990),谢谢支持! 写在最前: 本文的思路主要借鉴了2014年AnDevCon开发者大会的一个演讲PPT,加上 ...
- Android性能优化之常见的内存泄漏
前言 对于内存泄漏,我想大家在开发中肯定都遇到过,只不过内存泄漏对我们来说并不是可见的,因为它是在堆中活动,而要想检测程序中是否有内存泄漏的产生,通常我们可以借助LeakCanary.MAT等工具来检 ...
- Spark性能优化(1)——序列化、内存、并行度、数据存储格式、Shuffle
序列化 背景: 在以下过程中,需要对数据进行序列化: shuffling data时需要通过网络传输数据 RDD序列化到磁盘时 性能优化点: Spark默认的序列化类型是Java序列化.Java序列化 ...
- 【好书摘要】性能优化中CPU、内存、磁盘IO、网络性能的依赖
系统优化是一项复杂.繁琐.长期的工作,优化前需要监测.采集.测试.评估,优化后也需要测试.采集.评估.监测,而且是一个长期和持续的过程,不 是说现在优化了,测试了,以后就可以一劳永逸了,也不是说书本上 ...
- Android性能优化:谈话Bitmap内存管理和优化
最近除了那些忙着项目开发的事情,目前正在准备我的论文.短的时间没有写博客,今晚难得想总结.只要有一点时间.因此,为了凑合用,行.唠叨罗嗦,直接进入正题. 从事Android自移动终端的发展,想必是常常 ...
- 性能优化中CPU、内存、磁盘IO、网络性能的依赖(转)
关于系统性能优化,推荐一篇不错的博客! 系统优化是一项复杂.繁琐.长期的工作,优化前需要监测.采集.测试.评估,优化后也需要测试.采集.评估.监测,而且是一个长期和持续的过程,不 是说现在优化了,测试 ...
- 性能优化中CPU、内存、磁盘IO、网络性能的依赖
系统优化是一项复杂.繁琐.长期的工作,优化前需要监测.采集.测试.评估,优化后也需要测试.采集.评估.监测,而且是一个长期和持续的过程,不 是说现在优化了,测试了,以后就可以一劳永逸了,也不是说书本上 ...
- 转-性能优化中CPU、内存、磁盘IO、网络性能的依赖
转自:https://www.cnblogs.com/Javame/p/3665565.html 系统优化是一项复杂.繁琐.长期的工作,优化前需要监测.采集.测试.评估,优化后也需要测试.采集.评估. ...
随机推荐
- Redis之品鉴之旅(四)
发布订阅,简单场景下的发布订阅完全可以使用. 可以简单的理解,将一个公众号视为发布者,关注公众号的人视作订阅者,公众号发布一条文章或者消息,凡事订阅公众号的都可以收到消息.一个人可以订阅多个公众号,一 ...
- 基于深度学习的建筑能耗预测02——安装Tensorflow-gpu
一.检查显卡 ·查看自己的显卡配置是否能支持cuda,以及Tensorflow不同版本要求与CUDA及CUDNN版本对应关系: https://developer.nvidia.com/zh-cn/c ...
- 【原创】SystemVerilog中的typedef前置声明方式
SystemVerilog中,为了是代码简洁.易记,允许用户根据个人需要使用typedef自定义数据类型名,常用的使用方法可参见"define和typedef区别".但是在Syst ...
- equals之List
School类 package com.collection.equals; /* * 定义一个学校类 相当于一个学校模板 * 状态: 1.学校id 2.学校名称 * */ public class ...
- Go语言核心36讲(Go语言基础知识二)--学习笔记
02 | 命令源码文件 我们已经知道,环境变量 GOPATH 指向的是一个或多个工作区,每个工作区中都会有以代码包为基本组织形式的源码文件. 这里的源码文件又分为三种,即:命令源码文件.库源码文件和测 ...
- 【Azure 应用服务】App Service中运行Python 编写的 Jobs,怎么来安装Python包 (pymssql)呢?
问题描述 在App Service中运行Python编写的定时任务,需要使用pymssql连接到数据库,但是发现使用 python.exe -m pip install --upgrade -r re ...
- 【UE4】GAMES101 图形学作业4:贝塞尔曲线
总览 Bézier 曲线是一种用于计算机图形学的参数曲线. 在本次作业中,你需要实现de Casteljau 算法来绘制由4 个控制点表示的Bézier 曲线(当你正确实现该算法时,你可以支持绘制由更 ...
- 【c++ Prime 学习笔记】第9章 顺序容器
一个容器是特定类型对象的集合 顺序容器中元素的顺序与其加入容器的位置对应 关联容器中元素的顺序由其关联的关键字决定,关联容器分为有序关联容器和无序关联容器 所有容器类共享公有接口,不同容器按不同方式扩 ...
- 这样调优之后,单机也能扛下100W连接
1 模拟单机连接瓶颈 我们知道,通常启动一个服务端会绑定一个端口,例如8000端口,当然客户端连接端口是有限制的,除去最大端口65535和默认的1024端口及以下的端口,就只剩下1 024~65 53 ...
- 第五课第四周笔记4:Transformer Network变压器网络
Transformer Network变压器网络 你已经了解了 self attention,你已经了解了 multi headed attention.在这个视频中,让我们把它们放在一起来构建一个变 ...