OOM 分析 - Go 内存问题排查
OOM (Out Of Memory) 是 Go 应用常见的生产问题。理解 Go 内存管理、掌握 OOM 排查工具和技术是资深工程师的必备技能。
📌 核心概念
💾
内存分配
堆/栈分配
mallocgc
♻️
GC 机制
三色标记法
GOGC
📊
pprof
性能分析
heap profile
🔍
内存泄漏
Goroutine 泄漏
goleak
OOM 常见原因
⚠️ OOM 主要原因
- 内存泄漏: Goroutine 泄漏、全局变量累积
- 大对象分配: 超大切片、未限制缓存
- 高并发: 大量 Goroutine 同时运行
- GC 问题: GC 频率过高、STW 时间过长
- 外部资源: 文件句柄、数据库连接未关闭
内存泄漏排查
📝 Goroutine 泄漏检测
package main
import (
"fmt"
"runtime"
"time"
)
// 检测 Goroutine 数量
func countGoroutines() {
for i := 0; i < 10; i++ {
fmt.Printf("Goroutines: %d\n", runtime.NumGoroutine())
time.Sleep(time.Second)
}
}
// Goroutine 泄漏示例
func leak() {
ch := make(chan int)
go func() {
<-ch // 永久阻塞,无人发送
}()
}
// 修复:使用超时
func noLeak() {
ch := make(chan int)
go func() {
select {
case <-ch:
case <-time.After(time.Second):
return
}
}()
}
func main() {
// 模拟泄漏
for i := 0; i < 100; i++ {
leak()
}
countGoroutines()
}
💡 泄漏检测工具
- runtime.NumGoroutine(): 实时监控
- pprof heap: 堆内存分析
- goleak: 测试包检测泄漏
- go tool trace: 执行追踪
pprof 内存分析
📊 使用 pprof 分析内存
package main
import (
"fmt"
"net/http"
"net/http/pprof"
"runtime"
)
func init() {
// 注册 pprof 端点
http.HandleFunc("/debug/pprof/", pprof.Index)
http.HandleFunc("/debug/pprof/heap", pprof.Handler("heap").ServeHTTP)
}
func main() {
// 启动 HTTP 服务
go http.ListenAndServe(":6060", nil)
// 模拟内存分配
var data [][]byte
for i := 0; i < 1000; i++ {
data = append(data, make([]byte, 1024*1024)) // 1MB
}
// 手动触发 GC
runtime.GC()
// 打印内存统计
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("Alloc: %d MB\n", m.Alloc/1024/1024)
fmt.Printf("Sys: %d MB\n", m.Sys/1024/1024)
fmt.Printf("NumGC: %d\n", m.NumGC)
// 保持运行
select {}
}
📝 pprof 命令使用
# 1. 查看 heap profile
go tool pprof http://localhost:6060/debug/pprof/heap
# 2. 查看 top 对象
(pprof) top
# 3. 查看具体对象
(pprof) list main.leak
# 4. 生成 SVG 图
(pprof) svg
# 5. 本地分析
go tool pprof heap.prof
# 6. 比较两个 profile
go tool pprof -base old.prof new.prof
# 7. 查看 GC 统计
GODEBUG=gctrace=1 ./app
内存优化技巧
📝 减少内存分配
package main
import (
"bytes"
"sync"
)
// 技巧 1: 预分配切片容量
func createSlice(n int) []int {
slice := make([]int, 0, n) // 预分配
for i := 0; i < n; i++ {
slice = append(slice, i)
}
return slice
}
// 技巧 2: 使用 sync.Pool 复用对象
var bufferPool = sync.Pool{
New: func() interface{} {
return &bytes.Buffer{}
},
}
func getBuffer() *bytes.Buffer {
return bufferPool.Get().(*bytes.Buffer)
}
func putBuffer(buf *bytes.Buffer) {
buf.Reset()
bufferPool.Put(buf)
}
// 技巧 3: 使用 strings.Builder
func buildString(n int) string {
var sb strings.Builder
sb.Grow(n * 10) // 预分配
for i := 0; i < n; i++ {
sb.WriteString("item\n")
}
return sb.String()
}
// 技巧 4: 限制缓存大小
type LRUCache struct {
data map[string][]byte
limit int
}
func NewLRUCache(limit int) *LRUCache {
return &LRUCache{
data: make(map[string][]byte),
limit: limit,
}
}
GC 调优
📖 GC 相关环境变量
# GOGC: GC 触发阈值 (默认 100)
# 值越小,GC 越频繁,内存占用越低
GOGC=50 ./app # 更频繁 GC
GOGC=200 ./app # 更少 GC
# GOMEMLIMIT: 内存软限制 (Go 1.19+)
GOMEMLIMIT=512MiB ./app
# GODEBUG: 调试选项
GODEBUG=gctrace=1 ./app # 输出 GC 日志
GODEBUG=schedtrace=1000 ./app # 调度器追踪
# GC 日志格式:
# gc 1 @0.0s 0%: 0.05+0.5+0.05 ms clock, ...
# gc # : GC 编号
# @time: 相对于程序启动时间
# cpu%: CPU 使用百分比
# clock: 墙钟时间 (STW 时间)
最佳实践
✅ 内存管理建议
- 监控 Goroutine: 定期检查 runtime.NumGoroutine()
- 预分配容量: 切片、map 预分配容量
- 对象复用: 使用 sync.Pool 复用对象
- 限制缓存: 设置合理的缓存大小限制
- 及时关闭: 文件、连接等资源及时关闭
- GC 调优: 根据场景调整 GOGC