Netty源碼分析之自定義編解碼器崩潰堆棧還原技術大揭祕

0x00 前言

當應用出現崩潰的時候,程序員的第一反應肯定是:在我這好好的,肯定不是我的問題,不信我拿日誌來定位一下,於是千辛萬苦找出用戶日誌,符號表,提取出崩潰堆棧,拿命令開幹,折騰好一個多小時,拿到了下面的結果:

addr2line -ipfCe libxxx.so 007da904 007da9db 007d7895 00002605 007dbdf1
logging::Logging::~Logging() LINE: logging.cc:856
logging::ErrLogging::~ErrLogging() LINE: logging..cc:993
base::internal::XXXX::Free(int) LINE: scoped____.cc:54
base::___Generic<int, base::internal::_____loseTraits>::_____sary() LINE: scoped_______.h:153
base::___Generic<int, base::internal::_____loseTraits>::_____eric() LINE: scoped_______.h:90
複製代碼


如果是接入了嶽鷹全景監控平臺,場景就完全不一樣了。測試同學:發來一個鏈接,附言研發哥哥,這是你的bug,請注意查收。研發哥哥:點開鏈接,就可以在平臺看到這條崩潰信息啦,如下圖:

image.png

 

 


那麼問題來了,嶽鷹上有這麼多的應用版本,再加上海量的日誌,對於Native崩潰,總不能每個崩潰點都用addr2line或者相關的命令去符號化吧?
嶽鷹的符號化系統正是爲了解決該問題而設計。嶽鷹最初上線的版本1.0,支持同時符號化解析數量有限,對iOS符號化時依賴Mac系統,不支持容器化部署,消耗機器資源較多。
爲了更好的滿足用戶業務需求,嶽鷹在年初啓動了2.0版本的改造,並且制定以下目標:

  • 同時解析不限數量的符號表
  • 提升符號化的效率
  • 解除Mac系統依賴,支持全容器化部署


那這樣一個分佈式的符號化系統該如何設計呢?接下來小編就來詳細介紹下。

0x01 方案的選擇

結合當前系統設計以及業界常見方案,我們有以下幾條路可以走:

  1. 嶽鷹1.0方案,用大磁盤,高CPU性能的機器搭建符號化機器,符號文件存放到磁盤,需要符號化時再調用addr2line;
  2. 建立一箇中央存儲,把符號文件上傳到中央存儲,符號化機器需要符號化的時候再過去拉,然後用addr2line符號化;
  3. 把符號信息按key-value方式提取出來,存入hbase或者其它中間件,符號化時通過類sql查詢實現。

結合嶽鷹2.0的目標,我們對三個方案進行對比:

對比項 方案1 方案2 方案3
符號表入庫速度
內存佔用 常態高 常態高 入庫時高
CPU佔用 常態高 常態高 入庫時高
安全性
可擴展性
部署複雜度


方案1:符號文件上傳倒是很快,如果需要高可用,還需要鏡像一份到備機,且在做addr2line的時候,會帶來高內存及高cpu的佔用,而且不支持動態擴容,安全性也幾乎沒有,拿到機器就拿到了源碼;

方案2:符號文件存放於中央存儲,做好備份機制後,能保障文件不會丟失,但機器在符號化時,都需要去中央存儲拉符號文件,之後的處理同方案1,查詢效率不高,而且安全性也不高;

方案3:在符號入庫時,把符號信息按key-value方式提取出來,然後加密存入hbase,這裏要解決符號表全量導出及入庫的速度及空間問題。

結合嶽鷹2.0目標,我們對日誌處理的及時性,可擴展性,安全性,以及海量版本同時解析的要求,我們選擇了方案3。下面我們先給大家簡單介紹下原理,再深入看看選擇方案3要解決哪些問題。

0x02 原理(大神請忽略這一節)

國際慣例,我們先來了解一下原理,符號表是什麼?符號表是記錄着地址或者混淆代碼與源碼的對應關係表。下面我們分別用一個小demo程序來講解符號表及符號化的過程。

0x02-1 iOS-OC、Android-SO符號化原理

a.示例源碼:

int add(){
	int a = 1;
	a ++;
	int b = a+3;
	return b;
}
int div(){
	int a = 1;
	a ++;
	int b = a/0;                //這裏除0會引發崩潰
	return b;
}
int _tmain(int argc, _TCHAR* argv[]){
	add();
	sub();
	return 0;
}
複製代碼

b.對應符號表,這裏簡化了符號表,沒帶行號信息

0x00F913B0 ~ 0x00F913F0    add()
0x00F91410 ~ 0x00F91450    div()
0x00F91A90 ~ 0x00F91ACD    _tmain()
複製代碼

c.現有一崩潰堆棧

0x00F9143A
0x00F91AB0
複製代碼

d.進行符號化

0x00F9143A    div()    //查找符號表,地址0x00F9143A的符號名,在0x00F91410 ~ 0x00F91450範圍內
0x00F91AB0    _tmain() //查找符號表,地址0x00F91AB0的符號名,在0x00F91A90 ~ 0x00F91ACD範圍內
複製代碼

0x02-2 Android-Java 符號化原理

a.示例源碼:

package com.uc.meg.wpk
class User{
    int count;
    UserDTO userDto;
    UserDTO get(int id){...}
    int set(UserDTO userDto){...} 
}
class UserDTO{
    int id;
    String name;
}
複製代碼

b.符號表

com.a.b.c.d -> com.uc.meg.wpk.User
    int count -> a
    com.uc.meg.wpk.UserDTO -> b
    com.uc.meg.wpk.UserDTO get(int) -> c
    int set(com.uc.meg.wpk.UserDTO) -> d

com.a.b.c.e -> com.uc.meg.wpk.UserDTO
    int id -> a
    String name -> b
複製代碼

c.現有一崩潰堆棧

com.a.b.c.d.d(com.a.b.c.e)
複製代碼

d.進行符號化

//符號化com.a.b.c.d.d(com.a.b.c.e)    
//查找com.a.b.c.d, 命中com.uc.meg.wpk.User
//查找com.uc.meg.wpk.User.d 命中 set()
//查找com.a.b.c.e,命中 com.uc.meg.wpk.UserDTO
//符號化結果爲com.uc.meg.wpk.User.set(com.uc.meg.wpk.UserDTO) 
複製代碼

0x03 新的難題

選擇方案3後,主要瓶頸在符號表上傳之後處理,這裏主要工作是要把符號表轉換爲key-value,然後再寫入hbase。現在主流的app開發有android的java及C++,iOS的OC,我們下面主要討論這三種符號。
因爲android的java符號化有google的開源工具支持,這裏就不再展開。
OC因爲是iOS系統,封閉系統,標準統一,上架AppStrore的應用,只用XCode進行編譯,沒有各種定製的需求。我們原來有一個OC實現的符號表kv提取程序,但是隻能用於OSX系統,不便於線上佈署,所以我們選擇了用java重寫了提取符號kv的功能。
但是對於Android的C++庫so符號表,即ELF格式,存在着各種版本,各種定製下不同的編譯參數,會大幅增加用java重寫的成本,所以我們使用了Java跟C++結合的方式去實現ELF的符號表kv的提取,先用Java程序把ELF的基礎信息,地址表讀取出來,然後再用addr2line去遍歷這個地址表,然後再把結果存入hbase,這個爲100%的符號化成功率打下基礎。

0x03-1 addr2line的問題

改進前後的對比

  改進前 改進後
應用場景 十幾個地址的符號化 批量的地址符號化
QPS 50 800
地址傳遞方式 參數,有限長度 文件,無限長度
額外內存開銷 1 0.7
多任務模式 不支持 支持

當然,這個addr2line,是要經過改造才能達到我們的要求,原來的addr2line是給開發者以單條命令去使用,不是給程序做批量查詢的,每次查詢都是要把整個ELF文件加載到內存,像UC內核,還有一些遊戲的so文件,大小要到幾百M的級別,每個addr2line進程都要一份獨立的內存。假設一個500M的so符號,一臺64核的機器,假如用60核去100%跑addr2line,加上其它開銷,它就需要35G的內存。
面對這麼高的cpu和內存佔用,而且是一個較低的QPS,單核大約100QPS,我們也嘗試去優化addr2line的binutils中的bfd部分,但是最終的接口都是調用系統內核的,這條路,短期好像走不通。面對這樣的性能問題,期間也多次嘗試用Java去重寫這部分邏輯,但是最終結果只能實現與addr2line的90%匹配度,而且還有很多未知的兼容性問題,最後還是選擇了改造addr2line,改造點主要有以下三點:

  • 從文件讀取地址表,使用批量請求去addr2line,減少bfd初始化的次數,因爲這個過程中,bfd接口在調用一些特定的地址轉換後,會導致qps降到個位數,需要重啓進程纔行;
  • 減少額外的內存開銷;
  • 支持多進程,多容器分佈式任務調度,支持動態擴縮容,提高資源利用率。

改造後,單核的QPS大約提升到800QPS,上面舉的500M的so符號的例子,大約需要15分鐘,基本能滿足我們的需求。

0x03-2 存儲的問題

解決完提取的問題,接下來就是存儲的問題。
符號表都是經過精心設計的高度壓縮的數據結構,我們通過上面的方案把它提取出kv的格式,容量上增加了10+倍,而且很多信息都是重複的,如函數名,文件名這些,雖然空間對於hbase來說不是什麼問題,但是在追求極致的面前,我們還可以再折騰折騰。
前面提到我們因爲要考慮數據的安全性,需要把存入hbase的數據做加密,所以不能直接用hbase本身的壓縮功能,要求在加密前先做好壓縮,如果是按行壓縮再加密,總體的壓縮比不會太高,我們可以把00006740~000069eb這一段當成一個大段,把它們壓縮在一起再加密,這樣因爲重複信息較多,壓縮比會很高,最終的體積可以縮小5+倍,相當於只是比原始符號表大3~4倍。
hbase rowkey的設計,因爲後面的查詢會需要用到scan,我們把符號表kv的結束地址作爲rowkey的一部分,至於爲什麼這麼設計,往下讀,你就明白了。

0x03-3 查詢的問題

根據0x01原理,對hbase的查詢,需要get,scan的支持,get的話相對簡單,直接通過rowkey命中就好了,適用於java符號化的場景,對於C++/OC的符號化,就需要scan的支持,因爲地址是一個範圍,不能用get直接命中,下面用僞代碼舉例說明scan的流程:

//1. 掃描libxxx.so符號,地址範圍0x00001234 ~ 0xffffffff, 只取一條結果
//這裏利用了scan的特性,我們存的rowkey是符號的結束地址,所以掃描出的第一個,
//就是最接近0x00001234的一個符號
raw = scan("libxxx.so", 0x00001234, 0xffffffff, limit=1);
//2. 解密,解壓,判斷有效性預處理
data = pre(raw);
//3. 精確定位地址,根據0x04-2的打包存入,再做切割拆分
result = splitData(data);
複製代碼


舊系統我們只用了應用級的緩存,每次重啓緩存就會丟失,爲了減小hbase的壓力,我們增加一級分佈式緩存,使用redis作爲緩存,進一步減少了末端的查詢QPS。

0x03-4 如果保證100%的符號化成功率

我們知道,如果符號化失敗,就會出現不一樣的崩潰點,這樣就不能把這些崩潰點聚合在一起,會把一些嚴重的問題分散掉,同時會產生很多新的崩潰點,導致開發,測試無法分辨真實的崩潰情況,我們使用以下技術保障成功率:

  • 高併發,低延遲的符號化查詢服務,保障解析效率,防止超時出現符號化失敗的情況;
  • 多級緩存保障,減少hbase的scan操作;
  • 使用原生addr2line提取符號kv;
  • 重試機制。

0x04 總結

0x04-1 符號化系統的核心能力

通過幾個平臺的符號化反能力對比,我們可以看到嶽鷹2.0取得的階段性成果。

對比項 方案1 方案2 方案3
符號表入庫速度
內存佔用 常態高 常態高 入庫時高
CPU佔用 常態高 常態高 入庫時高
安全性
可擴展性
部署複雜度

0x04-2 運行效果的提升

指標 嶽鷹1.0到2.0的提升
CPU核心數 -50%
平均CPU水位 -40%
內存 持平
符號入庫速度(OC) +20%
符號入庫速度(Java) 持平
符號入庫速度(SO <= 100M) 持平
符號入庫速度(SO > 100M) 20分鐘以內
符號化響應速度 100ms -> 9ms
容器化部署 全容器化


## 0x05 歡迎免費試用 嶽鷹爲阿里集團衆多使用UC內核的app(如手淘,支付寶,天貓,釘釘,優酷等)提供內核so的崩潰符號化功能,實現了Java,Native C++的質量監控完整閉環,並在Native C++上的支持上明顯優於其它競品,開發者能快速地還原現場並找出問題,同時整個系統支持動態擴縮容,爲更多業務接入打下了堅實的基礎。
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章