高性能 Go 语言发行版优化与落地实践 | 青训营笔记

128 阅读10分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记。

这节课开源收获什么?

《高性能Go语言发行版优化与落地实践》

  • 优化
    • 内存管理优化
    • 编译器优化
  • 背景
    • 自动内存管理和Go内存管理机制
    • 编译器优化的基本问题和思路
  • 实践:字节跳动遇到的性能问题以及优化方案

性能优化是什么?

提升软件系统处理能力,减少不必要的消耗,充分发掘计算机算力

为什么要做性能优化

用户体验:带来用户体验的提升。让刷抖音更丝滑,让双十一购物不再卡顿 资源高效利用:降低成本,提高效率一很小的优化乘以海量机器会是显著的性能提升和成本节约

性能优化层面

  • 业务层优化
    • 针对特定场景,具体问题,具体分析
    • 容易获得较大性能收益
  • 语言运行时优化
    • 解决更通用的性能问题
    • 考虑更多场景

数据驱动

  • 自动化性能分析工具----pprof
  • 依靠数据而非猜测
  • 首先优化最大瓶颈

1.自动内存管理

Tracing garbage collection:跟踪垃圾回收

Generational GC :分代GC

Reference counting:引用技术

  • 动态内存
    • 程序在运行时根据需求动态分配的内存:nalIoc()
  • 自动内存管理( 垃圾回收):由程序语言的运行时系统管理动态内存
    • 避免手动内存管理,专注于实现业务逻辑
    • 保证内存使用的正确性和安全性: double-free problem, use-after-free problem
  • 三个任务
    • 为新对象分配空间
    • 找到存活对象
    • 回收死亡对象的内存空间

GC算法

课后查阅了解

1.标记-清除算法

此算法主要有两个步骤:

  • 标记
  • 清除

来看一下标记清除算法的具体步骤:

**第一步:**暂停程序的业务逻辑,然后分出可达和不可达对象,然后做上标记

**第二步:**以程序为根节点来找出它可达的对象,并坐上标记

**第三步:**清除未标记的对象

**第四步:**停止暂停,让程序继续跑起来,然后循环这个过程,直到程序的生命周期结束。

标记清除算法的缺点:

  • STW ,stop the world;让程序暂停,程序会出现卡顿的;
left
  • 标记需要扫描整个heap
  • 清除数据会产出heap碎片

在执行GC的基本流程就是首先启动STW暂停,然后执行标记,再执行数据回收,最后停止STW,全部的GC时间都是包裹在STW范围之内的,这样貌似程序暂停的时间过长,后续做过简单的优化,但是重要的问题是,无论怎么优化,标记清除算法都会暂停整个程序。

为了解决这个问题,Go在V1.5就用三色并发标记法来优化这个问题

Go V1.5 的三色并发标记法


Golang中的垃圾回收主要采用三色标记法。GC过程和其他goroutine可并发进行,但需要一定时间的STW,所谓三色标记法实际就是通过三个阶段的标记来确定清除的对象有哪些。

所谓三色,指的是初始的“白色”,过程的”灰色“,安心的”黑色“

第一步: 每次创建了新的对象,默认颜色都被标记为“白色”,将他们都发在白色集合中

第二步: 每次GC回收开始,会从根节点开始遍历所有对象,把遍历到的对象从白色标记为灰色,放入灰色集合(ps:这里的遍历为一次遍历而非递归遍历!!)

第三步: 遍历灰色集合,将灰色对象引用的对象从白色标记为灰色,之后将此灰色对象放入黑色集合。

第四步: 重复第三步,直到灰色中无任何对象。当我们全部的可达对象都比遍历完后,灰色标记将不再存在灰色对象,目前全部内存的数据只有黑色和白色,那么黑色对象就是我们程序逻辑可达对象,不可删除。白色的对象是全部不可达对象,那么白色对象就是内存中目前的垃圾数据,需要被删除。

第五步: 回收所有的白色标记表的对象,也就是回收垃圾

从以上的例子中,我们已经清楚的体现了三色的特性,但是这里面可能会有很多并发流程均会被扫描,执行并发流程的内存可能相互依赖,为了在GC过程中保证数据的安全,我们在开始三色标记之前就会加上STW,在扫描确实确定黑白对象之后再放开STW,但是这样GC扫描性能还是太低了



没有STW的三色标记法

我们基于上述的三色标记法来说,它一定要依赖STW,因为如果不暂停程序,程序的逻辑该表对象引用关系,这种动作如果在标记阶段做了修改,会影响标记结果的正确性。

接下来,我们来看一下在标记过程中不使用STW将会发生什么事?

我们把初始状态设置为一轮扫描后,对象2是通过指针p指向对象3的,如图:

现在三色标记过程不启动STW,在GC扫描过程中,在任意的对象均可能发生读写操作,如下图,在没扫描对象二时,已经标记为黑色的对象4,此时创建了指针q,并且指向对象3。

此时移除指针p,那么对象三实则就是挂在了已经扫描完成的对象四下,

然后按照正常三色标记法的逻辑,对象2和对象7被标记为黑色

接着执行三色标记法的最后一步,将所有的白色对象进行垃圾回收。

我们可以发现,被对象4合法引用的对象3被GC误杀掉了

所有我们发现,在三色标记法中,我们不希望发生:

  • 条件1: 一个白色对象被黑色对象引用**(白色被挂在黑色下)**

  • 条件2: 灰色对象与它之间的可达关系的白色对象遭到破坏**(灰色同时丢了该白色)** 如果当以上两个条件同时满足时,就会出现对象丢失现象!

    屏障机制

要让GC回收器,满足下列两种情况并且保证对象不丢失,方法就是“强三色不变式”和“弱三色不变式”

  • 条件1: 一个白色对象被黑色对象引用**(白色被挂在黑色下)**
  • 条件2: 灰色对象与它之间的可达关系的白色对象遭到破坏**(灰色同时丢了该白色)** 如果当以上两个条件同时满足时,就会出现对象丢失现象!

多说无益,看图!!

强三色不变式:不存在黑色对象引用到白色对象的指针。

screen-capture

弱三色不变式:所有被黑色对象引用的白色对象都处于灰色保护状态。

screen-capture

弱三色强调,黑色可以引用白色,但是这个白色必须存在其他灰色对象对其的引用,或者其上游有可达它的灰色对象。

为了遵循着两种方式,GC算法推出了两种屏障方式,“插入屏障”,“删除屏障”

插入屏障

**具体操作:**在A对象引用B对象的时候,B对象被标记为灰色(将B挂在A下游,B必须被标记为灰色)

满足: 强三色不变式. (不存在黑色对象引用白色对象的情况了, 因为白色会强制变成灰色)

添加下游对象(当前下游对象slot, 新下游对象ptr) {   
  //1
  标记灰色(新下游对象ptr)   
  
  //2
  当前下游对象slot = 新下游对象ptr                    
}

​ 这段伪码逻辑就是写屏障,. 我们知道,黑色对象的内存槽有两种位置, 栈和堆. 栈空间的特点是容量小,但是要求相应速度快,因为函数调用弹出频繁使用, 所以“插入屏障”机制,在栈空间的对象操作中不使用. 而仅仅使用在堆空间对象的操作中.

​ 接下来,我们用几张图,来模拟整个一个详细的过程, 希望您能够更可观的看清晰整体流程。



screen-capture

screen-capture


screen-capture

(7) 停止STW

(8)清除白色

删除屏障

具体操作: 被删除的对象,如果自身为灰色或者白色,那么被标记为灰色。

满足: 弱三色不变式. (保护灰色对象到白色对象的路径不会断)

伪代码:

添加下游对象(当前下游对象slot, 新下游对象ptr) { //1 if (当前下游对象slot是灰色 || 当前下游对象slot是白色) { 标记灰色(当前下游对象slot) //slot为被删除对象, 标记为灰色 }

//2 当前下游对象slot = 新下游对象ptr }

screen-capture

screen-capture

screen-capture

Go v1.8的混合写屏障机制

插入写屏障和删除写屏障的短板:

插入写屏障:结束时需要STW来重新扫描栈,标记栈上引用的白色对象的存活;

删除写屏障:回收精度低,GC开始时STW扫描堆栈来记录初始快照,这个过程会保护开始时刻的所有存活对象。

混合写屏障规则

具体操作:

1、GC开始将栈上的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描,无需STW),

2、GC期间,任何在栈上创建的新对象,均为黑色。

3、被删除的对象标记为灰色。

4、被添加的对象标记为灰色。

满足: 变形的弱三色不变式.

添加下游对象(当前下游对象slot, 新下游对象ptr) {
      //1 
        标记灰色(当前下游对象slot)    //只要当前下游对象被移走,就标记灰色
      
      //2 
      标记灰色(新下游对象ptr)
          
      //3
      当前下游对象slot = 新下游对象ptr
}

场景一:

screen-capture

场景二:

new 栈对象9;
对象9->对象3 = 对象3//将栈对象3 挂在 栈对象9 下游
对象2->对象3 = null;      //对象2 删除引用 对象3

screen-capture

screen-capture

场景三:

堆对象10->对象7 = 堆对象7//将堆对象7 挂在 堆对象10 下游
堆对象4->对象7 = null;         //对象4 删除引用 对象7

screen-capture

screen-capture

场景四:对象从一个栈对象删除引用,成为另一个堆对象的下游

堆对象10->对象7 = 堆对象7//将堆对象7 挂在 堆对象10 下游
堆对象4->对象7 = null;         //对象4 删除引用 对象7

screen-capture

screen-capture

​ Golang中的混合写屏障满足弱三色不变式,结合了删除写屏障和插入写屏障的优点,只需要在开始时并发扫描各个goroutine的栈,使其变黑并一直保持,这个过程不需要STW,而标记结束后,因为栈在扫描后始终是黑色的,也无需再进行re-scan操作了,减少了STW的时间。

参考文献

字节跳动第三届后端青训营PPT