當應用出現崩潰的時候,程序員的第一反應確定是:在我這好好的,確定不是個人問題,不信我拿日誌來定位一下,因而千辛萬苦找出用戶日誌,符號表,提取出崩潰堆棧,拿命令開幹,折騰好一個多小時,拿到了下面的結果: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
那麼問題來了,嶽鷹上有這麼多的應用版本,再加上海量的日誌,對於Native崩潰,總不能每一個崩潰點都用addr2line或者相關的命令去符號化吧?
嶽鷹的符號化系統正是爲了解決該問題而設計。嶽鷹最初上線的版本1.0,支持同時符號化解析數量有限,對iOS符號化時依賴Mac系統,不支持容器化部署,消耗機器資源較多。
爲了更好的知足用戶業務需求,嶽鷹在年初啓動了2.0版本的改造,而且制定如下目標:
程序員
那這樣一個分佈式的符號化系統該如何設計呢?接下來小編就來詳細介紹下。
redis
結合當前系統設計以及業界常見方案,咱們有如下幾條路能夠走:
sql
結合嶽鷹2.0的目標,咱們對三個方案進行對比:緩存
對比項 | 方案1 | 方案2 | 方案3 |
---|---|---|---|
符號表入庫速度 | 快 | 快 | 慢 |
內存佔用 | 常態高 | 常態高 | 入庫時高 |
CPU佔用 | 常態高 | 常態高 | 入庫時高 |
安全性 | 低 | 低 | 高 |
可擴展性 | 低 | 低 | 高 |
部署複雜度 | 高 | 高 | 低 |
方案1:符號文件上傳卻是很快,若是須要高可用,還須要鏡像一份到備機,且在作addr2line的時候,會帶來高內存及高cpu的佔用,並且不支持動態擴容,安全性也幾乎沒有,拿到機器就拿到了源碼;安全
方案2:符號文件存放於中央存儲,作好備份機制後,能保障文件不會丟失,但機器在符號化時,都須要去中央存儲拉符號文件,以後的處理同方案1,查詢效率不高,並且安全性也不高;bash
方案3:在符號入庫時,把符號信息按key-value方式提取出來,而後加密存入hbase,這裏要解決符號表全量導出及入庫的速度及空間問題。數據結構
結合嶽鷹2.0目標,咱們對日誌處理的及時性,可擴展性,安全性,以及海量版本同時解析的要求,咱們選擇了方案3。下面咱們先給你們簡單介紹下原理,再深刻看看選擇方案3要解決哪些問題。併發
國際慣例,咱們先來了解一下原理,符號表是什麼?符號表是記錄着地址或者混淆代碼與源碼的對應關係表。下面咱們分別用一個小demo程序來說解符號表及符號化的過程。
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範圍內
複製代碼
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)
複製代碼
選擇方案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%的符號化成功率打下基礎。
改進先後的對比
改進前 | 改進後 | |
---|---|---|
應用場景 | 十幾個地址的符號化 | 批量的地址符號化 |
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,改造點主要有如下三點:
改造後,單核的QPS大約提高到800QPS,上面舉的500M的so符號的例子,大約須要15分鐘,基本能知足咱們的需求。
解決完提取的問題,接下來就是存儲的問題。
符號表都是通過精心設計的高度壓縮的數據結構,咱們經過上面的方案把它提取出kv的格式,容量上增長了10+倍,並且不少信息都是重複的,如函數名,文件名這些,雖然空間對於hbase來講不是什麼問題,可是在追求極致的面前,咱們還能夠再折騰折騰。
前面提到咱們由於要考慮數據的安全性,須要把存入hbase的數據作加密,因此不能直接用hbase自己的壓縮功能,要求在加密前先作好壓縮,若是是按行壓縮再加密,整體的壓縮比不會過高,咱們能夠把00006740~000069eb這一段當成一個大段,把它們壓縮在一塊兒再加密,這樣由於重複信息較多,壓縮比會很高,最終的體積能夠縮小5+倍,至關於只是比原始符號表大3~4倍。
hbase rowkey的設計,由於後面的查詢會須要用到scan,咱們把符號表kv的結束地址做爲rowkey的一部分,至於爲何這麼設計,往下讀,你就明白了。
根據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。
咱們知道,若是符號化失敗,就會出現不同的崩潰點,這樣就不能把這些崩潰點聚合在一塊兒,會把一些嚴重的問題分散掉,同時會產生不少新的崩潰點,致使開發,測試沒法分辨真實的崩潰狀況,咱們使用如下技術保障成功率:
經過幾個平臺的符號化反能力對比,咱們能夠看到嶽鷹2.0取得的階段性成果。
對比項 | 方案1 | 方案2 | 方案3 |
---|---|---|---|
符號表入庫速度 | 快 | 快 | 慢 |
內存佔用 | 常態高 | 常態高 | 入庫時高 |
CPU佔用 | 常態高 | 常態高 | 入庫時高 |
安全性 | 低 | 低 | 高 |
可擴展性 | 低 | 低 | 高 |
部署複雜度 | 高 | 高 | 低 |
指標 | 嶽鷹1.0到2.0的提高 |
---|---|
CPU核心數 | -50% |
平均CPU水位 | -40% |
內存 | 持平 |
符號入庫速度(OC) | +20% |
符號入庫速度(Java) | 持平 |
符號入庫速度(SO <= 100M) | 持平 |
符號入庫速度(SO > 100M) | 20分鐘之內 |
符號化響應速度 | 100ms -> 9ms |
容器化部署 | 全容器化 |