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