背景提要
深度召回服务在浏览器个性化推荐召回阶段占着重要作用。现在基线上有n路深度召回;
深度召回服务流程主要分为两大阶段:用户特征过模型得到用户向量;用户向量调用annoy获取相似资讯;
我们发现当前的深度召回服务存在内存泄漏问题,服务进程占用的内存总是缓慢增长,直至触发阈值告警。
问题定位
深度召回服务内部需要加载模型和加载annoy向量文件,初步怀疑内存泄漏与这两个部分有关系。
仔细观察增长图像,发现内存增长的速率和请求量成正比,白天涨的快,晚上涨的慢;所以内存泄漏点与在线处理流程关系更加密切,与模型加载替换部分没有关系。
在线处理流程中,涉及到的内存有Java堆内存和堆外内存。其中堆内存大小我们通过JVM启动参数已经控制,而泄漏的内存大小,远远大于我们设置的堆内存大小。我们通过调整堆内存大小,观察堆内存使用情况和GC回收情况,确定不是堆内存泄漏导致的;
深度召回堆外内存的使用主要有两个方面,一个是通过推理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内存增长注意是:
这部分内存增长导致的。
Java_com_spotify_annoy_jni_base_AnnoyIndexImpl_cppGetNearestByVector 这个方法是公司自己封装的annoy库接口,使用native的方法提供接口,供Java线上服务调用;传入向量,返回相似资讯的id;对应召回资讯的步骤。
进一步,进入annoy库内部C++代码,排查问题
这个方法,是native JNI 方法的具体实现,在方法体的第一行,可以看到,C++代码向env申请了内存;查看资料发现“GetFloatArrayElements”方法是Java提供给native方法,申请数组内存的方法。这个方法对应的有一个对应的释放内存的方法,“ReleaseFloatArrayElements”,而这里,只有申请内存,没有释放内存;导致了内存泄露。
此外,从上述命令行中也能看出一些蛛丝马迹,这里也提到了内存申请的方法。
加上内存释放的代码,重新编译打包成so文件,上线测试,内存平稳,相比对照组,不再有增加。
再次重新执行堆外内存追踪,相应的指标,不再明显增加