https://mp.weixin.qq.com/s/101wu0rg4mZWCxD9EuUYeA

如果分配堆外内存,则最好保证较长的生命周期,并重复利用,以及将 filechannel.write() 方法加锁才能实现真正的顺序写

2018-07-13 天池中间件大赛百万队列存储设计总结【复赛】
2018-11-27 文件 IO 操作的一些最佳实践
2019-02-02【石冲】第一届阿里云 PolarDB 数据库性能大赛”RDP飞起来”队伍攻略总结
2019-03-02 Java 文件 IO 操作之 DirectIO
2019-03-15 一文探讨堆外内存的监控与回收
2021-09-17 文件 IO 中如何保证掉电不丢失数据
2021-10-07 使用堆内内存HeapByteBuffer的注意事项
2021-10-12 聊聊Unsafe的一些使用技巧
2021-10-19 Unsafe与ByteBuffer那些事
2021-11-02【参赛总结】第二届云原生编程挑战赛-冷热读写场景的RocketMQ存储系统设计
2021-11-08 重新认识 Java 中的内存映射(mmap)

【1】从Linux内核理解JAVA的NIO
从Linux内核理解Java中的IO

https://www.cnblogs.com/rickiyang/p/13265043.html
https://www.cnblogs.com/Courage129/p/14225658.html

理解 Linux 的虚拟内存
Linux内存、Swap、Cache、Buffer详细解析
7个示例科普CPU CACHE

硬核大佬博客各种Linux源码分析和innodb分析: https://www.leviathan.vip/, 其中 Linux 环境写文件如何稳定跑满磁盘 I/O 带宽? 吊的一比

前言

IO 可以简单分为磁盘 IO网络 IO ,磁盘 IO 相对于网络 IO 速度会快一点。JAVA 对 NIO 抽象为 Channel , Channel 又可以分为 FileChannel (磁盘 IO)和 SocketChannel (网络 IO)【1】

FileChannel 与零拷贝

FileChannel 本身不是基于零拷贝实现的,而是基于块来实现的。FileChannel 配合着 ByteBuffer,将读写的数据缓存到内存中,然后以批量/缓存的方式read/write,省去了非批量操作时的重复中间操作,操纵大文件时可以显著提高效率。FileChannel 的 write 方法将数据写入 PageCache 后就认为落盘了,最终还是要操作系统完成 PageCache 到磁盘的最终写入,一次 FileChannel 的 write 操作,是需要经过两次上下文切换的(用户态到内核态),一次 CPU COPY 和一次 DMA COPY。FileChannel 的 force 方法则是用于通知操作系统进行及时的刷盘。

FileChannel 中的零拷贝体现在 transferTo(...)transferFrom(...)两个方法是实现了零拷贝的。而在 Netty 中也通过在 FileRegion 中包装了 NIO 的 FileChannel.transferTo() 方法实现了零拷贝。RocketMQ在涉及到网络传输的地方也使用了该方法。

先看 FileChannel,下面两段代码,你认为谁更快?

  1. // 方法一: 4kb 刷盘
  2. FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel();
  3. ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_4kb);
  4. for (int i = 0; i < _4kb; i++) {
  5. byteBuffer.put((byte)0);
  6. }
  7. for (int i = 0; i < _GB; i += _4kb) {
  8. byteBuffer.position(0);
  9. byteBuffer.limit(_4kb);
  10. fileChannel.write(byteBuffer);
  11. }
  12. // 方法二: 单字节刷盘
  13. FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel();
  14. ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1);
  15. byteBuffer.put((byte)0);
  16. for (int i = 0; i < _GB; i ++) {
  17. byteBuffer.position(0);
  18. byteBuffer.limit(1);
  19. fileChannel.write(byteBuffer);
  20. }
  1. 使用方法一:4kb 缓冲刷盘(常规操作),在作者测试机器上只需要 1.2s 就写完了 1G。
  2. 使用方法二:没有任何缓冲,几乎是直接卡死,文件增长速度非常缓慢,在等待了 5 分钟还没写完后,中断了测试

使用写入缓冲区是一个非常经典的优化技巧,用户只需要设置 4kb 整数倍的写入缓冲区,聚合小数据的写入,就可以使得数据从 pageCache 刷盘时,尽可能是 4kb 的整数倍,避免写入放大问题。但这不是重点,大家有没有想过,pageCache 其实本身也是一层缓冲,实际写入 1byte 并不是同步刷盘的,相当于写入了内存,pageCache 刷盘由操作系统自己决策。那为什么方法二慢呢?主要就在于 filechannel 的 read/write 底层相关联的系统调用,是需要切换内核态和用户态的,注意,这里跟内存拷贝没有任何关系,导致态切换的根本原因是 read/write 关联的系统调用本身。方法比方法一多切换了 4096 倍,上下文的切换成为了瓶颈,导致耗时严重。阶段总结一下重点,在 DRAM 中设置用户写入缓冲区这一行为有两个意义:

  • 方便做 4kb 对齐,ssd 刷盘友好
  • 减少用户态和内核态的切换次数,cpu 友好