最近做了许多有关Go内存优化的工作,总结了一些定位、调优方面的套路和经验,于是,想通过这篇文章与大家分享讨论。
发现问题
性能优化领域有一条总所周知的铁律,即:不要过早地优化。编写一个程序,首先应该保证其功能的正确性,以及诸如设计是否合理、需求等是否满足,过早地优化只会引入不必要的复杂度以及设计不合理等各种问题。
那么何时才能开始优化呢?一句话,问题出现时。诸如程序出现频繁OOM,CPU使用率异常偏高等情况。如今,在这微服务盛行的时代,公司内部都会拥有一套或简单或复杂的监控系统,当系统给你发出相关告警时,你就要开始重视起来了。
问题定位
1. 查看内存曲线
首先,当程序发生OOM时,首先应该查看程序的内存使用量曲线,可以通过现有监控系统查看,或者prometheus之类的开源工具。
曲线一般都是呈上升趋势,比如goroutine泄露的曲线一般是使用量缓慢上升直至OOM,而内存分配不合理往往时在高负载时快速攀升以致OOM。
2. 问题复现
这块是可选项,但是最好能保证复现。如果能在本地或debug环境复现问题,这将非常有利于我们反复进行测试和验证。
3. 使用pprof定位
Go官方工具提供了pporf来专门用以性能问题定位,首先得在程序中开启pprof收集功能,这里假定问题程序已开启pprof。(对这块不够了解的同学,建议通过这两篇文章(1, 2)学习下pprof工具的基本用法)
接下来,我们复现问题场景,并及时获取heap和groutine的采样信息。
这里你可能想问,这样就够了吗?
当然不是,只获取一份样本信息是不够的。内存使用量是不断变化的(通常是上升),因此我们需要的也是期间heap、gourtine信息的变化信息,而非瞬时值。一般来说,我们需要一份正常情况下的样本信息,一份或多份内存升高期间的样本信息。
数据收集完毕后,我们按照如下3个方面来排查定位。
排查goroutine泄露
go tool pprof --base g1.out g2.out
top
go tool pprof --base g2.out g3.out
排查内存使用量
go tool pprof --base h1.out h2.out
top
排查内存分配量
当上述排查方向都没发现问题时,那就要查看期间是否有大量的内存申请了。
我们知道Go预言中的GC有三个触发点:
- 两分钟没有GC,就强制GC
- 用户手动调用runtime.GC(一般只测试用)
- 程序当前使用的堆内存达到了runtime中动态计算的GC heap goal
若进程过于频繁地申请大内存,那么就会把GC heap goal不断提高,到超过内存限制后,就会OOM。
go tool pprof --alloc_space --base h1.out h2.out
top
web
问题优化
定位到问题根因后,接下来就是优化阶段了。这个阶段需要对Go本身足够熟悉,还得对问题程序的业务逻辑有所了解。
我梳理了一些常见的优化手段,仅供参考。实际场景还是得实际分析。
goroutine泄露
context.Context
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
default:
}
...
}
}(ctx)
对象复用
sync.Pool
var pool = sync.Pool{New: func() interface{} { return make([]byte, 4096) }}
func fn() {
buf := pool.Get().([]byte) // takes from pool or calls New
// do work
pool.Put(buf) // returns buf to the pool
}
避免[]byte和string转换
string()[]byte()*(*[]byte)(unsafe.Pointer(&s))
除此之外,还有很多优化方法,可以多看看dave cheney大神的这篇文章,写得很全面。
优化验证
最后一步,我们需要验证优化的结果,毕竟你至少得说服自己,你的优化是的确有成效的。
b.ReportAllocs()
总结
性能调优是一项必备但是较为困难的技能,不仅需要熟悉语言、操作系统等基本知识,还需要一定的经验积累。
本文介绍了针对Go程序内存问题的发现、定位、优化以及验证,希望能对你排查内存问题有所帮助(还有某些情况未能没考虑到,欢迎评论区参与讨论)。