Java 文件 IO 操做之 DirectIO

在前文《文件IO操做的一些最佳实践》中,我介绍了一些 Java 中常见的文件操做的接口,而且就 PageCache 和 DIrect IO 进行了探讨,最近我本身封装了一个 Direct IO 的库,趁着这个机会,本文重点谈谈 Java 中 Direct IO 的意义,以及简单介绍下我本身的轮子。java

Java 中的 Direct IO

若是你阅读过我以前的文章,应该已经了解 Java 中经常使用的文件操做接口为:FileChannel,而且没有直接操做 Direct IO 的接口。这也就意味着 Java 没法绕开 PageCache 直接对存储设备进行读写,但对于使用 Java 语言来编写的数据库,消息队列等产品而言,的确存在绕开 PageCache 的需求:git

  • PageCache 属于操做系统层面的概念,用户层面很难干预,User BufferCache 显然比 Kernel PageCache 要可控
  • 现代操做系统会使用尽量多的空闲内存来充当 PageCache,当操做系统回收 PageCache 内存的速度低于应用写缓存的速度时,会影响磁盘写入的速率,直接表现为写入 RT 增大,这被称之为“毛刺现象”

PageCache 可能会好心办坏事,采用 Direct IO + 自定义内存管理机制会使得产品更加的可控,高性能。github

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

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 选手的差距,岂不是美哉?我也将这个库发到了中央仓库,方便你们在本身的代码中引用。dom

后续会视需求,会这个小小的轮子增长注入 fadvise,mmap 等系统调用的映射,也欢迎对文件操做感兴趣的同窗一块儿参与进来,pull request & issue are welcome!工具

扩展阅读

《文件IO操做的一些最佳实践》

《PolarDB数据库性能大赛Java选手分享》

欢迎关注个人微信公众号:「Kirito的技术分享」,关于文章的任何疑问都会获得回复,带来更多 Java 相关的技术分享。

关注微信公众号
相关文章
相关标签/搜索