众所周知,golang非常适合用于开发后台应用,但也通常是各种各样的应用层软件。
开发系统软件, 目前的首选还是C++, C, rust等语言。相比应用软件,系统软件需要更加稳定,更加高效。其维持自身运行的资源消耗要尽可能小,然后才可以把更多CPU、内存等资源用于业务处理上。简单来说,系统软件在CPU、内存、磁盘、带宽等计算机资源的使用上要做到平衡且极致。
golang代码经过写法上的优化,是可以达到接近C的性能的。现在早已出现了很多用golang完成的系统软件,例如很优秀的etcd, VictoriaMetrics等。VictoriaMetrics是Metric处理领域优秀的TSDB存储系统, 在阅读其源码后,结合其他一些golang代码优化的知识,我将golang开发系统软件的知识总结如下:
个人认为GC扫描对象、及其GC引起的STW,是golang最大的性能杀手。本小节讨论优化golang GC的各种技巧。
下面一段神奇的代码,能够减少GC的频率,从而提升程序性能:
func main(){ ballast := make([]byte, 10*1024*1024*1024) runtime.KeepAlive(ballast) // do other things }
其原理是扩大golang runtime的堆内存,使得实际分配的内存不容易超过堆内存的一定比例,进而减少GC的频率。GC的频率低了,STW的次数和时间也就更少,从而程序的性能也提升了。
具体的细节请参考文章:
众所周知,golang中分配太多对象,会给GC造成很大压力,从而影响程序性能。
那么,我在golang runtime的堆以外分配内存,就可以绕过GC了。
可以通过mmap系统调用来使用堆外内存,具体请见:《Go Mmap 文件内存映射简明教程》
对于堆外内存的应用,在此推荐一个非常经典的golang组件:fastcache。具体请看这篇我对fastcache的分析文章:《介绍一个golang库:fastcache 》。
也需要注意,这里有个坑: 如果使用mmap去映射一个文件,则某个虚拟地址没有对应的物理地址时,操作系统会产生缺页终端,并转到内核态执行,把磁盘的内容load到page cache。如果此时磁盘IO高,可能会长时间的阻塞……进一步地,导致了golang调度器的阻塞。
对象太多会导致GC压力,但又不可能不分配对象。因此对象复用就是减少分配消耗和减少GC的释放消耗的好办法。
下面分别通过不同的场景来讨论如何复用对象。
假设有很多几个字节或者几十个字节的,数以万计的对象。那么最好不要一个个的new出来,会有两个坏处:
海量微型对象的影响,请看我曾经遇到过的这个问题:《【笔记】对golang的大量小对象的管理真的是无语了……》
因此,海量微型对象的场景,这样解决:
当然,也有缺点:不好缩容。
对于大量的小型对象,sync.Pool是个好选择。
推荐阅读这篇文章:《Go sync.Pool 保姆级教程》
sync.Pool不如上面的方法节省内存,但好处是可以缩容。
有的时候,我们可能需要一些定额数量的对象,并且对这些对象复用。
这时可以使用channel来做内存池。需要时从channel取出,用完放回channel。
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) }
此技巧源于valyala大神。
假设有一个很小的map需要插入和查询,那么把所有key-value顺序追加到一个slice中,然后遍历查找——其性能损耗可能比分配map带来的GC消耗还要小。
具体请见这篇:《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逃逸场景有哪些》
强烈建议在main.go的import中加入下面的代码:
import _ "go.uber.org/automaxprocs"
特别是在容器环境运行的程序,要让程序利用上所有的CPU核。
在k8s的有的版本(具体记不得了),会有一个恶心的问题:容器限制了程序只能使用比如2个核,但是runtime.GOMAXPROCS(0)
代码却获取到了所有的物理核。这时就导致进程的物理线程数接近逻辑CPU的个数,而不是容器限制的核数。从而,大量的CPU时间消耗在物理线程切换上。我曾经在腾讯云上测试过,这种现象发生时,容器内单核性能只有物理机上单核性能的43%。
标签:golang,后台应用,c++,c语言,系统软件,程序性能 来源:
本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享; 2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关; 3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关; 4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除; 5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。