Android 高性能日誌寫入方案

前言

公司目前在作一款企業級智能客服系統,對於系統穩定性要求很高,不過難保用戶在使用中不會出現問題,而 Android SDK 集成在客戶的 APP 中,同時因爲 Android 碎片化的問題,對於 SDK 的問題排查就顯得尤其困難,所以記錄下用戶的操做日誌就顯得極爲重要。緩存

初始方案

一開始,SDK 記錄日誌的方式是直接經過寫文件,當有一條日誌要寫入的時候,首先,打開文件,而後寫入日誌,最後關閉文件。這樣作的問題就在於頻繁的IO操做,影響程序的性能,並且 SDK 爲了保證消息的及時性,還維護了一個後臺進程,當其中一個進程進行日誌寫入時,另外一個就會被鎖在門外等着,問題就愈發嚴重。使用這種方案雖然當前看上去對程序的影響不大,可是隨着日誌量的增長,更多的IO操做,必定會形成性能瓶頸。bash

下面咱們來分析下直接寫入文件的流程:app

  1. 用戶發起 write 操做
  2. 操做系統查找頁緩存 a.若未命中,則產生缺頁異常,而後建立頁緩存,將用戶傳入的內容寫入頁緩存 b.若命中,則直接將用戶傳入的內容寫入頁緩存
  3. 用戶 write 調用完成
  4. 頁被修改後成爲髒頁,操做系統有兩種機制將髒頁寫回磁盤 a.用戶手動調用 fsync() b.由 pdflush 進程定時將髒頁寫回磁盤

能夠看出,數據從程序寫入到磁盤的過程當中,其實牽涉到兩次數據拷貝:一次是用戶空間內存拷貝到內核空間的緩存,一次是回寫時內核空間的緩存到硬盤的拷貝。當發生回寫時也涉及到了內核空間和用戶空間頻繁切換。dom

並且相對於機械硬盤,SSD 存儲還有一個「寫入放大」的問題。這個問題主要和 SSD 存儲的物理結構有關。當 SSD 被所有寫過一遍以後,再寫入的數據是不能夠直接更新,只能夠經過覆蓋重寫,在覆蓋以前須要先擦除數據。但寫入的最小單位是 Page,擦除的最小單位是 Block,而 Block 遠大於 Page,因此在寫入新數據時就須要先把 Block 上的數據讀出來和要寫入的數據合併在一塊兒,再把 Block 擦除,最後把讀出來的數據從新寫入到存儲上,這樣致使實際寫入的數據可能遠遠大於最開始須要寫入的數據。函數

沒想到簡單的寫文件居然涉及了這麼多操做,只是對於應用層透明而已。性能

既然每寫一次文件會執行這麼屢次操做,那麼咱們能不能將日誌緩存起來,當達到必定的數量後再一次性的寫入磁盤中呢?測試

這樣確實可以大量減小 IO 次數,可是卻會引起另外一個更嚴重的問題——丟日誌優化

把日誌緩存在內存中,當程序發生 Crash 或進程被殺後就沒法保證日誌的完整性,並且因爲 SDK 存在多進程,也沒法保證多進程下日誌的順序。spa

一個完善的日誌方案,須要知足操作系統

  • 高效,不能影響系統性能,不能由於引入了日誌模塊而形成應用卡頓
  • 保證日誌的完整性,若是不能保證日誌完整,那麼日誌收集就沒有意義了
  • 對於多進程應用,要保證最終看到的日誌順序的準確性

高性能方案

既然沒法減小寫入次數,那麼咱們能不能在寫文件的過程當中去優化呢?

答案是能夠的,使用 mmap

mmap是一種內存映射文件的方法,即將一個文件或者其它對象映射到進程的地址空間,實現文件磁盤地址和進程虛擬地址空間中一段虛擬地址的一一對映關係,函數原型以下

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
複製代碼

mmap操做提供了一種機制,讓用戶程序直接訪問設備內存,這種機制,相比較在用戶空間和內核空間互相拷貝數據,效率更高。在要求高性能的應用中比較經常使用。

同時 mmap 可以保證日誌的完整性,mmap 的回寫時機:

  • 內存不足
  • 進程退出
  • 調用 msync 或者 munmap
  • 不設置 MAP_NOSYNC 狀況下 30s-60s(僅限FreeBSD)

當映射一個文件後,程序就會在 native 內存中申請一塊相同大小的空間,所以建議每次映射一小段內容,如 64k,寫滿後再從新映射文件後面的內容。

日誌寫入性能和完整性的問題解決了,那麼如何保證多進程下日誌的順序呢?

因爲 mmap 是採用共享內存的方式寫入數據,若是兩個進程同時映射一個文件,那麼必定會形成日誌覆蓋的問題。

既然不能直接保證順序,那咱們只能退而求其次,兩個進程分別映射不一樣的文件,天天合併一次,合併時對日誌進行排序。

繼續優化

根據上述方案,設計 jni 接口,打包 so,引入 SDK,看似沒什麼問題了,可是做爲一款 SDK,總以爲包含 so 不太友好,在必定程度上會增長接入的難度。

那麼能不能不用 so 呢?

其實 Java 中已經提供了內存映射的實現——MappedByteBuffer

MappedByteBuffer 位於 Java NIO 包下,用於將文件內容映射到緩衝區,使用的便是 mmap 技術。經過 FileChannel 的 map 方法能夠建立緩衝區

RandomAccessFileraf = new RandomAccessFile(file, "rw");
MappedByteBuffer buffer = raf.getChannel().map(FileChannel.MapMode.READ_WRITE, position, size);
複製代碼

爲了測試 MappedByteBuffer 的效率,咱們把 64byte 的數據分別寫入內存、MappedByteBuffer 和磁盤文件 50 萬次,並統計耗時

方法 耗時
內存 384ms
MappedByteBuffer 700ms
磁盤文件 16805ms

能夠看出 MappedByteBuffer 雖然不及寫入內存的性能,可是相比較寫入磁盤文件,已經有了質的提高。

總結

本文主要分析了直接寫文件記錄日誌方式存在的問題,並引伸出高性能文件寫入方案 mmap,兼顧了寫入性能和完整性,並經過補償方案確保多進程下日誌的順序。最後發現了內存映射在 Java 層的實現,避免了引入 so。

遷移自個人簡書 2018.01.28

相關文章
相關標籤/搜索