服务启动时出现 OOM
# 前言
本文详细记录了一次在Kubernetes环境中Golang服务启动时出现OOM(Out of Memory)问题的排查和解决过程。服务在启动约2分钟后出现内存溢出,通过pprof工具分析发现主要问题源于bytes.Buffer对象的频繁扩容。
# 现象
有一个golang后台服务跑在了k8s集群上,在启动过程中,大约 2 分钟后,该服务所在的pod出现了 OOM,也就是超过了部署该 POD 的 limit 内存大小。
# 分析
该现象是可复现的,所以我通过重启该pod,在其还没出现 OOM 时使用 pprof 将服务的 profile 文件给获取到。
curl http://localhost:8080/debug/pprof/heap -o profile
然后使用下面的命令在网页上查看服务的内存使用情况。
go tool pprof -http=:8081 profile
在网页上,在顶部的导航栏中,选择 View -> Top,SAMPLES -> alloc_space,来查看从服务启动时,申请内存Top。
首先可以看到第一名申请的内存是 bytes.makeSlice,看名字大胆的猜测,是因为切片内存申请过多导致的,接下来需要验证。
选中该行,然后选择 View -> Graph,就会进入进入到一个函数调用链。
我们先找到调用链的底端,可以看到 bytes.makeSlice 占用了1.12G 内存,调用者是 bytes(*Buffer).grow (opens new window),通过源码分析是 Buffer 对象扩容导致的。
通过调用链往上找到在业务代码上创建并使用 Buffer 对象的地方,最终找到了 Serialize 函数。
我们看想该函数,该函数的作用是,将value对象序列化成byte数组。然后看到其创建了 Buffer 对象,并将 value 对象序列化存储到其中。
func Serialize(value interface{}) ([]byte, error) {
// gob.Register(value)
buf := bytes.Buffer{}
if err := gob.NewEncoder(&buf).Encode(&value); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
2
3
4
5
6
7
8
但我们立马发现了问题,我们创建 Buffer{} 并没有指定其大小,可以看到其结构体也没有默认大小,当写入数据时,则会进行调用 grow 进行扩容。
type Buffer struct {
buf []byte // contents are the bytes buf[off : len(buf)]
off int // read at &buf[off], write at &buf[len(buf)]
lastRead readOp // last read operation, so that Unread* can work correctly.
}
func (b *Buffer) Write(p []byte) (n int, err error) {
b.lastRead = opInvalid
m, ok := b.tryGrowByReslice(len(p))
if !ok {
m = b.grow(len(p))
}
return copy(b.buf[m:], p), nil
}
2
3
4
5
6
7
8
9
10
11
12
13
14
再结合业务进行分析,服务启动时会有大量的数据需要进行序列化,每次调用该函数都是创建一个未指定大小的 Buffer 对象,然后序列化时进行不断地扩容,从而导致内存迅速增加,从而导致 OOM。
# 解决办法
使用 sync.Pool 来复用 Buffer 对象,并且在创建 Buffer 对象时指定一个合适的大小,这样就可以减少内存的申请从而避免了 OOM。
// 缓冲区池,复用 bytes.Buffer 对象
var bufferPool = sync.Pool{
New: func() interface{} {
// 预分配 1KB 容量,减少扩容次数
buf := make([]byte, 0, 1024)
return bytes.NewBuffer(buf)
},
}
func Serialize(value interface{}) ([]byte, error) {
if value == nil {
return nil, fmt.Errorf("input nil value")
}
// 从池中获取缓冲区
buf := bufferPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
bufferPool.Put(buf)
}()
encoder := gob.NewEncoder(buf)
if err := encoder.Encode(&value); err != nil {
return nil, err
}
// 高效的字节切片克隆返回数据
return bytesClone(buf.Bytes()), nil
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 总结
- 在编码方面需要注意内存管理和对象复用对系统稳定性的重要性。
- 善于利用工具来辅助排查问题。