轉(zhuǎn)載:字節(jié)跳動(dòng)
iOS OOM 崩潰在生產(chǎn)環(huán)境中的歸因一直是困擾業(yè)界已久的疑難問(wèn)題冬耿,字節(jié)跳動(dòng)旗下的頭條圣猎、抖音等產(chǎn)品也面臨同樣的問(wèn)題。
在字節(jié)跳動(dòng)性能與穩(wěn)定性保障團(tuán)隊(duì)的研發(fā)實(shí)踐中,我們自研了一款基于內(nèi)存快照技術(shù)并且可應(yīng)用于生產(chǎn)環(huán)境中的 OOM 歸因方案——線上 Memory Graph精拟∈愁恚基于此方案滚躯,3 個(gè)月內(nèi)頭條抖音 OOM 崩潰率下降 50%+胃惜。
本文主要分享下該解決方案的技術(shù)背景,技術(shù)原理以及使用方式鼎姐,旨在為這個(gè)疑難問(wèn)題提供一種新的解決思路钾麸。
OOM 崩潰背景介紹
OOM
OOM 其實(shí)是Out Of Memory
的簡(jiǎn)稱,指的是在 iOS 設(shè)備上當(dāng)前應(yīng)用因?yàn)閮?nèi)存占用過(guò)高而被操作系統(tǒng)強(qiáng)制終止炕桨,在用戶側(cè)的感知就是 App 一瞬間的閃退饭尝,與普通的 Crash 沒(méi)有明顯差異。但是當(dāng)我們?cè)谡{(diào)試階段遇到這種崩潰的時(shí)候谋作,從設(shè)備設(shè)置->隱私->分析與改進(jìn)
中是找不到普通類型的崩潰日志芋肠,只能夠找到Jetsam
開頭的日志,這種形式的日志其實(shí)就是 OOM 崩潰之后系統(tǒng)生成的一種專門反映內(nèi)存異常問(wèn)題的日志遵蚜。那么下一個(gè)問(wèn)題就來(lái)了帖池,什么是Jetsam
奈惑?
Jetsam
Jetsam
是 iOS 操作系統(tǒng)為了控制內(nèi)存資源過(guò)度使用而采用的一種資源管控機(jī)制。不同于MacOS
睡汹,Linux
肴甸,Windows
等桌面操作系統(tǒng),出于性能方面的考慮囚巴,iOS 系統(tǒng)并沒(méi)有設(shè)計(jì)內(nèi)存交換空間的機(jī)制原在,所以在 iOS 中,如果設(shè)備整體內(nèi)存緊張的話彤叉,系統(tǒng)只能將一些優(yōu)先級(jí)不高或占用內(nèi)存過(guò)大的進(jìn)程直接終止掉庶柿。
[圖片上傳失敗...(image-aeea8d-1603760345328)]
<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important; text-align: justify; color: rgb(136, 136, 136); font-size: 14px;">Jetsam 日志解讀
</figcaption>
上圖是截取一份Jetsam
日志中最關(guān)鍵的一部分。關(guān)鍵信息解讀:
- pageSize:指的是當(dāng)前設(shè)備物理內(nèi)存頁(yè)的大小秽浇,當(dāng)前設(shè)備是
iPhoneXs Max
浮庐,大小是 16KB,蘋果 A7 芯片之前的設(shè)備物理內(nèi)存頁(yè)大小則是 4KB柬焕。 - states:當(dāng)前應(yīng)用的運(yùn)行狀態(tài)审残,對(duì)于
Heimdallr-Example
這個(gè)應(yīng)用而言是正在前臺(tái)運(yùn)行的狀態(tài),這類崩潰我們稱之為FOOM
(Foreground Out Of Memory)斑举;與此相對(duì)應(yīng)的也有應(yīng)用程序在后臺(tái)發(fā)生的 OOM 崩潰搅轿,這類崩潰我們稱之為BOOM
(Background Out Of Memory)。 - rpages:是
resident pages
的縮寫富玷,表明進(jìn)程當(dāng)前占用的內(nèi)存頁(yè)數(shù)量璧坟,Heimdallr-Example 這個(gè)應(yīng)用占用的內(nèi)存頁(yè)數(shù)量是 92800,基于 pageSize 和 rpages 可以計(jì)算出應(yīng)用崩潰時(shí)占用的內(nèi)存大小:16384 * 92800 / 1024 /1024 = 1.4GB赎懦。 - reason:表明進(jìn)程被終止的的原因沸柔,
Heimdallr-Example
這個(gè)應(yīng)用被終止的原因是超過(guò)了操作系統(tǒng)允許的單個(gè)進(jìn)程物理內(nèi)存占用的上限。
Jetsam
機(jī)制清理策略可以總結(jié)為下面兩點(diǎn):
單個(gè) App 物理內(nèi)存占用超過(guò)上限2. 整個(gè)設(shè)備物理內(nèi)存占用收到壓力按照下面優(yōu)先級(jí)完成清理:
后臺(tái)應(yīng)用>前臺(tái)應(yīng)用
內(nèi)存占用高的應(yīng)用>內(nèi)存占用低的應(yīng)用
用戶應(yīng)用>系統(tǒng)應(yīng)用
Jetsam
的代碼在開源的XNU
代碼中可以找到铲敛,這里篇幅原因就不具體展開了,具體的源碼解析可以參考本文最后第 2 和第 3 篇參考文獻(xiàn)会钝。
為什么要監(jiān)控 OOM 崩潰
前面我們已經(jīng)了解到伐蒋,OOM 分為FOOM
和BOOM
兩種類型,顯然前者因?yàn)橛脩舻母兄黠@迁酸,所以對(duì)用戶的體驗(yàn)的傷害更大先鱼,下文中提到的 OOM 崩潰僅指的是FOOM
。那么針對(duì) OOM 崩潰問(wèn)題有必要建立線上的監(jiān)控手段嗎奸鬓?
答案是有而且非常有必要的焙畔!原因如下:
- 重度用戶也就是使用時(shí)間更長(zhǎng)的用戶更容易發(fā)生
FOOM
,對(duì)這部分用戶體驗(yàn)的傷害導(dǎo)致用戶流失的話對(duì)業(yè)務(wù)損失更大串远。 - 頭條宏多,抖音等多個(gè)產(chǎn)品線上數(shù)據(jù)均顯示
FOOM
量級(jí)比普通崩潰還要多儿惫,因?yàn)檫^(guò)去缺乏有效的監(jiān)控和治理手段導(dǎo)致問(wèn)題被長(zhǎng)期忽視。 - 內(nèi)存占用過(guò)高即使沒(méi)導(dǎo)致
FOOM
也可能會(huì)導(dǎo)致其他應(yīng)用BOOM
的概率變大伸但,一旦用戶發(fā)現(xiàn)從微信切換到我們 App 使用肾请,再切回微信沒(méi)有停留在之前微信的聊天頁(yè)面而是重新啟動(dòng)的話,對(duì)用戶來(lái)說(shuō)更胖,體驗(yàn)是非常糟糕的铛铁。
OOM 線上監(jiān)控
翻閱XNU
源碼的時(shí)候我們可以看到在Jetsam
機(jī)制終止進(jìn)程的時(shí)候最終是通過(guò)發(fā)送SIGKILL
異常信號(hào)來(lái)完成的。
define SIGKILL 9 kill (cannot be caught or ignored)
從系統(tǒng)庫(kù) signal.h 文件中我們可以找到SIGKILL
這個(gè)異常信號(hào)的解釋却妨,它不可以在當(dāng)前進(jìn)程被忽略或者被捕獲饵逐,我們之前監(jiān)聽異常信號(hào)的常規(guī) Crash 捕獲方案肯定也就不適用了。那我們應(yīng)該如何監(jiān)控 OOM 崩潰呢彪标?
正面監(jiān)控這條路行不通倍权,2015 年的時(shí)候Facebook
提出了另外一種思路,簡(jiǎn)而言之就是排除法捐下。具體流程可以參考下面這張流程圖:
我們?cè)诿看?App 啟動(dòng)的時(shí)候判斷上一次啟動(dòng)進(jìn)程終止的原因账锹,那么已知的原因有:
- App 更新了版本
- App 發(fā)生了崩潰
- 用戶手動(dòng)退出
- 操作系統(tǒng)更新了版本
- App 切換到后臺(tái)之后進(jìn)程終止
如果上一次啟動(dòng)進(jìn)程終止的原因不是上述任何一個(gè)已知原因的話,就判定上次啟動(dòng)發(fā)生了一次FOOM
崩潰坷襟。
曾經(jīng)Facebook
旗下的Fabric
也是這樣實(shí)現(xiàn)的奸柬。但是通過(guò)我們的測(cè)試和驗(yàn)證,上述這種方式至少將以下幾種場(chǎng)景誤判:
- WatchDog 崩潰
- 后臺(tái)啟動(dòng)
- XCTest/UITest 等自動(dòng)化測(cè)試框架驅(qū)動(dòng)
- 應(yīng)用 exit 主動(dòng)退出
在字節(jié)跳動(dòng) OOM 崩潰監(jiān)控上線之前婴程,我們已經(jīng)排除了上面已知的所有誤判場(chǎng)景廓奕。需要說(shuō)明的是,因?yàn)榕懦ó吘箾](méi)有直接的監(jiān)控來(lái)的那么精準(zhǔn)档叔,或多或少總有一些 bad case桌粉,但是我們會(huì)保證盡量的準(zhǔn)確。
自研線上 Memory Graph衙四,OOM 崩潰率下降 50%+
OOM 生產(chǎn)環(huán)境歸因
目前在 iOS 端排查內(nèi)存問(wèn)題的工具主要包括 Xcode 提供的 Memory Graph 和 Instruments 相關(guān)的工具集铃肯,它們能夠提供相對(duì)完備的內(nèi)存信息,但是應(yīng)用場(chǎng)景僅限于開發(fā)環(huán)境传蹈,無(wú)法在生產(chǎn)環(huán)境使用押逼。由于內(nèi)存問(wèn)題往往發(fā)生在一些極端的使用場(chǎng)景,線下開發(fā)測(cè)試一般無(wú)法覆蓋對(duì)應(yīng)的問(wèn)題惦界,Xcode 提供的工具無(wú)法分析處理大多數(shù)偶現(xiàn)的疑難問(wèn)題挑格。
對(duì)此,各大公司都提出了自己的線上解決方案沾歪,并開源了例如MLeaksFinder
漂彤、OOMDetector
、FBRetainCycleDetector
等優(yōu)秀的解決方案。
在字節(jié)跳動(dòng)內(nèi)部的使用過(guò)程中挫望,我們發(fā)現(xiàn)現(xiàn)有工具各有側(cè)重立润,無(wú)法完全滿足我們的需求。主要的問(wèn)題集中在以下兩點(diǎn):
- 基于 Objective-C 對(duì)象引用關(guān)系找循環(huán)引用的方案士骤,適用范圍比較小范删,只能處理部分循環(huán)引用問(wèn)題,而內(nèi)存問(wèn)題通常是復(fù)雜的拷肌,類似于內(nèi)存堆積到旦,Root Leak,C/C++層問(wèn)題都無(wú)法解決巨缘。
- 基于分配堆棧信息聚類的方案需要常駐運(yùn)行添忘,對(duì)內(nèi)存、CPU 等資源存在較大消耗若锁,無(wú)法針對(duì)有內(nèi)存問(wèn)題的用戶進(jìn)行監(jiān)控搁骑,只能廣撒網(wǎng)又固,用戶體驗(yàn)影響較大仲器。同時(shí)肢扯,通過(guò)某些比較通用的堆棧分配的內(nèi)存無(wú)法定位出實(shí)際的內(nèi)存使用場(chǎng)景,對(duì)于循環(huán)引用等常見(jiàn)泄漏也無(wú)法分析欢摄。
為了解決頭條熬丧,抖音等各產(chǎn)品日益嚴(yán)峻的內(nèi)存問(wèn)題,我們自行研發(fā)了一款基于內(nèi)存快照技術(shù)的線上方案怀挠,我們稱之為——線上 Memory Graph析蝴。上線后接入了集團(tuán)內(nèi)幾乎所有的產(chǎn)品害捕,幫助各產(chǎn)品修復(fù)了多年的歷史問(wèn)題,OOM 率降低一個(gè)數(shù)量級(jí)闷畸,3 個(gè)月之內(nèi)抖音最新版本 OOM 率下降了 50%尝盼,頭條下降了 60%。線上突發(fā) OOM 問(wèn)題定位效率大大提升佑菩,徹底告別了線上 OOM 問(wèn)題歸因“兩眼一抹黑”的時(shí)代盾沫。
線上 Memory Graph 核心的原理是掃描進(jìn)程中所有 Dirty 內(nèi)存,通過(guò)內(nèi)存節(jié)點(diǎn)中保存的其他內(nèi)存節(jié)點(diǎn)的地址值建立起內(nèi)存節(jié)點(diǎn)之間的引用關(guān)系的有向圖殿漠,用于內(nèi)存問(wèn)題的分析定位赴精,整個(gè)過(guò)程不使用任何私有 API。這套方案具備的能力如下:
- 完整還原用戶當(dāng)時(shí)的內(nèi)存狀態(tài)绞幌。
- 量化線上用戶的大內(nèi)存占用和內(nèi)存泄漏蕾哟,可以精確的回答 App 內(nèi)存到底大在哪里這個(gè)問(wèn)題。
- 通過(guò)內(nèi)存節(jié)點(diǎn)符號(hào)和引用關(guān)系圖回答內(nèi)存節(jié)點(diǎn)為什么存活這個(gè)問(wèn)題莲蜘。
- 嚴(yán)格控制性能損耗谭确,只有當(dāng)內(nèi)存占用超過(guò)異常閾值的時(shí)候才會(huì)觸發(fā)分析。沒(méi)有運(yùn)行時(shí)開銷票渠,只有采集時(shí)開銷逐哈,對(duì) 99.9%正常使用的用戶幾乎沒(méi)有任何影響。
- 支持主要的編程語(yǔ)言庄新,包括 OC鞠眉,C/C++,Swift择诈,Rust 等械蹋。
內(nèi)存快照采集
線上 Memory Graph 采集內(nèi)存快照主要是為了獲取當(dāng)前運(yùn)行狀態(tài)下所有內(nèi)存對(duì)象以及對(duì)象之間的引用關(guān)系,用于后續(xù)的問(wèn)題分析羞芍。主要需要獲取的信息如下:
- 所有內(nèi)存的節(jié)點(diǎn)哗戈,以及其符號(hào)信息(如
OC/Swift/C++
實(shí)例類名,或者是某種有特殊用途的 VM 節(jié)點(diǎn)的 tag 等)荷科。 - 節(jié)點(diǎn)之間的引用關(guān)系唯咬,以及符號(hào)信息(偏移,或者實(shí)例變量名)畏浆,
OC/Swift
成員變量還需要記錄引用類型胆胰。
由于采集的過(guò)程發(fā)生在程序正常運(yùn)行的過(guò)程中,為了保證不會(huì)因?yàn)椴杉瘍?nèi)存快照導(dǎo)致程序運(yùn)行異常刻获,整個(gè)采集過(guò)程需要在一個(gè)相對(duì)靜止的運(yùn)行環(huán)境下完成蜀涨。因此,整個(gè)快照采集的過(guò)程大致分為以下幾個(gè)步驟:
- 掛起所有非采集線程。
- 獲取所有的內(nèi)存節(jié)點(diǎn)厚柳,內(nèi)存對(duì)象引用關(guān)系以及相應(yīng)的輔助信息氧枣。
- 寫入文件。
- 恢復(fù)線程狀態(tài)别垮。
下面會(huì)分別介紹整個(gè)采集過(guò)程中一些實(shí)現(xiàn)細(xì)節(jié)上的考量以及收集信息的取舍便监。
內(nèi)存節(jié)點(diǎn)的獲取
程序的內(nèi)存都是由虛擬內(nèi)存組成的,每一塊單獨(dú)的虛擬內(nèi)存被稱之為VM Region
碳想,通過(guò) mach 內(nèi)核的vm_region_recurse/vm_region_recurse64
函數(shù)我們可以遍歷進(jìn)程內(nèi)所有VM Region
烧董,并通過(guò)vm_region_submap_info_64
結(jié)構(gòu)體獲取以下信息:
- 虛擬地址空間中的地址和大小。
- Dirty 和 Swapped 內(nèi)存頁(yè)數(shù)移袍,表示該
VM Region
的真實(shí)物理內(nèi)存使用解藻。 - 是否可交換,Text 段葡盗、共享 mmap 等只讀或隨時(shí)可以被交換出去的內(nèi)存螟左,無(wú)需關(guān)注。
- user_tag觅够,用戶標(biāo)簽胶背,用于提供該
VM Region
的用途的更準(zhǔn)確信息。
大多數(shù) VM Region 作為一個(gè)單獨(dú)的內(nèi)存節(jié)點(diǎn)喘先,僅記錄起始地址和 Dirty钳吟、Swapped 內(nèi)存作為大小,以及與其他節(jié)點(diǎn)之間的引用關(guān)系窘拯;而 libmalloc 維護(hù)的堆內(nèi)存所在的 VM Region 則由于往往包含大多數(shù)業(yè)務(wù)邏輯中的 Objective-C 對(duì)象红且、C/C++對(duì)象、buffer 等涤姊,可以獲取更詳細(xì)的引用信息暇番,因此需要單獨(dú)處理其內(nèi)部節(jié)點(diǎn)、引用關(guān)系思喊。
在 iOS 系統(tǒng)中為了避免所有的內(nèi)存分配都使用系統(tǒng)調(diào)用產(chǎn)生性能問(wèn)題壁酬,相關(guān)的庫(kù)負(fù)責(zé)一次申請(qǐng)大塊內(nèi)存,再在其之上進(jìn)行二次分配并進(jìn)行管理恨课,提供給小塊需要?jiǎng)討B(tài)分配的內(nèi)存對(duì)象使用舆乔,稱之為堆內(nèi)存。程序中使用到絕大多數(shù)的動(dòng)態(tài)內(nèi)存都通過(guò)堆進(jìn)行管理剂公,在 iOS 操作系統(tǒng)上希俩,主要的業(yè)務(wù)邏輯分配的內(nèi)存都通過(guò)libmalloc
進(jìn)行管理,部分系統(tǒng)庫(kù)為了性能也會(huì)使用自己的單獨(dú)的堆管理纲辽,例如WebKit
內(nèi)核使用bmalloc
颜武,CFNetwork
也使用自己獨(dú)立的堆贫母,在這里我們只關(guān)注libmalloc
內(nèi)部的內(nèi)存管理狀態(tài),而不關(guān)心其它可能的堆(即這部分特殊內(nèi)存會(huì)以VM Region
的粒度存在盒刚,不分析其內(nèi)部的節(jié)點(diǎn)引用關(guān)系)。
我們可以通過(guò)malloc_get_all_zones
獲取libmalloc
內(nèi)部所有的zone
绿贞,并遍歷每個(gè)zone
中管理的內(nèi)存節(jié)點(diǎn)因块,獲取 libmalloc 管理的存活的所有內(nèi)存節(jié)點(diǎn)的指針和大小。
符號(hào)化
獲取所有內(nèi)存節(jié)點(diǎn)之后籍铁,我們需要為每個(gè)節(jié)點(diǎn)找到更加詳細(xì)的類型名稱涡上,用于后續(xù)的分析。其中拒名,對(duì)于 VM Region 內(nèi)存節(jié)點(diǎn)吩愧,我們可以通過(guò) user_tag 賦予它有意義的符號(hào)信息;而堆內(nèi)存對(duì)象包含 raw buffer增显,Objective-C/Swift雁佳、C++等對(duì)象。對(duì)于 Objective-C/Swift同云、C++這部分糖权,我們通過(guò)內(nèi)存中的一些運(yùn)行時(shí)信息,嘗試符號(hào)化獲取更加詳細(xì)的信息炸站。
Objective/Swift 對(duì)象的符號(hào)化相對(duì)比較簡(jiǎn)單星澳,很多三方庫(kù)都有類似實(shí)現(xiàn),Swift
在內(nèi)存布局上兼容了Objective-C
旱易,也有isa
指針禁偎,objc
相關(guān)方法可以作用于兩種語(yǔ)言的對(duì)象上。只要保證 isa 指針合法阀坏,對(duì)象實(shí)例大小滿足條件即可認(rèn)為正確如暖。
C++對(duì)象根據(jù)是否包含虛表可以分成兩類。對(duì)于不包含虛表的對(duì)象全释,因?yàn)槿狈\(yùn)行時(shí)數(shù)據(jù)装处,無(wú)法進(jìn)行處理。
對(duì)于對(duì)于包含虛表的對(duì)象浸船,在調(diào)研 mach-o 和 C++的 ABI 文檔后妄迁,可以通過(guò) std::type_info 和以下幾個(gè) section 的信息獲取對(duì)應(yīng)的類型信息。
-
type_name string
- 類名對(duì)應(yīng)的常量字符串李命,存儲(chǔ)在__TEXT/__RODATA
段的__const section
中登淘。 -
type_info
- 存放在__DATA/__DATA_CONST
段的__const section
中。 -
vtable
- 存放在__DATA/__DATA_CONST
段的__const section
中封字。
在 iOS 系統(tǒng)內(nèi)黔州,還有一類特殊的對(duì)象耍鬓,即CoreFoundation
。除了我們熟知的CFString
流妻、CFDictionary
外等牲蜀,很多很多系統(tǒng)庫(kù)也使用 CF 對(duì)象,比如CGImage
绅这、CVObject
等涣达。從它們的 isa 指針獲取的Objective-C
類型被統(tǒng)一成__NSCFType
。由于 CoreFoundation 類型支持實(shí)時(shí)的注冊(cè)证薇、注銷類型度苔,為了細(xì)化這部分的類型,我們通過(guò)逆向拿到 CoreFoundation 維護(hù)的類型 slot 數(shù)組的位置并讀取其數(shù)據(jù)浑度,保證能夠安全的獲取準(zhǔn)確的類型寇窑。
引用關(guān)系的構(gòu)建
整個(gè)內(nèi)存快照的核心在于重新構(gòu)建內(nèi)存節(jié)點(diǎn)之間的引用關(guān)系。在虛擬內(nèi)存中箩张,如果一個(gè)內(nèi)存節(jié)點(diǎn)引用了其它內(nèi)存節(jié)點(diǎn)甩骏,則對(duì)應(yīng)的內(nèi)存地址中會(huì)存儲(chǔ)指向?qū)Ψ降闹羔樦怠伏钠;谶@個(gè)事實(shí)我們?cè)O(shè)計(jì)了以下方案:
- 遍歷一個(gè)內(nèi)存節(jié)點(diǎn)中所有可能存儲(chǔ)了指針的范圍獲取其存儲(chǔ)的值 A横漏。
- 搜索所有獲得的節(jié)點(diǎn),判斷 A 是不是某一個(gè)內(nèi)存節(jié)點(diǎn)中任何一個(gè)字節(jié)的地址熟掂,如果是缎浇,則認(rèn)為是一個(gè)引用關(guān)系。
- 對(duì)所有內(nèi)存節(jié)點(diǎn)重復(fù)以上操作赴肚。
對(duì)于一些特定的內(nèi)存區(qū)域素跺,為了獲取更詳細(xì)的信息用于排查問(wèn)題,我們對(duì)棧內(nèi)存以及 Objective-C/Swift 的堆內(nèi)存進(jìn)行了一些額外的處理誉券。
其中指厌,棧內(nèi)存也以VM Region
的形式存在,棧上保存了臨時(shí)變量和 TLS 等數(shù)據(jù)踊跟,獲取相應(yīng)的引用信息可以幫助排查諸如 autoreleasepool 造成的內(nèi)存問(wèn)題踩验。由于棧并不會(huì)使用整個(gè)棧內(nèi)存,為了獲取 Stack 的引用關(guān)系商玫,我們根據(jù)寄存器以及棧內(nèi)存獲取當(dāng)前的椈叮可用范圍,排除未使用的棧內(nèi)存造成的無(wú)效引用拳昌。
而對(duì)于Objective-C/Swift
對(duì)象袭异,由于運(yùn)行時(shí)包含額外的信息,我們可以獲得Ivar
的強(qiáng)弱引用關(guān)系以及Ivar
的名字炬藤,帶上這些信息有助于我們分析問(wèn)題御铃。通過(guò)獲得Ivar
的偏移碴里,如果找到的引用關(guān)系的偏移和Ivar
的偏移一致,則認(rèn)為這個(gè)引用關(guān)系就是這個(gè)Ivar
上真,可以將Ivar
相關(guān)的信息附加上去咬腋。
數(shù)據(jù)上報(bào)策略
我們?cè)?App 內(nèi)存到達(dá)設(shè)定值后采集 App 當(dāng)時(shí)的內(nèi)存節(jié)點(diǎn)和引用關(guān)系,然后上傳至遠(yuǎn)端進(jìn)行分析睡互,可以精準(zhǔn)的反映 App 當(dāng)時(shí)的內(nèi)存狀態(tài)帝火,從而定位問(wèn)題,總的流程如下:
整個(gè)線上 Memory Graph 模塊工作的完整流程如上圖所示湃缎,主要包括:
- 后臺(tái)線程定時(shí)檢測(cè)內(nèi)存占用,超過(guò)設(shè)定的危險(xiǎn)閾值后觸發(fā)內(nèi)存分析蠢壹。
- 內(nèi)存分析后數(shù)據(jù)持久化嗓违,等待下次上報(bào)。
- 原始文件壓縮打包图贸。
- 檢查后端上報(bào)許可蹂季,因?yàn)閱蝹€(gè)文件很大,后端可能會(huì)做一些限流的策略疏日。
- 上報(bào)到后端分析偿洁,如果成功后清除文件,失敗后會(huì)重試沟优,最多三次之后清除涕滋,防止占用用戶太多的磁盤空間。
后臺(tái)分析
這是字節(jié)監(jiān)控平臺(tái) Memory Graph 單點(diǎn)詳情頁(yè)的一個(gè) case:
我們可以看到這個(gè)用戶的內(nèi)存占用已經(jīng)將近 900MB挠阁,我們分析時(shí)候的思路一般是:
- 從對(duì)象數(shù)量和對(duì)象內(nèi)存占用這兩個(gè)角度嘗試找到類列表中最有嫌疑的那個(gè)類宾肺。
- 從對(duì)象列表中隨機(jī)選中某個(gè)實(shí)例,向它的父節(jié)點(diǎn)回溯引用關(guān)系侵俗,找到你認(rèn)為最有嫌疑的一條引用路徑锨用。
- 點(diǎn)擊引用路徑模塊右上角的
Add Tag
來(lái)判斷當(dāng)前選中的引用路徑在同類對(duì)象中出現(xiàn)過(guò)多少次。 - 確認(rèn)有問(wèn)題的引用路徑之后再判斷究竟是哪個(gè)業(yè)務(wù)模塊發(fā)生的問(wèn)題隘谣。
通過(guò)上圖中引用路徑的分析我們發(fā)現(xiàn)增拥,所有的圖片最終都被TTImagePickController
這個(gè)類持有,最終排查到是圖片選擇器模塊一次性把用戶相冊(cè)中的所有圖片都加載到內(nèi)存里寻歧,極端情況下會(huì)發(fā)生這個(gè)問(wèn)題掌栅。
整體性能和穩(wěn)定性
采集側(cè)優(yōu)化策略
由于整個(gè)內(nèi)存空間一般包含的內(nèi)存節(jié)點(diǎn)從幾十萬(wàn)到幾千萬(wàn)不等,同時(shí)程序的運(yùn)行狀態(tài)瞬息萬(wàn)變熄求,采集過(guò)程有著很大的性能和穩(wěn)定性的壓力渣玲。
我們?cè)谇懊娴幕A(chǔ)上還進(jìn)行了一些性能優(yōu)化:
- 寫出采集數(shù)據(jù)使用
mmap
映射,并自定義二進(jìn)制格式保證順序讀寫弟晚。 - 提前對(duì)內(nèi)存節(jié)點(diǎn)進(jìn)行排序忘衍,建立邊引用關(guān)系時(shí)使用二分查找逾苫。通過(guò)位運(yùn)算對(duì)一些非法內(nèi)存地址進(jìn)行提前快速剪枝。
對(duì)于穩(wěn)定性部分枚钓,我們著重考慮了下面幾點(diǎn):
- 死鎖
由于無(wú)法保證 Objective-C 運(yùn)行時(shí)鎖的狀態(tài)铅搓,我們將需要通過(guò)運(yùn)行時(shí) api 獲取的信息在掛起線程前提前緩存。同時(shí)搀捷,為了保證libmalloc
鎖的狀態(tài)安全星掰,在掛起線程后我們對(duì) libmalloc 的鎖狀態(tài)進(jìn)行了判斷,如果已經(jīng)鎖住則恢復(fù)線程重新嘗試掛起嫩舟,避免堆死鎖氢烘。
- 非法內(nèi)存訪問(wèn)
在掛起所有其他線程后,為了減少采集本身分配的內(nèi)存對(duì)采集的影響家厌,我們使用了一個(gè)單獨(dú)的malloc_zone
管理采集模塊的內(nèi)存使用播玖。
性能損耗
因?yàn)樵跀?shù)據(jù)采集的時(shí)候需要掛起所有線程,會(huì)導(dǎo)致用戶感知到卡頓饭于,所以字節(jié)模塊還是有一定性能損耗的蜀踏,經(jīng)過(guò)我們測(cè)試,在iPhone8 Plus
設(shè)備上掰吕,App 占用 1G 內(nèi)存時(shí)果覆,采集用時(shí) 1.5-2 秒,采集時(shí)額外內(nèi)存消耗 10-20MB殖熟,生成的文件 zip 后大小在 5-20MB局待。
為了嚴(yán)格控制性能損耗,線上 Memory Graph 模塊會(huì)應(yīng)用以下策略菱属,避免太頻繁的觸發(fā)打擾用戶正常使用燎猛,避免自身內(nèi)存和磁盤等資源過(guò)多的占用:
穩(wěn)定性
該方案已經(jīng)在字節(jié)全系產(chǎn)品線上穩(wěn)定運(yùn)行了 6 個(gè)月以上,穩(wěn)定性和成功率得到了驗(yàn)證照皆,目前單次采集成功率可以達(dá)到 99.5%重绷,剩下的失敗基本都是由于內(nèi)存緊張?zhí)崆?OOM,考慮到大多數(shù)應(yīng)用只有不到千分之一的用戶會(huì)觸發(fā)采集膜毁,這種情況屬于極低概率事件昭卓。
試用路徑
目前,線上 Memory Graph 已搭載在字節(jié)跳動(dòng)火山引擎旗下應(yīng)用性能管理平臺(tái)(APMInsight)上賦能給外部開發(fā)者使用瘟滨。
APMInsight 的相關(guān)技術(shù)經(jīng)過(guò)今日頭條候醒、抖音、西瓜視頻等眾多應(yīng)用的打磨杂瘸,已沉淀出一套完整的解決方案倒淫,能夠定位移動(dòng)端、瀏覽器败玉、小程序等多端問(wèn)題敌土,除了支持崩潰镜硕、錯(cuò)誤、卡頓返干、網(wǎng)絡(luò)等基礎(chǔ)問(wèn)題的分析兴枯,還提供關(guān)聯(lián)到應(yīng)用啟動(dòng)、頁(yè)面瀏覽矩欠、內(nèi)存優(yōu)化的眾多功能财剖。目前 Demo 已開放大部分能力,歡迎各位注冊(cè)賬號(hào)試用:https://www.volcengine.cn/product/apminsight
加入我們
本技術(shù)方案由字節(jié)跳動(dòng) APM 中臺(tái)和抖音基礎(chǔ)技術(shù)團(tuán)隊(duì)深度合作聯(lián)合打造癌淮,歡迎對(duì)我們兩個(gè)團(tuán)隊(duì)感興趣的同學(xué)加入:
APM 中臺(tái)
字節(jié)跳動(dòng) APM 中臺(tái)目前致力于提升整個(gè)集團(tuán)內(nèi)全系產(chǎn)品的性能和穩(wěn)定性表現(xiàn)躺坟,技術(shù)棧覆蓋 iOS/Android/Flutter/Web/Hybrid/PC/游戲/小程序等,工作內(nèi)容包括但不限于線上監(jiān)控乳蓄,線上運(yùn)維瞳氓,深度優(yōu)化,線下防劣化等栓袖。長(zhǎng)期期望為業(yè)界輸出更多更有建設(shè)性的問(wèn)題發(fā)現(xiàn)和深度優(yōu)化手段。
歡迎各位有識(shí)之士加入我們店诗,一起為了“更快裹刮,更穩(wěn),更省庞瘸,更有品質(zhì)”的極致目標(biāo)攜手前行捧弃。我們?cè)诒本钲趦傻鼐姓衅感枨蟛聊遥?jiǎn)歷投遞郵箱:** tech@bytedance.com 违霞;郵件標(biāo)題:姓名 - 工作年限 - APM 中臺(tái) - 技術(shù)棧方向(如 iOS/Android/Web/后端)**。
抖音基礎(chǔ)技術(shù)
我們是負(fù)責(zé)抖音客戶端基礎(chǔ)能力研發(fā)和新技術(shù)探索的團(tuán)隊(duì)瞬场。我們?cè)诠こ?業(yè)務(wù)架構(gòu)买鸽,研發(fā)工具,編譯系統(tǒng)等方向深耕贯被,支撐業(yè)務(wù)快速迭代的同時(shí)眼五,保證超大規(guī)模團(tuán)隊(duì)的研發(fā)效能和工程質(zhì)量。在性能/穩(wěn)定性等方面不斷探索彤灶,努力為全球數(shù)億用戶提供最極致的基礎(chǔ)體驗(yàn)看幼。
如果你對(duì)技術(shù)充滿熱情,歡迎加入抖音基礎(chǔ)技術(shù)團(tuán)隊(duì)幌陕,讓我們共建億級(jí)全球化 App诵姜。目前我們?cè)谏虾!⒈本┎ā⒑贾菖锼簟⑸钲诰姓衅感枨笙境啵瑑?nèi)推可以聯(lián)系郵箱:** tech@bytedance.com** ;郵件標(biāo)題: **姓名 - 工作年限 - 抖音 - 基礎(chǔ)技術(shù) - iOS/Android **瑟俭。
參考文獻(xiàn)
[1] https://zhuanlan.zhihu.com/p/49829766
[2] http://satanwoo.github.io/2017/10/18/abort/
[3] https://jinxuebin.cn/2019/07/OOM底層原理探究/
[4] https://engineering.fb.com/ios/reducing-fooms-in-the-facebook-ios-app/