随着技术的不断进步,计算机的速度越来越快。但是磁盘IO速度往往让欲哭无泪,和内存中的读取速度有着指数级的差距;然而由于互联网的普及,网民数量不断增加,对系统的性能带来了巨大的挑战,系统性能往往是无数技术人不断追求的方向。
CPU,内存,IO三者之间速度差异很大。对于高并发,低延迟的系统来说,磁盘IO往往最先成为系统的瓶颈;为了减少其影响,往往会引入缓存来提升性能。但是由于内存空间有限,往往只能保存部分数据;并且数据需要持久化,所以磁盘IO仍然不可避免。
无论是从HDD(机械硬盘)到SSD(固态硬盘)的硬件提升;还是从BIO(阻塞IO)到 NIO(非阻塞IO)的软件上的提升;都使得磁盘IO效率得到了很大的提升,但是相比内存读取速度仍然有着接近巨大的差距。今天笔者将介绍一种更加高效的IO解决方案Mmap(内存映射文件,memory mapped file)
1. 用户态和内核态
为了安全,操作系统将虚拟内存划分为两个模块,即用户态和内核态。它们之间是相互隔离的,即使用户程序崩溃了也不会影响系统的运行。
用户态和内核态包含很多复杂的概念,在此不做过多介绍。简单来说,用户态是用户程序代码运行的地方,而内核态则是所有进程共享的空间。所以,当进行数据读写操作时,往往需要进行用户空间和内核空间的交互。
传统的IO模型进行磁盘数据读写时,一般大致需要2个步骤,拿写入数据为例:1.从用户空间拷贝到内核空间;2.从内核空间写入磁盘。
2. Mmap是什么
Mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系.
对文件进行Mmap后,会在进程的虚拟内存分配地址空间,创建与磁盘的映射关系。 实现这样的映射后,就可以以指针的方式读写操作映射的虚拟内存,系统则会自动回写磁盘;相反,内核空间对这段区域的修改也直接反映到用户空间,从而可以实现不同进程间的数据共享。与传统IO模式相比,减少了一次用户态copy到内核态的操作。
3. 性能测试
从实现原理上来看,我们可以大胆预测,Mmap的性能应该是优于传统IO。为了尽可能保证的数据的确性,笔者使用JMH工具对传统IO与Mmap的读和写进行基准测试。测试代码可到笔者github中获取。
需要注意的是,笔者的测试结果并不严谨,真实的差距要比以下结果要明显的多;原因在于,测试方法运行时间包含了文件的创建,内容初始化以及删除操作所需要的时间。以下是笔者电脑的测试结果「系统:macOS 处理器:2.6GHz 六核 i7 内存:16G 磁盘类型:SSD」
随机读性能测试:
随机写性能测试:
从读和写的结果报告中都不难看出,无论是读和写的结果印证了我们的猜想以及理论依据,Mmap的性能要远优于传统IO,而在Java中传统IO中的NIO又优于BIO。
4.Mmap在RocketMQ中的应用
RocketMQ是一个分布式消息和流平台,具有低延迟、高性能和可靠性、万亿级容量和灵活的可伸缩性。那么问题来了,对于海量消息的处理它是怎么保证高性能和可靠性的呢?
- RocketMQ的大致执行流程
RocketMQ中消息生产, 存储和消费流程大致可以分为以下几个流程:
- 生产者发送消息到Broker「消息中转角色,负责存储,转发消息」
- Broker中将消息存储在CommitLog中,并在对应的ConsumerQueue中写入消息的commitLogOffset,msgSize,tagCode等信息「消息在CommitLog中的位置,大小,以及标签信息」
- 消费者从对应的ConsumerQueue中读取到消息的信息,根据消息的位置从CommitLog中读取消息体,然后进行消费
- RocketMQ中的Mmap
CommitLog是消息主体以及元数据的存储主体,存储Producer端写入的消息主体内容,消息内容是不定长的。单个CommitLog文件大小是固定的,默认1G ;文件名长度为20位,左边补零,剩余为起始偏移量,比如00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1073741824;当第一个文件写满了,第二个文件为00000000001073741824,起始偏移量为1073741824,以此类推。消息主要是顺序写入日志文件,当文件满了,写入下一个文件。
消息存储在CommitLog文件中,每个消费者消费消息时,都是根据消息在文件中偏移量, 大小去读取消息。读取消息的过程伴随着随机访问读取,严重影响性能。RocketMQ主要通过Mmap技术对CommitLog文件进行读写,将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率。
正因为需要使用内存映射机制,故RocketMQ的文件存储都使用定长结构来存储,方便一次将整个文件映射至内存。
5.Q&A
使用Mmap对文件的读写操作跨过内核空间,减少1次数据的拷贝,进而提高了文件IO效率。
需要注意的是,进行Mmap映射时,并不是直接申请与磁盘文件一样大小的内存空间;而是使用进程的地址空间与磁盘文件地址进行映射,当真正的文件读取是当进程发起读或写操作时。
当进行IO操作时,发现用户空间内不存在对应数据页时(缺页),会先到交换缓存空间(swap cache)去读取,如果没有找到再去磁盘加载(调页)。
进程间通信:从自身属性来看,Mmap具有提供进程间共享内存及相互通信的能力,各进程可以将自身用户空间映射到同一个文件的同一片区域,通过修改和感知映射区域,达到进程间通信和进程间共享的目的。
大数据高效存取: 对于需要管理或传输大量数据的场景,内存空间往往是够用的,这时可以考虑使用Mmap进行高效的磁盘IO,弥补内存的不足。例如RocketMQ,MangoDB等主流中间件中都用到了Mmap技术;总之,但凡需要用磁盘空间替代内存空间的时候都可以考虑使用Mmap。
内存映射文件需要在进程的占用一块很大的连续逻辑地址空间。对于Intel的IA-32的4GB逻辑地址空间,可用的连续地址空间远远小于2---3 GiB。
一旦使用内存关联文件,在程序运行期间,程序的执行可能受关联文件的错误影响。相关联的文件的I/O错误(如可拔出驱动器或光驱被弹出,磁盘满时写操作等)的内存映射文件会向应用程序报异常;而通常的内存操作是无需考虑这些异常的。
有内存管理单元(MMU)才支持内存映射文件。