线上内存泄漏排查思路

内存泄漏排查

背景了解:告知 线上 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 查出进程启动信息
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352

推荐阅读更多精彩内容