RocketMQ工作原理_消息的生产

消息的生产

消息的生产过程

Producer可以将消息写入到某Broker中的Queue中,其经历了如下过程:

  • 首先他会把所有的naneServer列表写到Broker配置文件里面
    • 优点 :nameServer集群搭建非常简单
    • 缺点 :Broker,必须明确指出所有 NameServer地址
  • 启动NameServer,开始监听端口,等待Broker,Consumer,Producer连接
  • 启动Broker,Broker会与所有的NameServer建立长连接,然后每30秒向NameServer定时发送心跳包
  • Producer发送消息之前,会先向NameServer发出获取消息Topic的路由信息的请求
  • Nameserver返回该Topic的路由表和Broker列表
    • 路由表: key为Topic名称,value是一个QueueData(所有queue)实例列表的map
      • 一个Broker中该Topic的所有Queue对应一个QueueData
      • 只要涉及到该Topic的Broker,一个Broker对应一个Queue
      • QueueData中包含brokerName
    • Broker列表:key为brokerName,value为BrokerData

      • 一个Broker对应一个Broker实例。对吗?不对。

      • 一个BrokerData对应着BrokerName相同的Master-slave集群。

      • BrokerData其中包含BrokerName及一个map

        • 该map的key为brokerId,value为broker对应的地址,

          • brokerId为0表示该Broker为Master

          • 非0为salve。

    • RPC请求:远程过程调用 (RPC) 是一种协议,程序可使用这种协议向网络中的另一台计算机上的程序请求服务。
  • Producer根据代码中指定的Queue选择策略,从Queue列表中选出一个队列,用于后续存储消息
  • Producer对消息进行一些特殊处理,例如,消息本身超过4M,则会对其进行压缩
  • Producer想选择出的Queue所在的Broker发出RPC请求,将消息发送到选择的Queue上
Queue选择算法

对于无序消息,其Queue选择算法,也称为消息投递算法,常见的有两种:

轮询算法

轮询,一个接着一个。默认选择算法。该算法保证了每个Queue中可以均匀的获取到消息:

该算法存在一个问题:

由于某些原因(上一个消息投递延迟,下一个迟迟无法投递),在某些Broker上的Queue可能投递延迟较严重。从而导致Producer的缓存队列中出现较大的消息积压,影响消息的投递性能。

最小投递延迟算法

该算法会统计每次消息投递的时间延迟,然后根据统计出的结果将消息投递到时间延迟最小的Queue。如果延迟相同,则采用轮询算法投递。该算法可以有效提升消息的投递性能。

该算法也存在一个问题:消息再Queue上的分配不均匀。投递延迟小的Queue其可能存在大量的消息。(queue内消息分配不均匀导致消费者组中的消费者忙的忙死闲的闲死)而对该Queue的消费者压力会增大,降低消息的消费能力,可能会导致MQ中消息的堆积。

消息的储存

  • abort:该文件在Broker启动后会自动创建,正常关闭Broker,该文件会自动消失。若在没有启动Broker情况下,发现这个文件是存在的,则说明之前Broker的关闭时非正常关闭。
  • checkpoint:其中存放着commintlog、consumerqueue、index文件的最后沙盘时间
  • commitlog:其中存放着commitlog文件,而消息是写在commitlog文件中的
  • config:存放着Broker运行期间的一些配置数据
  • consumequeue:其中存放着consumer queue文件,队列就放在这个目录中
  • index:其中存放着消息索引文件indexFile
  • lock:运行期间使用到的全局资源锁

Commitlog文件

目录与文件

commitlog目录中存放着很多的mappedFile文件,当前Broker中所有消息都是落盘到这些mappedFiel文件中的。

mappedFile文件容量为1G(其中存放的数据可能会小于1G,因为该文件剩余的容量不足以储存下一条消息,此时下一条消息就会储存到另一个mappedFile文件中),文件由20位十进制数构成,表示当前文件的第一条消息的起始唯一偏移量。

第一个mappedFile文件名一定为20位0构成的。因为第一个文件的第一条消息的偏移量commitlog offset为0。
当第一个文件放满时,则会自动生成第二个文件继续存放消息。假设第一个文件大小是
1073741820字节(1G=1073741824字节),第二个文件的大小为10字节,则第二个文件名就是00000000001073741824。以此类推,第n个文件就为前n-1个文件大小之和
如果第一个文件储存了不足1G的消息,第二个文件的的偏移量就是第一个文件存放消息的大小

需要注意的是,一个Broker中仅包含着一个commit log目录,所有的mappedFile文件都是存放在该目录中的。即无论当前Broker中存放着多少Topic的消息,这些消息都是被顺序写入到mappedFile文件中的。也就是说,这些消息在Broker中存放时并没有按照Topic进行分类存放

一个Broker = 一个commit log目录 = n个mappedFile文件

mappedFile文件是顺序读写的文件,所以其访问效率很高

消息单元

mappedFile文件内容有一个个的消息单元构成。每个消息单元中包含如下:

  • MsgLen :消息总长度MsgLen
  • PhysicalOffset :消息的物理地址(物理偏移量)
  • Body :消息体内容 真正的内容
  • BodyLength :消息体长度
  • BornHost :消息生产者
  • BonTimestamp : 消息发送时间戳
  • Topic : 消息的主题
  • Queueld :消息所在的队列QueueId
  • QueueOffset :消息在Queue中储存的偏移量
  • 。。。。。。等近20项详细相关属性

需要注意到,消息单元中是包含 Queue相关属性的。所以,我们在后续的学习中,就需要十分留意commitlog与queue间的关系是什么?
 

一个mappedFile文件中第n+1个消息单元的commitlog offset偏移量
L(n + 1) = L(n) + MsgLen(n)(n>0)

L1 = Ln + MsgLen(n)

  ConsumeQueue文件

上图 0 1 2 3 叫做queueId

点击进去查看consumequeue文件

首先 生产者把消息发送到broker里面的Commit Log文件里面的Body,保存消息的内容,同时也会Topic创建相应的Comsume Queue,存储消息在Commit Log的位置信息。
重点:

  1. 为了提高效率,会为每个在~/store/consumeQueue中创建一个目录,目录名为Topic名称
  2. 在该Topic目录下,会在为每个该Topic的Queue创建一个目录,目录名为queueId
  3. 每个目录中存放着若干consumequeue文件consumequeue文件是commitlog的索引文件,可以根据consume queue定位到具体的消息。

consumequeue文件名也是由20位数字构成,表示当前文件的第一个索引条目的起始偏移量。与mappedFile文件名不同的是,其后续文件名是固定的。因为consumequeue文件大小是固定不变的。

索引条目 

每个consumequeue文件可以包含30W个索引条目,每个索引条目包含了三个消息重要属性:

  • 消息在mappedFile文件中的
    • 偏移量CommitLogOffset
    • 消息长度
    • 消息Tag的hashCode置

这三个属性占20个字节,所以每个文件的大小是固定的30W*20字节

一个consumequeue文件中所有消息的Topic一定是相同的。但每条消息Tag可能不同的

(RocketMq存在根据tag进行查询,消息可以根据消息Tag的hashCode置去查询在Topic内的CommitLog目录下的ComsumeQueue里面索引目录进行查询)

对文件的写入 

消息写入

一条消息进入到Broker后经历了以下几个过程才最终被持久化。

  • Borker会根据queueId获取到该消息对应索引条目要在consumequeue目录的写入偏移量,即QueueOffset
  • 将queueId、queueOffset等数据,与消息一起封装为消息单元
  • 将消息单元写入到commitlog
  • 同时形成消息索引条目
  • 将消息索引条目分发到相应的consumequeue
消息拉取

当Conusmer来拉去消息时会经历以下几个步骤:

  • Consumer获取到其要消费消息所在Queue的消费偏移量offset计算出要消费消息的消息offset
    • 消费offset及消费进度,consumer对某个Queue的消费offset,即消费到了该Queue的第几条消息
    • 消息offset = 消费offset + 1
  • Consumer向Broker发送拉去请求,其中会包含其拉取消息的Queue、消息offset以及消息Tag
  • Broker计算在该consume queue中的queueOffset
    • queueOffset = 消息offset * 20字节
  • 从该queueOffset处开始向后查找第一个指定Tag的索引条目
  • 解析该索引条目的前8个字节,即可定位到该消息在commitlog中的commitlog offset
  • 从对应的commitlog offset中读取消息单元,并发送给Consumer
性能提升

RocketMQ中,无论是消息本身还是消息索引,都是存储在磁盘上的。其不会影响消息的消费吗?当然不会。

其实RocketMQ的性能在目前的MQ产品中性能是非常高的。因为系统通过一系列相关机制人大提升了性能。

  • 首先,RocketMQ对文件的读写操作是通过mmap零拷贝进行的,将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率。
  • 其次,consumequeue中的数据是顺序存放的,还引入了PageCache的预读取机制,使得对consumequeue
    • PageCache机制,页缓存机制,是OS对文件的缓存机制,用于加速对文件的读写操作。一般来说,程序对文件进行顺序读写的速度几乎接近于内存读写速度,主要原因是由于OS使用PageCache机制对读写访问操作进行性能优化,将一部分的内存用作PgeCache。
      • 写操作:OS会先将数据写入到PageCache中,随后会以异步方式由pdflush (page dirtyflush)内核线程将Cache中的数据刷盘到物理磁盘
      • 读操作:若用户要读取数据,其首先会从PageCache中读取,若没有命中,则OS在从物理磁盘上加载该数据到PageCache的同时,也会顺序对其相邻数据块中的数据进行预读取。

文件的读取几乎接近于内存读取,即使在有消息堆积情况下也不会影响性能。

RocketMQ中可能会影响性能的是对commitlog文件的读取。因为对commitlog文件来说,读取消息时会产生大量的随机访问,而随机访问会严重影响性能。不过,如果选择合适的系统10调度算法,比如没置调度算法为Deadline(采用SSD固态硬盘的话),随机读的性能也会有所提升。

深入理解

读操作:若用户要读取数据,其首先会从PageCache中读取,若没有命中,则OS在从物理磁盘上加载该数据到PageCache的同时,也会顺序对其相邻数据块中的数据进行预读取。

  1. 当用户要读取数据时,操作系统首先会检查PageCache(页面缓存),这是一个位于内存中的缓存,用于存储最近访问过的数据页。如果用户请求的数据在PageCache中已经存在(命中),则操作系统可以直接从PageCache中读取数据,而无需访问物理磁盘,这样可以提高读取速度。

  2. 如果用户请求的数据不在PageCache中(未命中),操作系统就需要从物理磁盘上加载该数据到PageCache中。在这个过程中,除了加载用户请求的数据,操作系统还会顺序预读取相邻数据块中的数据到PageCache中。

  3. 预读取相邻数据块的操作是为了利用空间局部性原理,即一旦访问了某个数据块,可能会接下来访问其相邻的数据块。通过预读取相邻数据块,操作系统可以提前将这些数据加载到内存中,以便在用户访问时能够更快地响应请求。

这种预读取的机制有助于减少对物理磁盘的实际访问次数,提高数据读取的效率和性能。通过在加载用户请求数据的同时预读取相邻数据块,操作系统可以优化数据访问过程,使得后续访问相邻数据块时能够更快地获取数据。这种优化有助于提高系统整体的读取性能。

举个简单的例子来说明相邻数据块中的数据块:

假设物理磁盘上存储了一些学生的信息,每个数据块包含多个学生的记录。

当用户请求读取第一个数据块中的学生信息时,操作系统会加载这个数据块到PageCache中。

在这种情况下,相邻数据块可能是指与第一个数据块相邻的其他数据块,这些数据块可能包含接下来的学生信息记录。

例如,如果学生信息按照学号的顺序存储在磁盘上,用户请求读取学号为1001-2000的学生信息,那么操作系统可能会顺序加载包含学号为2001-3000的数据块到PageCache中,因为这些数据块与用户请求的数据块相邻,操作系统预测用户可能会继续访问这些相邻数据块中的信息。

这样,在用户访问相邻数据块时,可以直接从PageCache中获取数据,而不必再次从物理磁盘上读取,从而提高数据访问的效率和性能。

与Kafka的对比

RocketMQ的很多思想来源于Kafka,其中commitlog与consumequeue就是。
RocketMQ中的commitlog目录与consumequeue的结合就类似于Kafka中的partition分区目录。
mappedFile文件就类似于Kafka中的segment段。

Kafka中的Topic的消息被分割为一个或多个partition。partition是一个物理概念,对应到系统上就是topic目录下的一个或多个目录。每个partition中包含的文件称为segment,是具体存放消息的文件。
Kafka中消息存放的目录结构是:

  • topic目录下有partition目录
  • partition目录下有segment文件
  • Kafka中没有二级分类标签Tag这个概念
  • Kafka中无需索引文件。

因为生产者是将消息直接写在了partition中的,消费者也是直接从partition中读取数据的

indexFile 

  1. 除了通过通常的指定Topic进行消息消费外
  2. RocketMQ还提供了根据key进行消息查询的功能

该查询是通过store目录中的index子目录中的indexFile进行索引实现的快速查询。当然,这个indexFile中的索引数据是在包含了key的消息被发送到Broker时写入的。如果消息中没有包含key,则不会写入。

索引条目结构

每个Broker中包含一组indexFile,每个indexFile都是以一个时间戳命名的(这个indexFile被创建时的时间戳)。每个indexFile由三部分构成:

  • indexHeader
  • slots槽位
  • indexes索引单元。

每个indexFile文件中包含500W个slot槽【这个数量可能是根据系统的需求、性能要求以及资源限制等因素综合考虑得出的最佳选择。而每个slot槽又可能会挂在很多的index索引单元。

indexHeader

indexHeader固定40个字节,其中存放着如下数据:

  • beginTimestamp:该indexFile中第一条消息的存储时间
  • endTimestamp:该indexFile中最后一条消息存储时间
  • beginPhyoffset:该indexFile中第一条消息在commitlog中的偏移量commitlog offset
  • endPhyoffset:该indexFile中最后一条消息在commitlog中的偏移量commitlog offset
  • hashSlotCount:已经填充有index的slot数量 (并不是每个slot槽下都挂载有index索引单元,这里统计的是所有挂载了index索引单元的slot槽的数量)
  • indexCount:该indexFile中包含的索引个数 (统计出当前indexFile中所有slot槽下挂载的所有index索引单元的数量之和)
slots槽位

indexFile中最复杂的是Slots与Indexes间的关系。在实际存储时,Indexes是在Slots后面的,但为了便于理解,将它们的关系展示为如下形式:

  • key的hash值%500w的结果即为slot槽位,然后将该slot值修改为该index索引单元的indexNo

indexNo是一个在indexFile中的流水号,从0开始依次递增。即在一个indexFile中所有indexNo是以此递增的。indexNo在 index索引单元中是没有体现的,其是通过 indexes中依次数出来的。

  • 根据这个indexNo可以计算出该index单元在indexFile中的位置
    • 不过,该取模结果的重复率是很高的,为了解决该问题,在每个index索引单元中增加了preIndexNo,用于指定该slot中当前index索引单元的前一个index索引单元。
  • 而slot中始终存放的是其下最新的index索引单元的indexNo,这样的话,只要找到了slot。就可以找到其最新的index索引单元,而通过这个index索引单元就可以找到其之前的所有index索引单元
index索引单元

index索引单元默写20个字节,其中存放着以下四个属性:

  • keyHash:消息中指定的业务key的hash值
  • phyOffset:当前key对应的消息在commitlog中的偏移量commitlog offset
  • timeDiff:当前key对应消息的存储时间与当前indexFile创建时间的时间差
  • preIndexNo:当前slot下当前index索引单元的前一个index索引单元的indexNo
indexFile 的创建

indexFile的文件名为当前文件被创建时的时间戳。这个时间戳有什么用处呢

根据业务key进行查询时,查询条件除了key之外,还需要指定一个要查询的时间戳,表示要查询不大于该时间戳的最新的消息。这个时间戳文件名可以简化查询,提高查询效率。具体后面会详细讲解。

indexFile文件是何时创建的?其创建的条件(时机)有两个:

  • ·当第一条带key的消息发送来后,系统发现没有indexFile,此时会创建第一个indexFile文件
  • ·当一个indexFile中挂载的index索引单元数量超出2000w个时,会创建新的indexFile。当带key的消息发送到来后,系统会找到最新的indexFile,并从其indexHeader的最后4字节中读取到indexCount。若indexCount >=2000w时,会创建新的indexFile。

由于可以推算出,一个indexFile的最大大小是:(40+500w*4+2000w*20)字节


查询流程

当消费者通过业务key来查询相应的消息时,其需要经过一个相对较复杂的查询流程。不过,在分析查询流程之前,首先要清楚几个定位计算式子:

计算指定消息key的s1ot槽位序号slot槽位序号  =  keyhash   %   500w
计算槽位序号为n的slot在indexFile中的起始位置slot(n)位置  =  40  +  (n-1)  *  4
计算indexNo为m的index在indexFile中的位置index(m)位置  =  40  +  500w  * 4  +(  m-1) * 20

  • 21
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值