当前位置: 首页 > 科技观察

基于Rust的AndroidNative内存分析方案

时间:2023-03-18 17:25:27 科技观察

背景:高德地图上运行的车载系统环境大多是基于Android的定制系统,高德的底层代码都是C/C++Native代码。因此,Android上需要一个通用的Native内存性能分析方案。MemTower是基于开源项目memory-profiler并移植到Android的优化改进方案。解决了之前方案的痛点,满足一般Native内存性能分析需求。该项目使用Rust语言编写,利用Rust的一些特性完成了Native内存访问的Hook。一、AndroidNative内存分析痛点与诉求本节主要介绍我们为什么要这样做,以及我们期望达到什么样的目标。1.1现有工具缺陷Android在Java层面有完整的性能分析工具,但在Native层面还没有完整的解决方案。主要表现是:不支持Android4.x,而网上统计显示4.x版本的车还是占据了很大的比重,所以这成为了一个不容忽视的问题。Android自带的malloc_debug函数在不同版本上表现不同,而且大部分车载Android系统都是由系统厂商定制的,所以不能保证这些函数都可用。因此,Native的内存性能分析是无法根据Android系统自身的功能来做的。我们团队之前在这方面取得了一定的成绩,但是还存在以下问题:通过修改编译参数的方式来hookNative代码函数的入口/结束位置,导致性能严重下降;由于侵入式分析,对内存问题的分析需要单独编译打包分析,大大降低了求解效率,而且排查内存泄漏问题的成本是按天计算的。缺乏精确的内存使用数据。1.2打造一套完整的Native内存性能分析方案结合上门问题的痛点,我们希望有一套完整的Native内存性能分析方案。具体需求如下:支持包括Android4.x在内的大部分Android系统。无创分析,同时完成内存问题的发现和精准定位。性能优良,开销小。支持长期内存泄漏压力测试。研发团队,包括车厂客户,都会对导航进行压力测试,需要能够支持长时间的压力测试,定位内存泄漏。函数级内存使用数据。原方案着重解决内存泄漏问题,获取的内存使用数据不够准确。并且我们希望新方案能够获取详细的内存使用数据,以支持内存性能优化。2.MemTower方案本节主要介绍memory-profiler项目的实现和将MemTower方案移植到Android平台的过程以及对原有方案的改进。解释我们如何实现和满足上述要求。2.1上门需求选择Rust&Memory-profiler,希望能找到新的解决方案。当时正好在研究Rust,于是在GitHub上结合关键词搜索发现了memory-profiler(以下简称mp)这个项目。作者koute是前诺基亚工程师。然后是后面的记忆塔。本节主要讲解mp如何结合Rust实现memoryprofile的相关原理和功能。2.1.1Hook实现Native内存性能分析通常采用的方案是Hookmalloc和free等内存调用请求。mp的原理也是一样的,使用LD_PRELOAD预加载自定义库,实现内存操作函数的Hook。这种方案最大的问题是容易造成malloc循环调用。如下图,程序内存请求被hook后,hook服务本身的内存请求也会触发内存请求,导致malloc调用循环,导致栈崩溃。mp的方法利用了Rust的可自定义内存分配器(Allocator)的特性,将之前Rust默认的内存分配器jemalloc作为自定义分配器,将jemalloc-sys的c代码中最终的内存申请mmap替换成了自定义的函数入口(这样也区分了应用程序和自己的mmap调用),最后调用mmap系统调用。将Rust内存请求转发给系统调用后,还需要继续将申请内存请求传递给系统libc。mp的方法是使用Rust特性开关,可以选择两种方式来处理申请内存请求,这两种方式都是通过在Rust中指定link_name属性:直接通过__libc_malloc的link_name将申请内存请求转发给libc通过将其指定为jemallocator的函数入口_rjem_malloc,应用程序和Rust可以共享jemalloc。最后,Hook业务可以使用完整的Rust语言功能,而不必担心Rust自身代码导致的循环调用崩溃。2.1.2高性能栈逆向除了利用Rust系统编程语言特性避免内存循环调用外,作者还利用Rust的高性能特性实现了几种高性能栈逆向。使用ELF的.eh_frame部分(C++异常处理机制)提供的堆栈回溯信息。根据.ARM.exidx+.ARM.extab的堆栈回溯,这是ARM提供的展开表。具体实现可以参考作者的Cratenot-perf。这里选择第二个选项进行说明。如下图所示,为每个线程的栈维护了一组栈帧缓存,线程本地存储。这个缓存来自ELF文件中的展开表信息。当栈帧不在缓存中命中时,对应的二进制unwindtable会被加载到内存中,命中时不需要读取文件。通常二进制文件加载后地址空间不会改变,所以缓存效率很高。缺点是每个线程都有一整套缓存。从系统层面来说,占用的内存开销是非常大的。2.1.3强大的数据分析功能从mp的页面可以看到,除了内存配置文件外,还有相应的数据分析服务器,它使用actix-web框架,分析功能非常强大。主要特点如下:内存使用和泄漏两个角度的时序曲线非常直观。配备非常强大的过滤器,可以针对内存生命周期、函数、时间等实现多维过滤查询和相应的内存火焰图功能。所有功能都有RESTfulAPI接口,可以很容易地定制。详细的使用说明这里不再介绍。2.2移植在了解了mp的基本原理后,本节主要讲解Android平台移植过程中遇到的各种问题(陷阱)。2.2.1Android平台自定义AllocatormpHook方案存在很多问题,主要体现在以下几点:Jemalloc本身也在Android5.0引入Android,mp自带的jemalloc-sys会导致两个一个jemalloc,到头来在不同版本上出现各种异常crash,排错成为障碍。__libc_malloc是glibc提供的malloc函数入口的别名,但是在Android平台上没有对应的实现。因此,我们使用最原始的dlsym方法获取内存相关函数的入口,然后封装成一个RustAllocator。应用程序的内存请求也使用这些函数地址。如下图所示,所有的内存请求最终都传递给了libc,这样Rust的业务代码对libc是透明的。2.2.2栈回溯栈回溯也有一些移植修改。上面提到,作者提供了一种基于C++异常处理机制的栈回溯方法,但是这个方案需要依赖C++库。而C会在Android8.0之后成为默认依赖。这就要求运行8.0之前版本的应用程序也必须依赖C++库。所以我们去掉了这个堆栈回溯方案,丢弃了这个依赖。2.2.3地址空间重载当程序启动或调用dlopen/dlclose时,链接器会加载(或卸载)ELF文件。相应的,程序的地址空间也会发生变化。这时,栈回溯缓存中的地址空间可能会发生变化。如果失败,则需要重新加载(reload)。reload操作扫描整个地址空间的变化,代价很大。同时,也需要一种低成本的方式来获取地址空间的变化。mp的实现方式主要有两种:libc提供的接口dl_iterate_phdr。AndroidAPI_LEVEL低于21(即5.0之前)。5.0以后该函数的结构与Android高版本的实现有所不同。所以Rust定义的单一C结构格式,会导致读取脏数据作为重载的依据,导致重载频率非常高;Perf的PERF_RECORD_MMAP2事件,需要内核版本大于3.16。所以这在Android4.x上也不可用。在实际运行过程中,程序加载所有依赖的ELF后,地址空间很少发生变化。因此,我们修改为只在加载新的ELF时才进行地址空间的重新加载。火焰图结果表明,Hook的计算成本可以大大降低。2.3改进目前记忆塔可以在支持LD_PRELOAD的Android版本(包括4.x)上正常运行。但是,以上需求还有一点无法满足:长期的内存泄漏压力测试。而在数据分析的过程中,我们希望有更多维度的信息。因此,本小节主要介绍我们对记忆塔的改进。2.3.1内存泄漏压力测试mp的最初定位,顾名思义,是一款记录内存全量信息的内存性能分析工具。这决定了它的数据量的大小。在多个长时间测压一个小时的业务场景中,根据内存使用量不同,生成的样本数据文件大小从1G??B到7GB不等。这样的数据量无法满足业务的需求。因此,我们添加了内存泄漏检测模式(ONLY_LEAKED)。这种模式的原理是:将内存开发中记录的每一层栈帧记录成一个TrieTree,同时记录内存开发的大小。释放内存时更新字典树对应的节点信息。当前泄漏是否达到某个阈值(比如100MB),如果是,则停止采样。采样结束时,将整个字典树中存储的未释放内存记录写入文件。这种模式的好处是最终的数据量很小,压测一小时的实际数据文件大小在100-200MB之间。经过mp自带的postprocess子命令压缩后,大小不到100MB。缺点是内存塔需要在内存中缓存全量的栈历史数据,当没有新的栈帧记录出现时,内存增长会趋于稳定。2.3.2增强分析过滤导航业务模块划分和线程较多,增加线程过滤和库规则过滤选项。2.3.3MemoryFlameGraph改进mp原方案的内存火焰图。内存大小(已分配)用作火焰图的维度。在分析内存性能的时候,内存分配(allocations)的数量也是一个很重要的指标,所以把内存分配的数量加到火焰图上。这是最早改进的功能,火焰图的形状类似于塔,所以项目更名为:MemTower。最后一点是原方案中的火焰图信息没有按线程划分。我们把栈信息按线程划分后会更直观。AllocationtimesFlamegraphAllocationsizeFlamegraph3.内存塔的能力和更多的可能性最后一节介绍了内存塔提供的能力、好处和可能性。3.1能力MemTower依赖于setpropwrap.com.xxx.xxx和Android8.0及以下的root权限。如果8.0以上版本没有root权限,也可以配置Android工程wrap.sh加载内存塔库。此外,由于mp原生支持Linux,我们也成功适配了奔驰、戴姆勒等嵌入式Linux项目车辆。支持平台:Android4.x、5.1.1、7及以上(5.0、6系统存在bug,无法设置setprop)。Linuxx86_64、AArch64、ARM。采样方式:非侵入式。非Root设备是可选的Intrusive方式。采样模式:常规性能分析模式和内存泄漏压力测量模式。特点:高性能栈反解,完善的内存分析Insight体验(多维过滤分析、内存火焰图等)。找出内存泄漏问题并重新打包二次压测分析,进而推断可能的泄漏点的过程是按天计算的。经过内存塔(MemTower)的测试,提炼后的数据可以在几分钟内完成分析,大大降低了内存性能问题分析的成本。mp提供的这套Hook思路和高性能栈反方案不仅可以局限于内存的分析,还可以用于IO性能分析或者其他问题。