惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
美团技术团队
Last Week in AI
Last Week in AI
WordPress大学
WordPress大学
博客园 - 三生石上(FineUI控件)
博客园 - 聂微东
雷峰网
雷峰网
阮一峰的网络日志
阮一峰的网络日志
博客园 - 叶小钗
IT之家
IT之家
Google DeepMind News
Google DeepMind News
D
Docker
J
Java Code Geeks
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - 【当耐特】
V
V2EX
Hugging Face - Blog
Hugging Face - Blog
博客园 - Franky
月光博客
月光博客
宝玉的分享
宝玉的分享
酷 壳 – CoolShell
酷 壳 – CoolShell
aimingoo的专栏
aimingoo的专栏
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More

郑文峰的博客

使用dify对接飞书多维表格 使用n8n对接飞书多维表格 一次服务升级时pg表DDL执行超时失败 Go语言高效IO缓冲技术详解 Go语言延迟初始化(Lazy Initialization)最佳实践 Go语言字符串拼接性能对比与优化指南 Go语言结构体内存对齐完全指南 Go语言空结构体:零内存消耗的高效编程 Go语言堆栈分配与逃逸分析深度解析 Go语言原子操作完全指南 Go语言内存预分配完全指南 Go语言不可变数据共享:无锁并发编程实践 Go语言零拷贝技术完全指南 Go语言遍历性能深度解析:从原理到优化实践 Go语言Interface Boxing原理与性能优化指南 Go协程池深度解析:原理、实现与最佳实践 使用etcd分布式锁导致的协程泄露与死锁问题 基于pre-commit的Python代码规范落地实践 初识 MCP Server pulsar阻塞导致logstash无法接入日志 django-prometheus使用及源码分析 kube-proxy源码分析 kubernetes service如何通过iptables转发 tcp缓存引起的日志丢失 django-apschedule定时任务异常停止 理解calico容器网络通信方案原理 理解flannel的三种容器网络方案原理 理解Linux IPIP隧道 理解VXLAN网络 理解Linux TunTap设备
服务启动时出现 OOM
zhengwenfeng · 2025-09-22 · via 郑文峰的博客

# 前言

本文详细记录了一次在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. 善于利用工具来辅助排查问题。