Android高性能日誌寫入方案-mmap

本文轉自 2019-08-18-Android高性能日誌寫入方案-mmap,用於學習Android 日誌寫入優化。

1. 常規方案的缺陷

  • 性能問題:一開始日誌的寫入就是通過標準I/O直接寫文件,當有一條日誌要寫入的時候,首先,打開文件,然後寫入日誌,最後關閉文件。但是寫文件是 IO 操作,隨着日誌量的增加,更多的IO操作,一定會造成性能瓶頸。爲什麼這麼說呢?因爲數據從程序寫入到磁盤的過程中,其實牽涉到兩次數據拷貝:一次是用戶空間內存拷貝到內核空間的緩存,一次是回寫時內核空間的緩存到硬盤的拷貝。當發生回寫時也涉及到了內核空間和用戶空間頻繁切換。
  • 丟日誌:爲了解決性能問題,直接想到就是減少I/O操作,我們可以先把日誌緩存到內存中,當達到一定數量或者在合適的時機將內存裏的日誌寫入磁盤中。這樣似乎可以減少I/O操作,但是在將內存裏的日誌寫入磁盤的過程中,app被強殺了或者Crash了的話,這樣會造成更嚴重的問題,日誌丟失。

看到這裏,難道真的就沒有高性能又能保證日誌完整性的方案了嗎?答案是mmap。mmap是個什麼鬼?我們接着往下看。

2. 什麼是mmap

mmap是一種內存映射文件的方法,即將一個文件或者其他對象映射到進程的地址空間,實現文件磁盤地址和進程虛擬地址空間中一段虛擬地址的一一對應關係;實現這樣的映射關係後,進程就可以採用指針的方式讀寫操作這一塊內存,而系統會自動回寫髒頁面到對應的文件磁盤上,即完成了對文件的操作而不必調用read,write等系統調用函數,相反,內核空間堆這段區域的修改也直接反應到用戶空間,從而可以實現不同進程間的文件共享。

網上很多文章都說 mmap 完全繞開了頁緩存機制,其實這並不正確。我們最終映射的物理內存依然在頁緩存中,它可以帶來的好處有:

  • 減少系統調用。我們只需要一次 mmap() 系統調用,後續所有的調用像操作內存一樣,而不會出現大量的 read/write 系統調用。
  • 減少數據拷貝。普通的 read() 調用,數據需要經過兩次拷貝;而 mmap 只需要從磁盤拷貝一次就可以了,並且由於做過內存映射,也不需要再拷貝回用戶空間。
  • 可靠性高。mmap 把數據寫入頁緩存後,跟緩存 I/O 的延遲寫機制一樣,可以依靠內核線程定期寫回磁盤。

在這裏插入圖片描述

從上面的圖看來,我們使用 mmap 僅僅只需要一次數據拷貝。

3. mmap使用場景

mmap 比較適合於對同一塊區域頻繁讀寫的情況,推薦也使用線程來操作。

  • 用戶日誌、數據上報都滿足這種場景,微信開源的 mars 框架中的 xlog模塊也是基於 mmap 特性實現的。
  • 需要跨進程同步的時候,mmap 也是一個不錯的選擇,Android 跨進程通信有自己獨有的 Binder 機制,它內部也是使用 mmap 實現。

4. 具體實現

在Android中可以將文件通過Java提供的MappedByteBuffer映射到內存,然後進行讀寫。(微信的xlog模塊mmap實現是基於C++代碼實現)

MappedByteBuffer 位於 Java NIO 包下,用於將文件內容映射到緩衝區,使用的即是 mmap 技術。通過 FileChannel 的 map 方法可以創建緩衝區。

RandomAccessFile raf = new RandomAccessFile(file, "rw");
//position映射文件的起始位置,size映射文件的大小
MappedByteBuffer buffer = raf.getChannel().map(FileChannel.MapMode.READ_WRITE, position, size);
//往緩衝區裏寫入字節數據
buffer.put(log);

有一點比較坑,Java 雖然提供了 map 方法,但是並沒有提供 unmap 方法,通過 Google 得知 unmap 方法是有的,不過是私有的,我們可以通過反射調用獲取unmap方法(Android 9.0以上對反射做了限制,可以參考這篇博文繞過限制)

    /**
     * 解除內存與文件的映射
     * */
    private void unmap(MappedByteBuffer mbbi) {
        if (mbbi == null) {
            return;
        }
        try {
            Class<?> clazz = Class.forName("sun.nio.ch.FileChannelImpl");
            Method m = clazz.getDeclaredMethod("unmap", MappedByteBuffer.class);
            m.setAccessible(true);
            m.invoke(null, mbbi);
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

在這裏記錄一個點,剛開始在寫入文件的時候,我是使用mmap將日誌直接寫入文件,這樣的話需要通過代碼去實現動態擴容,挺麻煩的,後來發現xlog是將日誌寫入高速緩衝區,這塊高速緩衝區是使用mmap映射出來的內存區,被映射的磁盤文件是它新建的一個緩存文件,當高速緩衝區內容寫到一定閾值時,通知後臺線程將緩衝區的內容寫入文件。借鑑xlog的方案,後面我們也改爲先寫入緩存中,當寫滿了之後,再flush到目標文件,也可以手動調用flush,將緩存刷新到目標文件。

至於微信爲什麼這麼做,肯定也是出於性能的原因啦,具體的可參考微信跨平臺組件mars-xlog架構分析及遷移思路

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章