随机读写之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) 时:
- 操作系统从磁盘加载了磁盘块(block) 16kb进入 PageCache,这被称为预读(为了减少实际磁盘IO)
- 应用程序(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。
引用资料
更多推荐
所有评论(0)