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