RocketMQ应用及原理剖析

主流消息队列选型对比分析

基础项对比

可用性、可靠性对比

功能性对比

对比分析

  • Kafka:系统间的流数据通道
  • RocketMQ:高性能的可靠消息传输
  • RabbitMQ:可靠消息传输

RocketMQ剖析

RocketMQ拓扑图

RocketMQ架构组成

  • Producer:消息发布的角色,支持分布式集群方式部署。Producer通过MQ的负载均衡模块选择相应的Broker集群队列进行消息投递,投递的过程支持快速失败并且低延迟。
  • Consumer:消息消费的角色,支持分布式集群方式部署。支持以push推,pull拉两种模式对消息进行消费。同时也支持集群方式和广播方式的消费,它提供实时消息订阅机制,可以满足大多数用户的需求。
  • NameServer:NameServer是一个非常简单的Topic路由注册中心,其角色类似Dubbo中的zookeeper,支持Broker的动态注册与发现。主要包括两个功能:Broker管理,NameServer接受Broker集群的注册信息并且保存下来作为路由信息的基本数据。然后提供心跳检测机制,检查Broker是否还存活;路由信息管理,每个NameServer将保存关于Broker集群的整个路由信息和用于客户端查询的队列信息。然后Producer和Conumser通过NameServer就可以知道整个Broker集群的路由信息,从而进行消息的投递和消费。NameServer通常也是集群的方式部署,各实例间相互不进行信息通讯。Broker是向每一台NameServer注册自己的路由信息,所以每一个NameServer实例上面都保存一份完整的路由信息。当某个NameServer因某种原因下线了,Broker仍然可以向其它NameServer同步其路由信息,Producer,Consumer仍然可以动态感知Broker的路由的信息。
  • BrokerServer:消息中转角色,负责存储消息、转发消息。代理服务器在RocketMQ系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备。代理服务器也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等。

部署架构

集群工作流程

  • 启动NameServer,NameServer起来后监听端口,等待Broker、Producer、Consumer连上来,相当于一个路由控制中心。
  • Broker启动,跟所有的NameServer保持长连接,定时发送心跳包。心跳包中包含当前Broker信息(IP+端口等)以及存储所有Topic信息。注册成功后,NameServer集群中就有Topic跟Broker的映射关系。
  • 收发消息前,先创建Topic,创建Topic时需要指定该Topic要存储在哪些Broker上,也可以在发送消息时自动创建Topic。
  • Producer发送消息,启动时先跟NameServer集群中的其中一台建立长连接,并从NameServer中获取当前发送的Topic存在哪些Broker上,轮询从队列列表中选择一个队列,然后与队列所在的Broker建立长连接从而向Broker发消息。
  • Consumer跟Producer类似,跟其中一台NameServer建立长连接,获取当前订阅Topic存在哪些Broker上,然后直接跟Broker建立连接通道,开始消费消息。
 

RocketMQ设计

消息存储

  1. CommitLog:存储消息的主体。product生产的消息主要是顺序写入日志文件,当文件满了,写入下一个文件。
  2. ConsumerQueue:消息的消费队列。引入的目的主要是提高消息消费的性能,由于RocketMQ是基于主题topic的订阅模式,消息消费是针对主题进行的,如果要遍历commitlog文件中根据topic检索消息是非常低效的。
  3. FileIndex:索引文件。提供了一种可以通过key或时间区间来查询消息的方法

消息刷盘

  1. 同步刷盘:性能低,可靠性高。
  2. 异步刷盘:性能高,可靠性低。
一般线上采用异步刷盘+异步复制。如果保证绝对可靠性需要同步刷盘+同步双写,但性能很低,可以针对特别重要的消息,单独部署broker。

协议设计与编解码

在Client和Server之间完成一次消息发送时,需要对发送的消息进行一个协议约定,因此就有必要自定义RocketMQ的消息协议。同时,为了高效地在网络中传输消息和对收到的消息读取,就需要对消息进行编解码。在RocketMQ中,RemotingCommand这个类在消息传输过程中对所有数据内容的封装,不但包含了所有的数据结构,还包含了编码解码操作。
Header字段 类型 Request说明 Response说明
code int 请求操作码,应答方根据不同的请求码进行不同的业务处理 应答响应码。0表示成功,非0则表示各种错误
language LanguageCode 请求方实现的语言 应答方实现的语言
version int 请求方程序的版本 应答方程序的版本
opaque int 相当于requestId,在同一个连接上的不同请求标识码,与响应消息中的相对应 应答不做修改直接返回
flag int 区分是普通RPC还是onewayRPC的标志 区分是普通RPC还是onewayRPC的标志
remark String 传输自定义文本信息 传输自定义文本信息
extFields HashMap<String, String> 请求自定义扩展信息 响应自定义扩展信息
传输内容主要可以分为以下4部分:
(1) 消息长度:总长度,四个字节存储,占用一个int类型;
(2) 序列化类型&消息头长度:同样占用一个int类型,第一个字节表示序列化类型,后面三个字节表示消息头长度;
(3) 消息头数据:经过序列化后的消息头数据;
(4) 消息主体数据:消息主体的二进制字节数据内容;
public ByteBuffer encode() {
    // 1> header length size
    int length = 4;

    // 2> header data length
    byte[] headerData = this.headerEncode();
    length += headerData.length;

    // 3> body data length
    if (this.body != null) {
        length += body.length;
    }

    ByteBuffer result = ByteBuffer.allocate(4 + length);

    // length
    result.putInt(length);

    // header length
    result.put(markProtocolType(headerData.length, serializeTypeCurrentRPC));

    // header data
    result.put(headerData);

    // body data;
    if (this.body != null) {
        result.put(this.body);
    }

    result.flip();

    return result;
}

 

负载均衡

RocketMQ中的负载均衡都在Client端完成,具体来说的话,主要可以分为Producer端发送消息时候的负载均衡和Consumer端订阅消息的负载均衡。

product端负载均衡

  1. 定期获取TopicPublishInfo路由信息
  2. product发送消息时选取一个messageQueue发送消息(默认的负载均衡策略:随机递增取模)
  3. 容错机制(故障延时:指对之前失败的,按一定的时间做退避。发送失败默认有会有重试(同步:2次,异步:1次)同步重试会避开上一次发失败的broker

Consumer端负载均衡

mq消息消费方式
PUSH :消息队列主动将消息推送给消费者
PULL:消费者主动去消息队列拉取
push 和pull 两种方式的对比:
push:消息实时性高,但没有考虑消费端的消费能力
pull:消息实时性低,可能造成大量无效请求
consumer获取消息的模式:
在RocketMQ中,Consumer端的两种消费模式(Push/Pull)都是基于拉模式来获取消息的,为了平衡push/pull的各自的弊端,使用了一种长轮询机制来拉取消息。Push模式只是对pull模式的一种封装,其本质实现为消息拉取线程在从服务器拉取到一批消息后,然后提交到消息消费线程池后,又继续向服务器再次尝试拉取消息。如果未拉取到消息,则延迟一下又继续拉取。
负载均衡
  1. 定时发送心跳包到broker
  2. consumer开始订阅消息会rebalance 一次
  3. 定期rebalance(20s)
  • 获取队列信息
  • 获取消费者信息
  • 排序平均分配(默认)
  • 与上次结果对比
  •  

     

RocketMQ功能实现分析

RocketMQ延时消息

rockeketMQ支持18个级别的延时等级,默认值为:“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h”

 

 

实现原理

  1. 替换主题SCHEDULE_TOPIC_XXX,根据延时等级放入对应的队列
  2. 18个Queue对应18个延时等级
  3. 每个队列创建定时任务进行调度
  4. 恢复到期消息重新投递到真实的topic

消息重试

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

 

 

消费失败策略

  • 重试16次
  • 重试时间间隔递增(通过延时对列完成)
  • 失败后进入私信队列

事务消息

RocketMQ事务消息流程概要

上图说明了事务消息的大致方案,其中分为两个流程:正常事务消息的发送及提交、事务消息的补偿流程。
1.事务消息发送及提交:
(1) 发送消息(half消息)。
  • HALF消息:RMQ_SYS_TRANS_HALF_TOPIC(临时存放消息信息)
    • 事务消息替换主体,保存原主题和对列信息
    • 半消息对Consumer不可见,不会被投递
(2) 服务端响应消息写入结果。
(3) 根据发送结果执行本地事务(如果写入失败,此时half消息对业务不可见,本地逻辑不执行)。
(4) 根据本地事务状态执行Commit或者Rollback(Commit操作生成消息索引,消息对消费者可见)
 
2.补偿流程:
(1) 对没有Commit/Rollback的事务消息(pending状态的消息),从服务端发起一次“回查”
(2) Producer收到回查消息,检查回查消息对应的本地事务的状态
(3) 根据本地事务状态,重新Commit或者Rollback
其中,补偿阶段用于解决消息Commit或者Rollback发生超时或者失败的情况。

怎么记录二阶段的操作?

RocketMQ事务消息方案中引入了Op消息的概念,用Op消息标识事务消息已经确定的状态(Commit或者Rollback)。如果一条事务消息没有对应的Op消息,说明这个事务的状态还无法确定(可能是二阶段失败了)。引入Op消息后,事务消息无论是Commit或者Rollback都会记录一个Op操作。Commit相对于Rollback只是在写入Op消息前创建Half消息的索引。
  • OP消息:RMQ_SYS_TARNS_OP_HALF_TOPIC(记录二阶段的操作)
    • Rollback:只做记录
    • Commit:根据备份信息重新构造消息并投递
 

扩展

 

RocketMQ事务消息对业务侵入性强的解决方案

 

 

  1. 开启事务
  2. 操作本地业务数据
  3. 插入事务消息数据
  4. 提交事务
  5. 发送mq消息
  6. mq send响应
  7. mq消息发送成功删除事务消息表中的记录
  8. 定时补偿模块扫描事务消息表
  9. 补偿发送mq消息
  10. mq send 响应
  11. mq消息发送成功删除事务消息表中的记录

伪代码

@Transactional
public void pay(Order order){
    PayTransaction t = buildPayTransaction(order);
    payDao.append(t);
    //producer.sendMessage(buildMessage(t));
    final Message message = buildMessage(t);
    messageDao.insert(message);
    //在事务提交后执行
    triggerAfterTransactionCommit(()->{
        messageClient.send(message);
        messageDao.delete(message);
    });
}

 

 

事务消息表

CREATE TABLE mq_message(
id bigint NOT NULL AUTO_INCREMENT,
content varchar(255) NOT NULL,
topic char(64) NOT NULL,
tag char(64),
status tinyint,
createtime timestamp,
PRIMARY KEY(id)
 
)

 

任意时间延时消息实现方案

 

 

改造步骤

  1. Dispatch改造
  2. 延时消息存储
  3. 内存索引(时间轮)
  4. 延时消息投递
 
Dispatch改造点,增加一种特殊队列存储任意时间延时 改动量比较大,可以增加一种消息类型即可改造
class CommitLogDispatcherBuildConsumeQueue implements CommitLogDispatcher {
 
@Override
public void dispatch(DispatchRequest request) {
final int tranType = MessageSysFlag.getTransactionValue(request.getSysFlag());
switch (tranType) {
case MessageSysFlag.TRANSACTION_NOT_TYPE:
case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
DefaultMessageStore.this.putMessagePositionInfo(request);
break;
case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
break;
}
}
}
 

 

posted @ 2021-10-14 10:42  晋级在路上  阅读(303)  评论(0编辑  收藏  举报