最近做了许多有关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有三个触发点:

  1. 两分钟没有GC,就强制GC
  2. 用户手动调用runtime.GC(一般只测试用)
  3. 程序当前使用的堆内存达到了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程序内存问题的发现、定位、优化以及验证,希望能对你排查内存问题有所帮助(还有某些情况未能没考虑到,欢迎评论区参与讨论)。

参考