Apache下开源的另外一款MQ—ActiveMQ(默认采用的KahaDB做消息存储)可选用JDBC的方式来做消息持久化,通过简单的xml配置信息即可实现JDBC消息存储。由于,普通关系型数据库(如Mysql)在单表数据量达到千万级别的情况下,其IO读写性能往往会出现瓶颈。在可靠性方面,该种方案非常依赖DB,如果一旦DB出现故障,则MQ的消息就无法落盘存储会导致线上故障
目前业界较为常用的几款产品(RocketMQ/Kafka/RabbitMQ)均采用的是消息刷盘至所部署虚拟机/物理机的文件系统来做持久化(刷盘一般可以分为异步刷盘和同步刷盘两种模式)。消息刷盘为消息存储提供了一种高效率、高可靠性和高性能的数据持久化方式。除非部署MQ机器本身或是本地磁盘挂了,否则一般是不会出现无法持久化的故障问题。
性能对比 文件系统>关系型数据库DB
消息存储
磁盘如果使用得当,磁盘的速度完全可以匹配上网络的数据传输速度。目前的高性能磁盘,顺序写速度可以达到600MB/s, 超过了一般网卡的传输速度。但是磁盘随机写的速度只有大概100KB/s,和顺序写的性能相差6000倍!因为有如此巨大的速度差别,好的消息队列系统会比普通的消息队列系统速度快多个数量级。RocketMQ的消息用顺序写,保证了消息存储的速度。
消息发送
Linux操作系统分为【用户态】和【内核态】,文件操作、网络操作需要涉及这两种形态的切换,免不了进行数据复制。
一台服务器 把本机磁盘文件的内容发送到客户端,一般分为两个步骤:
1)read;读取本地文件内容;
2)write;将读取的内容通过网络发送出去。
这两个看似简单的操作,实际进行了4次数据复制,分别是:
RocketMQ充分利用了上述特性,也就是所谓的“零拷贝”技术,提高消息存盘和网络发送的速度。
这里需要注意的是,采用MappedByteBuffer这种内存映射的方式有几个限制,其中之一是一次只能映射1.5~2G 的文件至用户态的虚拟内存,这也是为何RocketMQ默认设置单个CommitLog日志数据文件为1G的原因了
RocketMQ消息的存储是由ConsumeQueue和CommitLog配合完成的,消息真正的物理存储文件是CommitLog,ConsumeQueue是消息的逻辑队列,类似数据库的索引文件,存储的是指向物理存储的地址。每 个Topic下的每个Message Queue都有一个对应的ConsumeQueue文件
上面有提到CommitLog日志数据文件为1G,这里设置这么大是因为这个存储文件中包括Topic、QueueId、Message的元数据信息的,1G存满后会自动创建1G的文件进行存储!那么这里从1G的文件中检索数据肯定会麻烦,性能肯定不咋地,那么这里就能体现ConsumeQueue的作用,ConsumeQueue中存储的是消息的索引,如消费者需要消费某条消息时,并不是直接去CommitLog1G的文件中去查询的,是先在ConsumeQueue中得到消息的索引,找到索引对应的消息位置,然后再去CommitLog中精准的定位到某条数据,这种处理方式就能加快CommitLog读取的速度,ConsumeQueue中是不存储消息内容的,只存储索引的,ConsumeQueue是个MessageQueue一一对应的,ConsumeQueue是在内存中的,如果系统宕机后,那么重启时就会在CommitLog中从新获取一遍。
说明:我们这里称CommitLog为CommitLog文件,但是在源码中该文件被称为mappedFile
这里的CommitLog其实也就是我们broker配置文件中配置的消息存储目录,如下图,这里也能查看出这个文件大小为1G!小于等于1G,这里并不是只有一个CommitLog文件,当存储数据大于1G时会在创建文件进行存储!文件名由20位10进制数构成,标识当前文件的第一条消息其实位移偏移量
注意:一个Broker中仅包含一个CommitLog目录,所有的mappedFile文件都是存放在该目录中的,即无论当前Broker中存放着多少Topic消息,这些消息都是被顺序写入到mappedFile文件中的,也就是说,这些消息在Broker中存放时并没有被按照Topic进行分类
下图就是ConsumerQueue,每 个Topic下的每个Message Queue都有一个对应的ConsumeQueue文件
Index如下图
RocketMQ除了通过指定Topic进行消息消费外,RocketMQ还提供根据Key进行消息查询的功能,该查询是通过store目录中的index子目录中的indexFile进行索引的快速查询,当然,这个indexFile中的索引数据是包含了可以的消息发送到Borker时写入的,如果消息中没有包含Key,则不会写入。
索引条目结构
每个Broker中会包含一组indexFile,每个indexFile都是以一个时间戳命名的,每个indexFile文件又三部分构成,indexHander、slots槽位,indexes索引数据,每个indexFile文件中包含500万个slot槽,而每个slot槽有可能挂载很多的index索引单元
indexFile文件名
indexFile的文件名为当前文件被创建的时间戳,这个时间戳作用是根据业务key进行查询时,查询条件错了key之外,还需要指定一个要查询的时间戳,表示要查询不大于该时间戳的最新的消息,这个时间戳文件名可以简化查询,提高查询效率!
indexFile创建
indexFile文件创建时机有两个
由于可以推算出,一个indexFile的最大大小是(40 + 500W * 4 + 200W * 20)字节
indexFile查询流程
当消费者通过业务key来查询相应的消息时,其需要进过一个相对复杂的查询流程,不过,在分析查询流程之前,首先要清楚几个定位计算式子
计算指定key的slot槽位序号
slot槽位号=key的hash % 500W
计算槽位序号为n的slot在indexFile中起始位置
slot(n)位置=40 + (n - 1) * 4
计算indexNo为m的index在indexFile中的位置
index(m)位置 = 40 + 500w * 4 + (m - 1)* 20
RocketMQ的消息是存储到磁盘上的,这样既能保证断电后恢复, 又可以让存储的消息量超出内存的限制。RocketMQ为了提高性能,会尽可能地保证磁盘的顺序写。消息在通过Producer写入RocketMQ的时 候,有两种写磁盘方式,分布式同步刷盘和异步刷盘。
同步刷盘
在返回写成功状态时,消息已经被写入磁盘。具体流程是,消息写入内存的PAGECACHE后,立刻通知刷盘线程刷盘, 然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,返回消息写 成功的状态。
异步刷盘
在返回写成功状态时,消息可能只是被写入了内存的PAGECACHE,写操作的返回快,吞吐量大;当内存里的消息量积累到一定程度时,统一触发写磁盘动作,快速写入。
那么实际企业中使用哪种模式,就要考虑安全性(同步),性能(异步)这二者之间做取舍
同步刷盘还是异步刷盘,都是通过Broker配置文件里的flushDiskType 参数设置的,这个参数被配置成SYNC_FLUSH、ASYNC_FLUSH中的 一个。