用golang开发系统软件的一些细节

(本文的pdf版本)

众所周知,golang非常适合用于开发后台应用,但也通常是各种各样的应用层软件 。
开发系统软件,目前的首选还是C++, C, rust等语言 。相比应用软件,系统软件需要更加稳定,更加高效 。其维持自身运行的资源消耗要尽可能小,然后才可以把更多CPU、内存等资源用于业务处理上 。简单来说,系统软件在CPU、内存、磁盘、带宽等计算机资源的使用上要做到平衡且极致 。
golang代码经过写法上的优化,是可以达到接近C的性能的 。现在早已出现了很多用golang完成的系统软件,例如很优秀的etcd, VictoriaMetrics等 。VictoriaMetrics是Metric处理领域优秀的TSDB存储系统,在阅读其源码后,结合其他一些golang代码优化的知识,我将golang开发系统软件的知识总结如下:
golang的第一性能杀手:GC个人认为GC扫描对象、及其GC引起的STW,是golang最大的性能杀手 。本小节讨论优化golang GC的各种技巧 。
压舱物ballast下面一段神奇的代码,能够减少GC的频率,从而提升程序性能:
func main(){    ballast := make([]byte, 10*1024*1024*1024)    runtime.KeepAlive(ballast)    // do other things}其原理是扩大golang runtime的堆内存,使得实际分配的内存不容易超过堆内存的一定比例,进而减少GC的频率 。GC的频率低了,STW的次数和时间也就更少,从而程序的性能也提升了 。
具体的细节请参考文章:

  • 一个神奇的golang技巧:扩大heap内存来降低gc频率 (本人)
  • Go Ballast 让内存控制更加丝滑
堆外内存众所周知,golang中分配太多对象,会给GC造成很大压力,从而影响程序性能 。那么,我在golang runtime的堆以外分配内存,就可以绕过GC了 。可以通过mmap系统调用来使用堆外内存,具体请见:《Go Mmap 文件内存映射简明教程》对于堆外内存的应用,在此推荐一个非常经典的golang组件:fastcache 。具体请看这篇我对fastcache的分析文章:《介绍一个golang库:fastcache》 。
也需要注意,这里有个坑:如果使用mmap去映射一个文件,则某个虚拟地址没有对应的物理地址时,操作系统会产生缺页终端,并转到内核态执行,把磁盘的内容load到page cache 。如果此时磁盘IO高,可能会长时间的阻塞……进一步地,导致了golang调度器的阻塞 。对象复用对象太多会导致GC压力,但又不可能不分配对象 。因此对象复用就是减少分配消耗和减少GC的释放消耗的好办法 。
下面分别通过不同的场景来讨论如何复用对象 。
海量微型对象的情况假设有很多几个字节或者几十个字节的,数以万计的对象 。那么最好不要一个个的new出来,会有两个坏处:
  • 对象的管理会需要额外的内存,考虑内存对齐等因素又会造成额外的内存浪费 。因此海量微型对象需要的总内存远远大于其自身真实使用的字节数;
  • GC的压力源于对象的个数,而不是总字节数 。海量微型对象必然增大GC压力 。
海量微型对象的影响,请看我曾经遇到过的这个问题:《【笔记】对golang的大量小对象的管理真的是无语了……》
因此,海量微型对象的场景,这样解决:
  • 分配一大块数组,在数组中索引微型对象
  • 考虑fastcache这样的组件,通过堆外内存绕过GC
当然,也有缺点:不好缩容 。
大量小型对象的情况对于大量的小型对象,sync.Pool是个好选择 。
推荐阅读这篇文章:《Go sync.Pool 保姆级教程》

经验总结扩展阅读