前言
mars 是微信官方的終端基礎(chǔ)組件,是一個使用 C++ 編寫的業(yè)務(wù)性無關(guān),平臺性無關(guān)的基礎(chǔ)組件。目前已接入微信 Android、iOS、Mac、Windows、WP 等客戶端?,F(xiàn)正在籌備開源中,它主要包括以下幾個部分:
comm:可以獨立使用的公共庫,包括 socket、線程、消息隊列等
xlog:可以獨立使用的日志模塊
sdt:可以獨立使用的網(wǎng)絡(luò)診斷模塊
stn:可以獨立使用的信令分發(fā)網(wǎng)路模塊
本文章是 mars 系列的第一篇:高性能跨平臺日志模塊。
正文
對于移動開發(fā)者來說,最大的尷尬莫過于用戶反饋程序出現(xiàn)問題,但因為不能重現(xiàn)且沒有日志無法定位具體原因。這樣看來客戶端日志頗有點“養(yǎng)兵千日,用兵一時”的感覺,只有當(dāng)出現(xiàn)問題且不容易重現(xiàn)時才能體現(xiàn)它的重要作用。為了保證關(guān)鍵時刻有日志可用,就需要保證程序整個生命周期內(nèi)都要打日志,所以日志方案的選擇至關(guān)重要。
常規(guī)方案
方案描述: 對每一行日志加密寫文件
例如 Android 平臺使用 java 實現(xiàn)日志模塊,每有一句日志就加密寫進文件。這樣在使用過程中不僅存在大量的 GC,更致命的是因為有大量的 IO 需要寫入,影響程序性能很容易導(dǎo)致程序卡頓。選擇這種方案,在 release 版本只能選擇把日志關(guān)掉。當(dāng)有用戶反饋時,就需要給用戶重新編一個打開日志的安裝包,用戶重新安裝重現(xiàn)后再通過日志來定位問題。不僅定位問題的效率低下,而且并不能保證每個需要定位的問題都能重現(xiàn)。這個方案可以說主要是為程序發(fā)布前服務(wù)的。
來看一下直接寫文件為什么會導(dǎo)致程序卡頓

當(dāng)寫文件的時候,并不是把數(shù)據(jù)直接寫入了磁盤,而是先把數(shù)據(jù)寫入到系統(tǒng)的緩存(dirty page)中,系統(tǒng)一般會在下面幾種情況把 dirty page 寫入到磁盤:
定時回寫,相關(guān)變量在/proc/sys/vm/dirty_writeback_centisecs和/proc/sys/vm/dirty_expire_centisecs中定義。
調(diào)用 write 的時候,發(fā)現(xiàn) dirty page 占用內(nèi)存超過系統(tǒng)內(nèi)存一定比例,相關(guān)變量在/proc/sys/vm/dirty_background_ratio( 后臺運行不阻塞 write)和/proc/sys/vm/dirty_ratio(阻塞 write)中定義。
內(nèi)存不足。
數(shù)據(jù)從程序?qū)懭氲酱疟P的過程中,其實牽涉到兩次數(shù)據(jù)拷貝:一次是用戶空間內(nèi)存拷貝到內(nèi)核空間的緩存,一次是回寫時內(nèi)核空間的緩存到硬盤的拷貝。當(dāng)發(fā)生回寫時也涉及到了內(nèi)核空間和用戶空間頻繁切換。
dirty page 回寫的時機對應(yīng)用層來說又是不可控的,所以性能瓶頸就出現(xiàn)了。
這個方案存在的最主要的問題:因為性能影響了程序的 流暢性 。對于一個 App 來說,流暢性尤為重要,因為流暢性直接影響用戶體驗,最基本的流暢性的保證是使用了日志不會導(dǎo)致卡頓,但是流暢性不僅包括了系統(tǒng)沒有卡頓,還要盡量保證沒有 CPU 峰值。所以一個優(yōu)秀的日志模塊必須保證 流暢性 :
不能影響程序的性能。最基本的保證是使用了日志不會導(dǎo)致程序卡頓
我覺得絕大部分人不會選擇這一個方案。
進一步思考
在上個方案中,因為要寫入大量的 IO 導(dǎo)致程序卡頓,那是否可以先把日志緩存到內(nèi)存中,當(dāng)?shù)揭欢ù笮r再加密寫進文件,為了進一步減少需要加密和寫入的數(shù)據(jù),在加密之前可以先進行壓縮。至于 Android 下存在頻繁 GC 的問題,可以使用 C++ 來實現(xiàn)進行避免,而且通過 C++ 可以實現(xiàn)一個平臺性無關(guān)的日志模塊。
方案描述: 把日志寫入到作為 log 中間 buffer 的內(nèi)存中,達(dá)到一定條件后壓縮加密寫進文件。
這個方案的整體的流程圖:

這個方案基本可以解決 release 版本因為流暢性不敢打日志的問題,并且對于流暢性解決了最主要的部分:由于寫日志導(dǎo)致的程序卡頓的問題。但是因為壓縮不是 realtime compress,所以仍然存在 CPU 峰值。但這個方案卻存在一個致命的問題:丟日志。
理想中的情況:當(dāng)程序 crash 時, crash 捕捉模塊捕捉到 crash, 然后調(diào)用日志接口把內(nèi)存中的日志刷到文件中。但是實際使用中會發(fā)現(xiàn)程序被系統(tǒng)殺死不會有事件通知,而且很多異常退出,crash 捕捉模塊并不一定能捕捉到。而這兩種情況恰恰是平時跟進的重點,因為沒有 crash 堆棧輔助定位問題,所以丟日志的問題這個時候顯得尤為凸顯。