全局map内存泄漏问题的排查和解决

  1. pods表现为内存持续增长
    一次典型内存泄漏的排查

  2. 使用pprof定位到具体函数
    一次典型内存泄漏的排查

  3. 找到对应代码

    type EBus struct {
     eventChan     chan *Event // 内部事件
     eventHandlers map[int]func(*Event)
     subscribers   map[string]map[Subscriber]struct{} // 订阅者
     running       bool                               // 记录服务是否正在运行
    }
    func (p *EBus) pub(e *Event) {
     ...
    }
    // 订阅消息
    func (p *EBus) sub(e *Event) {
     if _, ok := p.subscribers[e.name]; !ok {
         p.subscribers[e.name] = make(map[Subscriber]struct{})
         p.subscribers[e.name][e.subscriber] = struct{}{}
         return
     }
     if _, ok := p.subscribers[e.name][e.subscriber]; ok {
         return
     }
     p.subscribers[e.name][e.subscriber] = struct{}{}
    }
    // 取消指定消息订阅
    func (p *EBus) unSub(e *Event) {
     ...
     delete(p.subscribers[e.name], e.subscriber)
    }
    // 取消所有消息订阅
    func (p *EBus) unSubAll(e *Event) {
     ...
     delete(p.subscribers, e.name)
    }
  4. 分析问题
    根据pprof能看出内存泄漏在sub函数
    一次典型内存泄漏的排查
    sub里面会分配内存的就只有p.subscribers
    subscribers是一个map
    可以看到unSub、unSubAll都是删除map对应key的方法
    首先怀疑是不是每正常调用这两个函数取消订阅,但是读完业务代码后发现是正常unSubAll了的
    最后想起来,map,delete key后是会释放value的空间,但是map自身的空间是不会被释放的
    这是常见的全局map出现内存泄漏的问题

  5. 验证问题

    func printMemUsage() {
     var m runtime.MemStats
     runtime.ReadMemStats(&m)
     fmt.Println("Memory Allocation:", m.Alloc/1024/1024, "MB")
    }
    var myMap = make(map[int]int)
    func TestMap(t *testing.T) {
     for i := 0; i < 1000000; i++ {
         myMap[i] = i
     }
     for i := 0; i < 1000000; i++ {
         delete(myMap, i)
     }
     // 触发垃圾收集并获取内存统计信息前的内存分配量
     printMemUsage()
     // 强制进行垃圾收集并读取内存统计信息
     debug.FreeOSMemory()
     runtime.GC()
     // 打印对象创建后的内存分配量
     printMemUsage()
    }

    一次典型内存泄漏的排查
    修改代码:map放到函数内,即从全局map改为函数内map

    func TestMap(t *testing.T) {
     // 创建一个对象
     var myMap = make(map[int]int)
     for i := 0; i < 1000000; i++ {
         myMap[i] = i
     }
     for i := 0; i < 1000000; i++ {
         delete(myMap, i)
     }
    ... ...

    一次典型内存泄漏的排查
    可以看到修改后的内存明显下降了

  6. 怎么修复
    全局map的内存泄漏是典型的问题
    修复这个问题只能想办法让这个map被回收
    我这里是加了个定时器,每分钟重置下map
    全局map内存泄漏问题的排查和解决

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 12

感觉用sync.Pool来解决更好。我看你使用的是重建策略。

1个月前 评论
ezreal_rao (作者) 1个月前
symphony09 1个月前
Iwanna (楼主) 1个月前

经实测 把map定义放到函数内并没减少内存:

import (
"fmt" 
"runtime"
)
func printMemUsage() {
 var m runtime.MemStats
 runtime.ReadMemStats(&m)
 fmt.Println("Memory Allocation:", m.Alloc/1024/1024, "MB")
}

func TestMap() {
 var myMap = make(map[int]int)
 for i := 0; i < 1000000; i++ {
     myMap[i] = i
 }
 for i := 0; i < 1000000; i++ {
     delete(myMap, i)
 }
 // 触发垃圾收集并获取内存统计信息前的内存分配量
 printMemUsage()
 runtime.GC()
 // 打印对象创建后的内存分配量
 printMemUsage()
}

以下是测试结果:

gomacro> TestMap()
Memory Allocation: 73 MB
Memory Allocation: 41 MB
1个月前 评论
bangnuo 1个月前
Iwanna (楼主) 1个月前
zzzkkk (作者) 1个月前
zzzkkk (作者) 1个月前
Iwanna (楼主) 4周前

虽然 map delete 后内存不会被释放,但是在后续继续写map时,这些内存是会被复用的。全局 map 泄露只能解释内存居高不下,但是不能解释持续增长。

1个月前 评论
Iwanna (楼主) 1个月前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!