本文主要介绍 Broker 底层的存储原理,理解什么是「刷盘」以及 RocketMQ 为了底层存储性能的提升做了哪些事情。
让我们先来补充一些 Linux 的基础知识。
虚拟内存
直观来看,每个进程共享使用内存资源,但是实际上每个进程的内存资源是需要隔离管理的。因为如果进程直接操作物理内存,很容易出现冲突的情况,造成数据的不一致和安全性问题。所以,为了安全起见,防止进程之间互相修改对应的内存,操作系统有一套内存管理的机制,就是「虚拟内存」。
虚拟内存,即在物理内存之上为每个进程抽象了一个虚拟内存,从每个进程的视角来看就是独占了完整的物理内存。进程实际上去访问虚拟内存的某个地址时,CPU 会根据页表中的页表项做虚拟内存和物理内存上的映射,并到物理内存上去访问。
「页表」简单理解就是一个映射表,记录某个虚拟地址映射到物理地址,由操作系统维护,每个进程都有自己的页表,具体映射到什么物理地址上,以及实际物理内存的访问都由操作系统来控制,每个进程自己是无法控制的。
用户态和内核态
应用程序如果需要访问底层硬件的一些接口,都需要通过内核来实现。比如我们需要访问磁盘上的文件内容,这时候就需要调用内核提供的接口去访问。
内核就是操作系统的核心,简单来说就是操作系统将很多的敏感操作都进行了很好的封装,暴露出接口,普通程序只能够委托内核帮助自己去做这些敏感操作 => 为了安全。封装暴露的这些接口成为系统调用。
用户进程无法直接访问内核空间,只有内核态的系统调用才能够访问所有的内存空间、I/O 设备等。
【Java 读取文件】
- 程序通过
read
系统调用,进入内核态,读取磁盘上某个文件的某块数据; - 从磁盘加载数据到内核空间的读缓存中,如果已经存在则不需要拷贝;
- 从读缓存拷贝到用户空间;
- 内核态切换回用户态;
- 应用程序读取这块数据内容。
【Java 通过网络发送文件】
- 程序通过
write
系统调用,进入内核态; - 将用户空间中的数据内容拷贝至内核中的 socket buffer 中;
- 从 socket buffer 再拷贝至网卡中发送;
- 内核态切换回用户态。
零拷贝
从上图的流程来看,有些拷贝是多余的。文件系统的实现使用的就是缓存 I/O,即在内核缓存一份数据,目的是为了其他进程读的时候,发现磁盘数据已经存在内核缓存中了,可以直接读取缓存了,不需要再读磁盘。并且写的时候,写到内核缓存中,可以延迟批量将脏页刷盘,对性能更好。
但是在有些场景下,拷贝的次数太多了,需要减少,于是乎就出现了「零拷贝」。零拷贝不是说完全不需要拷贝,只是说减少拷贝次数,毕竟从磁盘拷贝到内存肯定是需要拷贝的。
零拷贝的适用场景:比如如果程序仅仅是读取磁盘文件,然后不对数据做处理直接发送至网络,那么完全不需要再拷贝到用户空间了。零拷贝的实现主要通过
sendfile
系统调用。
【sendfile】
【sendfile + DMA gather】
【mmap】
mmap 可以让用户空间读取到数据,又可以减少一次拷贝,本质上是内存映射。mmap 将磁盘文件对应的内核缓冲区和用户缓存映射一个地址,在用户空间操作这块缓存就能够直接作用到内核的读缓存,因为它们本质上是一块物理内存。
本质原理:调用 mmap 后,操作系统给这个进程的虚拟内存分配了对应文件的映射关系,此时文件数据并没有加载到内存中,还在磁盘上。后面进程访问到对应的数据时,发现这个数据不在内存中,就会触发缺页中断的系统调用,将文件数据从磁盘拷贝到对应的物理内存(PageCache),而进程用户缓存的虚拟内存映射的地址就是这个物理地址,因此不需要拷贝,直接通过指针能访问和操作这块物理内存的数据。
RocketMQ 中的零拷贝
RocketMQ 对应的 commitlog、consumeQueue 等文件都使用到了 mmap,此处就会减少一次内存拷贝,提升了性能。此外,commitlog 采用的是 mmap,消息的写入是直接写到了操作系统的 PageCache 中,这时候并没有将消息刷到磁盘上,默认是等操作系统统一将脏页刷到磁盘上才是落盘了,这就是「异步刷盘」,此时如果断电,内存中的数据就丢失了。RocketMQ 也支持同步刷盘,即写入到 PageCache 就立即执行刷盘,性能会低一点,不过可以保证消息不丢失。
文件预热
mmap 只是在虚拟内存上做了映射关系,物理内存中实际上并没有分配资源,只有当进程访问到相应的虚拟内存,发现并没有数据才会触发缺页中断,分配资源,但是这个缺页中断是系统调用,涉及到上下文切换,比较耗费时间,对 RocketMQ 消息的写入动作来说,会产生性能波动。
为了解决这个问题,RocketMQ
采用了「文件预热」,即预先将当前映射的文件,每一页遍历过去,写入一个 0
字节,然后再调用 mlock
和 madvise
。
- 遍历写 0 字节,是为了触发缺页中断,预先分配好内存;
mlock
,将进程使用的部分或者全部的地址空间锁定在物理内存中,防止其被交换到 swap 空间;madvise
,给操作系统建议,说明这文件在不久的将来要被访问,因此预读几页是个好主意。
RocketMQ write
当消费者来 Broker 拉取消息的时候,Broker 并没有采用
sendfile
等方式,而是直接利用 write
返回消息。由于 mmap
的关系,消息的发送不需要用户缓存和 read
buffer 的拷贝,只需要拷贝到 socket buffer 中即可。
为什么不使用拷贝次数更少的
sendfile + gather
呢?
这是因为 sendfile
是无法在用户空间读取和修改数据内容的,只是一个转发的操作,而
mmap
可以将文件内容读取到用户空间进行解析。RocketMQ SQL
过滤是需要解析消息的内容的,才能够进行过滤,因此肯定是要读取到用户空间的。