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

——《golang 逃逸分析详解》
逃逸的场景,这篇文章有详细的介绍:《go逃逸场景有哪些》
CPU使用层面的优化声明使用多核强烈建议在main.go的import中加入下面的代码:
import _ "go.uber.org/automaxprocs"特别是在容器环境运行的程序,要让程序利用上所有的CPU核 。
在k8s的有的版本(具体记不得了),会有一个恶心的问题:容器限制了程序只能使用比如2个核,但是runtime.GOMAXPROCS(0)代码却获取到了所有的物理核 。这时就导致进程的物理线程数接近逻辑CPU的个数,而不是容器限制的核数 。从而,大量的CPU时间消耗在物理线程切换上 。我曾经在腾讯云上测试过,这种现象发生时,容器内单核性能只有物理机上单核性能的43% 。
因此,发现性能问题时,可以通过ls /proc/$(pidof xxx)/tasks | wc来查看进程的物理线程数,如果这个数量远远高于从容器要求的核数,那么在部署的时候建议加上环境变量来解决:export -p GOMAXPROC=2
golang不适合做计算密集型的工作协程的调度,本质上就是一个一直在运行的循环,不断的调用各个协程函数 。然后协程函数在适当的时机保存上下文,放弃执行,把程序流程再转回到主循环 。
这里有几个要点:

  • 主循环来负责唤起每个协程函数,如果存在很多协程函数,轮一遍的周期很长 。
  • 协程函数一定不能阻塞
  • 协程函数也不能阻塞太长的时间
  • 主循环唤起协程函数,以及协程函数切换回主循环是有开销的 。协程越多,开销越大
因此,每个协程函数:在做IO操作的时候一定会切换回主循环,编译器也会在协程函数内编译进去可以切换上下文的代码 。新版的golang runtime还存在强制调度的机制,如果某个正在执行的协程不会退出,会强制进行切换 。
由于存在协程切换的调度机制,golang是不适合做计算密集型的工作的 。例如:音视频编解码,压缩算法等 。以zstd压缩库为例,golang版本的性能不如cgo的版本,即便cgo调用存在一定开销 。(我举的例子比较极端,当需要让golang的性能达到与C同一个级别时,标题的结论才成立 。)
克制使用协程数由runtime的调度器原理可知,协程数不是越多越好,过多的协程会占用很多内存,且占用调度器的资源 。
如何克制的使用协程,请参考我的这篇文章:《VictoriaMetrics中的golang代码优化方法》
总结起来就是:
  • 最合适情况:核心的工作协程的数量,与可用的CPU核数相当 。
  • 区分IO协程和工作协程,把繁重的计算任务交给工作协程处理 。
协程优先级机制关于优先级的案例,请参考我写的这篇文章:《VictoriaMetrics中协程优先级的处理方式》
当业务环境需要区分重要和不太重要的情况时,要通过一定的机制来协调协程的优先级 。比如存贮系统中,写入的优先级高于查询,当资源受限时,要让查询的协程主动让出调度 。
不能让调度器来均匀调度,不能创建更多的某类协程来获得争抢优势 。
要深入理解golang的runtime,推荐阅读yifhao同学的这篇文章:《万字长文带你深入浅出 Golang Runtime》
并发层面并发层面的问题是通用性的知识,与语言的特性并无直接的关系 。本节列出golang中处理并发的惯用方法,已经对golang的并发处理很熟悉的同学可以跳过本小节 。锁关于锁的使用,VictoriaMetrics这个开源组件中有很多经典的案例 。也可以移步参考这篇文章的总结:《VictoriaMetrics中的golang代码优化方法》(本人)

经验总结扩展阅读