您好,登錄后才能下訂單哦!
這篇文章主要介紹“如何理解基于Rust的Android Native內(nèi)存分析”,在日常操作中,相信很多人在如何理解基于Rust的Android Native內(nèi)存分析問(wèn)題上存在疑惑,小編查閱了各式資料,整理出簡(jiǎn)單好用的操作方法,希望對(duì)大家解答”如何理解基于Rust的Android Native內(nèi)存分析”的疑惑有所幫助!接下來(lái),請(qǐng)跟著小編一起來(lái)學(xué)習(xí)吧!
1. Android Native內(nèi)存分析痛點(diǎn)與訴求
這一節(jié)主要介紹我們?yōu)槭裁匆鲞@件事以及對(duì)于這件事我們期望達(dá)到什么樣的目標(biāo)。
1.1 現(xiàn)有工具缺陷
Android在Java層面有很完善的性能分析工具,但是在Native層面沒(méi)有完整的解決方案。主要表現(xiàn)在:
不支持Android 4.x,線上統(tǒng)計(jì)數(shù)據(jù)顯示4.x版本的車(chē)機(jī)仍占有較大比重,因此這點(diǎn)成為了無(wú)法忽視的問(wèn)題。
安卓自帶的malloc_debug功能在不同的版本上行為不同,而且車(chē)機(jī)安卓系統(tǒng)大多經(jīng)過(guò)了系統(tǒng)廠商的定制,不能保證這些功能可用。
因此, 無(wú)法基于Android系統(tǒng)自有的功能做到Native內(nèi)存性能分析。
我們團(tuán)隊(duì)之前也在這方面做出了一些成果,但還是存在下面幾個(gè)問(wèn)題:
通過(guò)修改編譯參數(shù)對(duì)Native代碼函數(shù)入口/結(jié)束位置插樁來(lái)進(jìn)行Hook,導(dǎo)致了性能?chē)?yán)重下降;
由于是侵入式分析,對(duì)內(nèi)存問(wèn)題分析需要單獨(dú)編譯出包分析,解決效率大幅降低,一個(gè)內(nèi)存泄漏問(wèn)題的排查成本按天計(jì)算。
缺少精準(zhǔn)內(nèi)存使用數(shù)據(jù)。
1.2 打造一套完整的Native內(nèi)存性能分析方案
結(jié)合上門(mén)的問(wèn)題痛點(diǎn),我們希望能夠有一套完整的Native內(nèi)存性能分析方案。具體訴求表現(xiàn)在下面幾點(diǎn):
支持安卓4.x在內(nèi)的絕大多數(shù)安卓系統(tǒng)。
無(wú)侵入式分析,內(nèi)存問(wèn)題的發(fā)現(xiàn)與精準(zhǔn)定位同時(shí)完成。
性能優(yōu)異,overhead低。
支持長(zhǎng)時(shí)間內(nèi)存泄漏壓測(cè)。包括車(chē)廠客戶(hù)在內(nèi)的研發(fā)團(tuán)隊(duì)都會(huì)對(duì)導(dǎo)航進(jìn)行壓測(cè),需要能夠支持長(zhǎng)時(shí)間的壓測(cè)并定位內(nèi)存泄漏問(wèn)題。
函數(shù)級(jí)內(nèi)存使用數(shù)據(jù)。原先的方案重點(diǎn)在于解決內(nèi)存泄漏的問(wèn)題,獲取的內(nèi)存使用數(shù)據(jù)不夠精確。而我們希望新的方案能夠獲得詳細(xì)的內(nèi)存使用數(shù)據(jù),用來(lái)支持內(nèi)存性能優(yōu)化。
2. 內(nèi)存塔(MemTower)方案
本節(jié)主要介紹memory-profiler項(xiàng)目的實(shí)現(xiàn)和內(nèi)存塔(MemTower)方案在移植該項(xiàng)目至Android平臺(tái)上的過(guò)程和對(duì)原方案的改進(jìn)。闡述我們是如何實(shí)現(xiàn)并滿(mǎn)足上述的訴求。
2.1 選擇Rust & Memory-profiler
針對(duì)上門(mén)的訴求,期望能夠找到一種新的解決方案。當(dāng)時(shí)正好在研究Rust,因此在GitHub上結(jié)合關(guān)鍵字搜索便發(fā)現(xiàn)了memory-profiler(以下簡(jiǎn)稱(chēng)mp)項(xiàng)目,作者koute是前Nokia工程師。接著才有了后面的內(nèi)存塔。本節(jié)主要闡述mp如何結(jié)合Rust實(shí)現(xiàn)內(nèi)存Profile的相關(guān)原理和功能。
2.1.1 Hook實(shí)現(xiàn)
通常對(duì)Native內(nèi)存性能分析使用的方案是Hook malloc 和 free 等內(nèi)存調(diào)用請(qǐng)求。mp的原理也是如此,利用LD_PRELOAD 預(yù)加載自定義庫(kù)實(shí)現(xiàn)對(duì)內(nèi)存操作函數(shù)的Hook。這種方案最大的問(wèn)題是容易引發(fā)循環(huán)malloc調(diào)用。如下圖,Hook了程序內(nèi)存請(qǐng)求后,Hook業(yè)務(wù)自身的內(nèi)存請(qǐng)求也會(huì)觸發(fā)內(nèi)存請(qǐng)求,從而造成了malloc循環(huán)調(diào)用,引發(fā)棧崩潰。
mp的做法利用了Rust的可自定義內(nèi)存分配器(Allocator)的特性,將曾經(jīng)的Rust默認(rèn)內(nèi)存分配器jemalloc作為自定義分配器,并在jemalloc-sys的c代碼中將最終的內(nèi)存申請(qǐng)mmap替換成自定義的函數(shù)入口(從而也區(qū)分應(yīng)用和自身的mmap調(diào)用),最終調(diào)用mmap系統(tǒng)調(diào)用。
將Rust內(nèi)存請(qǐng)求轉(zhuǎn)發(fā)給系統(tǒng)調(diào)用后,還需要將應(yīng)用的內(nèi)存請(qǐng)求繼續(xù)傳遞給系統(tǒng)libc. mp的做法是通過(guò)Rust的feature開(kāi)關(guān),可以自行選擇兩種方式處理應(yīng)用內(nèi)存請(qǐng)求,這兩種方式都是通過(guò)在Rust中指定link_name 屬性實(shí)現(xiàn):
直接通過(guò)__libc_malloc的link_name將應(yīng)用內(nèi)存請(qǐng)求轉(zhuǎn)發(fā)給libc
通過(guò)指定成jemallocator的函數(shù)入口 _rjem_malloc,使應(yīng)用和Rust共用jemalloc.
最終可以使Hook業(yè)務(wù)使用完整的Rust語(yǔ)言功能而不用擔(dān)心Rust自身代碼引起的循環(huán)調(diào)用崩潰。
2.1.2 高性能堆棧反解
除了利用Rust系統(tǒng)編程語(yǔ)言特性避開(kāi)內(nèi)存循環(huán)調(diào)用之外,作者還利用Rust的高性能特點(diǎn)實(shí)現(xiàn)了幾種高性能堆棧反解。
利用ELF的.eh_frame 節(jié)(C++異常處理機(jī)制)提供的?;厮菪畔ⅰ?/p>
基于.ARM.exidx + .ARM.extab的?;厮?,這個(gè)是ARM提供的unwind table.
具體實(shí)現(xiàn)可以看作者的這個(gè)Crate not-perf。這里選擇第二種做說(shuō)明,如圖下,對(duì)每個(gè)線程的堆棧都用線程局部存儲(chǔ)維護(hù)了一套棧幀緩存,這個(gè)緩存來(lái)自于ELF文件中的unwind table信息,當(dāng)堆棧的幀在緩存未命中時(shí)會(huì)把對(duì)應(yīng)二進(jìn)制的unwind表被加載到內(nèi)存,而命中的時(shí)候,就不需要去讀取文件。通常二進(jìn)制被加載后它的地址空間就不會(huì)發(fā)生變化,所以緩存的效率很高。缺點(diǎn)是每個(gè)線程都有一套完整的緩存。從系統(tǒng)層面看占用的內(nèi)存overhead很大。
2.1.3 強(qiáng)大的數(shù)據(jù)分析功能
從mp的頁(yè)面可以看到它除了內(nèi)存Profile外,還有一個(gè)對(duì)應(yīng)的數(shù)據(jù)分析Server端,采用actix-web框架,且具備一個(gè)非常強(qiáng)大的分析功能。主要特性有下面幾點(diǎn):
內(nèi)存使用量和泄漏兩種視角的時(shí)序曲線非常直觀。
搭配了一個(gè)非常強(qiáng)大的過(guò)濾器,可以實(shí)現(xiàn)針對(duì)內(nèi)存生命周期、函數(shù)、時(shí)間等多維度做過(guò)濾查詢(xún)及其對(duì)應(yīng)的內(nèi)存火焰圖功能。
所有功能具備RESTful API接口,可以非常容易的實(shí)現(xiàn)定制。
詳細(xì)的使用說(shuō)明這里不做過(guò)多的介紹。
2.2 移植
了解完mp的基本原理后,本節(jié)我們主要闡述在移植安卓平臺(tái)過(guò)程中遇到的各種問(wèn)題(坑)。
2.2.1 自定義Allocator
mp的Hook方案在Android平臺(tái)上存在較多問(wèn)題,主要體現(xiàn)在下面幾點(diǎn):
Jemalloc本身也才是Android 5.0開(kāi)始引入安卓,mp自帶的jemalloc-sys會(huì)導(dǎo)致一個(gè)應(yīng)用里存在兩個(gè)jemalloc,最終表現(xiàn)為在不同的版本上有著各種各樣的異常崩潰,問(wèn)題排查成了阻礙。
__libc_malloc是glibc提供的malloc函數(shù)入口別名,但在Android平臺(tái)沒(méi)有對(duì)應(yīng)這類(lèi)實(shí)現(xiàn)。
因此,我們采用最原始的dlsym 方法獲取內(nèi)存相關(guān)函數(shù)入口,再將其封裝成Rust Allocator. 應(yīng)用的內(nèi)存請(qǐng)求也使用這些函數(shù)地址。如下圖,最終所有內(nèi)存請(qǐng)求都傳給libc,這樣Rust的業(yè)務(wù)代碼對(duì)libc來(lái)說(shuō)是透明的。
2.2.2 棧回溯
?;厮葸@塊同樣有一些移植修改。上面說(shuō)到作者提供了基于C++異常處理機(jī)制的?;厮莘椒?,但是這個(gè)方案要求依賴(lài)C++庫(kù)。而C在Android 8.0之后才會(huì)成為默認(rèn)依賴(lài)。這要求在8.0之前的版本運(yùn)行時(shí)應(yīng)用必須也依賴(lài)C++庫(kù)。因此我們移除了這個(gè)棧回溯方案,舍去了這個(gè)依賴(lài)。
2.2.3 地址空間重載
在程序啟動(dòng)或調(diào)用dlopen/dlclose時(shí)鏈接器會(huì)加載(或卸載)ELF文件,相應(yīng)的,程序的地址空間會(huì)發(fā)生變化,這時(shí)候?;厮菥彺胬锏牡刂房臻g就可能會(huì)失效,需要重新加載(reload),reload操作掃描整個(gè)地址空間的變更,這個(gè)成本很高。與此同時(shí)還需要一種低成本獲取地址空間變化的方式. mp的實(shí)現(xiàn)主要有兩種方式:
libc提供的接口dl_iterate_phdr. Android API_LEVEL低于21(即5.0之前)沒(méi)有,5.0之后這個(gè)函數(shù)的結(jié)構(gòu)體和在高版本Android的實(shí)現(xiàn)不同。所以Rust定義的單一C結(jié)構(gòu)體格式會(huì)導(dǎo)致讀取到臟數(shù)據(jù)作為reload依據(jù),導(dǎo)致非常高頻繁地reload.;
Perf的 PERF_RECORD_MMAP2 事件,這個(gè)要求內(nèi)核版本大于3.16。因此這在Android 4.x上也不具備。
實(shí)際運(yùn)行過(guò)程中程序在加載完所有依賴(lài)ELF后,地址空間幾乎很少再變。因此,我們修改為只有在新的ELF被加載時(shí)才進(jìn)行地址空間重載?;鹧鎴D結(jié)果顯示可以大幅降低Hook時(shí)的計(jì)算成本。
2.3 改進(jìn)
到目前為止, 內(nèi)存塔已經(jīng)可以在支持 LD_PRELOAD 的Android版本上正確運(yùn)行了(含4.x)。但是上面訴求中還有一點(diǎn)無(wú)法滿(mǎn)足:長(zhǎng)時(shí)間內(nèi)存泄漏壓測(cè)。而且在數(shù)據(jù)分析過(guò)程中,我們希望有更多維度的信息。因此,本小節(jié)主要介紹我們對(duì)內(nèi)存塔的改進(jìn)。
2.3.1 內(nèi)存泄漏壓測(cè)
mp原先的定位正如它的名稱(chēng)表述,是一款內(nèi)存性能分析工具,它記錄的是全量?jī)?nèi)存信息。這點(diǎn)決定了它的數(shù)據(jù)量規(guī)模。在長(zhǎng)時(shí)間壓測(cè)一小時(shí)的多個(gè)業(yè)務(wù)場(chǎng)景中,根據(jù)內(nèi)存使用量不同,生成的采樣數(shù)據(jù)文件有1GB~7GB之多。這樣的數(shù)據(jù)量無(wú)法滿(mǎn)足業(yè)務(wù)的需要。
因此,我們?cè)黾恿藘?nèi)存泄漏檢測(cè)模式(ONLY_LEAKED),這個(gè)模式的原理如下:
將記錄到內(nèi)存開(kāi)辟的每一層棧幀記錄到一個(gè)字典樹(shù)(Trie Tree)中,同時(shí)記錄開(kāi)辟的內(nèi)存大小。
內(nèi)存釋放時(shí)更新字典樹(shù)對(duì)應(yīng)的節(jié)點(diǎn)信息。當(dāng)前泄漏是否達(dá)到某個(gè)閾值(如100MB), 是則停止采樣。
在結(jié)束采樣時(shí)把整個(gè)字典樹(shù)存儲(chǔ)的未釋放內(nèi)存記錄寫(xiě)入文件。
這種模式的優(yōu)點(diǎn)是最終的數(shù)據(jù)量非常的小,實(shí)際壓測(cè)一小時(shí)數(shù)據(jù)文件大小在100~200MB之間。再進(jìn)過(guò)mp自帶的postprocess 子命令壓縮后,大小不足100MB。不足之處是內(nèi)存塔需要在內(nèi)存中緩存一個(gè)全量的堆棧歷史數(shù)據(jù),當(dāng)沒(méi)有新的棧幀記錄出現(xiàn)后這個(gè)內(nèi)存增長(zhǎng)才會(huì)趨于穩(wěn)定。
2.3.2 增強(qiáng)分析過(guò)濾器
導(dǎo)航的業(yè)務(wù)模塊劃分和線程很多,因此增加了按線程和庫(kù)正則篩選過(guò)濾器選項(xiàng)。
2.3.3 內(nèi)存火焰圖完善
mp原方案的內(nèi)存火焰圖是以?xún)?nèi)存大小(allocated)作為火焰圖維度,在分析內(nèi)存性能時(shí)內(nèi)存開(kāi)辟次數(shù)(allocations)也是一個(gè)很重要的指標(biāo),因此加入內(nèi)存開(kāi)辟次數(shù)火焰圖。這是當(dāng)初最早改進(jìn)的功能,而且火焰圖的形狀類(lèi)似塔狀,就把該項(xiàng)目重命名為:內(nèi)存塔(MemTower)。
最后一點(diǎn)是原方案的火焰圖信息沒(méi)有以線程為單位劃分,我們把堆棧信息按線程區(qū)分后會(huì)更加直觀。
分配次數(shù)火焰圖
分配大小火焰圖
3. 內(nèi)存塔的能力及更多可能
最后一節(jié)介紹下內(nèi)存塔提供了什么樣的能力、收益以及還有哪些可能。
3.1 能力
內(nèi)存塔(MemTower)在Android 8.0以下依賴(lài)setprop wrap.com.xxx.xxx 和 root權(quán)限的能力,8.0以上版本如果沒(méi)有root權(quán)限還可以通過(guò)配置Android項(xiàng)目wrap.sh來(lái)加載內(nèi)存塔庫(kù)。另外,由于mp原生支持Linux的原因,我們也成功適配了奔馳戴姆勒這類(lèi)嵌入式Linux項(xiàng)目車(chē)機(jī)。
支持平臺(tái):Android 4.x、5.1.1和7或更高以上版本(5.0和6系統(tǒng)存在Bug, 無(wú)法設(shè)置setprop ). Linux x86_64, AArch74, Arm.
采樣方式: 非侵入式. 非Root設(shè)備可選侵入式方式。
采樣模式: 常規(guī)性能分析模式和內(nèi)存泄漏壓測(cè)模式。
特點(diǎn): 高性能堆棧反解、完善的內(nèi)存分析Insight體驗(yàn)(多維度過(guò)濾器分析、內(nèi)存火焰圖等)。
原先發(fā)現(xiàn)內(nèi)存泄漏問(wèn)題重新出包二次壓測(cè)分析,再推斷可能泄漏點(diǎn)的流程耗費(fèi)時(shí)間按天計(jì)算。利用內(nèi)存塔(MemTower)做一遍測(cè)試后幾分鐘即可解析出精細(xì)化數(shù)據(jù),大幅降低了內(nèi)存性能問(wèn)題分析成本。mp提供的這套Hook思路和高性能堆棧反解其實(shí)可以不僅僅局限在內(nèi)存方面的分析,還可以針對(duì)IO性能分析或其它問(wèn)題上。
到此,關(guān)于“如何理解基于Rust的Android Native內(nèi)存分析”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注億速云網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)?lái)更多實(shí)用的文章!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。