Java 檔案 IO 操作之 DirectIO

Kirito的技術分享發表於2019-03-04

在前文《檔案IO操作的一些最佳實踐》中,我介紹了一些 Java 中常見的檔案操作的介面,並且就 PageCache 和 DIrect IO 進行了探討,最近我自己封裝了一個 Direct IO 的庫,趁著這個機會,本文重點談談 Java 中 Direct IO 的意義,以及簡單介紹下我自己的輪子。

Java 中的 Direct IO

如果你閱讀過我之前的文章,應該已經瞭解 Java 中常用的檔案操作介面為:FileChannel,並且沒有直接操作 Direct IO 的介面。這也就意味著 Java 無法繞開 PageCache 直接對儲存裝置進行讀寫,但對於使用 Java 語言來編寫的資料庫,訊息佇列等產品而言,的確存在繞開 PageCache 的需求:

  • PageCache 屬於作業系統層面的概念,使用者層面很難干預,User BufferCache 顯然比 Kernel PageCache 要可控
  • 現代作業系統會使用盡可能多的空閒記憶體來充當 PageCache,當作業系統回收 PageCache 記憶體的速度低於應用寫快取的速度時,會影響磁碟寫入的速率,直接表現為寫入 RT 增大,這被稱之為“毛刺現象”

PageCache 可能會好心辦壞事,採用 Direct IO + 自定義記憶體管理機制會使得產品更加的可控,高效能。

Direct IO 的限制

在 Java 中使用 Direct IO 最終需要呼叫到 c 語言的 pwrite 介面,並設定 O_DIRECT flag,使用 O_DIRECT 存在不少限制

  • 作業系統限制:Linux 作業系統在 2.4.10 及以後的版本中支援 O_DIRECT flag,老版本會忽略該 Flag;Mac OS 也有類似於 O_DIRECT 的機制
  • 用於傳遞資料的緩衝區,其記憶體邊界必須對齊為 blockSize 的整數倍
  • 用於傳遞資料的緩衝區,其傳遞資料的大小必須是 blockSize 的整數倍。
  • 資料傳輸的開始點,即檔案和裝置的偏移量,必須是 blockSize 的整數倍

檢視系統 blockSize 大小的方式:stat /boot/|grep "IO Block"

ubuntu@VM-30-130-ubuntu:~$ stat /boot/|grep "IO Block" Size: 4096 Blocks: 8 IO Block: 4096 directory

通常為 4kb

Java 使用 Direct IO

專案地址

https://github.com/lexburner/kdio

引入依賴

<dependency>
    <groupId>moe.cnkirito.kdio</groupId>
    <artifactId>kdio-core</artifactId>
    <version>1.0.0</version>
</dependency>
複製程式碼

注意事項

// file path should be specific since the different file path determine whether your system support direct io
public static DirectIOLib directIOLib = DirectIOLib.getLibForPath("/");
// you should always write into your disk the Integer-Multiple of block size through direct io.
// in most system, the block size is 4kb
private static final int BLOCK_SIZE = 4 * 1024;
複製程式碼

Direct IO 寫

private static void write() throws IOException {
    if (DirectIOLib.binit) {
        ByteBuffer byteBuffer = DirectIOUtils.allocateForDirectIO(directIOLib, 4 * BLOCK_SIZE);
        for (int i = 0; i < BLOCK_SIZE; i++) {
            byteBuffer.putInt(i);
        }
        byteBuffer.flip();
        DirectRandomAccessFile directRandomAccessFile = new DirectRandomAccessFile(new File("./database.data"), "rw");
        directRandomAccessFile.write(byteBuffer, 0);
    } else {
        throw new RuntimeException("your system do not support direct io");
    }
}
複製程式碼

Direct IO 讀

public static void read() throws IOException {
    if (DirectIOLib.binit) {
        ByteBuffer byteBuffer = DirectIOUtils.allocateForDirectIO(directIOLib, 4 * BLOCK_SIZE);
        DirectRandomAccessFile directRandomAccessFile = new DirectRandomAccessFile(new File("./database.data"), "rw");
        directRandomAccessFile.read(byteBuffer, 0);
        byteBuffer.flip();
        for (int i = 0; i < BLOCK_SIZE; i++) {
            System.out.print(byteBuffer.getInt() + " ");
        }
    } else {
        throw new RuntimeException("your system do not support direct io");
    }
}
複製程式碼

主要 API

  1. DirectIOLib.java 提供 Native 的 pwrite 和 pread
  2. DirectIOUtils.java 提供工具類方法,比如分配 Block 對齊的 ByteBuffer
  3. DirectChannel/DirectChannelImpl.java 提供對 fd 的 Direct 包裝,提供類似 FileChannel 的讀寫 API。
  4. DirectRandomAccessFile.java 通過 DIO 的方式開啟檔案,並暴露 IO 介面。

總結

這個簡單的 Direct IO 框架參考了smacke/jaydio,這個庫自己搞了一套 Buffer 介面跟 JDK 的類庫不相容,且讀寫實現裡面加了一塊 Buffer 用於快取內容至 Block 對齊有點破壞 Direct IO 的語義。同時,感謝塵央同學的指導,這個小輪子的程式碼量並不多,初始程式碼引用自他的一個小 demo(已獲得本人授權)。為什麼需要這麼一個庫?主要是考慮後續會出現像「中介軟體效能挑戰賽」和「PolarDB效能挑戰賽」這樣的比賽,Java 本身的 API 可能不足以發揮其優勢,如果有一個庫可以遮蔽掉 Java 和 CPP 選手的差距,豈不是美哉?我也將這個庫發到了中央倉庫,方便大家在自己的程式碼中引用。

後續會視需求,會這個小小的輪子增加註入 fadvise,mmap 等系統呼叫的對映,也歡迎對檔案操作感興趣的同學一起參與進來,pull request & issue are welcome!

擴充套件閱讀

《檔案IO操作的一些最佳實踐》

《PolarDB資料庫效能大賽Java選手分享》

歡迎關注我的微信公眾號:「Kirito的技術分享」,關於文章的任何疑問都會得到回覆,帶來更多 Java 相關的技術分享。

關注微信公眾號

相關文章