郑文峰的博客 郑文峰的博客
首页
  • Go语言高性能编程
  • Bug 通缉令
分类
标签
归档
关于
  • 导航 (opens new window)
  • 代码片段 (opens new window)
  • 收藏
  • 友链
  • 外部页面

    • 开往 (opens new window)
GitHub (opens new window)

zhengwenfeng

穷则变,变则通,通则久
首页
  • Go语言高性能编程
  • Bug 通缉令
分类
标签
归档
关于
  • 导航 (opens new window)
  • 代码片段 (opens new window)
  • 收藏
  • 友链
  • 外部页面

    • 开往 (opens new window)
GitHub (opens new window)
  • 一次服务升级时pg表DDL执行超时失败
  • 服务启动时出现 OOM
    • 前言
    • 现象
    • 分析
    • 解决办法
    • 总结
  • Bug 通缉令
zhengwenfeng
2025-09-22
目录

服务启动时出现 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
1

然后使用下面的命令在网页上查看服务的内存使用情况。

go tool pprof -http=:8081 profile
1

在网页上,在顶部的导航栏中,选择 View -> Top,SAMPLES -> alloc_space,来查看从服务启动时,申请内存Top。

17585287225681758528722158.png

首先可以看到第一名申请的内存是 bytes.makeSlice,看名字大胆的猜测,是因为切片内存申请过多导致的,接下来需要验证。

选中该行,然后选择 View -> Graph,就会进入进入到一个函数调用链。

17585288355611758528835189.png

我们先找到调用链的底端,可以看到 bytes.makeSlice 占用了1.12G 内存,调用者是 bytes(*Buffer).grow (opens new window),通过源码分析是 Buffer 对象扩容导致的。

17585294490741758529449060.png

通过调用链往上找到在业务代码上创建并使用 Buffer 对象的地方,最终找到了 Serialize 函数。

17585295595181758529558760.png

我们看想该函数,该函数的作用是,将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
}
1
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
}
1
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
}
1
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

# 总结

  1. 在编码方面需要注意内存管理和对象复用对系统稳定性的重要性。
  2. 善于利用工具来辅助排查问题。
#go语言
上次更新: 2025/09/22, 08:52:15
一次服务升级时pg表DDL执行超时失败

← 一次服务升级时pg表DDL执行超时失败

最近更新
01
一次服务升级时pg表DDL执行超时失败
09-14
02
Go语言高效IO缓冲技术详解
06-14
03
Go语言延迟初始化(Lazy Initialization)最佳实践
06-14
更多文章>
Theme by Vdoing | Copyright © 2022-2025 zhengwenfeng | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式