← Panic 处理 | 轻量级 GLS →

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

📖 延伸阅读