系列文章目录
rocketmq—安装篇(一)rocketmq—手把手搭建集群模式(二)rocketmq—各类型消息实战(三)rocketmq—名词解释(四) rocketmq—消息存储(五)
文章目录
- 系列文章目录
- 前言
- 一 集群架构
- 1.1 集群架构图
- 二 消息存储
- 2.1 消息传输过程
- 2.2 存储介质
- 2.3 消息存储与发送
- 2.3.1 消息存储
- 2.3.2 消息发送
- 2.4 消息存储结构
- 2.5 刷盘方式
- 三 问题
- 1、index file每个文件限定2000W个索引,怎么定的?
- 2、index file每个文件的slotNum 怎么确定的?
- 3、查询的流程:查询的传入值除了key外,为什么还包含一个时间起始值以及截止值?
- 4、通过key+timestamp 在indexfile中找到对应的索引后,知晓了对应的commitlog Offset,但是没有size如何取得完整的msg消息呢?
前言
前面几章讲解了各个角色组成部分、集群搭建、发送各种类型消息等。但作为高可用的分布式消息队列,必定是要实现消息的持久化存储的。这篇文章就要讲rocketmq是如何实现持久化存储了,here we go … …
一 集群架构
1.1 集群架构图
把之前的角色部分又粘贴过来了,温故而知新,让老铁们加深点影响。
Nameserver:管理broker ,通过它知道从哪个broker读写数据。
Producer:生产者,发送消息
Broker:master slave ,负责消息存储、投递、查询
Consumer:消费者,消费消息
Topic:对消息进行分类,一个Producer可以发送消息给一个或者多个Topic,一个Consumer可以订阅一个或者多个Topic消息
Message Queue:相当于Topic 的分区,用于并行发送和接收消息。(同一个Topic 的消息,可以发送到多个Queue里面)
二 消息存储
2.1 消息传输过程
- 消息生产者发送消息到MQ。
- MQ收到消息,将消息进行持久化,即在存储系统中新增一条记录。
- 返回ACK确认消息给生产者。
- 然后MQ推送消息给对应的消费者,等待消费者返回ACK。
- 如果消息消费者在指定时间内成功返回ACK,那么MQ认为消息消费成功,在存储系统中删除消息,即执行第6步;如果MQ在指定时间内没有收到ACK,则认为消息消费失败,会尝试重新推送消息,重复执行4、5、6步骤。
2.2 存储介质
关系型数据库、KV非关系型数据库、文件系统
- 普通关系型数据库(如Mysql)在单表数据量达到千万级别的情况下,其IO读写性能往往会出现瓶颈。在可靠性方面,该种方案非常依赖DB,如果一旦DB出现故障,则MQ的消息就无法落盘存储会导致线上故障。
- 非关系型数据库,处理海量数据效率高,但是不支持事务、数据与数据间没有关系,适合处理海量数据,保证效率,不一定安全。
- 消息刷盘至所部署虚拟机/物理机的文件系统来做持久化(刷盘一般可以分为异步刷盘和同步刷盘两种模式)为消息存储提供了一种高效率、高可靠性和高性能的数据持久化方式。除非部署MQ机器本身或是本地磁盘挂了,否则一般是不会出现无法持久化的故障问题。
性能对比: 文件系统>KV非关系型数据库>关系型数据库DB
2.3 消息存储与发送
2.3.1 消息存储
- 目前高性能磁盘顺序写可以达到600MB/s,磁盘随机写大概只有100K/s。rocketmq的消息存储采用的就是顺序写,随机度的机制,保证了高效率。
2.3.2 消息发送
文件操作、网络操作需要涉用户态、内核态的切换。
一台服务器把本机磁盘文件的内容发送到另一个客户端,一般分为两个步骤:
- read:读取本地文件内容;
- write:将读取的内容通过网络发送出去。
这两个看似简单的操作,实际进行了4 次数据复制,分别是:
- 从磁盘复制数据到内核态内存;
- 从内核态内存复制到用户态内存;
- 从用户态内存复制到网络驱动的内核态内存;
- 从网络驱动的内核态内存复 制到网卡中进行传输。
通过使用“零拷贝”-mmap的方式,省去向【用户态】的内存复制,提高速度。这种机制在Java中是通过MappedByteBuffer实现的。
采用MappedByteBuffer这种内存映射的方式有几个限制,其中之一是一次只能映射1.5~2G的文件至用户态的虚拟内存,这也是为何RocketMQ默认设置单个CommitLog日志数据文件为1G的原因了。
2.4 消息存储结构
rocketmq 消息存储目录store下面的目录结构:
abort
checkpoint
config
|-- consumerFilter.json
|-- consumerOffset.json ## 保存consumer消费进度
|-- delayOffset.json
|-- subscriptionGroup.json
|-- topics.json
commitlog
|-- 00000000000000000000
consumer
|-- HelloTopic
|-- 0
|-- 1
|-- 2
|-- 3
|-- TestTopic
|-- 0
|-- 1
|-- 2
|-- 3
|-- index
|-- 20210116144023028
主要组成:
- config:
- consumerOffset.json 存储消息消费进度
- CommitLog:存储消息的元数据。所有的topic的消息都存储在一个commitlog中,顺序写,随机读;受mmap文件内存映射限制,单个文件默认为1G大小。
1、单个文件大小默认1G ,文件名长度为20位,左边补零,剩余为起始偏移量,比如00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1073741824;当第一个文件写满了,第二个文件为00000000001073741824,起始偏移量为1073741824,以此类推。消息主要是顺序写入日志文件,当文件满了,写入下一个文件; - ConsumerQueue:消息消费的逻辑队列。
1、每个topic有自己的多个consumerqueue文件,每个文件里有多个msg,每个msg由CommitLog Offset、Size 和Msg Tag Hashcode组成;
2、consumequeue里面单个msg里之所以存tag,是因为在进行Broker端消息过滤时比对tag决定是否消费消息;存tag的hashcode而不是字符串,是因为定长方式存储,节约空间。 - IndexFile:存储消息的索引key,在实际的物理存储上,文件名则是以创建时的时间戳命名的。为了消息查询提供了一种通过key或时间区间来查询消息的方法,这种通过IndexFile来查找消息的方法不影响发送与消费消息的主流程。
1、有了commitlog和consumequeue,消息发送和消费逻辑已经可以得到满足,引入index file 是为消息建立索引方便问题排查:在给定topic和key的前提下,快速定位到消息;msgid是有broker地址和commitlog offset组成,通过它也很容易定位到消息。(没有size如何取得完整的msg消息呢?)
2、根据key的hashcode取模slotNum,得到在slotTable中的具体位置。(那通过timestamp 如何定位槽位呢?通过文件的名称)
3、每个slot value 指向该slot 对应的index Linked list 中最新的msg index(倒排索引,头插法);每个msg index 的next index offset 连接下一个msg index.
4、固定的单个IndexFile文件大小约为400M,一个IndexFile可以保存 2000W个索引; - MappedFileQueue: 对连续物理存储的抽象封装类,可以通过消息存储的物理偏移量位置快速定位该offset所在MappedFile(具体物理存储位置的抽象)、创建、删除MappedFile等操作;
- MappedFile: 对文件存储的直接内存映射抽象封装类(直接在os内核分配内存),通过操作该类,可以把消息写入pagecache或者原子性地将消息持久化刷盘。
详解见图:
2.5 刷盘方式
同步刷盘:
- 消息通过java程序写入到系统缓冲区pagecache后,主线程唤醒同步刷盘线程刷盘,等刷盘完毕,刷盘线程唤醒等待线程,返回写成功的状态
- 安全性高,吞吐量小
异步刷盘:
- 消息通过java程序写入到系统缓冲区pagecache后,立刻返回成功的状态。
- 写操作快,吞吐量大
配置:
# SYNC_FLUSH、ASYNC_FLUSH 选择一个
flushDiskType = SYNC_FLUSH
三 问题
1、index file每个文件限定2000W个索引,怎么定的?
答:规定每个文件存储的索引个数最大为2000W。文件大小是固定的,等于40(header)+500W4(slot)+2000W20(索引)= 420000040个字节大小。
2、index file每个文件的slotNum 怎么确定的?
答:默认槽位500W个,每个固定4byte。平均每个slot存4个索引
3、查询的流程:查询的传入值除了key外,为什么还包含一个时间起始值以及截止值?
答:一个indexFile写完一个会继续写下一个,仅仅一个key无法定位到具体的indexFile,时间范围就为了更精确的定位到具体的indexFile,缩小查找的范围,indexFile文件名是一个时间戳,根据日期就可以定位到传入的日期范围对应在哪个或者哪些indexFile中
4、通过key+timestamp 在indexfile中找到对应的索引后,知晓了对应的commitlog Offset,但是没有size如何取得完整的msg消息呢?