一、简介
RocketMQ是一个分布式消息中间件,阿里内部使用的版本叫MetaQ。主要用于分布式系统的解耦、异步处理、削峰填谷。
- 数据同步,比如mysql binlog同步,cache主从同步。
- 电商交易订单下发,广告归因计费,商品,会员变更消息下发,金融理财等。
- IM实时消息,如来往、钉钉消息总线。
本文参考提炼了两篇官方文档的部分内容,借用了部分图片:
二、RocketMQ与Kafka优劣势对比
RocketMQ在设计上比Kafka要复杂一些,也提供了更多丰富的功能,像消息tag过滤,延迟队列,顺序队列。以及出错时使用的重试队列,死信队列等等。
个人认为在业务应用开发场景下,使用RocketMQ会更加简单有效。下面是我从原理、应用方面总结和Kafka对比的优劣。
Kafka
优势
- Kafka消息存储上一个topic对应一个物理文件,设计简单,具有更高的吞吐量,适合应用于日志采集、大数据等领域。
- Kafka拥有丰富的生态系统,包括各种流处理框架和监控工具,可以轻松集成到Kafka中,扩展其功能。
劣势
- Kafka功能上不如RocketMQ那么丰富。没有消息tag过滤,延迟队列,顺序队列,消息事务,push/pull模式。以及出错时使用的重试队列,死信队列等等功能。
- 当Kafka一个集群中部署很多topic时,由于不同topic会写入不同消息文件,会变成随机写,写入性能会急剧下降。
- Kafka则是以Topic粒度维护状态机,如果按阿里RocketMQ部署方式,一个Broker上有很多topic,某个Topic中节点宕机会导致上万个状态机切换对注册中心形成冲击。风险较高。
RocketMQ
优势
- RocketMQ 单机支持更多的 topic,且具有更高的可靠性(一致性支持),因此适用于复杂的业务处理。
- RocketMQ功能丰富。提供了消息tag、属性过滤,延迟队列,顺序队列,消息事务,push/pull模式。以及出错时使用的重试队列,死信队列等等功能。
- RocketMQ的Broker采用的是小集群模式,某个分片Broker出现故障只会更新NameServer局部状态,不会像Kafka更新所有topic状态,可靠性更高。
劣势
- 虽然RocketMQ在阿里内部得到了广泛应用,但在开源社区的活跃度和生态系统方面不如Kafka。
- RocketMQ文件设计上较Kafka复杂,有CommitLog、ConsumeQueue、index索引三个文件,而Kafka只有一个文件,当集群只有一个Topic时,RocketMQ吞吐量是不如Kafka的。
三、物理架构
Kafka物理架构
Kafka4.0版本之前注册中心依赖Zookeeper实现,主要是用来选主。Partition master挂了,第1步先在Zk机器中选出KafkaController,第2步再由Controller决定每个partition的master是谁。
由于Zookeeper在大量节点发生状态变更时性能会遇到瓶颈,状态不一致、部署复杂、配置容易出错等问题。从Kafka 4.0版本起,ZooKeeper彻底被移除,基于raft协议选主,使用内部的仲裁(Quorum)控制器来取代ZooKeeper,实现了自我管理元数据的管理方式。
RocketMQ物理架构
NameServer
集群注册中心,注册消息topic与broker的关系,即消息存储的位置。
- 每台NameServer节点都维护着所有Broker的地址列表、所有topic、以及topic对应的Queue信息。
- Producer在发送消息之前,会先与任意一台NameServer建立链接,获取Broker服务器地址列表,然后根据负载均衡算法从列表中选一台Broker发送消息。
- RocketMQ从一开始就采用raft协议选出新master。raft协议 简单概括就是master挂了后第一个发现的slave节点会发起投票,选举出一个数据配置版本最新的slave做为master。与Kafka不同:
- Kafka4.0版本之前注册中心依赖Zookeeper实现。
- Kafka 4.0之后就不存在物理上需要单独部署的注册中心了,而是集成在Broker内的组件,这与RocketMQ不同。
Broker
主要负责消息的存储和转发。一个集群有很多组不同名称的Broker。可以横向扩展支持大流量消息。
- 一组Broker,有master和slave节点,是一写多读关系。
- master与slave数据同步的方式有同步复制,异步复制两种。
- 同步:写入消息时master会等待所有slave数据同步成功后才返回ACK。默认采用此方式。
- 异步:异步复制,部分slave会因为刷盘失败,会有少量消息丢失。
- 每个Broker服务器都会与NameServer集群建立长链接。注意是和每台NameServer。
- 不同Broker节点处理速度不同。节点水位不同,高水位节点对消费者不可见。
- master宕机会通过raft协议 选举出新的master。
Producer
消息生产者
- 与NameServer随机一个节点建立长链接,定时从NameServer获取topic路由信息
- 与路由到的master broker建立长链接,不与slave建立链接。
- 有Group概念,同一个Group生产/消费逻辑相同。比如主站交易订单,下游有很多BU消费,一般一个BU消费对应一个Group。
Consumer
消息消费者
- 与NameServer随机一个节点建立长链接,定时从NameServer获取topic路由信息,获取想要消费的queue
- 可以和提供服务的master或slave建立长链接。即可从master订阅消息,也可以从slave订阅消息。
四、消息存储
消息存储于生产者和消费者之间的一个消息代理。Message Broker上。
RocketMQ重要概念
- Message,单位消息
- Topic, 消息类型,比如交易订单,会员,优惠等不同业务活动实体,会使用不同Topic分类。
- Tag, Topic下的二级分类,比如交易订单内细分服饰、虚拟物品、本地生活....
- Queue,物理分区,一个Topic对应多个Queue
- Group,用来唯一标识一组生产者/消费者逻辑分组。同一组内生产/消费逻辑一致。比如交易订单下游消费者、广告、权益优惠、支付宝各自会定义各自的Consumer,保证消费进度互相不影响。
- Offset,偏移值,表示消费到。即将消费到的位置。
各类存储模型优劣
- ActiveMQ,依靠关系数据库队列表来存储消息,依靠轮询、加锁等方式检查和处理消息。对于QPS很高的系统来说,积压庞大的数据在表中会导致B+树索引层级加深,影响查询效率。
- KV数据库采用LSM树作为索引结构,对读性能有较大牺牲。因为消费者要频繁读取消息,对消息队列而言很难接受。
- RocketMQ/Kafka/RabbitMQ 等消息队列会采用顺序写日志结构。可以避免频繁随机访问而导致的性能问题,而且利于延迟写入等优化手段能快速保存日志。
- Kafka会为每个topic划分出多个Partition物理文件,便于Consumer顺序消费,消息被读取后不会立刻删除,可以持久存储,当topic数量增加时,一个broker的分区文件数量会增大,会使得本来速度很快的顺序写变成随机写【不同文件之间移动】,性能大幅下降。
-
- Kafka会为每个topic划分出多个Partition物理文件,便于Consumer顺序消费,消息被读取后不会立刻删除,可以持久存储,当topic数量增加时,一个broker的分区文件数量会增大,会使得本来速度很快的顺序写变成随机写【不同文件之间移动】,性能大幅下降。
RocketMQ 2.0存储模型
CommitLog
物理存储不定长的完整消息记录,逻辑上是完全连续的日志,单个文件大小是1GB。文件名是当前文件首地址在CommitLog中的偏移量。该文件落盘成功即认为消息发送成功。
- CommitLog顺序写是实现RocketMQ高性能的基础之一。顺序写速度600MB/s,随机写速度100KB/s。差了600倍。
- 由于一个Topic逻辑上对应一个CommitLog,顺序写效率较高。但读取的时候不同Topic的不同Consumer消费进度【位点】却完全不同,这样会导致CommitLog完全变成随机读。
- 为解决CommitLog消费时随机读问题,引入ConsumeQueue。
-
ConsumeQueue
定长轻量级索引队列,包含【8字节offset,4字节size,8字节tagHashCode】。单个Consume Queue最多可包括30万个条目。
- 对消费者暴露的主要是ConsumeQueue逻辑视图,提供队列访问接口。不同Consumer Group通过提交Queue的位点来维护消费进度。
- RocketMQ会启动一个定时服务,依据Commitlog定时调用(1ms)来异步生成ConsumeQueue和其它索引文件。异步生成索引文件也是RocketMQ和Kafka核心不同的地方。
- ConsumeQueue非常小,Consumer消费时又是批量顺序读取各个ConsumerQueue索引,速度几乎接近内存读。
- 同时在page cache和良好的空间局部性作用下,CommitLog访问也非常快速。
-
-
index索引
支持根据 key 值进行筛选,查找时,可以根据消息的 key 计算 hash 槽的位置,hash 槽中存储着 Index 条目的位置,可以根据这个 index 条目获得一个链表(尾),每个 index 条目包含在 CommitLog 上的消息主体的物理偏移量。
与kafka的核心不同
- 写入,kafka采用一个topic对应多个partion、以及index存储模型,消息日志及索引是同步写入。当topic非常多时会变成随机写,写入效率显著下降。另外Kaflka写入消息的同时要写入index文件。而RocketMQ发消息时仅顺序写入一个Commitlog文件。因此RocketMQ写入效率非常高。
- 读取,RocketMQ通过一个任务异步批量写ConsumeQueue及索引文件,ConsumeQueue文件定长特别小几乎都在内存中。加上利用操作系统分页读取CommitLog实现高效分队列消费。Kafka则是直接读取Partition索引。
五、消息链路
消费链路示例
RocketMQ 的消息可以根据 topic-queue 划分出确定的从生产者到消费者路由指向。
- producer 指定 broker 和 queue 发送消息 msg ;
- broker 接收消息,并完成缓存、刷盘和生成摘要(同时根据 tag 和 user properties 对 msg 进行打标)等操作;
- consumer 每隔一段时间( pullInterval )从 broker 端的(根据服务端消息过滤模式 tag 或 sql 过滤后)获取一定量的消息到本地消息队列中(单线程)
- consumer 按照配置并发分配上述队列消息并执行消费方法;
- consumer 返回 broker 消费结果并重置消费位点;
生产者
发送消息的具体操作如下:
- 查询本地缓存是否存储了TopicPublishInfo ,否则从 NameServer 获取
- 根据负载均衡选择策略获取待发送队列并轮训访问
- 获取消息队列对应的 broker 实际 IP
- 设置消息 Unique ID ,zip 压缩消息
- 消息校验(长度等),发送消息
- 发送消息时,Producer 通过负载均衡模块选择相应的 Broker 集群队列进行消息投递。消息发送时如果出现失败,默认会重试 2 次,在重试时会尽量避开刚刚接收失败的 Broker,而是选择其它 Broker 上的队列进行发送,从而提高消息发送的成功率。
消费者
消费方式
- 广播消费:Producer 向一些队列轮流发送消息,队列集合称为 Topic,每一个 Consumer 实例消费这个 Topic 对应的所有队列。
- 集群消费:多个 Consumer 实例平均消费这个 Topic 对应的队列集合。
负载均衡算法
RocketMQ 消费者端有多套负载均衡算法的实现,比较常见的是平均分配和平均循环分配,默认使用平均分配算法,给每个 Consumer 分配均等的队列。一个 Consumer 可以对应多个队列,而一个队列只能给一个 Consumer 进行消费,Consumer 和队列之间是一对多的关系。示例:
- 如果有 5 个队列,2 个 consumer,consumer1 会分配 3 个队列,consumer2 分配 2 个队列;
- 如果有 6 个队列,2 个 consumer,consumer1 会分配 3 个队列,consumer2 也会分配 3 个队列;
- 如果有 10 个队列,11 个 consumer,consumer1~consumer10 各分配一个队列,consumer11 无队列分配;
重新平衡
在消费时间过程中可能会遇到消息消费队列增加和减少、消息消费者增加或减少,此时需要对消息消费队列进行重新平衡,既重新分配 (rebalance),这就是所谓的重平衡机制。在 RocketMQ 中,每隔 20s 会根据当前队列数量、消费者数量重新进行队列负载计算,如果计算出来的结果与当前不一样,则触发消息消费队列的重分配。
失败重试
当出现消费失败的消息时,Broker 会为每个消费者组设置一个重试队列。当一条消息初次消费失败,消息队列会自动进行消费重试。达到最大重试次数后,若消费仍然失败,此时会将该消息发送到死信队列。对于死信消息,通常需要开发人员进行手动处理。
六、支持消息种类
普通消息
- 同步发送:Producer 发出一条消息后,会在收到 MQ 返回的 ACK 之后再发送下一条消息。
- 异步发送:Producer 发出消息后无需等待 MQ 返回 ACK ,直接发送下一条消息。
- 单向: Producer 仅负责发送消息,不等待,MQ 也不返回 ACK。
顺序消息
消息的顺序性分为两种:
- 全局顺序:对于指定的一个 Topic ,所有消息按照严格的先入先出的顺序进行发布和消费 (同一个 queue)。
- 分区顺序:对于一个指定的 Topic ,所有消息根据 sharding key 进行分区,同一个分区内的消息按照严格的 FIFO 顺序进行发布和消费,分区之间彼此独立。
RocketMQ 只支持同一个 queue 的顺序消息,且同一个 queue 只能被一台机器的一个线程消费,如果想要支持全局消息,那需要将该 topic 的 queue 的数量设置为 1,牺牲了可用性。
延迟消息
延迟消息可以用来处理交易系统中定时关闭订单这类业务。用户下单后xx分钟不付款自动关闭。
RocketMQ延迟消息的原理如下:
1、延迟级别设置,在Broker配置broker.conf中预先定义延迟级别。
messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
2、发送构造消息时,可以通过设置消息的delayTimeLevel
属性来指定其延迟级别。
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
public class ScheduledMessageProducer {
public static void main(String[] args) throws Exception {
// 创建Producer实例,并命名Producer组名
DefaultMQProducer producer = new DefaultMQProducer("scheduled_producer_group");
// 设置NameServer地址(根据实际情况修改)
producer.setNamesrvAddr("localhost:9876");
// 启动Producer实例
producer.start();
// 构造消息,指定Topic、Tag和消息体
String topic = "TestTopic";
String tags = "TagA";
String keys = "Key1";
String body = "Hello RocketMQ with scheduled message";
Message msg = new Message(topic, tags, keys, body.getBytes());
// 设置消息的延迟级别(单位:毫秒,这里以3秒后投递为例)
// RocketMQ提供了18个延迟级别,从1s到2h,每个级别对应不同的延迟时间
// 可以通过org.apache.rocketmq.common.message.Message.setDelayTimeLevel()方法设置
msg.setDelayTimeLevel(3); // 3级对应的是3秒后投递,具体级别时间参考官方文档
try {
// 发送消息,并获取发送结果
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
} catch (Exception e) {
e.printStackTrace();
}
// 关闭Producer实例
producer.shutdown();
}
}
3、实现原理
- 消息存储:当Producer发送延迟消息时,Broker会将其存储在特定的延迟队列中。这些延迟队列根据延迟级别进行划分,每个延迟级别对应一个或多个队列。
- 消息调度:Broker内部有一个调度线程,负责定时检查延迟队列中的消息。当消息的延迟时间到达时,调度线程会将其从延迟队列中取出,并放入正常的消息队列中等待被消费。
- 消息投递:消费者(Consumer)从正常的消息队列中拉取消息进行消费,不感知延迟队列存在。
消息事务
用消息系统来实现 2PC (二阶段提交) + 补偿机制(事务回查)的分布式事务功能,通过这种方式能达到分布式事务的最终一致
- 发送方向 MQ 服务端发送消息。
- MQ Server 将消息持久化成功之后,向发送方 ACK 确认消息已经发送成功,此时消息为半消息。
- 发送方开始执行本地事务逻辑。
- 发送方根据本地事务执行结果向 MQ Server 提交二次确认(Commit 或是 Rollback)
-
- MQ Server 收到 Commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;
- MQ Server 收到 Rollback 状态则删除半消息,订阅方将不会接受该消息。
- 在断网或者是应用重启的特殊情况下,上述步骤4提交的二次确认最终未到达 MQ Server,经过固定时间后 MQ Server 将对该消息发起消息回查。
- 发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
- 发送方根据检查得到的本地事务的最终状态再次提交二次确认,MQ Server 仍按照步骤 4 对半消息进行操作。
RocketMQ 3.0 以后,新的版本提供更加丰富的功能,支持消息属性、无序消息、延迟消息、广播消息、长轮询消费、高可用特性,这些功能基本上覆盖了大部分应用对消息中间件的需求。除了功能丰富之外,RocketMQ 基于顺序写,大概率顺序读的队列存储结构和 pull 模式的消费方式,使得 RocketMQ 具备了最快的消息写入速度和百亿级的堆积能力,特别适合用来削峰填谷。在 RocketMQ 3.0 版本的基础上,衍生了开源版本 RocketMQ。
七、高可用
重复消费问题
- 发送消息重复【messageId不同】,消息已成功发送到服务端并完成持久化。此时客户端宕机或网络闪断,MQ Producer意识到消息发送失败并尝试再次发送消息。
- 投递时消息重复【messageId相同】,MQ Consumer已经消费完,向Broker发送回执时网络闪断。Broker会再次尝试投递。
解决重复消费问题的关键是MQ Consumer端需要保证冥等。
消息丢失问题
- Producer投递失败时,默认会重试两次。
- 广播模式下,消费失败仅会返回重试状态,而不会重试。广播允许丢弃部分消息。
- 未指定顺序消费的集群模式,失败消息会进入重试队列,最多重试16次会进入死信队列
解决消息丢失问题的关键,是应用系统要做对账补偿措施,无论是Producer、还是Consumer。
主从切换
- RocketMQ是Broker小集群模式。为每个存储数据的Broker节点配置ClusterName,BrokerName相同的节点构成一个副本组。当某个Broker宕机损坏。仅在小Broker集群范围内选主,更新NameServer状态。因为RocketMQ单节点上Topic数量较多,这样设计可以避免惊群效应。
- Kafka则是以Topic粒度维护状态机,如果按阿里RocketMQ部署方式,一个Broker上有很多topic,某个Topic中节点宕机会导致上万个状态机切换对注册中心形成冲击。风险较高。
主从同步
- RocketMQ提供了类似MySQL主从复制的异步复制和同步功能,允许用户根据需求选择不同的可靠级别。同步复制可靠性高但会损失性能。异步复制性能高但可能会丢失数据。
- Kafka中,每个分区(Partition)都有一个leader副本和零或多个follower副本。所有的读写操作都直接与leader进行交互,而follower副本则通过复制leader副本的日志来保持数据的一致性。