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

基础项对比

可用性、可靠性对比

功能性对比

对比分析

  • 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次
  • 重试时间间隔递增(通过延时对列完成)
  • 失败后进入私信队列

事务消息

public class TransactionProducer {
public static void main(String[] args) throws MQClientException, InterruptedException {
TransactionListener transactionListener = new TransactionListenerImpl();
TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name");
ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("client-transaction-msg-check-thread");
return thread;
}
}); producer.setExecutorService(executorService);
//事务监听器
producer.setTransactionListener(transactionListener);
producer.start(); String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
for (int i = 0; i < 10; i++) {
try {
Message msg =
new Message("TopicTest1234", tags[i % tags.length], "KEY" + i,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
SendResult sendResult = producer.sendMessageInTransaction(msg, null);
System.out.printf("%s%n", sendResult); Thread.sleep(10);
} catch (MQClientException | UnsupportedEncodingException e) {
e.printStackTrace();
}
} for (int i = 0; i < 100000; i++) {
Thread.sleep(1000);
}
producer.shutdown();
}
} public interface TransactionListener {
/**
* When send transactional prepare(half) message succeed, this method will be invoked to execute local transaction.
*
* @param msg Half(prepare) message
* @param arg Custom business parameter
* @return Transaction state
*/
LocalTransactionState executeLocalTransaction(final Message msg, final Object arg); /**
* When no response to prepare(half) message. broker will send check message to check the transaction status, and this
* method will be invoked to get local transaction status.
*
* @param msg Check message
* @return Transaction state
*/
LocalTransactionState checkLocalTransaction(final MessageExt msg);
}

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;
}
}
}

RocketMQ应用及原理剖析的更多相关文章

  1. ASP.NET Core 运行原理剖析2:Startup 和 Middleware(中间件)

    ASP.NET Core 运行原理剖析2:Startup 和 Middleware(中间件) Startup Class 1.Startup Constructor(构造函数) 2.Configure ...

  2. ASP.NET Core 运行原理剖析1:初始化WebApp模版并运行

    ASP.NET Core 运行原理剖析1:初始化WebApp模版并运行 核心框架 ASP.NET Core APP 创建与运行 总结 之前两篇文章简析.NET Core 以及与 .NET Framew ...

  3. 【Xamarin挖墙脚系列:Xamarin.IOS机制原理剖析】

    原文:[Xamarin挖墙脚系列:Xamarin.IOS机制原理剖析] [注意:]团队里总是有人反映卸载Xamarin,清理不完全.之前写过如何完全卸载清理剩余的文件.今天写了Windows下的批命令 ...

  4. 【Xamarin 跨平台机制原理剖析】

    原文:[Xamarin 跨平台机制原理剖析] [看了请推荐,推荐满100后,将发补丁地址] Xamarin项目从喊口号到现在,好几个年头了,在内地没有火起来,原因无非有三,1.授权费贵 2.贵 3.原 ...

  5. iPhone/Mac Objective-C内存管理教程和原理剖析

    http://www.cocoachina.com/bbs/read.php?tid-15963.html 版权声明 此文版权归作者Vince Yuan (vince.yuan#gmail.com)所 ...

  6. 【Xamain 跨平台机制原理剖析】

    原文:[Xamain 跨平台机制原理剖析] [看了请推荐,推荐满100后,将发补丁地址] Xamarin项目从喊口号到现在,好几个年头了,在内地没有火起来,原因无非有三,1.授权费贵 2.贵 3.原生 ...

  7. Python字符串原理剖析------万恶的+号

    字符串原理剖析pyc文件,执行python代码时,如果导入了其他的.py文件,那么执行过程中会自动生成一个与其同名的.pyc文件,该文件就是python解释器变异之后产生的字节码 PS:代码经过编译可 ...

  8. MapReduce/Hbase进阶提升(原理剖析、实战演练)

    什么是MapReduce? MapReduce是一种编程模型,用于大规模数据集(大于1TB)的并行运算.概念"Map(映射)"和"Reduce(归约)",和他们 ...

  9. ASP.NET Core 运行原理剖析

    1. ASP.NET Core 运行原理剖析 1.1. 概述 1.2. 文件配置 1.2.1. Starup文件配置 Configure ConfigureServices 1.2.2. appset ...

随机推荐

  1. MarkDown学习内容总结

    MarkDown学习内容 标题 使用方法:通过 # 的个数实现多级标题. 举例如下: 一级标题格式为:# + 空格 + 标题名: 二级标题格式为:## + 空格 + 标题名: 三级标题格式为:### ...

  2. Python基础(列表生成式)

    import os; list1 = list(range(1,11)) list2 = [x*x for x in list1 if x % 2 == 0]#列表生成式时,把要生成的元素x * x放 ...

  3. requests的post请求基本使用

    import requests # 请求url url = 'https://fanyi.baidu.com/sug' # 请求头 headers = { 'User-Agent': 'Mozilla ...

  4. 从华为新发布的WeAutomate 3.0,看RPA如何在政企领域落地生长

    文/王吉伟 11月11日,是电商的重要节日.即便今年双11的气氛不如往年浓烈,人们依旧关注双11厂商战报,关注购物车里的商品有没有降价. 当然在RPA领域,大家除了关注双11的商品价格,更关注华为RP ...

  5. [cf1361E]James and the Chase

    称一个点是"好点",当且仅当其到其余所有点恰存在一条简单路径 结论1:$x$为好点当且仅当以$x$为根的dfs树包含所有点且非树边均为返祖边 若不包含所有点,那么$x$到不被包含的 ...

  6. [loj3500]矩阵游戏

    为了方便,令$a_{i,j}$的下标范围为$[0,n]$和$[0,m]$,$b_{i,j}$的下标范围为$[1,n]$和$[1,m]$ 当确定$a_{i,0}$和$a_{0,j}$后,即可通过$b_{ ...

  7. 十.Go并发编程--channel使用

    一.设计原理 Go 语言中最常见的.也是经常被人提及的设计模式就是: "不要通过共享内存来通信,我们应该使用通信来共享内存" 通过共享内存来通信是直接读取内存的数据,而通过通信来共 ...

  8. IPv4 寻址方式简介

    IPv4 支持三种不同类型的寻址模式.单播寻址方式.广播寻址方式和组播寻址方式.本章节我们来介绍这些寻址方式. 单播寻址方式 在这种模式下,数据只发送到一个目标主机.Destination Addre ...

  9. spring security 认证源码跟踪

    spring security 认证源码跟踪 ​ 在跟踪认证源码之前,我们先根据官网说明一下security的内部原理,主要是依据一系列的filter来实现,大家可以根据https://docs.sp ...

  10. CSP-S2021 挂分记

    赛前 数了数,这是我第五次来南航,不知道以后还有机会了(2018 NOIP, 2019CSP, 2020CSP, 2020NOIP). 上午一觉睡到 10 点,学了一下感觉很有用的 BIT 倍增,顺手 ...