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 符號化原理
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++上的支持上明顯優於其它競品,開發者能快速地還原現場並找出問題,同時整個系統支持動態擴縮容,爲更多業務接入打下了堅實的基礎。