Go程序性能优化
Go 应用性能优化
减少变量逃逸,尽量使用局部变量,在栈上分配对象
切片等结构提前分配足够容量
减少对象分配数量,复用对象;或使用 sync.Pool
减少协程数量,复用协程处理多个任务
减小锁粒度,减少锁竞争
pprof 分析程序性能瓶颈
寻找性能瓶颈
在压测时,我们通过以下步骤来逐渐提升接口的整体性能:
使用固定 QPS 压测,以阶梯形式逐渐增加压测 QPS,如 1000 -> 每分钟增加 1000 QPS
压测过程中观察系统的延迟是否异常
观察系统的 CPU 使用情况
如果 CPU 使用率在达到一定值之后不再上升,反而引起了延迟的剧烈波动,这时大概率是发生了阻塞,进入 pprof 的 web 页面,点击 goroutine,查看 top 的 goroutine 数,这时应该有大量的 goroutine 阻塞在某处,比如 Semacquire
如果 CPU 上升较快,未达到预期吞吐就已经过了高水位,则可以重点考察 CPU 使用是否合理,在 CPU 高水位进行 profile 采样,重点关注火焰图中较宽的“平顶山”
重复上述步骤,直至系统性能达到或超越我们设置的性能目标。
一些优化案例
gc mark 占用过多 CPU
在 Go 语言中 gc mark 占用的 CPU 主要和运行时的对象数相关,也就是我们需要看 inuse_objects。
定时任务,或访问流量不规律的应用,需要关注 alloc_objects。
优化主要是下面几方面:
1. 减少变量逃逸
尽量在栈上分配对象
2. 使用 sync.Pool 复用堆上对象
sync.Pool
用出花儿的就是 fasthttp
了,可以看看我之前写的这一篇:fasthttp
为什么快。
最简单的复用就是复用各种 struct, slice
,在复用时 put
时,需要判断 size
是否已经扩容过头,小心因为 sync.Pool
中存了大量的巨型对象导致进程占用了大量内存。
调度占用过多 CPU
goroutine
频繁创建与销毁会给调度造成较大的负担,如果我们发现 CPU 火焰图中 schedule,findrunnable
占用了大量 CPU,那么可以考虑使用开源的 workerpool
来进行改进,比较典型的有 fasthttp worker pool
。
如果客户端与服务端之间使用的是短连接,那么我们可以使用长连接来减少连接创建的开销,这里就包含了 goroutine
的创建与销毁。
锁冲突严重,导致吞吐量瓶颈
进行锁优化的思路无非就一个 拆 和一个 缩 字:
拆:将锁粒度进行拆分,比如全局锁,我能不能把锁粒度拆分为连接粒度的锁;如果是连接粒度的锁,那我能不能拆分为请求粒度的锁;在 logger fd 或 net fd 上加的锁不太好拆,那么我们增加一些客户端,比如从 1-> 100,降低锁的冲突是不是就可以了。
缩:缩小锁的临界区,业务允许的前提下,可以把 syscall 移到锁外面;有时只是想要锁 map 的读写逻辑,但是却不小心锁了连接读写的逻辑,或许简单地用 sync.Map 来代替 map Lock,defer Unlock 就能简单地缩小临界区了。
timer 相关函数占用大量 CPU
同样是在网关和海量连接的应用中较常见,优化手段:
使用时间轮/粗粒度的时间管理,精确到 ms 级一般就足够了
升级到 Go 1.14+,享受官方的升级红利
模拟真实工作负载
在前面的论述中,我们对问题进行了简化。真实世界中的后端系统往往不只一个接口,压测工具、平台往往只支持单接口压测。
公司的业务希望知道的是后端系统整体性能,即这些系统作为一个整体,在限定的资源条件下,能够承载多少业务量(如并发创建订单)而不崩溃。
虽然大家都在讲微服务,但单一服务往往也不只有单一功能,如果一个系统有 10 个接口(已经算是很小的服务了),那么这个服务的真实负载是很难靠人肉去模拟的。
这也就是为什么互联网公司普遍都需要做全链路压测。像样点的公司会定期进行全链路压测演练,以便知晓随着系统快速迭代变化,系统整体是否出现了严重的性能衰退。
通过真实的工作负载,我们才能发现真实的线上性能问题。讲全链路压测的文章也很多,本文就不再赘述了。
参考
Last updated