随机读写之DirectIO

在上一节中讲过MappedByteBuffer VS FileChannel它们称得上零拷贝技术,但留下了顺序读比随机读快,顺序写比随机写快的问题,在我们的实际应用场景中为了回避随机读写需求,通常的做法都是对其进行文件分片(又将随机变成了有序),而本节借助Direct IO来彻底解决该问题。

注:只有 Linux 系统才支持 Direct IO,还被 Linus 喷过,据说在 Jdk10 发布之后将会得到原生支持

buffered io

普通文件操作,对性能、吞吐量没有特殊要求,由kernel通过page cache统一管理缓存,page cache的创建和回收由kernel控制。

默认是异步写,如果使用sync,则是同步写,保证该文件所有的脏页落盘后才返回(对于db transaction很重要,通过sync保证redo log落盘,从而保证一致性)

mmap对文件操作的吞吐量、性能有一定要求,且对内存使用不敏感,不要求实时落盘的情况下使用。mmap对比buffered io,可以减少一次从page cache -> user space的内存拷贝,大文件场景下能大幅度提升性能。同时mmap将把文件操作转换为内存操作,避免lseek。通过msync回写硬盘(此处只说IO相关的应用,抛开进程内存共享等应用)。
direct io

性能要求较高、内存使用率也要求较高的情况下使用。

适合DB场景,DB会将数据缓存在自己的cache中,换入、换出算法由DB控制,因为DB比kernel更了解哪些数据应该换入换出,比如innodb的索引,要求常驻内存,对于redo log不需要重读读写,更不要page cache,直接写入磁盘就好。

顺序读写 & 随机读写

定义不难理解,其实就是要有序,不要随机切换位置,比如:

thread1:write position[0~4096)
thread2:write position[4096~8194)
而非
thread1:write position[0~4096)
thread3:write position[8194~12288)
thread2:write position[4096~8194)

ExecutorService executor = Executors.newFixedThreadPool(64);
AtomicLong position = new AtomicLong(0);
//并发写(达到随机写的描述)
for(int i=0;i<1024;i++){
    final int index = i;
    executor.execute(()->{
        fileChannel.write(ByteBuffer.wrap(new byte[4*1024]),position.getAndAdd(4*1024));
    })
}
//加锁保证了顺序写
for(int i=0;i<1024;i++){
    final int index = i;
    executor.execute(()->{
        write(new byte[4*1024]);
    })
}
public synchronized void write(byte[] data){
    fileChannel.write(ByteBuffer.wrap(new byte[4*1024]),position.getAndAdd(4*1024));
}

思考:顺序写为什么比随机写要快?

这正是page cache在起作用!

disk cache分为两种:buffer cache以块为单位,缓存裸分区中的内容,(如super block、inode)。page cache是以页为单位缓存分区中文件的内容(通常为4K) ,它是位于 application buffer(用户内存)和 disk file(磁盘)之间的一层缓存。linux2.4中,两者是并存的,但会造成mmap情况下,同一份数据会在两个cache空间内存在,造成空间浪费,在linux2.6中,两者合并,buffer cache也使用page cache的数据结构,只是在free统计内存时,会把page cache中缓存裸分区的部分统计为buffer cache。

当用户发起一个 fileChannel.read(4kb) 时:

  1. 操作系统从磁盘加载了磁盘块(block) 16kb进入 PageCache,这被称为预读(为了减少实际磁盘IO)
  2. 应用程序(application buffer)操作(read)其实是从 PageCache 拷贝 4kb 进入用户内存

关于扇区、磁盘块、Page(PageCache)在N年前谈过要有一定的了解!

常见的block大小为512Bytes,1KB,4KB,查看本机 blockSize 大小的方式,通常为 4kb

为何加载block为16kb,该值也很讲究blockSize*4,难道也发生了4次IO?非也,这又涉及到IO合并(readahead算法)!另外该值并非固定值值,内核的预读算法则会以它认为更合适的大小进行预读 I/O,比如16-128KB,当然可以手动进行调整。对这块敢兴趣的同学可以看文末的引用资料

当用户继续访问接下来的[4kb,16kb]的磁盘内容时,便是直接从 PageCache 去访问了!当我看到下图也有些不适应,确实我们离开发操作系统的老外(老码农)还远着呢,只能大致领略的它味道,这里致敬那些从事计算机基础技术的前辈,更期待华为的操作系统鸿蒙出世。

内存吃紧时PageCache 会受影响吗?

肯定是会影响的,PageCache 是动态调整的,可以通过 linux 的系统参数进行调整,默认是占据总内存的 20%。可以通过free命令进行监控(cache:page cache和slab所占用的内存之和,buff/cache:buffers + cache),它一个内核配置接口 /proc/sys/vm/drop_caches 可以允许用户手动清理cache来达到释放内存的作用,这个文件有三个值:1、2、3

Writing to this will cause the kernel to drop clean caches, dentries and inodes from memory, causing that memory to become free.

- To free pagecache:

- * echo 1 > /proc/sys/vm/drop_caches

- To free dentries and inodes:

- * echo 2 > /proc/sys/vm/drop_caches

- To free pagecache, dentries and inodes:

- * echo 3 > /proc/sys/vm/drop_caches

- As this is a non-destructive operation, and dirty objects are notfreeable, the user should run "sync" first in order to make sure allcached objects are freed.

- This tunable was added in 2.6.16.

毛刺现象

现代操作系统会使用尽可能多的空闲内存来充当 PageCache,当操作系统回收 PageCache 内存的速度低于应用写缓存的速度时,会影响磁盘写入的速率,直接表现为写入 RT 增大,这被称之为“毛刺现象”

Direct IO

去掉PageCache的确可以实现高效的随机读,的确也有存在的价值!采用 Direct IO + 自定义内存管理机制会使得产品更加的可控,高性能。

如何使用呢?

使用 Direct IO 最终需要调用到 c 语言的 pwrite 接口,并设置 O_DIRECT flag,使用 O_DIRECT 存在不少限制:

  • 操作系统限制:Linux 操作系统在 2.4.10 及以后的版本中支持 O_DIRECT flag,老版本会忽略该 Flag;Mac OS 也有类似于 O_DIRECT 的机制
  • 用于传递数据的缓冲区,其内存边界必须对齐为 blockSize 的整数倍
  • 用于传递数据的缓冲区,其传递数据的大小必须是 blockSize 的整数倍
  • 数据传输的开始点,即文件和设备的偏移量,必须是 blockSize 的整数倍

目前Java 目前原生并不支持,但github已经有封装好了 Java 的 JNA 库(smacke/jaydio),实现了 Java 的 Direct IO。

它自己搞了一套 Buffer 接口跟 JDK 的类库不兼容,且读写实现里面加了一块 Buffer 用于缓存内容至 Block 对齐有点破坏 Direct IO 的语义。

int bufferSize = 1<<23; // Use 8 MiB buffers
byte[] buf = new byte[bufferSize];

DirectRandomAccessFile fin = new DirectRandomAccessFile(new File("hello.txt"), "r", bufferSize);

DirectRandomAccessFile fout = new DirectRandomAccessFile(new File("world.txt"), "rw", bufferSize);

while (fin.getFilePointer() < fin.length()) {
    int remaining = (int)Math.min(bufferSize, fin.length()-fin.getFilePointer());
    fin.read(buf,0,remaining);
    fout.write(buf,0,remaining);
}

fin.close();
fout.close();

当然也有人对它进行了再度封装,可参阅J-DirectIO

引用资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值