京東零售云mPaaS移動端日志回撈探索實踐
1.1. 引言
移動操作系統(tǒng)為開發(fā)者提供了功能豐富的日志組件,比如說Android Studio 中的Logcat窗口會顯示系統(tǒng)消息,例如在進行垃圾回收時顯示的消息,以及使用Log類添加到應(yīng)用的消息, 能夠輔助開發(fā)者進行高效的開發(fā)工作。然而在生產(chǎn)環(huán)境中,當用戶(或者老板)反饋一些問題,又比較冷僻難以復(fù)現(xiàn)的時候(不是Crash),常常就會陷入一籌莫展的境地。此時,借助線上異常數(shù)據(jù)實時上報,我們只能是祈禱用戶網(wǎng)絡(luò)環(huán)境通暢,能夠及時把異常數(shù)據(jù)第一時間上報上來,然而這種做法并不能保證我們永遠那么幸運。
于是,我們需要研制一款性能較高的移動日志系統(tǒng)來解決我們當下的難題,該系統(tǒng)能具備日志信息完整、性能損耗低、輕量級(體積)、精確回撈的特點。 接下來介紹一下移動日志系統(tǒng)的研發(fā)歷程。
1.2. 設(shè)計方案
移動日志系統(tǒng)使用了Linux系統(tǒng)中提供的mmap作為日志文件的載體,目前業(yè)內(nèi)流行的XLOG日志組件、MMKV、美團Logan均采用了此方案,其最大的優(yōu)勢就是高效I/O、低損耗、跨進程 等優(yōu)勢,接下來引入下mmap的基本介紹。
1.2.1. 什么是mmap?
操作系統(tǒng)分為內(nèi)核態(tài)和用戶態(tài)兩種運行模式:
- 內(nèi)核態(tài)(Kernel MODE)能夠運行操作系統(tǒng)程序 用戶態(tài)(User MODE)能夠運行用戶程序
- 用戶態(tài)(即應(yīng)用程序)是不能直接對物理設(shè)備進行操作的(Ps:對物理設(shè)備進行操作,即對設(shè)備的物理地址寫數(shù)據(jù))。如果想讀取硬盤上的某一段數(shù)據(jù)通常都需要經(jīng)過 硬盤->內(nèi)核->用戶,即數(shù)據(jù)需要經(jīng)歷兩次拷貝,效率十分低下。 為了解決這樣的問題,內(nèi)存映射的概念出現(xiàn)了:內(nèi)核映射即mmap,mmap將設(shè)備的物理地址映射到進程的虛擬地址,則用戶操作虛擬內(nèi)存時就相當于對物理設(shè)備進行操作了,減少了內(nèi)核到用戶的一次數(shù)據(jù)拷貝,從而提高數(shù)據(jù)的吞吐率。
在Linux中可以使用mmap用來在進程虛擬內(nèi)存地址空間中分配地址空間,創(chuàng)建和物理內(nèi)存的映射關(guān)系 :
當使用mmap映射文件到進程后,就可以直接操作這段虛擬地址進行文件的讀寫等操作,不必再調(diào)用read,write等系統(tǒng)調(diào)用。但需注意,直接對該段內(nèi)存寫時不會寫入超過當前文件大小的內(nèi)容。
總之,mmap區(qū)別于以往的文件讀寫,具備以下幾個優(yōu)點:
- 減少了數(shù)據(jù)的拷貝次數(shù),用內(nèi)存讀寫取代I/O讀寫,提高了文件讀取效率
- 實現(xiàn)了用戶空間和內(nèi)核空間的高效交互方式。兩空間的各自修改操作可以直接反映在映射的區(qū)域內(nèi),從而被對方空間及時捕捉
- 提供進程間共享內(nèi)存及相互通信的方式。不管是父子進程還是無親緣關(guān)系的進程,都可以將自身用戶空間映射到同一個文件或匿名映射到同一片區(qū)域。從而通過各自對映射區(qū)域的改動,達到進程間通信和進程間共享的目的
- 同時,如果進程A和進程B都映射了區(qū)域C,當A第一次讀取C時通過缺頁從磁盤復(fù)制文件頁到內(nèi)存中;但當B再讀C的相同頁面時,雖然也會產(chǎn)生缺頁異常,但是不再需要從磁盤中復(fù)制文件過來,而可直接使用已經(jīng)保存在內(nèi)存中的文件數(shù)據(jù)
- 可用于實現(xiàn)高效的大規(guī)模數(shù)據(jù)傳輸。內(nèi)存空間不足,是制約大數(shù)據(jù)操作的一個方面,解決方案往往是借助硬盤空間協(xié)助操作,補充內(nèi)存的不足。但是進一步會造成大量的文件I/O操作,極大影響效率。這個問題可以通過mmap映射很好的解決。換句話說,但凡是需要用磁盤空間代替內(nèi)存的時候,mmap都可以發(fā)揮其功效
1.2.2. mmap的使用
對于移動端日志采集SDK來說,主要進行的工作就是將用戶寫入的數(shù)據(jù)保存到文件中,在這個過程中涉及到在native層調(diào)用mmap函數(shù)實現(xiàn)在進程虛擬內(nèi)存地址空間中分配地址空間,創(chuàng)建和物理內(nèi)存的映射關(guān)系。
接下來介紹一下Linux系統(tǒng)中mmap機制的使用流程:
mmap函數(shù)
- 函數(shù)聲明
- void* mmap(void* __addr, size_t __size, int __prot, int __flags, int __fd, off_t __offset);
- 返回值說明
成功執(zhí)行時,mmap()返回被映射區(qū)的指針。失敗時,mmap()返回MAP_FAILED[其值為(void *)-1],error被設(shè)為以下的某個值:
- EACCES:訪問出錯
- EAGAIN:文件已被鎖定,或者太多的內(nèi)存已被鎖定
- EBADF:fd不是有效的文件描述詞
- EINVAL:一個或者多個參數(shù)無效
- ENFILE:已達到系統(tǒng)對打開文件的限制
- ENODEV:指定文件所在的文件系統(tǒng)不支持內(nèi)存映射
- ENOMEM:內(nèi)存不足,或者進程已超出最大內(nèi)存映射數(shù)量
- EPERM:權(quán)能不足,操作不允許
- ETXTBSY:已寫的方式打開文件,同時指定MAP_DENYWRITE標志
- SIGSEGV:試著向只讀區(qū)寫入
- SIGBUS:試著訪問不屬于進程的內(nèi)存區(qū)
- 參數(shù)說明
mmap在移動端代碼中的使用
- //用于寫入文件的緩存Buffer
- static unsigned char *_buffer = NULL;
- // mmap緩存文件的大小
- static int mmap_cache_file = 100*1024;
- void init() {
- //第一步: 根據(jù)設(shè)置的緩存位置生成用于映射的文件
- makedir_mmapfile(cache_path);
- //第二步:打開緩存文件
- int fd = open(cache_path, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
- //mmap映射的文件的判斷
- if(fd != -1) {
- ......
- //第三步:mmap映射文件到buffer內(nèi)存中
- _buffer = (unsigned char *) mmap(0, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
- }
- //第四步:關(guān)閉文件句柄
- close(fd);
- }
- //第五步:操作mmap內(nèi)存讀寫
- void write(....) {
- // 將要寫入的數(shù)據(jù)封裝,壓縮和加密
- data_zlib_compress();
- //將mmap的緩存寫入到文件中
- fwrite(_buffer, sizeof(char), _buffer.length, dest_file);
- fflush(dest_file);
- // 文件大小變化等相關(guān)操作
- update();
- }
日志寫入的流程
1.2.3. 移動日志系統(tǒng)架構(gòu)介紹
客戶端日志SDK為開發(fā)者提供日志的打印,主要是將在線上運行期間產(chǎn)生的日志寫入文件中,根據(jù)開發(fā)者的需要撈取指定的日志,為開發(fā)者解決線上問題提供助力。我們設(shè)計了滿足基本功能的系統(tǒng),架構(gòu)如下圖所示:
1.2.4. 客戶端日志SDK介紹
日志SDK的架構(gòu)如圖展示,可以分為如下三層,每一層解決了不同的業(yè)務(wù)場景。
日志SDK在底層使用了流式壓縮加密操作,在接收到寫入的日志數(shù)據(jù),先將數(shù)據(jù)進行壓縮操作,然后再進行加密操作,整個過程中都是流式操作,避免了CPU峰值,減少對CPU性能負擔(dān)。在具體的實現(xiàn)中引入了MMAP機制解決了日志丟失問題,使用AES進行日志加密確保日志安全性。
日志SDK通過服務(wù)端下發(fā)的策略進行本地日志的動態(tài)上報,這里我們可以通過定時的拉取最新的策略,或者通過push通道更新本地的策略,再或者提供上報接口,在用戶的反饋中,讓用戶將日志數(shù)據(jù)上報上來。當前在下發(fā)的策略中我們進行了大量的自定義,對文件的大小,緩存時長,日志的寫入等級等相關(guān)的設(shè)置進行下發(fā)操作,實現(xiàn)應(yīng)用初始化后,篩選過濾,只將我們需要的日志寫入到文件中,為開發(fā)者使用。
日志SDK根據(jù)策略將指定的日志文件上傳到指定的服務(wù)器上,這個服務(wù)器將對上傳的日志進行解壓和解碼操作,將日志文件還原成原始的輸入數(shù)據(jù),具體的流程可以參考下面的業(yè)務(wù)流程。
日志SDK業(yè)務(wù)流程
日志SDK在的業(yè)務(wù)流程如下圖所示,根據(jù)服務(wù)端配置的策略,采集指定的日志并進行數(shù)據(jù)的壓縮加密等操作,然后主動將本地日志文件上傳到中轉(zhuǎn)服務(wù),將上傳結(jié)果等相關(guān)信息同步到信息展示的服務(wù)端。
日志SDK性能
上述設(shè)計中以及使用中,為了減少對CPU以及內(nèi)存的消耗,我們通過使用mmap技術(shù),將流式壓縮加密緩存等操作轉(zhuǎn)移到native層,那么這樣做相對于Java層的日志庫我們對于內(nèi)存以及CPU的使用率降低了多少,接下來我們將使用一個Java層的日志庫與使用mmap實現(xiàn)的native庫進行對比。
測試條件
性能測試中采用了在同一臺小米Note3 Android 9系統(tǒng)版本手機,分別測試了已有的Java日志庫、當前日志庫、美團Logan、騰訊XLog日志庫的寫入性能。通過寫入速度、GC頻率、CPU占用率幾個維度來衡量日志庫的寫入性能,測試的結(jié)果只限于衡量當前測試環(huán)境,并不代表Android平臺整體平均水準。
測試數(shù)據(jù)量:
測試結(jié)果
1. 內(nèi)存的GC測試結(jié)果
Java日志庫:
native日志庫:
從上邊的內(nèi)存性能圖片中可以看到,Java日志庫在大量寫日志的時候回造成頻繁的GC,雖然native日志庫不會出現(xiàn)這樣頻繁的GC,從圖中可以看到Java日志庫的GC頻率大約是1s/次,native日志庫的GC頻率大約是7.5s/次。
2. CPU使用率測試結(jié)果
Java日志庫:
native日志庫:
從上邊CPU性能圖片中可以看到,Java日志庫在頻繁寫入日志的時候CPU的平均使用率大約為13%,native日志庫在頻繁寫入日志的時候CPU的平均使用率大約為5%。
從上述內(nèi)存以及CPU占用率的對比中,我們可以看出native日志庫相較于Java日志庫來說,性能上有了很大的提示,對于內(nèi)存的占用較小,在頻繁的I/O操作以及加密壓縮操作的情況下cpu的使用率仍保持在較低值。
日志庫性能的對比
上邊我們與Java日志進行了對比,接下來我們將于其他使用mmap實現(xiàn)的日志庫進行下對比:
1.3. 實踐案例
在app的線上環(huán)境我們可能遇到各種問題,我們希望將出現(xiàn)問題當天的日志獲取到用于問題的分析,協(xié)助解決問題。這樣的業(yè)務(wù)場景幾乎覆蓋了大部分的業(yè)務(wù)場景,對于自助收銀機這樣的設(shè)備使用場景,運行時期的日志對于問題的排查尤為重要。
數(shù)科自助收銀設(shè)備主要服務(wù)于各大超市賣場的自如結(jié)賬,緩解多條人工收銀通道仍無法抵消的收銀壓力。當出現(xiàn)問題的時候,我們不可能對使用者進行回訪,所以運行時候的日志對于問題排查尤為重要。
在未使用移動日志系統(tǒng)之前,遇到問題后,由于缺少運營工具,對于問題的排查,需要占用較多的研發(fā)資源,在接入移動日志系統(tǒng)后,運營就可以獨自處理大部分的問題。這樣極大的提高了解決問題的效率,減少了研發(fā)側(cè)參與排查運營問題的時間。
1.4. 寫到最后
當前的sdk使用場景是定時拉取服務(wù)端的策略,根據(jù)下發(fā)的最新策略進行日志文件的上報,有一定的時間延后性,后期我們將開放主動上報日志的通道以及結(jié)合push推送消息,提高日志回撈的及時性以及成功率。
當前的sdk暫時只支持移動端(Android以及iOS),在后續(xù)我們將進行多端支持,將在RN,F(xiàn)lutter,小程序以及H5等各種應(yīng)用場景中統(tǒng)一使用當前日志庫進行日志的采集和存儲。