用golang开发系统软件的总结


众所周知,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 保姆级教程》

sync.Pool不如上面的方法节省内存,但好处是可以缩容。

数量可控的中型对象

有的时候,我们可能需要一些定额数量的对象,并且对这些对象复用。

这时可以使用channel来做内存池。需要时从channel取出,用完放回channel。

slice的复用

fasthttp, VictoriaMetrics等组件的作者 valyala可谓是把slice复用这个技巧玩上了天,具体可以看fasthttp主页上的Tricks with []byte buffers这部分介绍。

概要的总结起来就是:[]byte这样的数组分配后,不要释放,然后下次使用前,用slice=slice[:0]来清空,继续使用其上次分配好的cap指向的空间。

这篇中文的总结也非常不错:《fasthttp对性能的优化压榨》

valyala大神还写了个 bytebufferpool,对[]byte重用的场景进行了封装。

避免容器空间动态增长

对于slice和map而言,在预先可以预估其空间占用的情况下,通过指定大小来减少容器操作期间引起的空间动态增长。特别是map,不但要拷贝数据,还要做rehash操作。

func xxx(){
  slice := make([]byte, 0, 1024)  // 有的时候,golangci-lint会提示未指定空间的情况
  m := make(map[int64]struct{}, 1000)
}

大神技巧:用slice代替map

此技巧源于valyala大神。

假设有一个很小的map需要插入和查询,那么把所有key-value顺序追加到一个slice中,然后遍历查找——其性能损耗可能比分配map带来的GC消耗还要小。

  1. map变成slice,少了很多动态调整的空间
  2. 如果整个slice能够塞进CPU cache line,则其遍历可能比从内存load更加快速

具体请见这篇:《golang第三方库fasthttp为什么要使用slice而不是map来存储header?》

避免栈逃逸

golang中非常酷的一个语法特点就是没有堆和栈的区别。编译器会自动识别哪些对象该放在堆上,哪些对象该放在栈上。

func xxx() *ABigStruct{
  a := new(ABigStruct)  // 看起来是在堆上的对象
  var b ABigStruct      // 看起来是栈上的对象
  // do something
  // not return a   // a虽然是对象指针,但仅限于函数内使用,所以编译器可能把a放在栈上
  return &b   // b超出了函数的作用域,编译器会把b放在堆上。
}

valyala大神的经验:先找出程序的hot path,然后在hot path上做栈逃逸的分析。尽量避免hot path上的堆内存分配,就能减轻GC压力,提升性能。

fasthttp首页上的介绍:

Fast HTTP package for Go. Tuned for high performance. Zero memory allocations in hot paths. Up to 10x faster than net/http

这篇文章介绍了侦测栈逃逸的方法:

验证某个函数的变量是否发生逃逸的方法有两个:

  • go run -gcflags "-m -l" (-m打印逃逸分析信息,-l禁止内联编译);例:
➜  testProj go run -gcflags "-m -l" internal/test1/main.go
# command-line-arguments
internal/test1/main.go:4:2: moved to heap: a
internal/test1/main.go:5:11: main make([]*int, 1) does not escape
  • go tool compile -S main.go | grep runtime.newobject(汇编代码中搜runtime.newobject指令,该指令用于生成堆对象),例:
➜  testProj go tool compile -S internal/test1/main.go | grep newobject
        0x0028 00040 (internal/test1/main.go:4) CALL    runtime.newobject(SB)

TEXT 复制 全屏

——《golang 逃逸分析详解》

逃逸的场景,这篇文章有详细的介绍:《go逃逸场景有哪些》

CPU使用层面的优化

声明使用多核

强烈建议在main.go的import中加入下面的代码:

import _ "go.uber.org/automaxprocs"

特别是在容器环境运行的程序,要让程序利用上所有的CPU核。

在k8s的有的版本(具体记不得了),会有一个恶心的问题:容器限制了程序只能使用比如2个核,但是runtime.GOMAXPROCS(0)代码却获取到了所有的物理核。这时就导致进程的物理线程数接近逻辑CPU的个数,而不是容器限制的核数。从而,大量的CPU时间消耗在物理线程切换上。我曾经在腾讯云上测试过,这种现象发生时,容器内单核性能只有物理机上单核性能的43%。

本站声明:
1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;

2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;

3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;

4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;

5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/294503.html

(0)
上一篇 2022年12月4日
下一篇 2022年12月4日

相关推荐

发表回复

登录后才能评论