Bootstrap

人人都懂的RocketMQ基本原理

前言

MQ 作为一个消息中间件有着异步提升性能、降低系统耦合度、流量削峰的特点,成为了提升系统应用不可缺少的组件。现在主流公司都采用阿里巴巴的 RocketMQ 作为消息中间件,RocketMQ 提供了高吞吐量、高可用、数据不丢失、集群部署、支持高级功能(死信队列、重试队列等)的功能,并且基于 java 语言开发,方便进行源码剖析和二次改造,所以是作为一个剖析消息中间件源码的不二人选。

我们这一讲先简单介绍一下 RocketMQ 的一些特性和基本原理,后续我们会根据这些特性对源码进行一步一步的剖析。

RocketMQ 基本原理

image.png

RocketMQ 架构主要包含以下四个部分,如上图所示:

  • NameServer:NameServer 是一个非常简单的 Topic 路由注册中心,支持Broker的动态注册与发现。主要包括两个功能:Broker 管理和路由信息管理

    • Broker 管理:NameServer 接收到 Broker 集群的注册信息并且保存下来作为路由信息的基本数据,NameServer 会提供心跳检测机制,每 10s 检测 Broker 是否超过120s没有发送心跳,如果超过的话就从 NameServer 中摘除该 Broker。
    • 路由信息管理:每个 NameServer 会保存关于 Broker 集群的整个路由信息和用于客户端查询的队列信息,Producer 和 Consumer 可以通过 NameServer 获取到整个 Broker 集群的路由信息,从而根据路由规则选择 Broker 进行消息的投递和消费。

    NameServer 可以集群部署,各实例之间相互不进行信息通讯。Broker 需要向每一台 NameServer 都注册自己的路由信息,所以每一个 NameServer 实例上面都保存一份完整的路由信息。当某个 NameServer 下线了,Broker 可以向其它 NameServer 同步路由信息,Producer 和 Consumer 仍然可以动态感知 Broker 的路由信息。

  • BrokerServer:Broker 主要负责消息的存储、投递和查询以及服务高可用保证。消息存储包含 CommitLog、ConsumeQueue、Index 这三种文件的存储,还需要负责管理客户端(Producer/Consumer)和维护 Consumer 的 Topic 订阅信息。还提供高可用服务,对 Master Broker 和 Slave Broker 进行消息的同步,所以 Broker Server 是 RocketMQ 的核心组件。

  • Producer:消息生产者,往 Broker 发送指定 Topic 的消息,可以同步、异步、oneway 的方式进行发送。

  • Consumer:消息消费者,支持 Push、Pull 两种消息消费的模式,并且支持集群方式和广播方式进行消费,提供实时消息订阅机制。

RocketMQ 特性

延迟队列

定时消息(延迟队列)是指消息发送到 broker 后,不会立即被消费,等待特定时间投递给真正的topic。 broker有配置项 messageDelayLevel,默认值为“ 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h ”,18个 level。可以配置自定义 messageDelayLevel。

定时消息会暂存在名为 SCHEDULE_TOPIC_XXXX 的 topic 中,并根据 delayTimeLevel 存入特定的 queue,queueId = delayTimeLevel – 1,即一个 queue 只存相同延迟的消息,保证具有相同发送延迟的消息能够顺序消费。broker 会调度地消费 SCHEDULE_TOPIC_XXXX,将消息写入真实的 topic。

后续这块我们需要进行二次开发,这个延迟时间可以按照指定的时间进行设置,让整体变得更加灵活。

消息重试

Consumer消费消息失败后,要提供一种重试机制,令消息再消费一次。

RocketMQ会为每个消费组都设置一个 Topic 名称为 “ %RETRY% + consumerGroup ” 的重试队列(这里需要注意的是,这个 Topic 的重试队列是针对消费组,而不是针对每个 Topic 设置的),用于暂时保存因为各种异常而导致 Consumer 端无法消费的消息。考虑到异常恢复起来需要一些时间,会为重试队列设置多个重试级别,每个重试级别都有与之对应的重新投递延时,重试次数越多投递延时就越大。RocketMQ 对于重试消息的处理是先保存至 Topic 名称为“ SCHEDULE_TOPIC_XXXX ”的延迟队列中,后台定时任务按照对应的时间进行 Delay 后重新保存至“ %RETRY%+consumerGroup ”的重试队列中。

死信队列

死信队列用于处理无法被正常消费的消息。当一条消息初次消费失败,消息队列会自动进行消息重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列 不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。

RocketMQ将这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),将存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。在 RocketMQ 中,可以通过使用 console 控制台对死信队列中的消息进行重发来使得消费者实例再次进行消费。

事务消息

RocketMQ事务消息(Transactional Message)是指应用本地事务和发送消息操作可以被定义到全局事务中,要么同时成功,要么同时失败。RocketMQ的事务消息提供类似 X/Open XA 的分布事务功能,通过事务消息能达到分布式事务的最终一致。

RocketMQ 事务消息的底层原理也是我们源码需要进行分析的。

存储架构设计

消息存储

image.png

RocketMQ 消息存储包含一下三个部分:

  • CommitLog:存储消息的文件,通过顺序写入的方式提高文件的写入效率。文件默认大小是1G,文件名称是消息的起始偏移量。比如00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1073741824;当第一个文件写满了,第二个文件为00000000001073741824,起始偏移量为1073741824,以此类推。
  • ConsumeQueue:消息消费队列,为了提高消息的消费性能,由于 RocketMQ 是基于主题 topic 的订阅模式,消息消费是针对主题进行的,如果要遍历 commitlog 文件中根据 topic 检索消息是非常低效的。所以对于每个 Topic 的每个 Queue 都对应着一个 ConsumeQueue,保存了指定 Topic 下的队列消息在 CommitLog中的起始物理偏移量 offset,消息大小 size 和消息 Tag 的 HashCode 值。这样可以根据物理偏移量 offset 进行二分查找,找到消息对应的 CommitLog 文件,然后减去文件的起始偏移量,就是消息在文件中的绝对偏移量,然后通过类似于数组的下标的方式从文件中根据绝对偏移量获取消息。
  • IndexFile:IndexFile(索引文件)提供了一种可以通过key或时间区间来查询消息的方法。

页缓存与内存映射

什么是页缓存呢?优点:预读、内存缓存

页缓存(PageCache)是OS对文件的缓存,用于加速对文件的读写。一般来说,程序对文件进行顺序读写的速度几乎接近于内存的读写速度,主要原因就是由于OS使用PageCache机制对读写访问操作进行了性能优化,将一部分的内存用作PageCache。对于数据的写入,OS会先写入至Cache内,随后通过异步的方式由pdflush内核线程将Cache内的数据刷盘至物理磁盘上。对于数据的读取,如果一次读取文件时出现未命中PageCache的情况,OS从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取。

这种页缓存的机制适用于存储数据较少,并且是顺序读取的,在pageCache的预读取作用下,Consume Queue 文件的读性能几乎接近读内存,所以即使消息堆积也不会影响性能。

对于CommitLog消息存储的日志数据文件来说,读取消息内容时候会产生较多的随机访问读取,严重影响性能。如果选择合适的系统IO调度算法,比如设置调度算法为“Deadline”(此时块存储采用SSD的话),随机读的性能也会有所提升。

什么是内存映射?优点:减少内核态与用户态复制

磁盘的顺序写可以极大提高I/O写效率,并且使用常规的 Java API 带来的性能是有限的。

所以采用 NIO FileChannel map 方法创建内存映射文件。这样的好处就是减少了内核态与用户态之间来回拷贝带来的开销。对文件操作转化为对内存地址进行操作,极大地提高了文件的读写效率。

RocketMQ 的文件存储使用定长结构来存储,是方便一次将整个文件映射至内存中

灵活多变的刷盘策略

磁盘顺序写+内存映射对于RocketMQ写入性能有很大的提升,但消息是存储到页缓存,并没有持久化,那一条消息怎么算是发送成功了呢?是发送到页缓存直接返回,还是持久化到磁盘上之后再进行返回?

RocketMQ 提供了两种刷盘机制:同步刷盘、异步刷盘。根据性能和可靠性决定采用哪种刷盘机制。

同步刷盘:RocketMQ 实现中称为组提交,GroupCommitService。

异步刷盘:同步刷盘能保证消息不丢失,牺牲的是写入的性能。RocketMQ 提供了异步刷盘机制。

异步刷盘是消息存储到 pageCache 上后就直接返回,开启一个异步线程定时执行 FileChannel 的 force 方法,将内存中的数据定时写入磁盘,默认间隔时间是 500 ms。

总结

RocketMQ 主要由 NameServer、Broker、Producer、Consumer 四个部分组成,并且 RocketMQ 提供了一些基本特性包括:延迟队列、消息重试、死信队列、事务消息等。

并且简单了解了 RocketMQ 的存储架构设计,通过这种顺序写+内存映射来极大的提高了消息写入的速率。通过 ConsumeQueue 的设计方式来提高消息消费的效率。

后续我们就开始进行源码的剖析流程,先从 NameServer 进行剖析,然后消息发送流程、消息存储流程、消息消费流程,分析完核心流程后,就进行高级特性(延迟队列、事务消息等)剖析。

;