RocketMQ架构原理解析(三):消息索引
一、概述
“索引”一种数据结构,帮助我们快速定位、查询数据
前文我们梳理了消息在Commit Log文件的存储过程,讨论了消息的落盘策略,然而仅仅通过Commit Log存储消息是远远不够的,例如当我们需要消费某个topic的消息时,通过对Commit Log整体遍历寻找消息的方式无疑非常的低效。所以本章将引出2个很重要的概念:消费队列、IndexFile
二、消费队列
2.1 概念
什么是消费队列呢?其实在上一章的消息协议格式中,就有消息队列的体现,简单回顾一下协议的前6个字段:
- msg total len:消息总长度
- msg magic :魔法值,标记当前数据是一条消息
- msg CRC :消息内容的CRC值
- queue id :队列id
- msg flag :消息标记位
- queue offset :队列的偏移量,从0开始累加
其中第4个字段为消费队列的id,第6个字段为当前队列的偏移量;所以在消息产生的时候,消息所属的队列就已经确定
那么究竟该如何理解“消费队列”的概念呢?我们举例来说:假定某个RocketMQ集群部署了3个broker(brokerA、brokerB、brokerC),主题topicTest的消息分别存储在这3个broker中,而每个broker又对应一个commit log文件。我们把broker中的主题topicTest中的消息划分为多个队列,每个队列便是这个topic在当前broker的消费队列
2.2 数据结构
当然消费队列不会将消息体进行冗余存储,数据结构如下:
即在消费队列的文件中,需要存储20个字节的索引内容。RocketMQ中默认指定每个消费队列的文件存储30万条消息的索引,而一个索引占用20个字节,这样每个文件的大小便是固定值300000*20/1024/1024≈5.72M,而文件命名采用与commit log相似的方式,即总长度20位,高位补0
store/consumequeue/topicXX/0/00000000000000000000
第一个文件store/consumequeue/topicXX/1/00000000000006000000
第二个文件store/consumequeue/topicXX/2/00000000000012000000
第三个文件store/consumequeue/topicXX/3/00000000000018000000
第四个文件
与commitLog只有一个文件不同,consumeQueue
是每个topic的每个消费队列都会生产多个文件
为什么消费队列文件存储消息的个数要设置成30万呢?一个文件还不到6M,为何不能像commit log那样设置为1G呢?鄙人没有在源码及网上找到相关资料,猜测可能是个经验值。首先该值不宜设置的过大,因为消息总是有失效期的,例如3天失效,如果消费队列的文件设置过大的话,有可能一个文件中包含了过去一个月的消息,时间跨度过大,这样不利于及时删除已经过期的消息;其次该值也不宜过小,太小的话会产生大量的小文件,在管理维护上制造负担。最理想情况是一个消费队列文件对应一个commit log,这样commit log过期时,消费队列文件也跟着及时失效
2.3 消费队列之commit log视角
某个commit log会存储多个topic消息,而每个topic有可能会将消息划分至多个队列中;如上图所示,commit log按顺序依次存储消息,而某个topic的消息在commit log中大概率也是不连续的,而consume queue的作用便是将某个topic下同一个队列的消息依次标识,便于消费时顺序消费
2.4 消费队列之topic视角
上图描述了Topic A的消费队列分配情况,所有的消息相对均匀的分散在3个broker中,每个broker的消息又分为队列0及队列1,所以一共有6个消费队列,所以Topic A的consumer端也是建议开辟6个进程去消费数据
这里简单提一下消费端,我们知道一个消费队列同时只能被一个消费实例消费,所以消费实例的数量建议值为 <= consumeQueue 数量,理想情况是消费实例的个数完全等于consumeQueue个数,这样吞吐能达到最佳,以下:
consumerNum < consumeQueue
消费实例小于消费队列个数。例如某个topic的消费队列一共有6个,但是只有3个消费实例,RocketMQ会尽量均衡每个消费实例分配到的消费队列,所以每个消费实例实际会消费2个队列的内容。这种情况可能增加消费实例可以提高整体吞吐consumerNum > consumeQueue
消费实例大于消费队列个数。比如某个topic的消费队列有6个,但是有8个消费实例注册,因为一个消费实例只能对应一个消费队列,所以势必导致有2个消费实例处于空闲状态,不会拿到任何数据consumerNum == consumeQueue
消费实例等于消费队列个数。这比较理想的状态,不会有过载或饥饿产生
基于同一个消费队列只能被一个消费实例消费的特性,我们可以将某类消息均发送给一个队列,这样消费的时候能够严格保序。例如我们希望订单的流程是保序的,可以通过orderId % consumeQueue
来决定当前订单的消费发送给哪个队列,从而达到保序的目的
2.5 写入流程
消费队列的写入跟commit log的写入是同步进行的吗?答案是否定的,RocketMQ会启动一个独立的线程来异步构建消费队列(构建索引文件也是这个线程)
简单描述下流程:构建索引的线程为ReputMessageService
,跟写入commitLog的线程是异步关系,该线程会不断地将没有构建索引的消息从commit log中取出,将物理偏移量、消息长度、tag写入文件。值得一提的是,消息队列文件的写入跟commit log不同,commit log的写入有很多刷盘策略,而consumeQueue每条消息解析完毕都会刷盘,而且采用的是FileChannel
。
借此,我们抛出几个问题
问题1:为什么消费队列写入文件要用FileChannel?批次多,数据量小的场景用Mapp不香吗?
关于这个问题,我咨询了RocketMQ开源社区比较有影响力的大佬,给出的答复是:的确是这样,RocketMQ这样设计考虑欠佳,写文件这块应该向kafka学习,即消息写入用FileChannel
,索引写入用Mapp
问题2:为什么RocketMQ中很少有用到堆外内存?文件写入的话,使用堆外内存少一次内存拷贝,不是可以提高性能吗?
是这样的,类似这样的场景首选还是堆外内存;RocketMQ的确还有很多可优化的空间,在将来的某个版本,我们一定可以看到针对此处的优化
问题3:如果消息已经写入commit log,但还未写入消费队列,consumer端能正常消费到这条消息吗?
抛出这个问题,大家可以思考一下,在消息产生、消费的章节再回答
至此,消费队列在broker端的存储生命周期便结束了,不过在后文的消息生产、消费环节我们还会反复提及。虽然消费队列在文件操作时并不复杂,也没有像commit log那样复杂的刷盘策略,但我们需要深度理解topic、commit log、消费队列的概念及其从属关系,这样在后文介绍消息生产、消费时才能游刃有余
三、IndexFile
ReputMessageService
线程除了构建消费队列的索引外,还同时根据消息key构建了索引
3.1 IndexFile简介
除了正常的生产、消费消息外,RocketMQ还提供了根据msg key进行查询的功能,将消息key相同的消息一并查出;我们当然可以通过扫描全量的commit log将相同msg key类型的消息过滤出来,但性能堪忧,而且涉及大量的IO运算;IndexFile便是为了实现快速查找目标消息而衍生的索引文件
IndexFile的命名规范也有别于消费队列,IndexFile是按照创建时间来命名的,因为根据消息key进行匹配查询的时候,都要带上时间参数,文件名起到了快速定位索引数据位置的作用,下面列举一组IndexFile的文件名
rocketMQ/store/20211204094647480
rocketMQ/store/20211205094647480
rocketMQ/store/20211206094647480
rocketMQ/store/20211207094647480
3.2 IndexFile结构
我们具体看一下此文件的结构,与消费队列文件相同,IndexFile是定长的
由三部分组成:
- 文件头,占用 40 byte
- slot,hash槽儿,占用500w*4= 20000000 byte
- 索引内容 占用2000w*20= 400000000 byte
所以文件总大小为: 40+5000000*4+20000000*20=420000040byte ≈ 400M
3.3 存储原理
简单剖析一下各部分的字段
文件头 共 20 byte
- 开始时间(8 byte)存储前索引文件内,所有消息的最小时间
- 结束时间(8 byte)存储前索引文件内,所有消息的最大时间,因为根据key查询的时候 ,时间是必填选项 ,开始与结束时间用来快速定位消息是否命中
- 最小物理偏移量(8 byte)存储前索引文件内,所有消息的最小物理偏移量
- 最大物理偏移量(8 byte)存储前索引文件内,所有消息的最大物理偏移量;框定最小、最大物理偏移量,是为了给通过物理地址查询时快速索引
- 有效hash slot数量(4 byte)因为存储hash冲突的情况,所以最坏情况是,hash slot只有1个,最理想情况是有500万个
- index索引数量(4 byte)如果当前索引文件已经构建完毕,那么该值是固定值2000万
slot 4 byte
- 当前槽儿内的最近一次index的位置(4 byte)
索引内容 20 byte
- hash值(4 byte)
- 消息的物理地址(8 byte)
- 时间差(4 byte)当前消息与最早消息的时间差
- 索引(4 byte)当前槽儿内,上一条索引的位置
存储方式如下
当一条新的消息索引进来时,首先定位当前消息命中的slot,该slot存储着最近一条消息的存储位置。将消息的索引数据append至文件尾部的同时,将最新索引数据的next指向上一条索引位置,这样便形成了一条当前slot按照时间存入的倒序的链表
3.4 消息查询
根据前文的铺垫,同一个槽儿内的数据,已经被一个隐式的链儿串连在了一起,当我们根据topic+key进行数据查询时,直接通过topic + # + key
的hash值,定位到某个槽儿,进而依次寻找消息即可;当然同一个槽儿内的数据可能出现hash冲突,我们需要将不符合条件的数据过滤掉
当我阅读这部分源码的时候,发现了其内部存在的一个bug,其做消息过滤时,仅仅判断消息的hash字段是否相等,如果相等的话,继而认定为要寻找的数据从而返回
class : org.apache.rocketmq.store.index.IndexFile
if (keyHash == keyHashRead && timeMatched) {
phyOffsets.add(phyOffsetRead);
}
进而带来的一些问题,例如:
- 新建topic
AaTopic
,并向topic中发送一条消息,message key为Aa
- 新建topic
BBTopic
,并向topic中发送一条消息,message key为BB
当我们通过 topic=AaTopic && key=BB
查询时,预期应该返回空数据,但实际却返回了一条数据
其主要是因为Aa
与BB
拥有相同的HashCode2080
此bug以及解决办法已经在GitHub上做了提交:
3.5 message id
消息id,在RocketMQ中又定义为msg unique id,组成形式是“ip+物理偏移量”(ip非定长字段,会因ipv4与ipv6的不同而有所区别),其中ip及物理偏移量在消息的协议格式中均有体现;当我们拿到消息所属的broker地址,以及该消息的物理存储偏移量时,也就唯一定位了该条消息,所以使用“ip+物理偏移量”的方式作为消息id
在某些场景下,msg unique id也会存储在indexFile中,不是本文的重点,我们后文还会提及
四、索引查询(page cache)
查询这块,我们将放在消息发送、消费的章节来阐述,此处仅仅讨论索引结构设计中page cache所承担的角色
其实在整个流程中,RocketMQ是极度依赖page cache的,尤其是消费队列,数据查询是通过如下流程来查询消息的:
1、broker接受请求 -> 2、查询ConsumeQueue文件(20byte) -> 3、拿到消息的physicOffset -> 4、查询commitLog文件(msg size)
我们发现第二步及第四步都只是查询很小的数据量,如果没有page cache挡在磁盘前,整体的性能必将是断崖式下降。我有朋友做过禁掉page cache后,RocketMQ前后的性能的对比相差好几个量级,不禁感慨,page cache真是让我们又爱又恨,叹叹
RocketMQ架构原理解析(三):消息索引的更多相关文章
- RocketMQ架构原理解析(四):消息生产端(Producer)
RocketMQ架构原理解析(一):整体架构 RocketMQ架构原理解析(二):消息存储(CommitLog) RocketMQ架构原理解析(三):消息索引(ConsumeQueue & I ...
- RocketMQ架构原理解析(一):整体架构
RocketMQ架构原理解析(一):整体架构 RocketMQ架构原理解析(二):消息存储(CommitLog) RocketMQ架构原理解析(三):消息索引(ConsumeQueue & I ...
- RocketMQ架构原理解析(二):消息存储
一.概述 由前文可知,RocketMQ有几个非常重要的概念: broker 服务端,负责存储.收发消息 producer 客户端1,负责产生消息 consumer 客服端2,负责消费消息 既然是消息队 ...
- 分布式架构原理解析,Java开发必修课
1. 分布式术语 1.1. 异常 服务器宕机 内存错误.服务器停电等都会导致服务器宕机,此时节点无法正常工作,称为不可用. 服务器宕机会导致节点失去所有内存信息,因此需要将内存信息保存到持久化介质上. ...
- 消息中间件-技术专区-RocketMQ架构原理
RocketMQ是阿里开源的分布式消息中间件,跟其它中间件相比,RocketMQ的特点是纯JAVA实现:集群和HA实现相对简单:在发生宕机和其它故障时消息丢失率更低. 一.RocketMQ专业术语 P ...
- Tomcat 架构原理解析到架构设计借鉴
Tomcat 发展这么多年,已经比较成熟稳定.在如今『追新求快』的时代,Tomcat 作为 Java Web 开发必备的工具似乎变成了『熟悉的陌生人』,难道说如今就没有必要深入学习它了么?学习它我们又 ...
- Request 接收参数乱码原理解析三:实例分析
通过前面两篇<Request 接收参数乱码原理解析一:服务器端解码原理>和<Request 接收参数乱码原理解析二:浏览器端编码原理>,了解了服务器和浏览器编码解码的原理,接下 ...
- Scrapy框架的架构原理解析
爬虫框架--Scrapy 如果你对爬虫的基础知识有了一定了解的话,那么是时候该了解一下爬虫框架了.那么为什么要使用爬虫框架? 学习框架的根本是学习一种编程思想,而不应该仅仅局限于是如何使用它.从了解到 ...
- feign架构 原理解析
什么是feign? 来自官网的解释:Feign makes writing java http clients easier 在使用feign之前,我们怎么发送请求? 拿okhttp举例: publi ...
随机推荐
- css--元素居中常用方法总结
前言 元素居中是日常开发和学习中最常见的问题,同时也是面试中经常考察的知识点,本文来总结一下这方面的知识点. 正文 1.水平居中 (1)子父元素宽度固定,子元素设置 margin:auto,并且子元素 ...
- 问题 O: 寻找最大数(三)
[提交][状态][讨论版] 题目描述 给出一个整数N,每次可以移动2个相邻数位上的数字,最多移动K次,得到一个新的整数. 求这个新的整数的最大值是多少. 输入 多组测试数据. 每组测试数据占一行,每行 ...
- 『学了就忘』Linux软件包管理 — 42、对RPM软件包的查询操作
目录 1.查询RPM软件包是否安装 2.查询系统中所有已安装的RPM软件包 3.查询RPM软件包的详细信息 4.查询RPM软件包中的文件列表 5.查询系统文件属于哪个RPM包 6.查询RPM软件包所依 ...
- POI 4.0 读取Excel
... package POIXLS; import java.io.File; import java.io.FileInputStream; import java.util.ArrayList; ...
- win8中让cmd.exe始终以管理员身份运行
最近在学习配置本地服务器,在命令行启动mysql时总是由于权限不足而失败, Win+R -- cmd ,这样总是不能,还要找到cmd.exe右键以管理员身份运行cmd,再 net start mysq ...
- 重新整理 .net core 实践篇——— 权限源码阅读四十五]
前言 简单介绍一下权限源码阅读一下. 正文 一直有人对授权这个事情上争论不休,有的人认为在输入账户密码给后台这个时候进行了授权,因为认为发送了一个身份令牌,令牌里面可能有些用户角色信息,认为这就是授权 ...
- [loj2049]网络
考虑整体二分,假设二分到区间$[l,r]$,即要对若干个询问,判断这些询问的答案与$mid=\lfloor\frac{l+r}{2}\rfloor$的关系 根据题意,答案$\le mid$等价于重要度 ...
- [loj2978]杜老师
假设所有素数从小到大依次为$p_{1},p_{2},...,p_{k}$,我们将$x$转换为一个$k$位的二进制数,其中从低到高第$i$位为1当且仅当其$p_{i}$的幂次为奇数 不难发现以下两个性质 ...
- 【vue.js】vue项目使用Iconfont(阿里图标库)
vue项目使用Iconfont(阿里图标库) 2019-11-12 19:07:02 by冲冲 1.操作步骤 ① 登录阿里巴巴矢量图标库 https://www.iconfont.cn ,注册账号 ...
- 低代码开发Paas平台时代来了
概述 **本人博客网站 **IT小神 www.itxiaoshen.com 低代码理论 概念 低代码开发基于可视化和模型驱动的概念,结合了云原生和多终端体验技术,它可以在大多数业务场景中,帮助企业显著 ...