Bravoboy Software Development Engineer

go内存优化&毛刺排查

2019-08-23
go

背景

我们的分布式存储服务最上层是用go写的proxy,负责路由分发。最新加了一些新功能以后发现,偶尔出现100毫秒以上的大毛刺。 但是对应存储节点并没有出现大毛刺,毛刺耗时消耗在哪里?不会又是网络问题吧。统计毛刺的日志,发现并没有出现规律。 于是采用最笨的方法,小流量一台proxy,增加耗时统计日志,看看到底耗时消耗在哪里。通过日志发现了毛刺出现的时候主要耗时并不固定在一个地方。同时发现很简单的代码居然也能出现100毫秒的超时。代码如下:

usnow := data.Microseconds()

tracelog := &data.ReqTraceLog{
	RemoteAddr:   c.Sock.RemoteAddr().String(),
	LastRecordUs: usnow,
	Start:        usnow,
}

cmd := query[0]
sql := query[1:]
r := &data.Request{
	Start:    usnow,
	Wait:     &sync.WaitGroup{},
	TraceLog: tracelog,
	Sql: sql,
	Index: -1,
}
usnow2 := data.Microseconds()
r.TraceLog.RecordTimeWithoutCollect2("buid request cost", usnow2 - usnow)

上面这段代码就是简单的构建对象,并没有复杂操作,这让我感到非常困惑。好在go的相关工具非常全,代码里面引用了net/http/pprof包,这非常方便查看相关数据。

先通过/debug/vars接口看下大概的情况,打印出来发现PauseNs有非常大的值,关于这个值详细说明可以参考godoc,大致说一下PauseNs是一个固定大小为256的循环数组,记录最近的gc时间,数组里面一个元素表示一次gc耗时。虽然go的gc功能已经有所优化,但是还是会有stop world现象。同时观察PauseEnd数组,和PauseNs是对应的,记录每次gc的时间戳,刚好和我们出现毛刺的日志能对应上,说明毛刺的原因是gc导致。那么目标就很明确,优化内存使用。

优化过程

优化1

首页使用go tool pprof -alloc_space -cum -svg http://addr/debug/pprof/heap > heap.svg, 生成内存申请图,用chrome浏览器打开,可以查看哪些函数分配的内存比较多。 目标就是优化内存申请。 heap1

图中线条越粗说明内存申请越多,可以看到主要消耗在读包,解包和回包,仔细查看代码发现,对于短链接情况,每个连接都会申请对象接收包,自然想到的优化就是使用pool存储对象,回收的时候不直接gc,放到pool供后续链接复用。对于解包的代码,函数调用链比较长,中间会申请很多临时buffer,可以一开始申请大块buffer,然后都在大buffer里面分配内存,不用再向系统申请,回收的时候也统一释放。对于回包的代码也做同样的优化,申请大块buffer,在大buffer里面分配内存。 优化以后内存申请图 heap2 看监控效果图 heap3

黄色的线是优化之后的,内存使用在平常情况已经下降了很多,但是还是会出现内存突然上涨的情况,时间和PauseEnd能对应。

优化2

继续分析内存申请图,发现查询索引的地方申请内存很多。想到的方法就是使用内存池 下面是使用sync.Pool实现的。

BytePool = &sync.Pool{
        New: func() interface{} {
                b := make([]byte, 0, 64)
                return &b
        },
}
prefix := BytePool.Get().(*[]byte)
defer BytePool.Put(prefix)

使用过程遇到2个坑

  1. 从BytePool里面获取的byte slice需要reset。原因是上一次使用完放回pool里面,byte slice的length还保留,没有重置为0,导致append方法获取的结果不符合预期
  2. 从BytePool里面获取的指针,不能修改指针的值,否则会导致取出来的slice大小可能比较小,访问会panic。这个问题排查了很久才发现。 优化完之后看耗时监控图效果很明显 heap4 之后再次发现有内存突增情况,排查许久发现是业务的sql语句查询数据量太多导致内存上涨。

总结

golang中申请变量,用户是不知道变量在堆上还在栈上,编译器自己优化判断,在堆上面申请很多对象的时候会影响gc耗时。我们可以在编译的时候增加参数分析内存逃逸情况。-l 是禁止内联 -m 分析内存逃逸,最多提供4个-m,一般2个就够用了。使用方法:

go build -gcflags '-m -m -l' main.go

但是对于整个工程来说,没有找到内存逃逸分析的方法,如果有知道的同学,可以分享我一下。 感兴趣的同学可以搜索Golang escape analysis可以查到很多资料


上一篇 rocksdb Log解析

下一篇 rocksdb perf

Content