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.malgStep 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.profStep 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 ./myappGo 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缓存
- [ ] 实现过对象复用
- [ ] 建立过内存监控