0x00 前言
當(dāng)應(yīng)用出現(xiàn)崩潰的時(shí)候枢纠,程序員的第一反應(yīng)肯定是:在我這好好的,肯定不是我的問題黎棠,不信我拿日志來定位一下晋渺,于是千辛萬苦找出用戶日志,符號(hào)表脓斩,提取出崩潰堆棧木西,拿命令開干,折騰好一個(gè)多小時(shí)随静,拿到了下面的結(jié)果:
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
如果是接入了岳鷹全景監(jiān)控平臺(tái)八千,場(chǎng)景就完全不一樣了。測(cè)試同學(xué):發(fā)來一個(gè)鏈接燎猛,附言研發(fā)哥哥恋捆,這是你的bug,請(qǐng)注意查收重绷。研發(fā)哥哥:點(diǎn)開鏈接沸停,就可以在平臺(tái)看到這條崩潰信息啦,如下圖:
那么問題來了昭卓,岳鷹上有這么多的應(yīng)用版本愤钾,再加上海量的日志,對(duì)于Native崩潰候醒,總不能每個(gè)崩潰點(diǎn)都用addr2line或者相關(guān)的命令去符號(hào)化吧绰垂?
岳鷹的符號(hào)化系統(tǒng)正是為了解決該問題而設(shè)計(jì)。岳鷹最初上線的版本1.0火焰,支持同時(shí)符號(hào)化解析數(shù)量有限劲装,對(duì)iOS符號(hào)化時(shí)依賴Mac系統(tǒng),不支持容器化部署昌简,消耗機(jī)器資源較多占业。為了更好的滿足用戶業(yè)務(wù)需求,岳鷹在年初啟動(dòng)了2.0版本的改造纯赎,并且制定以下目標(biāo):
同時(shí)解析不限數(shù)量的符號(hào)表
提升符號(hào)化的效率
解除Mac系統(tǒng)依賴谦疾,支持全容器化部署
那這樣一個(gè)分布式的符號(hào)化系統(tǒng)該如何設(shè)計(jì)呢?接下來小編就來詳細(xì)介紹下犬金。
0x01 方案的選擇
結(jié)合當(dāng)前系統(tǒng)設(shè)計(jì)以及業(yè)界常見方案念恍,我們有以下幾條路可以走:
岳鷹1.0方案六剥,用大磁盤,高CPU性能的機(jī)器搭建符號(hào)化機(jī)器峰伙,符號(hào)文件存放到磁盤疗疟,需要符號(hào)化時(shí)再調(diào)用addr2line;
建立一個(gè)中央存儲(chǔ)瞳氓,把符號(hào)文件上傳到中央存儲(chǔ)策彤,符號(hào)化機(jī)器需要符號(hào)化的時(shí)候再過去拉,然后用addr2line符號(hào)化匣摘;
把符號(hào)信息按key-value方式提取出來店诗,存入hbase或者其它中間件,符號(hào)化時(shí)通過類sql查詢實(shí)現(xiàn)音榜。
結(jié)合岳鷹2.0的目標(biāo)庞瘸,我們對(duì)三個(gè)方案進(jìn)行對(duì)比:
方案1:符號(hào)文件上傳倒是很快,如果需要高可用赠叼,還需要鏡像一份到備機(jī)恕洲,且在做addr2line的時(shí)候,會(huì)帶來高內(nèi)存及高cpu的占用梅割,而且不支持動(dòng)態(tài)擴(kuò)容霜第,安全性也幾乎沒有,拿到機(jī)器就拿到了源碼户辞;
方案2:符號(hào)文件存放于中央存儲(chǔ)泌类,做好備份機(jī)制后,能保障文件不會(huì)丟失底燎,但機(jī)器在符號(hào)化時(shí)刃榨,都需要去中央存儲(chǔ)拉符號(hào)文件,之后的處理同方案1双仍,查詢效率不高枢希,而且安全性也不高;
方案3:在符號(hào)入庫時(shí)朱沃,把符號(hào)信息按key-value方式提取出來苞轿,然后加密存入hbase,這里要解決符號(hào)表全量導(dǎo)出及入庫的速度及空間問題逗物。
結(jié)合岳鷹2.0目標(biāo)搬卒,我們對(duì)日志處理的及時(shí)性,可擴(kuò)展性翎卓,安全性契邀,以及海量版本同時(shí)解析的要求,我們選擇了方案3失暴。下面我們先給大家簡(jiǎn)單介紹下原理坯门,再深入看看選擇方案3要解決哪些問題微饥。
0x02 原理(大神請(qǐng)忽略這一節(jié))
國際慣例,我們先來了解一下原理古戴,符號(hào)表是什么欠橘?符號(hào)表是記錄著地址或者混淆代碼與源碼的對(duì)應(yīng)關(guān)系表。下面我們分別用一個(gè)小demo程序來講解符號(hào)表及符號(hào)化的過程允瞧。
0x02-1 iOS-OC、Android-SO符號(hào)化原理
a.示例源碼:
int add(){
int a = 1;
a ++;
int b = a+3;
return b;
}
int div(){
int a = 1;
a ++;
int b = a/0; //這里除0會(huì)引發(fā)崩潰
return b;
}
int _tmain(int argc, _TCHAR* argv[]){
add();
sub();
return 0;
}
b.對(duì)應(yīng)符號(hào)表蛮拔,這里簡(jiǎn)化了符號(hào)表述暂,沒帶行號(hào)信息
0x00F913B0 ~ 0x00F913F0 add()
0x00F91410 ~ 0x00F91450 div()
0x00F91A90 ~ 0x00F91ACD _tmain()
c.現(xiàn)有一崩潰堆棧
0x00F9143A
0x00F91AB0
d.進(jìn)行符號(hào)化
0x00F9143A div() //查找符號(hào)表,地址0x00F9143A的符號(hào)名建炫,在0x00F91410 ~ 0x00F91450范圍內(nèi)
0x00F91AB0 _tmain() //查找符號(hào)表畦韭,地址0x00F91AB0的符號(hào)名,在0x00F91A90 ~ 0x00F91ACD范圍內(nèi)
<a name="3dba1efe"></a>
0x02-2 Android-Java 符號(hào)化原理
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.符號(hào)表
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.現(xiàn)有一崩潰堆棧
com.a.b.c.d.d(com.a.b.c.e)
d.進(jìn)行符號(hào)化
//符號(hào)化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
//符號(hào)化結(jié)果為com.uc.meg.wpk.User.set(com.uc.meg.wpk.UserDTO)
<a name="0a3a7298"></a>
0x03 新的難題
選擇方案3后,主要瓶頸在符號(hào)表上傳之后處理衍慎,這里主要工作是要把符號(hào)表轉(zhuǎn)換為key-value转唉,然后再寫入hbase。現(xiàn)在主流的app開發(fā)有android的java及C++稳捆,iOS的OC赠法,我們下面主要討論這三種符號(hào)。因?yàn)閍ndroid的java符號(hào)化有g(shù)oogle的開源工具支持乔夯,這里就不再展開砖织。OC因?yàn)槭莍OS系統(tǒng),封閉系統(tǒng)末荐,標(biāo)準(zhǔn)統(tǒng)一侧纯,上架AppStrore的應(yīng)用,只用XCode進(jìn)行編譯甲脏,沒有各種定制的需求眶熬。我們?cè)瓉碛幸粋€(gè)OC實(shí)現(xiàn)的符號(hào)表kv提取程序,但是只能用于OSX系統(tǒng)块请,不便于線上布署聋涨,所以我們選擇了用java重寫了提取符號(hào)kv的功能。但是對(duì)于Android的C++庫so符號(hào)表负乡,即ELF格式牍白,存在著各種版本,各種定制下不同的編譯參數(shù)抖棘,會(huì)大幅增加用java重寫的成本茂腥,所以我們使用了Java跟C++結(jié)合的方式去實(shí)現(xiàn)ELF的符號(hào)表kv的提取狸涌,先用Java程序把ELF的基礎(chǔ)信息,地址表讀取出來最岗,然后再用addr2line去遍歷這個(gè)地址表帕胆,然后再把結(jié)果存入hbase,這個(gè)為100%的符號(hào)化成功率打下基礎(chǔ)般渡。
0x03-1 addr2line的問題
改進(jìn)前后的對(duì)比
當(dāng)然懒豹,這個(gè)addr2line,是要經(jīng)過改造才能達(dá)到我們的要求驯用,原來的addr2line是給開發(fā)者以單條命令去使用脸秽,不是給程序做批量查詢的,每次查詢都是要把整個(gè)ELF文件加載到內(nèi)存蝴乔,像UC內(nèi)核记餐,還有一些游戲的so文件,大小要到幾百M(fèi)的級(jí)別薇正,每個(gè)addr2line進(jìn)程都要一份獨(dú)立的內(nèi)存片酝。假設(shè)一個(gè)500M的so符號(hào),一臺(tái)64核的機(jī)器挖腰,假如用60核去100%跑addr2line雕沿,加上其它開銷,它就需要35G的內(nèi)存猴仑。面對(duì)這么高的cpu和內(nèi)存占用晦炊,而且是一個(gè)較低的QPS,單核大約100QPS宁脊,我們也嘗試去優(yōu)化addr2line的binutils中的bfd部分断国,但是最終的接口都是調(diào)用系統(tǒng)內(nèi)核的,這條路榆苞,短期好像走不通稳衬。面對(duì)這樣的性能問題,期間也多次嘗試用Java去重寫這部分邏輯坐漏,但是最終結(jié)果只能實(shí)現(xiàn)與addr2line的90%匹配度薄疚,而且還有很多未知的兼容性問題,最后還是選擇了改造addr2line赊琳,改造點(diǎn)主要有以下三點(diǎn):
從文件讀取地址表街夭,使用批量請(qǐng)求去addr2line,減少bfd初始化的次數(shù)躏筏,因?yàn)檫@個(gè)過程中板丽,bfd接口在調(diào)用一些特定的地址轉(zhuǎn)換后,會(huì)導(dǎo)致qps降到個(gè)位數(shù),需要重啟進(jìn)程才行埃碱;
減少額外的內(nèi)存開銷猖辫;
支持多進(jìn)程,多容器分布式任務(wù)調(diào)度砚殿,支持動(dòng)態(tài)擴(kuò)縮容啃憎,提高資源利用率。
改造后似炎,單核的QPS大約提升到800QPS辛萍,上面舉的500M的so符號(hào)的例子,大約需要15分鐘羡藐,基本能滿足我們的需求贩毕。
0x03-2 存儲(chǔ)的問題
解決完提取的問題,接下來就是存儲(chǔ)的問題传睹。符號(hào)表都是經(jīng)過精心設(shè)計(jì)的高度壓縮的數(shù)據(jù)結(jié)構(gòu)耳幢,我們通過上面的方案把它提取出kv的格式岸晦,容量上增加了10+倍欧啤,而且很多信息都是重復(fù)的,如函數(shù)名启上,文件名這些邢隧,雖然空間對(duì)于hbase來說不是什么問題,但是在追求極致的面前冈在,我們還可以再折騰折騰倒慧。前面提到我們因?yàn)橐紤]數(shù)據(jù)的安全性,需要把存入hbase的數(shù)據(jù)做加密包券,所以不能直接用hbase本身的壓縮功能纫谅,要求在加密前先做好壓縮,如果是按行壓縮再加密溅固,總體的壓縮比不會(huì)太高付秕,我們可以把00006740000069eb這一段當(dāng)成一個(gè)大段,把它們壓縮在一起再加密侍郭,這樣因?yàn)橹貜?fù)信息較多询吴,壓縮比會(huì)很高,最終的體積可以縮小5+倍亮元,相當(dāng)于只是比原始符號(hào)表大34倍猛计。hbase rowkey的設(shè)計(jì),因?yàn)楹竺娴牟樵儠?huì)需要用到scan爆捞,我們把符號(hào)表kv的結(jié)束地址作為rowkey的一部分奉瘤,至于為什么這么設(shè)計(jì),往下讀煮甥,你就明白了毛好。
0x03-3 查詢的問題
根據(jù)0x01原理望艺,對(duì)hbase的查詢,需要get肌访,scan的支持找默,get的話相對(duì)簡(jiǎn)單,直接通過rowkey命中就好了吼驶,適用于java符號(hào)化的場(chǎng)景惩激,對(duì)于C++/OC的符號(hào)化,就需要scan的支持蟹演,因?yàn)榈刂肥且粋€(gè)范圍风钻,不能用get直接命中,下面用偽代碼舉例說明scan的流程:
//1. 掃描libxxx.so符號(hào)酒请,地址范圍0x00001234 ~ 0xffffffff骡技, 只取一條結(jié)果
//這里利用了scan的特性,我們存的rowkey是符號(hào)的結(jié)束地址羞反,所以掃描出的第一個(gè)布朦,
//就是最接近0x00001234的一個(gè)符號(hào)
raw = scan("libxxx.so", 0x00001234, 0xffffffff, limit=1);
//2. 解密,解壓昼窗,判斷有效性預(yù)處理
data = pre(raw);
//3. 精確定位地址是趴,根據(jù)0x04-2的打包存入,再做切割拆分
result = splitData(data);
舊系統(tǒng)我們只用了應(yīng)用級(jí)的緩存澄惊,每次重啟緩存就會(huì)丟失唆途,為了減小hbase的壓力,我們?cè)黾右患?jí)分布式緩存掸驱,使用redis作為緩存肛搬,進(jìn)一步減少了末端的查詢QPS。
0x03-4 如果保證100%的符號(hào)化成功率
我們知道毕贼,如果符號(hào)化失敗温赔,就會(huì)出現(xiàn)不一樣的崩潰點(diǎn),這樣就不能把這些崩潰點(diǎn)聚合在一起帅刀,會(huì)把一些嚴(yán)重的問題分散掉让腹,同時(shí)會(huì)產(chǎn)生很多新的崩潰點(diǎn),導(dǎo)致開發(fā)扣溺,測(cè)試無法分辨真實(shí)的崩潰情況骇窍,我們使用以下技術(shù)保障成功率:
高并發(fā),低延遲的符號(hào)化查詢服務(wù)锥余,保障解析效率腹纳,防止超時(shí)出現(xiàn)符號(hào)化失敗的情況;
多級(jí)緩存保障,減少hbase的scan操作嘲恍;
使用原生addr2line提取符號(hào)kv足画;
重試機(jī)制。
<a name="bb113271"></a>
0x04 總結(jié)
0x04-1 符號(hào)化系統(tǒng)的核心能力
通過幾個(gè)平臺(tái)的符號(hào)化反能力對(duì)比佃牛,我們可以看到岳鷹2.0取得的階段性成果淹辞。
0x04-2 運(yùn)行效果的提升
0x05 歡迎免費(fèi)試用
岳鷹為阿里集團(tuán)眾多使用UC內(nèi)核的app(如手淘,支付寶俘侠,天貓象缀,釘釘,優(yōu)酷等)提供內(nèi)核so的崩潰符號(hào)化功能爷速,實(shí)現(xiàn)了Java央星,Native C++的質(zhì)量監(jiān)控完整閉環(huán),并在Native C++上的支持上明顯優(yōu)于其它競(jìng)品惫东,開發(fā)者能快速地還原現(xiàn)場(chǎng)并找出問題莉给,同時(shí)整個(gè)系統(tǒng)支持動(dòng)態(tài)擴(kuò)縮容,為更多業(yè)務(wù)接入打下了堅(jiān)實(shí)的基礎(chǔ)廉沮。更多功能颓遏,歡迎來岳鷹全景監(jiān)控平臺(tái)平臺(tái)體驗(yàn)