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,請注意查收。研發哥哥:點開連結,就可以在平台看到這條崩潰資訊啦,如下圖:

那麼問題來了,嶽鷹上有這麼多的應用版本,再加上海量的日志,對于Native崩潰,總不能每個崩潰點都用addr2line或者相關的指令去符号化吧?
嶽鷹的符号化系統正是為了解決該問題而設計。嶽鷹最初上線的版本1.0,支援同時符号化解析數量有限,對iOS符号化時依賴Mac系統,不支援容器化部署,消耗機器資源較多。
為了更好的滿足使用者業務需求,嶽鷹在年初啟動了2.0版本的改造,并且制定以下目标:
- 同時解析不限數量的符号表
- 提升符号化的效率
- 解除Mac系統依賴,支援全容器化部署
那這樣一個分布式的符号化系統該如何設計呢?接下來小編就來詳細介紹下。
0x01 方案的選擇
結合目前系統設計以及業界常見方案,我們有以下幾條路可以走:
- 嶽鷹1.0方案,用大磁盤,高CPU性能的機器搭建符号化機器,符号檔案存放到磁盤,需要符号化時再調用addr2line;
- 建立一個中央存儲,把符号檔案上傳到中央存儲,符号化機器需要符号化的時候再過去拉,然後用addr2line符号化;
- 把符号資訊按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 符号化原理
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
com.a.b.c.d.d(com.a.b.c.e)
//符号化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取得的階段性成果。
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++上的支援上明顯優于其它競品,開發者能快速地還原現場并找出問題,同時整個系統支援動态擴縮容,為更多業務接入打下了堅實的基礎。
更多功能,歡迎來
嶽鷹平台體驗。