美文网首页
一次内存泄露问题排查

一次内存泄露问题排查

作者: aiwen2017 | 来源:发表于2021-09-28 17:02 被阅读0次

    背景提要

    深度召回服务在浏览器个性化推荐召回阶段占着重要作用。现在基线上有n路深度召回;

    深度召回服务流程主要分为两大阶段:用户特征过模型得到用户向量;用户向量调用annoy获取相似资讯;

    我们发现当前的深度召回服务存在内存泄漏问题,服务进程占用的内存总是缓慢增长,直至触发阈值告警。

    image1.png

    问题定位

    深度召回服务内部需要加载模型和加载annoy向量文件,初步怀疑内存泄漏与这两个部分有关系。

    仔细观察增长图像,发现内存增长的速率和请求量成正比,白天涨的快,晚上涨的慢;所以内存泄漏点与在线处理流程关系更加密切,与模型加载替换部分没有关系。

    在线处理流程中,涉及到的内存有Java堆内存和堆外内存。其中堆内存大小我们通过JVM启动参数已经控制,而泄漏的内存大小,远远大于我们设置的堆内存大小。我们通过调整堆内存大小,观察堆内存使用情况和GC回收情况,确定不是堆内存泄漏导致的;


    image2.png

    深度召回堆外内存的使用主要有两个方面,一个是通过推理SDK调用模型推理,计算用户向量;另外一个是通过调用Native方法封装Annoy接口,查询相似文章。这两个方面都有可能存在内存泄漏。
    排查堆外内存泄漏,这里我们使用jdk8提供的一些命令辅助:

    1. JVM启动参数,设置对堆外内存的追踪:“ -XX:NativeMemoryTracking=detail ”
    2. 服务启动后,设置baseline:
       jcmd 228156 VM.native_memory baseline
    

    经过一段时间后,对比不同块内存的增长情况,找出问题所在;(命令:jcmd 228156 VM.native_memory detail.diff scale=mb)

     $ jcmd 228156 VM.native_memory detail.diff scale=mb
     228156:
     
     Native Memory Tracking:
     
     Total: reserved=17563MB +3831MB, committed=16346MB +3843MB
     
     -                 Java Heap (reserved=10240MB, committed=10240MB)
                                 (mmap: reserved=10240MB, committed=10240MB)
      
     -                     Class (reserved=1112MB +2MB, committed=100MB +3MB)
                                 (classes #16112 +69)
                                 (malloc=2MB #28287 +3993)
                                 (mmap: reserved=1110MB +2MB, committed=98MB +3MB)
      
     -                    Thread (reserved=342MB -5MB, committed=342MB -5MB)
                                 (thread #340 -5)
                                 (stack: reserved=340MB -5MB, committed=340MB -5MB)
                                 (malloc=1MB #1726 -25)
      
     -                      Code (reserved=255MB +2MB, committed=71MB +14MB)
                                 (malloc=11MB +2MB #16481 +3047)
                                 (mmap: reserved=244MB, committed=60MB +12MB)
      
     -                        GC (reserved=546MB +1MB, committed=546MB +1MB)
                                 (malloc=134MB +1MB #118554 +5894)
                                 (mmap: reserved=412MB, committed=412MB)
      
     -                  Compiler (reserved=1MB, committed=1MB)
                                 (malloc=1MB #2189 +56)
      
     -                  Internal (reserved=4793MB +3605MB, committed=4793MB +3605MB)
                                 (malloc=4793MB +3605MB #14922900 +14749710)
      
     -                    Symbol (reserved=21MB, committed=21MB)
                                 (malloc=16MB #170771 +218)
                                 (arena=4MB #1)
      
     -    Native Memory Tracking (reserved=234MB +225MB, committed=234MB +225MB)
                                 (malloc=1MB #8349 +2466)
                                 (tracking overhead=233MB +225MB)
      
     -                   Unknown (reserved=20MB, committed=0MB)
                                 (mmap: reserved=20MB, committed=0MB)
      
     [0x00007f6e8818d654] ElfSymbolTable::ElfSymbolTable(_IO_FILE*, Elf64_Shdr)+0x64
     [0x00007f6e8818ce60] ElfFile::load_tables()+0x1d0
     [0x00007f6e88113975] ElfDecoder::decode(unsigned char*, char*, int, int*, char const*)+0xa5
     [0x00007f6e881131ea] Decoder::decode(unsigned char*, char*, int, int*, char const*)+0xea
                                  (malloc=1MB type=Internal +1MB #2 +2)
     
     [0x00007f6e882f3165] jni_GetFloatArrayElements+0x155
     [0x00007f6bb81ec618] Java_com_spotify_annoy_jni_base_AnnoyIndexImpl_cppGetNearestByVector+0x28
     [0x00007f6e6d05418a]
                                  (malloc=3621MB type=Internal +3601MB #14830743 +14748680)
     
     [0x00007f6e88083dcd] CodeBlob::set_oop_maps(OopMapSet*) [clone .part.5]+0xed
     [0x00007f6e880841c3] CodeBlob::CodeBlob(char const*, CodeBuffer*, int, int, int, int, OopMapSet*)+0xe3
     [0x00007f6e8850a42d] nmethod::nmethod(Method*, int, int, int, CodeOffsets*, int, DebugInformationRecorder*, Dependencies*, CodeBuffer*, int, OopMapSet*, ExceptionHandlerTable*, ImplicitExceptionTable*, AbstractCompiler*, int)+0x4d
     [0x00007f6e8850adb9] nmethod::new_nmethod(methodHandle, int, int, CodeOffsets*, int, DebugInformationRecorder*, Dependencies*, CodeBuffer*, int, OopMapSet*, ExceptionHandlerTable*, ImplicitExceptionTable*, AbstractCompiler*, int)+0x219
                                  (malloc=11MB type=Code +2MB #15348 +2963)
     
     [0x00007f6e8818d3da] ElfStringTable::ElfStringTable(_IO_FILE*, Elf64_Shdr, int)+0x4a
     [0x00007f6e8818cde5] ElfFile::load_tables()+0x155
     [0x00007f6e88113975] ElfDecoder::decode(unsigned char*, char*, int, int*, char const*)+0xa5
     [0x00007f6e881131ea] Decoder::decode(unsigned char*, char*, int, int*, char const*)+0xea
                                  (malloc=2MB type=Internal +2MB #3 +3)
     
     [0x00007f6e886fb11d] ReservedSpace::ReservedSpace(unsigned long, unsigned long, bool, char*, unsigned long)+0x1ad
     [0x00007f6e884b125e] VirtualSpaceNode::VirtualSpaceNode(unsigned long)+0x16e
     [0x00007f6e884b6ccb] VirtualSpaceList::create_new_virtual_space(unsigned long) [clone .part.64]+0x3b
     [0x00007f6e884b7443] VirtualSpaceList::get_new_chunk(unsigned long, unsigned long)+0x2f3
                                  (mmap: reserved=78MB +2MB, committed=78MB +3MB)
     
     [0x00007f6e8853b8b2] java_start(Thread*)+0x102
                                  (mmap: reserved=283MB -5MB, committed=283MB -5MB)
     
     [0x00007f6e886fc1c7] ReservedCodeSpace::ReservedCodeSpace(unsigned long, unsigned long, bool)+0x77
     [0x00007f6e8823a3b1] CodeHeap::reserve(unsigned long, unsigned long, unsigned long)+0xd1
     [0x00007f6e8808b7aa] codeCache_init()+0x7a
     [0x00007f6e88260b2c] init_globals()+0x3c
                                  (mmap: reserved=240MB, committed=59MB +12MB)
    

    可以看出经过一整子运行,进程所占内存增长了:3831MB,其中绝大部分内存是Internal区域增长的;

    进一步分析,Internal内存增长注意是:


    zz.png

    这部分内存增长导致的。

    Java_com_spotify_annoy_jni_base_AnnoyIndexImpl_cppGetNearestByVector 这个方法是公司自己封装的annoy库接口,使用native的方法提供接口,供Java线上服务调用;传入向量,返回相似资讯的id;对应召回资讯的步骤。

    3.png

    进一步,进入annoy库内部C++代码,排查问题


    4.png

    这个方法,是native JNI 方法的具体实现,在方法体的第一行,可以看到,C++代码向env申请了内存;查看资料发现“GetFloatArrayElements”方法是Java提供给native方法,申请数组内存的方法。这个方法对应的有一个对应的释放内存的方法,“ReleaseFloatArrayElements”,而这里,只有申请内存,没有释放内存;导致了内存泄露。

    此外,从上述命令行中也能看出一些蛛丝马迹,这里也提到了内存申请的方法。

    5.png

    加上内存释放的代码,重新编译打包成so文件,上线测试,内存平稳,相比对照组,不再有增加。

    6.png VC20210928-175559.png

    再次重新执行堆外内存追踪,相应的指标,不再明显增加


    8.png

    相关文章

      网友评论

          本文标题:一次内存泄露问题排查

          本文链接:https://www.haomeiwen.com/subject/qpdrnltx.html