Skip to content

10.4 内存调优实战 ​

内存性能分析 ​

内存指标监控 ​

go
import "runtime"

func printMemStats() {
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    
    fmt.Printf("Alloc = %v MB\n", m.Alloc/1024/1024)
    fmt.Printf("TotalAlloc = %v MB\n", m.TotalAlloc/1024/1024)
    fmt.Printf("Sys = %v MB\n", m.Sys/1024/1024)
    fmt.Printf("NumGC = %v\n", m.NumGC)
}

// Prometheus metrics
type Metrics struct {
    Alloc      uint64  // 当前分配
    TotalAlloc uint64  // 累计分配
    Sys        uint64  // 系统分配
    NumGC      uint32  // GC次数
}

内存泄露排查流程 ​

Step 1: 建立基线 ​

bash
# 记录初始内存
curl http://localhost:6060/debug/pprof/heap > baseline.prof

# 查看当前内存
go tool pprof -inuse_space baseline.prof
(pprof) top
     10MB  main.init
      5MB  runtime.malg

Step 2: 压测并监控 ​

bash
# 压测
ab -n 100000 -c 100 http://localhost:8080/api

# 定期采集内存
for i in {1..10}; do
    curl http://localhost:6060/debug/pprof/heap > heap_$i.prof
    sleep 60
done

# 观察内存增长趋势
go tool pprof -base baseline.prof heap_10.prof

Step 3: 定位泄露位置 ​

bash
(pprof) top
     500MB  main.GlobalCache.Set
     200MB  main.NewRequest
     100MB  main.processData

内存泄露案例 ​

案例1: 全局变量泄露 ​

问题代码:

go
// ❌ 全局map无限增长
var requestCache = make(map[string]*Request)

func handler(req *Request) {
    key := generateKey(req)
    requestCache[key] = req  // 永远不删除
}

解决方案:

go
// ✅ 使用LRU缓存
type LRUCache struct {
    mu       sync.Mutex
    items    map[string]*list.Element
    queue    *list.List
    maxSize  int
}

type cacheItem struct {
    key   string
    value *Request
}

func NewLRUCache(maxSize int) *LRUCache {
    return &LRUCache{
        items:   make(map[string]*list.Element),
        queue:   list.New(),
        maxSize: maxSize,
    }
}

func (c *LRUCache) Set(key string, value *Request) {
    c.mu.Lock()
    defer c.mu.Unlock()
    
    if elem, ok := c.items[key]; ok {
        c.queue.MoveToFront(elem)
        elem.Value.(*cacheItem).value = value
        return
    }
    
    if c.queue.Len() >= c.maxSize {
        oldest := c.queue.Back()
        if oldest != nil {
            c.queue.Remove(oldest)
            delete(c.items, oldest.Value.(*cacheItem).key)
        }
    }
    
    elem := c.queue.PushFront(&cacheItem{key, value})
    c.items[key] = elem
}

案例2: Goroutine泄露导致内存泄露 ​

问题代码:

go
// ❌ Goroutine永不退出
func subscribe(channel string) <-chan Message {
    ch := make(chan Message)
    go func() {
        for {
            msg := receiveFromRedis(channel)  // 阻塞
            ch <- msg
        }
    }()
    return ch
}

解决方案:

go
// ✅ 添加退出机制
func subscribe(ctx context.Context, channel string) (<-chan Message, error) {
    ch := make(chan Message, 100)
    
    go func() {
        defer close(ch)
        
        redisCh := redis.Subscribe(channel)
        defer redis.Unsubscribe(redisCh)
        
        for {
            select {
            case msg := <-redisCh:
                select {
                case ch <- msg:
                case <-ctx.Done():
                    return
                }
            case <-ctx.Done():
                return
            }
        }
    }()
    
    return ch, nil
}

// 使用
ctx, cancel := context.WithCancel(context.Background())
defer cancel()  // 确保退出

ch, _ := subscribe(ctx, "mychannel")
for msg := range ch {
    process(msg)
}

案例3: Time.Ticker泄露 ​

问题代码:

go
// ❌ Ticker未停止
func worker() {
    ticker := time.NewTicker(1 * time.Second)
    for {
        select {
        case <-ticker.C:
            doWork()
        case <-stopChan:
            return  // ticker未Stop
        }
    }
}

解决方案:

go
// ✅ 正确停止Ticker
func worker() {
    ticker := time.NewTicker(1 * time.Second)
    defer ticker.Stop()  // 确保停止
    
    for {
        select {
        case <-ticker.C:
            doWork()
        case <-stopChan:
            return
        }
    }
}

GC调优参数 ​

GOGC参数 ​

bash
# 默认值: 100
# 含义: 堆大小增长100%时触发GC

# 降低GC频率(适合内存充足场景)
GOGC=200 ./myapp

# 增加GC频率(适合内存紧张场景)
GOGC=50 ./myapp

# 禁用GC(不推荐)
GOGC=off ./myapp

Go 1.19+ 内存限制 ​

go
import "runtime/debug"

func main() {
    // 设置内存限制
    debug.SetMemoryLimit(1 * 1024 * 1024 * 1024)  // 1GB
    
    // 运行应用...
}

性能对比 ​

场景1: 内存充足,CPU密集
GOGC=100:  GC 10次/秒, CPU 80%
GOGC=200:  GC  5次/秒, CPU 65%  ✅

场景2: 内存紧张,IO密集
GOGC=100:  GC 10次/秒, 内存 2GB
GOGC=50:   GC 20次/秒, 内存 1.5GB  ✅

内存优化技巧 ​

技巧1: 预分配切片 ​

go
// ❌ 频繁扩容
result := []int{}
for i := 0; i < n; i++ {
    result = append(result, i)
}

// ✅ 预分配
result := make([]int, 0, n)
for i := 0; i < n; i++ {
    result = append(result, i)
}

// 内存分配对比:
// 不预分配: 10次扩容,10次内存分配
// 预分配:   1次内存分配

技巧2: 使用值类型 ​

go
// ❌ 使用指针
type User struct {
    Name string
    Age  int
}

func getUsers() []*User {  // 指针切片
    users := make([]*User, 100)
    for i := range users {
        users[i] = &User{Name: "User", Age: i}
    }
    return users
}

// ✅ 使用值类型
func getUsers() []User {  // 值切片
    users := make([]User, 100)
    for i := range users {
        users[i] = User{Name: "User", Age: i}
    }
    return users
}

// 内存对比:
// 指针: 100个User对象 + 100个指针 = 2400字节 + 800字节
// 值:   100个User对象 = 2400字节
// 节省: 33%

技巧3: 避免不必要的转换 ​

go
// ❌ 频繁转换
func process(data []byte) {
    for _, b := range data {
        s := string(b)  // 每次分配
        handle(s)
    }
}

// ✅ 直接处理字节
func process(data []byte) {
    for _, b := range data {
        handleByte(b)  // 无分配
    }
}

// 或使用unsafe(谨慎)
func process(data []byte) {
    s := unsafe.String(&data[0], len(data))
    handle(s)  // 零拷贝
}

内存监控告警 ​

Prometheus监控 ​

yaml
# 告警规则
groups:
  - name: memory
    rules:
      - alert: HighMemoryUsage
        expr: process_resident_memory_bytes > 1GB
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High memory usage"
      
      - alert: MemoryLeak
        expr: |
          rate(process_resident_memory_bytes[5m]) > 10MB/s
        for: 10m
        labels:
          severity: critical

运行时监控 ​

go
func monitorMemory() {
    ticker := time.NewTicker(30 * time.Second)
    defer ticker.Stop()
    
    for range ticker.C {
        var m runtime.MemStats
        runtime.ReadMemStats(&m)
        
        // 内存使用超过阈值
        if m.Alloc > 500*1024*1024 {  // 500MB
            log.Printf("High memory usage: %d MB", m.Alloc/1024/1024)
            
            // 触发GC
            runtime.GC()
        }
    }
}

内存优化检查清单 ​

[ ] 使用pprof分析内存分配
[ ] 定期检查内存泄露
[ ] 使用LRU缓存限制大小
[ ] Goroutine添加退出机制
[ ] Ticker/Timer及时Stop
[ ] 切片预分配容量
[ ] 合理使用值类型
[ ] 避免频繁类型转换
[ ] 使用sync.Pool复用对象
[ ] 配置合理的GC参数
[ ] 建立内存监控告警

学习检查点 ​

完成本章节后,验证:

  • [ ] 使用pprof排查过内存泄露
  • [ ] 修复过全局变量泄露问题
  • [ ] 解决过Goroutine泄露
  • [ ] 配置过GC参数
  • [ ] 使用过LRU缓存
  • [ ] 实现过对象复用
  • [ ] 建立过内存监控

延伸阅读 ​