貨拉拉客戶端通用日誌元件 - Glog

語言: CN / TW / HK

Glog 是貨拉拉移動端監控系統中的日誌儲存元件,Glog 意即 General log - 通用日誌。為了滿足我們對日誌格式的多種需求,我們在儲存方式、歸檔方式上做了一些探索和實踐,使得 Glog 的通用性和效能優於常見的日誌方案。Glog 已經在貨拉拉全線 App 中穩定運行了 1 年多,現在將其開源,我們希望 Glog 的開源能夠為移動開發者提供一種更加通用的日誌方案,同時希望 Glog 能夠從社群中汲取養分,不斷得到優化和完善。GitHub 地址:https://github.com/huolalatech/hll-wp-glog

背景簡介

移動端日誌系統通常來說,主要的目標是輔助開發同學排查線上問題,這些問題包括但不限於 1. 客訴渠道反饋的 App 使用問題; 2. Crash 系統上報的崩潰問題; 3. 其他線上冒煙問題。

為了能夠儘快定位問題,我們希望能夠快速、詳細的還原問題現場,這就需要在程式碼中 App 執行的關鍵節點埋入日誌,將出現問題時的執行狀態快速上報。這對日誌系統提出了兩個關鍵的要求,資訊完整性以及實時性。 在移動端,公司之前存在一套簡單的日誌系統,收集的日誌資訊比較有限,我們通過 App 的常見使用流程來看其覆蓋的關鍵節點

old-log-cover

另外,之前的日誌系統只能通過下發任務回撈,實時性較差,還存在 I/O 效能等問題。

為了解決這些問題,我們開發了新的移動端日誌系統,覆蓋了上面 App 使用流程的全節點資訊

new-log-cover

另一方面,為了提升日誌的實時性,我們增加了實時日誌,通過短輪詢來進行定時上報,此外還補充了監控日誌來支撐 App 的橫向資料對比、評估 App 的效能指標,最終的方案如下

log-system

系統主要組成包括 - Android/iOS 上層日誌採集 SDK - 跨平臺的儲存元件 Glog - 負責日誌儲存過濾的日誌後端 - 負責日誌展示的日誌前端

新的監控系統包括實時性要求較高的實時日誌,資訊較完整的離線日誌以及為大盤資料服務的監控日誌 - 實時日誌,快速上傳,資訊精簡,能夠接近實時的檢視,快速定位、排查使用者反饋的問題; - 離線日誌,通過後臺任務觸發上傳,按天歸檔,作為實時日誌的兜底,要求資訊完整詳盡; - 監控日誌,支援取樣,作為監控大盤的資訊源,實時性要求最高,日誌只包括監控資訊。

為了適配不同日誌的儲存格式,我們希望儲存元件能夠在格式上儘量通用,最好做到格式無關;另一方面我們也希望其效能、可靠和安全方面能夠對齊一線水平,在調研了市面上流行的日誌元件後,我們發現並沒有現成方案滿足我們的需求,因此我們自研了自己的日誌儲存元件 Glog。

glog-arch

方案概覽

應用上層對不同型別的日誌序列化(推薦 Protobuf)之後,將二進位制資料儲存到 Glog,對於上傳頻次較高的實時日誌和監控日誌,採用重新命名快取的方式快速歸檔;對於資訊較全而上傳頻次不高的離線日誌,我們採用 mmap 偏移對映的方式歸檔,相較標準 I/O 複製歸檔的方式,提升了效能。在可靠性和安全性方面我們也借鑑了當前的流行方案,例如 mmap I/O 提升效能和可靠性、流式的加密和壓縮防止 CPU 突發峰值,另外我們在日誌中加入了同步標記支援讀取容錯。

儲存方式

為了適應不同的日誌格式,Glog 儲存二進位制資料,上層依據自己的需要,將資料序列化後交給 Glog

glog-flow

具體的檔案格式:使用 2 個位元組描述每條日誌長度,在每條日誌末尾加入一個同步標誌,用於檔案損壞時的讀取容錯。

glog-format

歸檔方式

回顧一下常見的日誌元件中 mmap 的使用方式,首先 mmap I/O 需要對映一片大小為 page size (通常為 4KB) 整數倍大小的快取,隨著資料的寫入將這片空間耗盡後,我們無法持續擴充套件這片空間的大小(由於它佔用的是 App 的執行記憶體空間),因此需要將其中的資料歸檔,常見的方式是將其中內容 flush 追加到另一個歸檔檔案當中,之後再清空 mmap 快取,這個 flush 的過程一般使用標準 I/O

normal-format

而我們的實時、監控日誌為了快速上傳保證資料實時性,採用間隔較短的輪詢來觸發 flush 並上傳,這將導致 flush 頻率變得很高;而通常的 flush 方式採用標準 I/O 來複制資料,效能相對較低,後續的日誌寫入需要等待 flush 完成,這將影響我們的寫入效能,因此我們考慮兩種方案來提升 flush 速度以優化寫入效能 1. mmap 偏移對映,通過 mmap 對映歸檔檔案的末尾,之後通過記憶體拷貝將 mmap 快取追加到歸檔檔案末尾。這種方式將檔案複製變成記憶體複製,效能較好。 2. 檔案重新命名,對於可以快速上傳並刪除的日誌,我們可以在需要時將 mmap 快取重新命名成歸檔檔案,之後重建快取。這種方式直接去除了複製的環節,但是在日誌量較大時,可能產生很多零碎的歸檔檔案。

glog-archive

這兩種方案可以在我們的不同日誌場景應用,對於實時、監控日誌來說,對效能要求最高,選用第 2 種方案,這個方案帶來的零碎歸檔檔案問題,由於上傳和刪除較快,在這裡並不會堆積,另一方面,考慮到實時、監控日誌上傳週期較短,零碎的歸檔檔案也便於控制上傳的資料量;而離線日誌選用第 1 種方案,可以將每天的日誌歸檔在一個檔案中,相對常見的標準 I/O 也有效能上的優勢。

加密方式

Glog 使用了 ECDH + AES CFB-128,對每條日誌進行單獨加密。具體來說通過 ECDH 協商加密祕鑰,之後 AES CFB-128 進行對稱加密。

glog-encrypt

選擇 CFB-128 是因為 AES 通用性和安全性較好,加解密只需執行相同塊加密演算法,對 IV 隨機性要求低,ECC 相對 RSA 在加密強度相同的前提下,祕鑰更短。

| Security(In Bits) | RSA Key Length Required(In Bits) | ECC Key Length Required(In Bits) | | --- | --- | --- | | 80 | 1024 | 160-223 | | 112 | 2048 | 224-255 | | 128 | 3072 | 256-383 | | 192 | 7680 | 384-511 | | 256 | 15360 | 512+ |

壓縮方式

考慮到解壓縮的便捷性和通用性,Glog 使用了常見的 Deflate 無失真壓縮演算法,對日誌進行流式壓縮,即以每條日誌為壓縮單元,在每次寫入時進行同步壓縮。這樣避免了歸檔時對整個 mmap 快取做壓縮帶來的 CPU 波峰,具體的原理下面做一些解釋。

Deflate 演算法是 LZ77 與哈夫曼編碼的組合

LZ77

LZ77 將資料(短語)通過前向緩衝區,然後移動到滑動視窗中成為字典的一部分,之後從字典中搜索能與前向緩衝區匹配的最長短語,如果能夠命中,則成為短語標記作為結果儲存起來,不能命中則作為字元標記儲存。 解壓時,如果是字元標記則直接拷貝到滑動視窗中,如果是短語標記則在滑動視窗中查詢相應的偏移量,之後將滑動視窗中相應長度的短語拷貝到結果中。

短語標記包括了 1. 滑動視窗中的偏移量 2. 匹配命中的字元長度 3. 匹配結束後前向緩衝區的第一個字元

下面展示了對字元 LABLALALABLA 進行 LZ77 壓縮和解壓縮的過程,

glog-lz77

接下來霍夫曼編碼對 LZ77 的處理結果(包括前面提到的偏移量、長度、字元),按照出現頻率越高,佔用空間越少的方式進行編碼儲存。 在簡要說明原理之後,我們知道影響壓縮率的幾個因素:滑動視窗(字典)大小,輸入的資料(短語)長度、以及短語中字元的重複率。字典越大、短語越長,越容易從字典中找到匹配短語進而變成短語標記,那麼流式壓縮以每條日誌作為壓縮單元,輸入資料長度變短,我們如何保證壓縮率呢? 這裡我們能做的是儘量保證字典的大小,不頻繁重置字典,具體做法是隻在 mmap 快取歸檔時重置字典,對於歸檔前 mmap 快取的資料,複用字典來保證壓縮率。

訊息佇列

mmap 相對標準 I/O 在效能上有較大優勢,主要是由於其減少了核心空間與使用者空間的拷貝、以及 write lseek 系統呼叫帶來的上下文切換開銷

glog-mmap

但在系統資源不足時 mmap 仍有可能出現效能問題,舉個例子,我們知道 mmap 與標準 I/O 一樣也需要通過 Page Cache 回寫到磁碟

Page Cache 的生命週期: 當用戶通過標準 I/O 從使用者緩衝區向核心空間拷貝資料時,如果核心緩衝區中沒有這個 Page,將發生缺頁中斷分配一個 Page,之後拷貝資料,結束後這個 Page Cache 變成一個髒頁,然後該髒頁同步到磁碟中,同步結束後,這個 Page Cache 變成 Clean Page 儲存在系統中。

Android 中可以通過 showmap 命令觀察 mmap 寫入了 Page Cache

glog-pagecache

當系統記憶體不足時,系統將回收 Page Cache 來釋放記憶體,引起頻繁的磁盤迴寫,mmap 效能也會受到影響。 另一方面由於實時日誌、監控日誌需要高頻歸檔,而歸檔會阻塞後續的寫入。因此我們在 Glog 底層加入了訊息佇列來處理寫入和歸檔等操作,進一步提升效能,避免卡頓。

glog-message-queue

效能對比

| 手機型號 | 日誌 SDK | 1w 條日誌耗時 | 10w 條日誌耗時 | | --- | --- | --- | --- | | Samsung Galaxy S10+ Android 11 | glog | 21 ms | 182 ms | || glog+pb | 54 ms | 335 ms | || xlog | 207 ms | 1961 ms | || logan | 250 ms | 6469 ms | | Huawei Honor Magic 2 Android 10 | glog | 38 ms | 286 ms | || glog+pb | 84 ms | 505 ms | || xlog | 263 ms | 2165 ms | || logan | 242 ms | 3643 ms | |Xiaomi 10 Android 11| glog | 27 ms | 244 ms | || xlog | 198 ms | 1863 ms | || logan | 210 ms | 4238 ms | | Huawei Mate 40 pro HarmonyOS 2.0.0 | glog | 30 ms | 257 ms | || xlog | 275 ms | 2484 ms | || logan | 260 ms | 4020 ms | | OPPO R11 Android 8.1.0 | glog | 63 ms | 324 ms | || glog+pb | 234 ms | 1611 ms | || xlog | 464 ms | 3625 ms | || logan | 430 ms | 5355 ms | | iPhone 12 128G iOS 14.8 | glog | 7 ms | 29 ms | || xlog | 152 ms | 1079 ms | || logan | 162 ms | 12821 ms | | iPhone 8 64G iOS 13.7 | glog | 12 ms | 50 ms | || xlog | 242 ms | 2106 ms | || logan | 251 ms | 38312 ms |

Glog 使用非同步模式、按天歸檔

通過對比資料來看,Glog 非同步模式由於使用了訊息佇列,即使累加上 Protobuf 的序列化時間,寫入效能相對來說依然有較大優勢。

遇到的問題

  • 使用 mmap 偏移對映方式拷貝資料時,需要通過 mmap 對映檔案末尾,其偏移量也需要是 page size 的整數倍,而歸檔檔案和複製資料大小通常情況下都不是 page size 的整數倍,需要做額外的計算;
  • 如果只對歸檔檔案總體積作為閾值來清理,在重新命名歸檔這種情況下零碎檔案較多,可能在收集檔案列表的過程中導致 JNI 本地引用超限,需要限制檔案總個數、及時回收 JNI 本地引用;
  • 在跨天寫入日誌的情況下,mmap 快取中的資料可能無法及時歸檔,造成部分日誌誤寫入次日的歸檔檔案當中,需要在歸檔輪詢中增加時間視窗的判定;
  • 為了便於上層上傳日誌,在底層需要新增日誌解析模組。

總結

通過上面的介紹,可以看到 Glog 相較其他流行方案的主要區別是: - 儲存的是格式無關的二進位制資料,具有更好的定製性; - 底層實現的訊息佇列,效能更優使用也更方便; - 新的歸檔方式一方面提升效能,另一方面也便於高頻讀取。

當然這些手段也帶來了一些妥協,比如由於儲存的是二進位制資料,使用 Glog 需要額外新增序列化程式碼;非同步模式下,訊息佇列中的任務在 Crash 或斷電時可能丟失,這些問題在我們的使用場景基本可以忽略。 為了實現貨拉拉的業務需求,我們參考流行的日誌方案,站在巨人的肩膀上,在移動端儲存元件高效能、可靠、安全的基本要求之外,提供了更多的特性和額外的優化。在開源之後,也希望能夠反哺社群,為移動開發者提供一種更為通用的日誌方案。

以 Glog 為儲存模組的日誌系統,目前已經接入了公司的全線 app,實時日誌的單日日誌量達到數十億條,穩定執行在百萬級別的 App 上。為線上使用者反饋問題解決、App 崩潰排查提供了有力的幫助,除此之外,還為風控系統、監控大盤提供了資料支撐。

glog-app