崩潰堆棧還原技術大揭祕

0x00 前言

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

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,請注意查收。研發哥哥:點開連接,就能夠在平臺看到這條崩潰信息啦,以下圖: android

image.png


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

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


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

0x01 方案的選擇

結合當前系統設計以及業界常見方案,咱們有如下幾條路能夠走:
sql

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

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

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


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

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

方案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++上的支持上明顯優於其它競品,開發者能快速地還原現場並找出問題,同時整個系統支持動態擴縮容,爲更多業務接入打下了堅實的基礎。 更多功能,歡迎來[嶽鷹全景監控平臺](https://yueying.effirst.com/?vf=juejin)體驗。
相關文章
相關標籤/搜索