RocketMQ架构

219 阅读10分钟

RocketMQ架构

这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战

使用场景

​ 消息中间件的场景一般也就这几种: 异步, 解耦, 削峰填谷,数据分发等等

角色

  • Producer: 生产者, 与NameServer集群中的一个节点建立长连接, 定期获取Topic路由信息,向提供Topic服务的Master建立长连接, 定时发送心跳, 无状态, 集群部署
  • Consumer: 消费者, 与NameServer集群中的一个节点建立长连接, 定期获取Topic路由信息.向提供Topic服务的Master, Slave建立长连接, 定时发送心跳. Consumer既可以从Master订阅消息, 也可以从Slave订阅消息。消费者向Master拉取消息时, Master会根据拉取偏移量与最大偏移量的距离以及从服务器是否可读建议下一次从Master拉取还是Slave拉取。
  • Broker: 部署相对复杂, 分为Master和Slave。对应关系通过指定相同的BrokerName, 不同的BrokerId指定。 BrokerId为0表示Master, 非0 表示Slave Broker与NameServer集群的所有节点建立长连接,定时注册Topic到NameServer。只有brokerId为1的从服务器参与消息读负载。
  • NameServer: 管理Broker, 无状态节点, 集群部署,节点间无任何消息同步
  • Topic: 消息种类。
  • Message Queue: 并行发送和接收消息

特性

  • 订阅发布
  • 消息顺序
  • 消息过滤
  • 消息可靠性
  • 至少一次
  • 回溯消息
  • 事务消息
  • 定时消息
  • 消息重试
  • 消息重投
  • 流量控制
  • 死信队列

消息模式

RocketMQ有两种消息模式,一种Push模式,一种Pull模式,本质上都是消费端主动拉取, 采用长轮询。

消息发送

生产者向消息队列里写入消息,不同的业务场景需要生产者采用不同的写入策略。比如同步发送、异步发送、Oneway发送、延迟发送、发送事务消息等。

消息返回状态:

  • FLUSH_DISK_TIMEOUT:表示没有在规定时间内完成刷盘(需要Broker的刷盘策略被设置成SYNC_FLUSH才会报这个错误)。
  • . FLUSH_SLAVE_TIMEOUT:表示在主备方式下,并且Broker被设置成SYNC_MASTER方式,没有在设定时间内完成主从同步。
  • SLAVE_NOT_AVAILABLE:这个状态产生的场景和FLUSH_SLAVE_TIMEOUT类似,表示在主备方式下,并且Broker被设置成SYNC_MASTER,但是没有找到被配置成Slave的Broker
  • SEND_OK:表示发送成功,没有发生上面列出的三个问题状态就是SEND_OK。

提升写入性能

  • 使用oneway
  • 增加Producer的并发量,RocketMQ引入了一个并发窗口,在窗口内消息可以并发地写入DirectMem中,然后异步地将连续一段无空洞的数据刷入文件系统当中。
  • 顺序写CommitLog可让RocketMQ无论在HDD还是SSD磁盘情况下都能保持较高的写入性能。
  • 在Linux操作系统层级进行调优,推荐使用EXT4文件系统,IO调度算法使用deadline算法。

消息消费

提升消费性能

  • 优化代码逻辑
  • 提高消费并行度,但总Consumer不要超过read Queue的数量
  • 批量消费
  • 检测延时, 跳过非重要消息

消息存储

RocketMQ使用顺序写, 存储是由ConsumeQueue和CommitLog配合完成, 消息真正的物理存储文件是CommitLog, ConSumeQueue是逻辑队列, 类似索引,存储的是commitLogOffset, msgSize和tagsCode。

存储主要是:

  • CommitLog: 消息主题与元数据的存储主题, 消息内容不定长,单个文件默认1G, 顺序写日志文件
  • ConsumeQueue: 消息消费队列, 提高消息消费的性能,存储的是commitLogOffset, msgSize和tagsCode。采用定长设计, 单个文件30W个条目组成, 每个ConsumeQueue大小约5.72M
  • IndexFile: 索引文件,提供key或时间区间查询。文件名是创建时间戳, 固定大小400M, 保存2000W个索引, 底层设计是HashMap

过滤消息

RocketMQ 是在Consumer和Broker端进行消息过滤的。大的方面分为表达式过滤和类过滤两种模式。类过滤是通过提交过滤类到FilterServer进行过滤。这里主要说表达式过滤。

  • Tag过滤, 在服务端根据Tag的hashcode进行过滤,在消费时还要进行原始比对
  • SQL92过滤, 仅对push的消费者起作用,更加灵活

零拷贝

mmap 只是在虚拟内存分配了地址空间, 只有第一次访问虚拟内存时才分配物理内存。

  • 虽然叫零拷贝,实际上sendfile有2次数据拷贝的。第1次是从磁盘拷贝到内核缓冲区,第二次是从内核缓冲区拷贝到网卡(协议引擎)。如果网卡支持 SG-DMA技术,就无需从PageCache拷贝至 Socket 缓冲区;
  • 之所以叫零拷贝,是从内存角度来看的,数据在内存中没有发生过拷贝,只是在内存和I/O设备之间传输。很多时候我们认为sendfile才是零拷贝,mmap严格来说不算;
  • Linux中的API为sendfile、mmap,Java中的API为FileChanel.transferTo()、FileChannel.map()等;
  • Netty、Kafka(sendfile)、Rocketmq(mmap)、Nginx等高性能中间件中,都有大量利用操作系统零拷贝特性。

主从复制

主从复制是通过Broker端的brokerRole属性控制, SYNC_MASTER、ASYNC_MASTER, SLAVE。

高可用

消费端

消费端Master和Slave可以自动切换实现高可用

发送端

引入Dledger,写入消息时要求消息复制到半数节点,才给客户端返回成功, 支持通过选举动态切换主节点。当然也存在一些问题,比如选举过程不能提供服务, 最少需要3个节点, 要复制半数以上,性能差。

刷盘机制

RocketMQ 先写入PageCache,然后刷盘,有两种方式: 同步刷盘和异步刷盘。 配置 在Broker的FlushDiskType. SYNC_FLUSH(同步刷新)相比于ASYNC_FLUSH(异步处理)会损失很多性能,但是也更可靠,所以需要根据实际的业务场景做好权衡。

负载均衡

Producer负载均衡

发送消息通过轮询队列发送, 也可以自定义指定

Consumer负载均衡

负载均衡分配在Consumer端完成, Consumer从Broker获取全局消息, 做负载均衡,只处理分给自己的消息, 负载均衡的粒度只到Message Queue。默认采用AllocateMessageQueueAveragely。

消息重试

顺序消息重试

自动不断进行重试(间隔1秒)

无序消息重试

只针对集群消费方式有效。 重试次数最多16次, 可以通过返回ConsumerConcurrentlyStatus.RECONSUME_LATER返回。

Producer的send方法本身支持内部重试,重试逻辑如下:

  • 至多重试2次(同步发送为2次,异步发送为0次)。

  • 如果发送失败,则轮转到下一个Broker。这个方法的总耗时时间不超过sendMsgTimeout设置的值,默认10s。

  • 如果本身向broker发送消息产生超时异常,就不会再重试。

死信队列

  • 不会再被消费者正常消费
  • 有效期和正常消息相同,都是3天
  • 死信队列对应一个GroupId

延迟消息

设置delayLevel等级,等于0表示不是延迟消息。定时消息会暂存在SCHEDULE_TOPIC_XXX的topic中, 并根据delayTimeLevel存入特定的queue, 保证相同发送延迟的消息能够顺序消费。

顺序消息

部分有序

发送端把相同业务ID的消息发送到同一个MessageQueue, 消费过程不被并发处理,使用MessageListenerOrderly解决单MessageQueue的消息并发处理问题。

全局有序

  • setConsumeThreadMin 需要修改成1
  • setConsumeThreadMax 需要修改成1
  • setPullBatchSize 默认32 需要修改成1
  • setConsumeMessageBatchSize 默认是1

事务消息

事务消息发送及提交:

  • 发送消息(half消息)。

  • 服务端响应消息写入结果。

  • 根据发送结果执行本地事务(如果写入失败,此时half消息对业务不可见,本地逻辑不执行)。

  • 据本地事务状态执行Commit或者Rollback(Commit操作生成消息索引,消息对消费者可见)

补偿流程:

  • 对没有Commit/Rollback的事务消息(pending状态的消息),从服务端发起一次“回查”

  • Producer收到回查消息,检查回查消息对应的本地事务的状态

  • 根据本地事务状态,重新Commit或者Rollback

补偿阶段用于解决消息Commit或者Rollback发生超时或者失败的情况。

原理

  • 事务消息一阶段不可见

    如果消息是half消息,将备份原消息的主题与消息消费队列,然后改变主题为RMQ_SYS_TRANS_HALF_TOPIC。由于消费组未订阅该主题,故消费端无法消费half类型的消息。然后二阶段会显示执行提交或者回滚half消息(逻辑删除)。为了防止二阶段操作失RocketMQ会开启一个定时任务,从Topic为RMQ_SYS_TRANS_HALF_TOPIC中拉取消息进行消费,根据生产者组获取一个服务提供者发送回查事务状态请求,根据事务状态来决定是提交或回滚消息。

  • Commit和Rollback操作以及OP消息的引入

    在完成一阶段写入一条对用户不可见的消息后,二阶段如果是Commit操作,则需要让消息对用户可见;如果是Rollback则需要撤销一阶段的消息。先说Rollback的情况。对于Rollback,本身一阶段的消息对用户是不可见的,其实不需要真正撤销消息(实际上RocketMQ也无法去真正的删除一条消息,因为是顺序写文件的)。但是区别于这条消息没有确定状态(Pending状态,事务悬而未决),需要一个操作来标识这条消息的最终状态。RocketMQ事务消息方案中引入了Op消息的概念,用Op消息标识事务消息已经确定的状态(Commit或者Rollback)。如果一条事务消息没有对应的Op消息,说明这个事务的状态还无法确定(可能是二阶段失败了)。引入Op消息后,事务消息无论是Commit或者Rollback都会记录一个Op操作。Commit相对于Rollback只是在写入Op消息前创建Half消息的索引。

rocketmq并不会无休止的的信息事务状态回查,默认回查15次,如果15次回查还是无法得知事务状态,rocketmq默认回滚该消息。

为什么用NameServer不用Zookeeper呢?

  • 注册中心应该是AP, 只要在承诺的时间内达到一致状态就可以接受
  • 注册中心不能因为自己的任何原因破坏服务之间的可联通性。
  • 服务规模增大, Zookeeper的写性能堪忧
  • 注册中心不需要持久化实时的健康的服务列表, 只需要存储一些元数据, 并且提供检索能力
  • ZK服务健康检查只检查了TCP长链接活性探测, 注册中心应该提供更丰富的健康检测方案
  • 容灾, 即使注册中心宕机, 服务链路也不应该受到影响

消息零丢失方案

发送消息到MQ的零丢失

  • 同步发送消息 + 反复重试
  • 事务消息机制

MQ收到消息后的零丢失

开启同步刷盘+ 主从同步

消费消息的零丢失

不采用多线程异步处理消息的方式.