Skip to content

Latest commit

 

History

History
814 lines (626 loc) · 15.9 KB

File metadata and controls

814 lines (626 loc) · 15.9 KB

11.4 内存优化

📍 导航返回目录 | 上一节:IO优化 | 下一节:网络优化


内存问题识别

如何判断内存问题?

# 查看内存使用
free -h
top
htop

# 查看进程内存详情
pmap -x <pid>
cat /proc/<pid>/status | grep -i vm

# Go 程序内存分析
go tool pprof http://localhost:6060/debug/pprof/heap

内存问题特征

  • 内存使用持续增长
  • 频繁 GC(Go)或 OOM(Out of Memory)
  • Swap 使用率高
  • RSS(常驻内存)过大

内存泄漏排查

1. Go 内存泄漏

常见原因

原因 1:Goroutine 泄漏

// ❌ Goroutine 泄漏
func badHandler(ctx context.Context) {
    ch := make(chan int)
    
    go func() {
        for {
            select {
            case <-ch:
                // 处理
            // 缺少 ctx.Done() 退出机制
            }
        }
    }()  // Goroutine 永远不会退出
}

// ✅ 正确:带超时控制
func goodHandler(ctx context.Context) {
    ch := make(chan int)
    
    go func() {
        for {
            select {
            case <-ch:
                // 处理
            case <-ctx.Done():
                return  // 正确退出
            }
        }
    }()
}

原因 2:未关闭的资源

// ❌ 未关闭文件
func badReadFile(filename string) ([]byte, error) {
    file, _ := os.Open(filename)
    // 缺少 defer file.Close()
    return io.ReadAll(file)
}

// ✅ 正确:关闭资源
func goodReadFile(filename string) ([]byte, error) {
    file, err := os.Open(filename)
    if err != nil {
        return nil, err
    }
    defer file.Close()  // 确保关闭
    return io.ReadAll(file)
}

原因 3:全局变量缓存无限增长

// ❌ 无限增长的缓存
var globalCache = make(map[string]interface{})

func badCache(key string, value interface{}) {
    globalCache[key] = value  // 永不清理
}

// ✅ 使用 LRU 限制大小
import "github.com/hashicorp/golang-lru"

var lruCache, _ = lru.New(1000)  // 最多 1000 个元素

func goodCache(key string, value interface{}) {
    lruCache.Add(key, value)  // 自动淘汰旧数据
}

内存泄漏检测

import (
    "net/http"
    _ "net/http/pprof"
    "time"
)

func main() {
    // 启用 pprof
    go http.ListenAndServe("localhost:6060", nil)
    
    // 定期打印内存统计
    go func() {
        ticker := time.NewTicker(10 * time.Second)
        for range ticker.C {
            var m runtime.MemStats
            runtime.ReadMemStats(&m)
            log.Printf("Alloc=%v MB, TotalAlloc=%v MB, Sys=%v MB, NumGC=%v",
                m.Alloc/1024/1024,
                m.TotalAlloc/1024/1024,
                m.Sys/1024/1024,
                m.NumGC)
        }
    }()
    
    // 业务代码...
}

使用 pprof 分析

# 采集当前堆内存
go tool pprof http://localhost:6060/debug/pprof/heap

# 对比两次内存快照(找泄漏)
curl http://localhost:6060/debug/pprof/heap > heap1.prof
# 运行一段时间后
curl http://localhost:6060/debug/pprof/heap > heap2.prof

# 对比分析
go tool pprof -base heap1.prof heap2.prof

# 查看增长最多的函数
(pprof) top
(pprof) list <function_name>

2. C/C++ 内存泄漏

Valgrind 检测

# 编译时添加调试信息
gcc -g -o myapp myapp.c

# 使用 Valgrind 检测
valgrind --leak-check=full --show-leak-kinds=all ./myapp

# 输出示例:
# ==12345== LEAK SUMMARY:
# ==12345==    definitely lost: 4,096 bytes in 1 blocks
# ==12345==    indirectly lost: 0 bytes in 0 blocks

AddressSanitizer(ASan)

# 编译时启用 ASan
gcc -fsanitize=address -g -o myapp myapp.c

# 运行程序,自动检测内存错误
./myapp

内存分配优化

1. 对象池复用

import "sync"

// ✅ 对象池减少内存分配
var bufferPool = sync.Pool{
    New: func() interface{} {
        return make([]byte, 4096)
    },
}

func processRequest(data []byte) {
    // 从池中获取
    buf := bufferPool.Get().([]byte)
    defer bufferPool.Put(buf)  // 归还池中
    
    // 使用 buf 处理数据
    copy(buf, data)
    // ...
}

性能对比

  • 每次分配:100000 次/秒,1GB 内存分配
  • 对象池复用:1000000 次/秒,10MB 内存分配
  • 性能提升:10x,内存减少:100x

2. 预分配容量

// ❌ 动态扩容(多次分配)
func badAppend(n int) []int {
    var result []int
    for i := 0; i < n; i++ {
        result = append(result, i)  // 多次扩容
    }
    return result
}

// ✅ 预分配容量(一次分配)
func goodAppend(n int) []int {
    result := make([]int, 0, n)  // 预分配
    for i := 0; i < n; i++ {
        result = append(result, i)  // 无需扩容
    }
    return result
}

性能对比(n=10000):

  • 动态扩容:15 次扩容,500μs
  • 预分配:0 次扩容,100μs
  • 性能提升:5x

3. 栈分配 vs 堆分配

// ❌ 逃逸到堆(需要 GC)
func badAlloc() *int {
    x := 42
    return &x  // 逃逸分析:x 逃逸到堆
}

// ✅ 栈分配(无需 GC)
func goodAlloc() int {
    x := 42
    return x  // x 在栈上
}

// 查看逃逸分析
// go build -gcflags="-m" main.go

逃逸分析优化技巧

// ✅ 避免返回指针
func process() int {
    // 值返回,栈分配
    return calculate()
}

// ✅ 使用值接收者
type Counter struct {
    count int
}

func (c Counter) Inc() Counter {  // 值接收者
    c.count++
    return c
}

// ✅ 避免接口类型(可能逃逸)
func processValue(v int) {  // 具体类型
    // ...
}

4. 字符串优化

import (
    "strings"
    "unsafe"
)

// ❌ 大量字符串拼接(多次分配)
func concatSlow(strs []string) string {
    result := ""
    for _, s := range strs {
        result += s  // 每次创建新字符串
    }
    return result
}

// ✅ strings.Builder(一次分配)
func concatFast(strs []string) string {
    var builder strings.Builder
    totalLen := 0
    for _, s := range strs {
        totalLen += len(s)
    }
    builder.Grow(totalLen)  // 预分配
    
    for _, s := range strs {
        builder.WriteString(s)
    }
    return builder.String()
}

// ✅ 零拷贝转换(不安全,谨慎使用)
func stringToBytes(s string) []byte {
    return *(*[]byte)(unsafe.Pointer(&s))
}

func bytesToString(b []byte) string {
    return *(*string)(unsafe.Pointer(&b))
}

内存布局优化

1. 结构体字段对齐

// ❌ 内存浪费(24 字节)
type BadStruct struct {
    a bool   // 1 字节 + 7 字节 padding
    b int64  // 8 字节
    c bool   // 1 字节 + 7 字节 padding
}

// ✅ 优化对齐(16 字节)
type GoodStruct struct {
    b int64  // 8 字节
    a bool   // 1 字节
    c bool   // 1 字节 + 6 字节 padding
}

// 查看结构体大小
// unsafe.Sizeof(BadStruct{})  // 24
// unsafe.Sizeof(GoodStruct{}) // 16

自动检测工具

# 安装 fieldalignment
go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest

# 检测并修复
fieldalignment -fix ./...

2. 内存池实现

import "sync"

// ✅ 固定大小内存池
type MemoryPool struct {
    pools map[int]*sync.Pool
}

func NewMemoryPool() *MemoryPool {
    pools := make(map[int]*sync.Pool)
    
    // 预定义常用大小:1KB, 4KB, 64KB, 1MB
    sizes := []int{1024, 4096, 65536, 1048576}
    for _, size := range sizes {
        s := size  // 避免闭包问题
        pools[s] = &sync.Pool{
            New: func() interface{} {
                return make([]byte, s)
            },
        }
    }
    
    return &MemoryPool{pools: pools}
}

func (mp *MemoryPool) Get(size int) []byte {
    // 找到最接近的大小
    poolSize := mp.findPoolSize(size)
    if pool, ok := mp.pools[poolSize]; ok {
        return pool.Get().([]byte)[:size]
    }
    return make([]byte, size)  // 回退到直接分配
}

func (mp *MemoryPool) Put(buf []byte) {
    poolSize := cap(buf)
    if pool, ok := mp.pools[poolSize]; ok {
        pool.Put(buf[:cap(buf)])
    }
}

func (mp *MemoryPool) findPoolSize(size int) int {
    for _, s := range []int{1024, 4096, 65536, 1048576} {
        if size <= s {
            return s
        }
    }
    return size
}

GC 优化(Go)

1. GC 调优

import "runtime/debug"

// ✅ 调整 GC 百分比
func tuneGC() {
    // 默认 GOGC=100(堆增长 100% 触发 GC)
    // 设置 GOGC=200(减少 GC 频率,增加内存使用)
    debug.SetGCPercent(200)
    
    // 或使用环境变量
    // export GOGC=200
}

// ✅ 手动触发 GC
func manualGC() {
    runtime.GC()  // 适用于低峰期清理
}

// ✅ 关闭 GC(特殊场景)
func disableGC() {
    debug.SetGCPercent(-1)  // 禁用自动 GC
    
    // 在合适时机手动 GC
    defer runtime.GC()
}

2. 减少 GC 压力

// ✅ 复用对象减少 GC
var requestPool = sync.Pool{
    New: func() interface{} {
        return &Request{}
    },
}

func handleRequest() {
    req := requestPool.Get().(*Request)
    defer requestPool.Put(req)
    
    // 重置对象状态
    req.Reset()
    
    // 处理请求
    // ...
}

type Request struct {
    data []byte
}

func (r *Request) Reset() {
    r.data = r.data[:0]  // 清空但保留容量
}

3. 监控 GC 指标

import (
    "runtime"
    "time"
)

// ✅ 监控 GC 性能
func monitorGC() {
    ticker := time.NewTicker(10 * time.Second)
    defer ticker.Stop()
    
    var lastNumGC uint32
    var lastPauseTotal time.Duration
    
    for range ticker.C {
        var m runtime.MemStats
        runtime.ReadMemStats(&m)
        
        numGC := m.NumGC - lastNumGC
        pauseTotal := time.Duration(m.PauseTotalNs) - lastPauseTotal
        
        if numGC > 0 {
            avgPause := pauseTotal / time.Duration(numGC)
            log.Printf("GC: count=%d, avgPause=%v, heapAlloc=%v MB",
                numGC, avgPause, m.Alloc/1024/1024)
        }
        
        lastNumGC = m.NumGC
        lastPauseTotal = time.Duration(m.PauseTotalNs)
    }
}

大页内存(Huge Pages)

1. Linux Huge Pages

# 查看大页配置
cat /proc/meminfo | grep Huge

# 配置大页(2MB)
echo 512 > /proc/sys/vm/nr_hugepages  # 512 * 2MB = 1GB

# 永久配置
echo "vm.nr_hugepages=512" >> /etc/sysctl.conf
sysctl -p

2. Transparent Huge Pages(THP)

# 查看 THP 状态
cat /sys/kernel/mm/transparent_hugepage/enabled

# 启用 THP
echo always > /sys/kernel/mm/transparent_hugepage/enabled

# 禁用 THP(某些数据库推荐)
echo never > /sys/kernel/mm/transparent_hugepage/enabled

Go 程序使用大页

import (
    "syscall"
    "unsafe"
)

// ✅ 使用大页分配内存(Linux)
func allocHugePage(size int) ([]byte, error) {
    const MAP_HUGETLB = 0x40000
    
    data, err := syscall.Mmap(
        -1,
        0,
        size,
        syscall.PROT_READ|syscall.PROT_WRITE,
        syscall.MAP_PRIVATE|syscall.MAP_ANONYMOUS|MAP_HUGETLB,
    )
    return data, err
}

性能对比

  • 普通页(4KB):TLB miss 率 5%
  • 大页(2MB):TLB miss 率 0.01%
  • 性能提升:10-30%(内存密集型应用)

内存压缩

1. 数据压缩

import (
    "bytes"
    "compress/gzip"
)

// ✅ Gzip 压缩(节省内存)
func compressData(data []byte) ([]byte, error) {
    var buf bytes.Buffer
    writer := gzip.NewWriter(&buf)
    
    _, err := writer.Write(data)
    if err != nil {
        return nil, err
    }
    writer.Close()
    
    return buf.Bytes(), nil
}

// ✅ Gzip 解压
func decompressData(data []byte) ([]byte, error) {
    reader, err := gzip.NewReader(bytes.NewReader(data))
    if err != nil {
        return nil, err
    }
    defer reader.Close()
    
    var buf bytes.Buffer
    _, err = buf.ReadFrom(reader)
    return buf.Bytes(), err
}

压缩比对比

  • JSON 数据:压缩比 5:1
  • 日志数据:压缩比 10:1
  • 二进制数据:压缩比 2:1

2. 列式存储

// ❌ 行式存储(内存浪费)
type RowStore struct {
    records []struct {
        ID   int64
        Name string
        Age  int
    }
}

// ✅ 列式存储(内存高效)
type ColumnStore struct {
    IDs   []int64
    Names []string
    Ages  []int
}

// 查询单列时,列式存储只加载需要的列
func (cs *ColumnStore) GetIDs() []int64 {
    return cs.IDs  // 无需加载 Names 和 Ages
}

内存优化实战案例

案例 1:缓存系统内存优化

场景:缓存系统内存从 8GB 降低到 2GB

优化步骤

1. 问题分析

# pprof 分析发现大量字符串重复
go tool pprof -inuse_space http://localhost:6060/debug/pprof/heap

2. 优化方案

// ✅ 字符串去重(String Interning)
type StringInterner struct {
    mu    sync.RWMutex
    pool  map[string]string
}

func NewStringInterner() *StringInterner {
    return &StringInterner{
        pool: make(map[string]string),
    }
}

func (si *StringInterner) Intern(s string) string {
    si.mu.RLock()
    if interned, ok := si.pool[s]; ok {
        si.mu.RUnlock()
        return interned
    }
    si.mu.RUnlock()
    
    si.mu.Lock()
    defer si.mu.Unlock()
    
    // Double-check
    if interned, ok := si.pool[s]; ok {
        return interned
    }
    
    si.pool[s] = s
    return s
}

// 使用示例
var interner = NewStringInterner()

type CacheItem struct {
    Key   string
    Value string
}

func addToCache(key, value string) {
    item := CacheItem{
        Key:   interner.Intern(key),   // 去重
        Value: interner.Intern(value), // 去重
    }
    cache.Add(item)
}

3. 效果验证

性能对比

  • 优化前:100万条目,8GB 内存(大量重复字符串)
  • 优化后:100万条目,2GB 内存(字符串去重)
  • 内存减少:75%

案例 2:流式处理内存优化

场景:处理大文件时内存占用从 10GB 降低到 100MB

import (
    "bufio"
    "os"
)

// ❌ 一次性加载全部(内存爆炸)
func processBad(filename string) error {
    data, _ := os.ReadFile(filename)  // 10GB 文件全部加载
    
    for _, line := range strings.Split(string(data), "\n") {
        processLine(line)
    }
    return nil
}

// ✅ 流式处理(固定内存)
func processGood(filename string) error {
    file, _ := os.Open(filename)
    defer file.Close()
    
    scanner := bufio.NewScanner(file)
    scanner.Buffer(make([]byte, 64*1024), 1024*1024) // 1MB 缓冲
    
    for scanner.Scan() {
        processLine(scanner.Text())  // 逐行处理
    }
    return scanner.Err()
}

内存优化检查清单

内存泄漏

  • 是否关闭了所有资源?(文件、连接、定时器)
  • Goroutine 是否正确退出?
  • 全局缓存是否有大小限制?
  • 是否使用了 pprof 分析内存?

内存分配

  • 是否使用了对象池?(sync.Pool)
  • 是否预分配了容量?(make)
  • 是否避免了不必要的堆分配?(逃逸分析)
  • 是否优化了字符串拼接?

GC 优化

  • 是否调整了 GOGC 参数?
  • 是否复用了对象?
  • 是否监控了 GC 指标?

高级优化

  • 是否使用了大页内存?
  • 是否使用了数据压缩?
  • 是否优化了数据结构布局?

本章小结

核心要点

  1. 避免泄漏:关闭资源、正确退出 Goroutine、限制缓存大小
  2. 复用对象:对象池、预分配容量减少分配
  3. 栈优先:避免逃逸到堆,减少 GC 压力
  4. GC 调优:调整 GOGC、监控 GC 指标
  5. 工具分析:pprof、valgrind 定位问题

优化优先级

修复泄漏 > 对象复用 > 减少分配 > GC调优 > 高级优化

⏮️ 上一节:IO优化 | ⏭️ 下一节:网络优化