线上内存泄漏排查思路

内存泄漏排查

背景了解:告知 线上 room_work 运行一段时间内存就会慢慢往上涨,8G内存吃掉了4G。。。

思路

  1. 大概捋一下项目中有通过常驻内存操作实现业务逻辑的代码
1. room_work 这是个根据rid进行转发到不同node的,深度使用内存存储rid的各种业务数据
2. 道具方面,干冰跟骰子移入了room_work后,也是使用自身定义的内存对象承载的业务
......

cpu火焰图看看

直接本地环境全部启动之后,开始使用三个手机进入dj房间,进行所有功能疯狂乱点,生成cup火焰图,但讲真看不出来啥,才发现应该才内存才对


企业微信截图_6e098d68-30f7-48cb-8728-5ba3603caa2f.png

内存调用瞅瞅

企业微信截图_8988d82a-6734-4e77-b0db-6f2ca81b9610.png

上代码(go2cache)


//1. new一个go2cache的 server,这个对象负责将数据库的数据建立2层缓存(redis,memory),批量查内存优先,单个查忽略内存只取  redis
func NewServer(redis string, codec Codec) *Server {
    memory := newMemoryCache(codec)  //内存子对象
    redisCacheServer := newRedisCache(redis, memory, codec)
    db := newDbCache(redisCacheServer, codec)
    return &Server{
        redis: redisCacheServer,
        db:    db,
        codec: codec,
    }
}

//2. 着重看内存子对象
func newMemoryCache(codec Codec) *memoryCache {
    config := bigcache.DefaultConfig(time.Second * 60)
    config.Shards = 1024     //1024个分片,bigcache这个组件,没了节约内存,将我们的数据是按byte存入全局的[]byte里面去的
    config.MaxEntrySize = 1024 * 16   
    config.HardMaxCacheSize = 500 
    cache, _ := bigcache.NewBigCache(config)
    return &memoryCache{
        cache: cache,
        codec: codec,
    }
}

func NewBigCache(config Config) (*BigCache, error) {
    return newBigCache(config, &systemClock{})
}


func newBigCache(config Config, clock clock) (*BigCache, error) {
.......
    for i := 0; i < config.Shards; i++ {
        cache.shards[i] = initNewShard(config, onRemove, clock)   //重点来了~
    }
    .......
}

func initNewShard(config Config, callback onRemoveCallback, clock clock) *cacheShard {
    bytesQueueInitialCapacity := config.initialShardSize() * config.MaxEntrySize
    maximumShardSizeInBytes := config.maximumShardSizeInBytes()
    if maximumShardSizeInBytes > 0 && bytesQueueInitialCapacity > maximumShardSizeInBytes {
        bytesQueueInitialCapacity = maximumShardSizeInBytes
    }
    return &cacheShard{
        hashmap:      make(map[uint64]uint32, config.initialShardSize()),
        hashmapStats: make(map[uint64]uint32, config.initialShardSize()),
        entries:      *queue.NewBytesQueue(bytesQueueInitialCapacity, maximumShardSizeInBytes, config.Verbose),   //重点
        entryBuffer:  make([]byte, config.MaxEntrySize+headersSizeInBytes),
        onRemove:     callback,

        isVerbose:    config.Verbose,
        logger:       newLogger(config.Logger),
        clock:        clock,
        lifeWindow:   uint64(config.LifeWindow.Seconds()),
        statsEnabled: config.StatsEnabled,
    }
}

// NewBytesQueue initialize new bytes queue.
// capacity is used in bytes array allocation
// When verbose flag is set then information about memory allocation are printed
func NewBytesQueue(capacity int, maxCapacity int, verbose bool) *BytesQueue {
    return &BytesQueue{
        array:        make([]byte, capacity),
        capacity:     capacity,
        maxCapacity:  maxCapacity,
        headerBuffer: make([]byte, binary.MaxVarintLen32),
        tail:         leftMarginIndex,
        head:         leftMarginIndex,
        rightMargin:  leftMarginIndex,
        verbose:      verbose,
    }
}

最终内存好用结果打印

企业微信截图_16082c41-06dd-4bbf-afa7-1f2bc003238c.png

找我们都是怎么使用的

经过分析发现 /Flock-Server/rpc/server/internal/room/worker/base/base.go@Init 方法会在每次rid new一个work的时候被初始化一次, 了解下房间业务,发现房间是一个街区一个房间的,所以。。。

企业微信截图_08818812-8644-46e8-ab4a-93ccd84b3747.png

解决方式

方式一

//newMemoryCache 使用较小的内存做缓冲
func newMemoryCache(codec Codec) *memoryCache {
    config := bigcache.DefaultConfig(time.Second * 60)
    //config.Shards = 1024  
       config.Shards = 10// 改成系统默认的10个分片就行了,或者咱们的业务就别用内存了,直接对接redis, bigcache这个组件默认的1024估计是出于全局只要new一个考虑,而我们吧bigcache当子组件使用的时候,忘记这一茬了。。。
    config.MaxEntrySize = 1024 * 16 //16KB
    config.HardMaxCacheSize = 500
    cache, _ := bigcache.NewBigCache(config)
    return &memoryCache{
        cache: cache,
        codec: codec,
    }
}

然后把
func (r *RoomWorkerBase) Init() {
    r.Ctx = context.TODO()
    r.ServerCache = go2cache.NewServer(consts.RedisRoom, cache.RoomCodec{})  //想办法把这个对象编程单例
    r.I18n = i18n.NewI18n()  
    r.I18n.SetLanguage("en")

    r.msgCh = make(chan interface{}, 500)
    r.stopCh = make(chan interface{})
    r.SyncHdl = make(map[reflect.Type]SyncHandler)
    r.ASyncHdl = make(map[reflect.Type]AsyncHandler)
    r.SyncPbHdl = make(map[protoreflect.Descriptor]SyncProtoHandler)
    r.ASyncPbHdl = make(map[protoreflect.Descriptor]AsyncProtoHandler)
    r.CmdHdl = make(map[string]CmdHandler)
    r.timers = make(map[string]*time.Timer)

    r.CommonCache = cache.NewRoomCommon()

    r.RegisterHandlers()
}

方式二

直接把内存的二级缓存拿掉,让go2cache直接对接redis, 这种方式代码业务方无感知,只需要该go2cache内部

来个demo重现看看

func main() {

    var m runtime.MemStats

    asas := map[string]interface{}{}
    for i := 0; i < 20; i++ {
        asas[fmt.Sprintf("%d", i)] = go2cache.NewServer(consts.RedisRoom, RoomCodec{})

        fmt.Println(fmt.Sprintf("第 %d: 次循环\n", i))
        runtime.ReadMemStats(&m)
        fmt.Printf("%d M\n", m.Alloc/1024/1024)

        time.Sleep(time.Second * 7)

    }

}
企业微信截图_876f20c1-9d82-4624-993b-d1c8e09a03c9.png

对其他全局对象使用的一些思考

golang里面的map充当全局对象使用的时候, 要时刻提醒自己,这个map在被delete的时候,内存不会被gc的,只会被打tag,需要定时迁移新的map,
才能是老的map里面被tag的内存对象被回收。。。


企业微信截图_f7e090be-4947-4ef7-a02c-c2f93c6bbade.png
企业微信截图_02967465-cf10-4728-92eb-1a67863cbf15.png

总结

  1. go tool pprof --alloc_space http://127.0.0.1:6064/debug/pprof/heap 对所有内存对象的监控打印,其中包括被GC的
  2. go tool pprof http://127.0.0.1:6064/debug/pprof/heap 等价与 go tool pprof --inuse_space http://127.0.0.1:6064/debug/pprof/heap 对活跃内存对象打印,不包活会被GC掉的对象
  3. top -pid 1123 //对具体的pid进行top命令监控
  4. ps -ef | grep worker 等价于 ps -aux | grep nginx 根据进程名称查看进程的pid及启动信息
  5. 通过端口查出pid,进而查出进程的启动命令等信息
lsof -i :9527   //查看端口的pid
ps -ef | grep 12321  //根据pid 查出进程启动信息

你可能感兴趣的:(线上内存泄漏排查思路)