草庐IT

线上内存泄漏排查思路

Best博客 2023-03-28 原文

内存泄漏排查

背景了解:告知 线上 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 查出进程启动信息

有关线上内存泄漏排查思路的更多相关文章

  1. ruby-on-rails - Ruby net/ldap 模块中的内存泄漏 - 2

    作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代

  2. ruby-on-rails - Ruby 中的内存模型 - 2

    ruby如何管理内存。例如:如果我们在执行过程中采用C程序,则以下是内存模型。类似于这个ruby如何处理内存。C:__________________|||stack|||------------------||||------------------|||||Heap|||||__________________|||data|__________________|text|__________________Ruby:? 最佳答案 Ruby中没有“内存”这样的东西。Class#allocate分配一个对象并返回该对象。这就是程序

  3. 键删除后 ruby​​ 哈希内存泄漏 - 2

    你好,我无法成功如何在散列中删除key后释放内存。当我从哈希中删除键时,内存不会释放,也不会在手动调用GC.start后释放。当从Hash中删除键并且这些对象在某处泄漏时,这是预期的行为还是GC不释放内存?如何在Ruby中删除Hash中的键并在内存中取消分配它?例子:irb(main):001:0>`ps-orss=-p#{Process.pid}`.to_i=>4748irb(main):002:0>a={}=>{}irb(main):003:0>1000000.times{|i|a[i]="test#{i}"}=>1000000irb(main):004:0>`ps-orss=-p

  4. ruby-on-rails - HTTParty 的内存问题和下载大文件 - 2

    这会导致Ruby出现内存问题吗?我知道如果大小超过10KB,Open-URI会写入TempFile。但是HTTParty会在写入TempFile之前尝试将整个PDF保存到内存吗?src=Tempfile.new("file.pdf")src.binmodesrc.writeHTTParty.get("large_file.pdf").parsed_response 最佳答案 您可以使用Net::HTTP。参见thedocumentation(特别是标题为“流媒体响应机构”的部分)。这是文档中的示例:uri=URI('http://e

  5. 电脑启动后显示器黑屏怎么办?排查下面4个问题,快速解决 - 2

    电脑启动出现显示器黑屏是一个相当常见的问题。如果您遇到了这个问题,不要惊慌,因为它有很多可能的原因,可以采取一些简单的措施来解决它。在本文中,小编将介绍下面4种常见的电脑启动后显示器黑屏的原因,排查这些原因,快速解决! 演示机型:联想Ideapad700-15ISK-ISE系统版本:Windows10一、显示器问题如果出现电脑启动后显示器黑屏的情况。那么首先您需要检查一下显示器是否正常工作。您可以通过更换另一个显示器或将当前显示器连接到另一台计算机来检查显示器是否存在问题。如果问题仍然存在,那么您可以排除显示器故障的可能性。 二、显卡问题如果您的电脑配备了独立显卡,那么显卡故障也可能是导致电脑

  6. ruby-on-rails - 内存中具有相同 ID 的更多对象? - 2

    在部署在heroku上的Rails应用程序(v:3.1)中,我在内存中获得了更多具有相同ID的对象。我的heroku控制台日志:>>Project.find_all_by_id(92).size=>2>>ActiveRecord::Base.connection.execute('select*fromprojectswhereid=92').to_a.size=>1这怎么可能?可能是什么问题? 最佳答案 解决方案根据您的SQL查询,您的数据库中显然没有重复条目。也许您的类项目中的size或length方法已被覆盖。我试过find_

  7. ruby - rails 3.0.7 内存泄漏 - 2

    我的两个不同的Rails应用程序的内存有一些奇怪的问题。这两个应用程序都使用rails3.0.7。每个Controller请求分配20-30-50MB的内存。在生产模式下,这个数量减少到5-10。但这是同样的事情。这是两个应用程序使用的gem列表:gem'pg'gem'haml'gem'sass'gem'devise'gem'simple_form'gem'state_machine'gem"globalize3","0.1.0.beta"gem"easy_globalize3_accessors"gem'paperclip'gem'andand'关闭所有这些gem不会给我任何结果。我

  8. ruby - 如何强制 Ruby 释放内存给操作系统 - 2

    正如标题,我有一个处理大量数据的ruby​​程序。该程序占用了所有内存,其中调用了系统命令hostname,并且发生错误无法分配内存-主机名我试过GC.start但它不起作用。那么如何强制ruby释放未使用的内存呢?OK,这是别人的测试代码,最后报错是big_var被回收了。但是内存仍然没有释放。require"weakref"defreportputs"#{param}:\t\tMemory"+`psax-opid,rss|grep-E"^[[:space:]]*#{$$}"`.strip.split.map(&:to_i)[1].to_s+'KB'endbig_var=""#big

  9. ruby - 如何在 Ruby 中从内存中 HTTP 发布流数据? - 2

    我想上传我在运行时用Ruby生成的数据,就像从block中提供上传数据一样。我找到的所有示例仅展示了如何流式传输必须在请求之前位于磁盘上的文件,但我不想缓冲该文件。除了滚动我自己的套接字连接之外,最好的解决方案是什么?这是一个伪代码示例:post_stream('127.0.0.1','/stream/')do|body|generate_xmldo|segment|body 最佳答案 有效的代码。require'thread'require'net/http'require'base64'require'openssl'class

  10. 基于SpringBoot的线上日志阅读器 - 2

    软件特点部署后能通过浏览器查看线上日志。支持Linux、Windows服务器。采用随机读取的方式,支持大文件的读取。支持实时打印新增的日志(类终端)。支持日志搜索。使用手册基本页面配置路径配置日志所在的目录,配置后按回车键生效,下拉框选择日志名称。选择日志后点击生效,即可加载日志。windows路径E:\java\project\log-view\logslinux路径/usr/local/XX历史模式历史模式下,不会读取新增的日志。针对历史文件可以分页读取,配置分页大小、跳转。历史模式下,支持根据关键词搜索。目前搜索引擎使用的是jdk自带类库,搜索速度相对较低,优点是比较简单。2G日志全文搜

随机推荐