作者 | 王海超
背景介紹
直播 OOM 問題比較棘手難以定位,主要體現(xiàn)在涉及的業(yè)務(wù)很多,從定位到解決花費(fèi)時間比較久。為了提前觸達(dá)問題,提高定位的效率,也是對現(xiàn)有工具的補(bǔ)充,提出直播內(nèi)存抖動解決方案- MemoryThrashing。
為什么要提出這個方案 ?
- 現(xiàn)有的 “MemoryGraph” 工具可以通過抓取的“MemoryGraph” 文件分析 OOM 成因,比如內(nèi)存泄漏、內(nèi)存占用過高導(dǎo)致的 OOM 問題,但因為性能開銷很大,所以是采樣上報且采樣率很低,不容易觸達(dá)問題, 只能定向?qū)σ阎脩糸_啟才行。期望自研一個工具,在內(nèi)存增長時可以發(fā)現(xiàn)問題,也能用于 OOM 發(fā)生后的分析,同時具備性能開銷小、全采樣的能力;
- “MemoryGraph” 生成時可能不是內(nèi)存高位,比如設(shè)備內(nèi)存是 4G,生成的“MemoryGraph” 可能是 1G,會影響 OOM 分析;
什么是 Thrashing(抖動) ?
維基百科中 thrashing 定義:
- In computer science, thrashing occurs when a computer's virtual memory resources are overused, leading to a constant state of paging and page faults, inhibiting most application-level processing.([1]) This causes the performance of the computer to degrade or collapse.
從業(yè)務(wù)視角定義內(nèi)存 thrashing:
通俗講就是性能數(shù)據(jù)有大的波動,拿內(nèi)存來講,當(dāng)內(nèi)存短時間從 600M 漲到 800M 叫作一個抖動。希望通過自研工具找出這 200 M 內(nèi)存增長來自于哪里,在實際的 OOM 案例中因內(nèi)存突增導(dǎo)致的 OOM 是比較常見的,具體現(xiàn)象如下:
- 內(nèi)存未回落:內(nèi)存突增一般發(fā)生在一兩分鐘內(nèi),內(nèi)存從 1G 漲到 3G,這部分內(nèi)存會一直滯留在內(nèi)存中不會被釋放掉或者沒有機(jī)會釋放掉直接 OOM,同時助高了內(nèi)存水位很容易發(fā)生 OOM 問題;
- 內(nèi)存回落:內(nèi)存突增到一定水位開始回落未形成 OOM,這種現(xiàn)象通常是內(nèi)存問題不夠劣化,或者機(jī)器本身內(nèi)存足夠大不容易 OOM,雖然沒有造成 OOM 但也是一個潛在的問題;
以臨時對象、內(nèi)存堆積為例來闡述如何定位該類問題,通過“AllocTime Summary” 描述臨時對象分配次數(shù),通過 “Memory Summary” 描述內(nèi)存堆積。
臨時對象
臨時對象:短時間分配大量對象,導(dǎo)致直播穩(wěn)定性波動較大,可能使內(nèi)存、CPU 負(fù)載變高。這類問題通常表現(xiàn)為短時間內(nèi)存沖高或者直接 OOM,或之后開始迅速回落到正常水位,這類對象不會駐留內(nèi)存過久,通過監(jiān)控 “臨時對象” 可以提前發(fā)現(xiàn)這類問題。
以上是按分配次數(shù)(AllocTime Summary)統(tǒng)計的 TOP 臨時對象,“AllocTime Summary 1” 代表第一次采樣 Class 的分配次數(shù)其它依次類推。舉例:通過 diff “AllocTime Summary 2” 與 “AllocTime Summary 1” 差值可知 “LivexxxA” 在采樣周期分配了 7803 次,由于未采集到 “Memory Summary” 信息,可認(rèn)為未有內(nèi)存駐留。
內(nèi)存堆積
內(nèi)存堆積:內(nèi)存駐留了大量對象,而且這類對象短時間不會釋放掉,導(dǎo)致內(nèi)存水位居高不下,很容易觸發(fā) OOM 問題。
以上是按內(nèi)存駐留統(tǒng)計的 TOP 實例,“Memory Summary 1” 代表第一次采樣實例數(shù)量的內(nèi)存駐留信息其他依次類推。舉例:通過 diff “Memory Summary 2” 與 “Memory Summary 1” 可知 “LivexxxA” 在采樣周期內(nèi)增長了 56791 個,根據(jù)最后一次采樣可知內(nèi)存駐留了總共 69904 個實例,通過采樣可知“LivexxxA” 每次都是遞增的。
MemoryThrashing 方案
方案調(diào)研
方案思路是做內(nèi)存差值找出增長,通過采樣多個時刻的內(nèi)存信息(目前主要監(jiān)控 Class 的實例個數(shù)), Diff 出內(nèi)存信息找出 TOP 增長,達(dá)到歸因的目的。
- 內(nèi)存區(qū):通過內(nèi)存節(jié)點遍歷統(tǒng)計 Class 實例個數(shù);
- Runtime:通過 alloc、dealloc 計數(shù)實現(xiàn)統(tǒng)計實例存活數(shù)量;
內(nèi)存區(qū)
通過內(nèi)存節(jié)點遍歷與已注冊的 Class 比較統(tǒng)計實例個數(shù),該方案的優(yōu)點是可以監(jiān)控整個 APP 的 OC 對象實例個數(shù),面對直播業(yè)務(wù)場景需不需監(jiān)控整個 APP 的對象,目前看暫時用不到,需求出發(fā)點是監(jiān)控直播場景且滿足一定條件。比如:直播觀播一段時間后內(nèi)存的大幅波動,場景比較聚焦。另一個考慮是如果當(dāng)前內(nèi)存比較大,遍歷 zone 會比較耗時,如果不掛起線程會有潛在的崩潰問題、以及數(shù)據(jù)不準(zhǔn)問題。
RunTime
通過 Hook 的方式,統(tǒng)計 Class 實例的分配、釋放次數(shù),達(dá)到記錄實例存活個數(shù)的目的,可監(jiān)控固定場景的 OC 實例增長情況,如直播間內(nèi)的內(nèi)存突增,范圍比較小不需要統(tǒng)計過多的無用對象。該方案相對內(nèi)存區(qū)遍歷耗時小,且不會有野指針問題。但需要注意的是監(jiān)控對象時對性能的影響,目前采用的是 RunTime 方案,從線下直播間測試情況看對主線程的影響忽略不計。
方案設(shè)計
在實際開發(fā)過程中發(fā)現(xiàn)對象的創(chuàng)建、釋放處于復(fù)雜的多線程環(huán)境中,處理不當(dāng)會對業(yè)務(wù)產(chǎn)生潛在的影響,影響到業(yè)務(wù)執(zhí)行效率或者造成穩(wěn)定性問題:
- 容器置于多線程下會有線程安全問題;
- 過度的使用鎖會阻塞業(yè)務(wù)代碼執(zhí)行,也可能觸發(fā) Watchdog 機(jī)制導(dǎo)致 APP 被 kill;
經(jīng)過優(yōu)化采用多級緩存方案解決主線程的性能開銷問題,達(dá)到主線程幾乎零開銷。
監(jiān)控流程
在進(jìn)入直播間一段時間后開啟監(jiān)控,通過監(jiān)控內(nèi)存值變化來區(qū)分是否開啟采樣功能,開啟采樣后會進(jìn)入連續(xù)多次采樣階段,多次采樣完成后進(jìn)行數(shù)據(jù)上報,上報完成后會繼續(xù)監(jiān)控內(nèi)存。
數(shù)據(jù)展示
在高熱直播間多次采樣的內(nèi)存快照,采集 TOP 100 數(shù)據(jù),以 “LivexxxA” 為例兩次采樣中第二次增長了 4125 個實例,可以簡單歸因 “LivexxxA” 相關(guān)業(yè)務(wù)導(dǎo)致 “MemoryThrashing”,可以從 “LivexxxA” 相關(guān)業(yè)務(wù)入手排查。
方案優(yōu)缺點
方案 | 優(yōu)點 | 缺點 |
“MemoryThrashing” | 可以多次采樣,對比內(nèi)存增長趨勢;性能開銷小,可線上全量;提前感知內(nèi)存問題;上手簡單,通過對象數(shù)量就可以排查問題; | 不支持多語言,只限于 oc 語言;不具備通過內(nèi)存節(jié)點關(guān)系分析內(nèi)存泄漏問題,只能找出堆積的對象;不具備分析多個內(nèi)存區(qū)的能力;Hook 方式影響方法緩存; |
“MemoryGraph” | 問題發(fā)現(xiàn)能力強(qiáng):可以通過內(nèi)存節(jié)點關(guān)系分析內(nèi)存泄漏導(dǎo)致的 OOM 問題;可以統(tǒng)計內(nèi)存區(qū)的內(nèi)存占用情況;適用多語言;上手復(fù)雜,需要梳理內(nèi)存節(jié)點引用關(guān)系; | 線程掛起會影響業(yè)務(wù)執(zhí)行,用戶感知明顯;內(nèi)存使用越高,內(nèi)存區(qū)遍歷越耗時;只能少量采樣; |
實踐案例
目前 “MemoryThrashing” 已經(jīng)部署了,可以監(jiān)控測試環(huán)境,后續(xù)將部署到線上。通過線下看提前暴露了很多問題,相對以往方式只有問題發(fā)生了或者產(chǎn)生了明顯影響才能感知到,需要 QA 反饋到 RD,通過“MemoryThrashing”大大提升了排查效率,很好的將劣化問題前置發(fā)現(xiàn),以下抽取其中兩個案例。
內(nèi)存堆積
如下,多個采樣周期內(nèi)出現(xiàn)了大量對象的分配問題,且這些對象未釋放,并且導(dǎo)致了內(nèi)存明顯上漲,采樣周期 3 比采樣周期 2 多分配了 234024 個對象,且最后內(nèi)存駐留了 238800 個 “LivexxxBigDataRead” 對象,占用內(nèi)存 10.9M。
臨時對象
如下,是開播場景抓到的問題,在主播端開啟彈幕狂歡時,過 Effect 認(rèn)出人臉后,就會創(chuàng)建一個對應(yīng)的輪廓模型給到中臺去畫輪廓,頻率會很高,每 5 秒周期(實際時間更小)臨時對象增量高峰可到 6w 個(后兩次采樣差值),由于未生成 “Memory Summary” 信息可認(rèn)為未駐留內(nèi)存 ,累計過百萬次對象分配,對開播性能會產(chǎn)生直接影響:
未來規(guī)劃
歸因能力
只統(tǒng)計 OC 對象數(shù)據(jù)在某些情況下可能不夠,比如公共基礎(chǔ)對象異常增長,則沒有辦法追蹤到具體成因,如果帶有對象引用關(guān)系可以進(jìn)一步鎖定問題。當(dāng)然這些都是對 “Memory Graph” 能力的補(bǔ)充,如果“Memory Graph” 已經(jīng)抓到了數(shù)據(jù),可以結(jié)合“Memory Graph” 鎖定對象引用鏈路繼而找到業(yè)務(wù)。
- “MemoryThrashing” 可以加上對象引用關(guān)系計算,從效率上講沒必要對所有的對象查找其引用關(guān)系,查找引用關(guān)系是比較耗時的。只需查找 TOP 增長點的關(guān)鍵對象引用關(guān)系,實測可能只需要查找?guī)讉€對象的引用關(guān)系。
- 通過線程堆棧采樣記錄信息;
CPU 監(jiān)控
根據(jù)以往案例如:OOM、ANR 有不少會伴隨著高 CPU 使用率,比如某次案例由大量數(shù)據(jù)處理導(dǎo)致的 OOM 問題,經(jīng)排查發(fā)現(xiàn)負(fù)責(zé)該業(yè)務(wù)處理的線程 CPU 使用率很高,所以通過監(jiān)控線程 CPU 使用率,來補(bǔ)充監(jiān)控顯得很有必要,可以通過線程名字、堆棧, 鎖定懷疑的業(yè)務(wù)。