RocketMQ入门
RocketMQ设计基于主题的发布与订阅模式, 其核心功能包括消息发送, 消息存储, 消息消费. 整体设计追求简单与性能单一.
1. 消息队列
1.1 为什么使用消息队列
消息队列主要解决的问题一共有三个:
- 解耦: 解决不同重要程度、不同能力级别系统之间依赖
- 异步: 当存在一对多调用时, 可以发一条消息给消息系统, 让消息系统通知相关系统
- 削峰: 主要解决瞬时写压力大于应用服务能力导致消息丢失、系统奔溃等问题
1.2 消息队列优缺点
消息队列的优点就是上述说的: 解耦、异步、削峰. 而主要的缺点如下:
- 系统可用性降低: 引入消息队列后, 需要保证消息队列的高可用性, 避免MQ宕机导致整个系统不可用.
- 系统复杂度提高: 引入消息队列后, 需要解决: 如何保证消息不会重复消费, 如何处理消息丢失的情况, 如何保证消息的顺序等问题.
- 一致性问题: 引入消息队列后, 需要解决当某个节点处理成功, 而其他节点处理失败的情况下, 如何保证数据的一致性.
1.3 主流消息队列区别
现在主流的消息队列有Kafka、ActiveMQ、RabbitMQ、RocketMQ. 其中各个MQ之间的优缺点如下
特性 |
ActiveMQ |
RabbitMQ |
RocketMQ |
Kafka |
|---|---|---|---|---|
| 单机吞吐量 | 万级, 比 RocketMQ、Kafka 低一个数量级 | 同 ActiveMQ | 10 万级, 支撑高吞吐 | 10 万级, 高吞吐, 一般配合大数据类的系统来进行实时数据计算、日志采集等场景 |
| topic 数量对吞吐量的影响 | topic 可以达到几百/几千的级别, 吞吐量会有较小幅度的下降, 这是 RocketMQ 的一大优势, 在同等机器下, 可以支撑大量的 topic | topic 从几十到几百个时候, 吞吐量会大幅度下降, 在同等机器下, Kafka 尽量保证 topic 数量不要过多, 如果要支撑大规模的 topic, 需要增加更多的机器资源 | ||
| 时效性 | ms 级 | 微秒级, 这是 RabbitMQ 的一大特点, 延迟最低 | ms 级 | 延迟在 ms 级以内 |
| 可用性 | 高, 基于主从架构实现高可用 | 同 ActiveMQ | 非常高, 分布式架构 | 非常高, 分布式, 一个数据多个副本, 少数机器宕机, 不会丢失数据, 不会导致不可用 |
| 消息可靠性 | 有较低的概率丢失数据 | 基本不丢 | 经过参数优化配置, 可以做到 0 丢失 | 同 RocketMQ |
| 功能支持 | MQ 领域的功能极其完备 | 基于 erlang 开发, 并发能力很强, 性能极好, 延时很低 | MQ 功能较为完善, 还是分布式的, 扩展性好 | 功能较为简单, 主要支持简单的 MQ 功能, 在大数据领域的实时计算以及日志采集被大规模使用 |
2. RocketMQ角色

2.1 NameServer
NameServer的功能是为整个MQ集群提供服务协调与治理, 具体就是记录维护Topic、Broker的信息, 及存储Broker信息并监控Broker的运行状态(包括master和slave). 为client提供路由能力. 具体实现和zookeeper有区别, 每个NameServer节点是互相独立的, 不进行数据同步, 也就不存在任何的选主或者主从切换之类的问题, 因此NameServer与Zookeeper相比更轻量级.
2.2 Broker
Broker是具体提供业务的服务器, 每个Broker节点与所有的NameServer节点保持长连接及心跳, 并会定时将Topic信息注册到NameServer, 顺带一提底层的通信和连接都是基于Netty实现的. Broker中可以分为master和slave, 一个master对应多个salve, 一个slave对应一个master, master和slave通过配置相同的brokerName, 不同的brokerId(master为0)成为主从关系.
Broker与NameServer之间建立长连接, 定时(每隔30s)注册Topic信息到所有NameServer. NameServer定时(每隔10s)扫描所有存活broker的连接, 如果NameServer超过2分钟没有收到心跳, 则NameServer断开与Broker的连接.
2.3 Topic和Queue
RocketMQ的Topic/Queue和JMS中的Topic/Queue概念有一定的差异. 在RocketMQ中, 一个Topic中的数据会分布在各个broker中, 而broker中的topic数据又会被切分成若干份, 其中一份数据就是一个Queue, 这样就能突破单点的资源限制从而实现水平扩展.

2.4 Tag
标签可以被认为是对Topic的进一步细化. 一般在相同业务模块中通过引入标签来标记不同用途的消息.
2.5 Message
Message是消息的载体. 一个 Message 必须指定 topic, 相当于寄信的地址. Message 还有一个可选的 tag 设置, 以便消费端可以基于 tag 进行过滤消息. 也可以添加额外的键值对, 例如你需要一个业务 key 来查找 broker 上的消息, 方便在开发过程中诊断问题.
2.6 Producer
Producer从NameServer集群中的随机选择其中一个节点建立长连接, 定期从NameServer获取Topic路由信息, 并向提供Topic服务的Master建立长连接, 且定时向Master发送心跳.
Producer每隔30s(由ClientConfig的pollNameServerInterval决定)从NameServer获取所有topic队列的最新情况, 这意味着如果Broker不可用, Producer在最多30s之后才能够感知Broker不可用, 同时在此期间发往Broker的所有消息都会失败.
Producer每隔30s(由ClientConfig中heartbeatBrokerInterval决定)向所有关联的broker发送心跳, Broker每隔10s中扫描所有存活的连接, 如果Broker在2分钟内没有收到心跳数据, 则关闭与Producer的连接.
2.7 Consumer
Consumer从NameServer集群中随机选择其中一个节点建立长连接, 定期从NameServer取Topic路由信息, 并向提供Topic服务的Master、Slave建立长连接, 且定时向Master、Slave发送心跳. Consumer既可以从Master订阅消息, 也可以从Slave订阅消息, 订阅规则由Broker配置决定.
与Producer类似的, Consumer每隔30s(由ClientConfig的pollNameServerInterval决定)从NameServer获取topic的最新队列情况, 这意味着如果Broker不可用, Consumer最多需要30s之后才能感知到Broker不可用.
Consumer每隔30s(由ClientConfig中heartbeatBrokerInterval决定)向所有关联的broker发送心跳, Broker每隔10s扫描所有存活的连接, 若某个连接2分钟内没有发送心跳数据, 则关闭连接, 并向该Consumer Group的所有Consumer发出通知, Group内的Consumer重新分配队列, 然后继续消费.
当Consumer得到master宕机通知后, 转向slave消费, slave不能保证master的消息100%都同步过来了, 因此会有少量的消息丢失. 但是一旦master恢复, 未同步过去的消息会被最终消费掉.
在RocketMQ中, 消费者客户端有两种方式从队列中获取消息并消费.
- push方式: 有MQ主动将消息推送给消费者. 采用这种方式, 可以尽可能实时的将消息发送给消费者进行消费. 但是在消费者的处理消息能力较弱时(比如, 消费者端的业务系统处理一条消息的流程比较复杂, 其中的调用链路比较多导致消费时间比较久. 概括起来地说就是"慢消费问题"), MQ不断给消费者push消息, 消费端的缓冲区可能会溢出, 导致异常.
- pull方式: 由消费者客户端主动向MQ拉取消息. 采用pull方式, 如何设置Pull消息的频率需要重点去考虑. 比如, 可能1分钟内连续来了1000条消息, 然后2小时内没有新消息产生(概括起来说就是"消息延迟与忙等待"). 如果每次Pull的时间间隔比较久, 会增加消息的延迟, 即消息到达消费者的时间加长, MQ中消息的堆积量变大;若每次Pull的时间间隔较短, 但是在一段时间内MQ中并没有任何消息可以消费, 那么会产生很多无效的Pull请求的RPC开销, 影响MQ整体的网络性能. 并且在pull方式下, 需要consumer端完成比较多的事情(例如维护消息消费的偏移量等), 因此在实际应用中使用的比较少.
2.8 广播模式与集群模式
广播模式: 当消费者使用广播模式时, 每条消息都会被所有的消费者实例消费一次. 比如有5个消费者实例, 当一条消息到达时, 会被5个消费者都消费一次.
集群模式: 当消费者使用集群模式时, 每条消息只会被消费者实例中的某一个消费一次. 比如有5个消费者实例, 当一条消息到达时, 可能会被第一个消费者消费, 可能会被第二个消费者消费.
在默认情况下, RocketMQ消费者以集群模式进行消费.
3. 消息持久化
在ActiveMQ中, 消息持久化依赖MySQL, 当接收到消息后, 会将数据存放入MySQL中. 在RockerMQ中, 消息持久化依赖文件系统, 当接收到消息后, 会将数据放入文件中. 如果使用MySQL存储, 当单表数据过大时, 其IO性能会出现瓶颈, 一旦DB出现故障, 整个MQ也将不可用, 并且也不方便实现集群.
通过使用mmap的方式(零拷贝), 可以省去向用户态的内存复制, 提高速度. 这种机制在Java中是通过MappedByteBuffer实现的. RocketMQ充分利用了上述特性, 提高消息存盘和网络发送的速度. 这里需要注意的是, 采用MappedByteBuffer这种内存映射的方式有几个限制, 其中之一是一次只能映射1.5~2G 的文件至用户态的虚拟内存, 这也是为何RocketMQ 默认设置单个CommitLog日志数据文件为1G的原因了.
3.1 消息存储结构
RocketMQ消息的存储是由ConsumeQueue和CommitLog配合完成的, 消息真正的物理存储文件是CommitLog, ConsumeQueue是消息的逻辑队列, 类似数据库的索引文件, 存储的是指向物理存储的地址.

CommitLog: 所有的消息数据都会存放在commitLog目录中, 其中每个文件大小为1G, 可以在配置文件中配置storePathCommitLog.
ConsumerQueue: 存储消息在commitLog中的索引, 加快读取commitLog的速度. 该目录下的文件和queue是一一对应的(目录结构大致为{ConsumerQueue}/{topicName}/{queueId}/000000), 可以在配置文件中配置storePathConsumeQueue
IndexFile: 为消息查询提供了一种通过key或者时间来查询消息的方法, 这种通过IndexFile来查询消息的方法不影响发送与消费消息的主流程. 可以在配置文件中配置storePathIndex
3.2 刷盘机制
RocketMQ的消息最终会存储到磁盘上, 这样既能保证断电后恢复, 又可以让存储的消息量超出内存的限制. RocketMQ为了提高性能, 会尽可能地保证磁盘的顺序写. 消息在通过Producer写入RocketMQ的时候, 有两种写磁盘方式, 分布式同步刷盘和异步刷盘.
同步刷盘: 在返回写成功状态时, 消息已经被写入磁盘. 具体流程是, 消息写入内存的PAGECACHE后, 立刻通知刷盘线程刷盘, 然后等待刷盘完成, 刷盘线程执行完成后唤醒等待的线程, 返回消息写成功的状态.
异步刷盘: 在返回写成功状态时, 消息可能只是被写入了内存的PAGECACHE, 写操作的返回快, 吞吐量大; 当内存里的消息量积累到一定程度时, 统一触发写磁盘动作, 快速写入.
4. 高可用和主从复制
4.1 高可用
一般在部署RocketMQ时, 都是使用多master多slave方式进行部署, master和slave之间主要依靠不同的brokerId进行区分. 其中master主要负责broker的读与写操作, slave负责broker的读操作.
在消息创建时, 因为RocketMQ会将topic的多个queue创建在多个broker中. 此时如果某个broker的master不可用, 生产者仍然可以将消息发送到其他master上. 在消息消费时, 并不需要配置从master还是slave中读取消息. 因为在master繁忙或者不可用时, 消费者会自动切换到slave中进行消费. 有了这种自动切换机制, 当master出现故障时, 消费者仍然能从slave中读取消息.
4.2 主从复制
如果broker中有master与slave, 消息就需要从master复制到slave上. 消息同步方式有同步与异步两种.
同步复制: 在消息生产者发送消息时, 需要等master和slave都写入成功后才将结果返回给生产者. 在这种方式下, 如果master出现故障, slave上有所有的备份数据, 容易恢复. 但是这种方式也会增加数据写入延迟, 降低系统吞吐量.
异步复制: 在消息生产者发送消息时, 只要master写入成功就立即将结果返回给生产者. 在这种方式下, 系统拥有较低的延迟和较高的吞吐量, 但是如果master出现故障, 有些数据因为没有被写入slave, 有可能会出现消息丢失的情况.
在一般情况下, 建议刷盘机制设置为异步刷盘, 主从复制设置为同步复制. 这样可以降低磁盘的写操作, 同时也可以确保即使master宕机数据也不会丢失.
5. 负载均衡
5.1 Producer负债均衡
在发送消息时, 默认会轮询所有的queue进行发送, 以达到让消息平均分布在不同的queue上, 由于queue可以分布在不同的broker上, 所以发送的消息就发送到不同的broker上.
5.2 Consumer负债均衡
在广播模式下, 每个消费者都会消费同一条消息, 所以也就没有必要讨论负债均衡了.
在集群模式下, 按照queue的数量和消费者的实例数量, 平均分配queue给每个消费者. 比如集群中存在5个queue与2个消费者的情况下, RocketMQ指定第一个消费者消费2个queue, 第二个消费者消费3个queue. 如果后续增加消费者, 每个consumer分配到的queue就会相应减少. 当consumer数量超过queue数量时, 多余的consumer就不能进行消息消费.
6. 消息重试
对于顺序消息, 当消费者消费消息失败后, RocketMQ会不断进行消息重试(间隔1秒), 此时应用会出现消息消费被阻塞的情况. 因此务必保证应用能及时监控并处理失败的情况, 避免阻塞的情况.
对于无序消息, 当消费者消费消息失败后, 可以通过设置的返回状态达到重试的结果. 需要注意的是, 在广播模式下不提供重试的特性, 即消费失败, 不再进行重试, 继续消费新的消息.
| 重试次数 | 间隔时间 | 重试次数 | 间隔时间 | 重试次数 | 间隔时间 | 重试次数 | 间隔时间 |
|---|---|---|---|---|---|---|---|
| 1 | 10秒 | 5 | 3分钟 | 9 | 7分钟 | 13 | 20分钟 |
| 2 | 30秒 | 6 | 4分钟 | 10 | 8分钟 | 14 | 30分钟 |
| 3 | 1分钟 | 7 | 5分钟 | 11 | 9分钟 | 15 | 1小时 |
| 4 | 2分钟 | 8 | 6分钟 | 12 | 10分钟 | 16 | 2小时 |
RocketMQ默认允许每条消息重试16次, 当重试16次后仍然失败, 消息将不再投递. 同时RocketMQ允许配置最大重试次数, 当超过16次的情况下, 每次间隔依旧是2小时.
6.1 死信队列
当一条消息消费失败超过16次之后, RocketMQ并不会立即将消息丢弃, 而是将其放入死信队列中. 该队列中的消息无法被消费者正常消费, 并且在3天后会自动删除. 一个死信队列包含了对应GroupId产生的所有死信消息, 而不论该消息属于哪个Topic.
在一条消息进入死信队列时, 意味着某些原因导致消费者无法正常消费该消息, 因此需要开发人员进行特殊处理, 排查可疑因素并且解决问题后, 可疑重新发送该消息, 让消费者重新消费一次.
7. 常见问题
7.1 消息重复
对于所有的MQ来说, 都是有可能出现重复消费问题的, 因为这种问题不是MQ自己保重的, 而是需要开发人员进行保证. 在RocketMQ中, 生产者发送消息后收到应答表示是否发送成功. 而消费者在消费完数据后返回应答表示是否消费成功, 在正常情况下可以确保消息不重复. 但是在以下情况就有出现重复消息的可能.
1. 发送重复的消息
当一条消息已经成功发送到服务端并完成持久化, 此时出现了网络闪断或者客户端宕机, 导致服务端对客户端应答失败, 生产者意识到消息发送失败并尝试再次发送消息. 在消费者消费时会收到两条内容相同并且MessageID也相同的消息.
2. 消费者消费重复消息
某条消息被消费者消费完成并且完成业务处理, 当客户端向服务端进行应答反馈时, 出现网络闪断或者服务端宕机. 为了保证消息至少被消费一次, 消息队列RocketMQ的服务端将在重启或者网络恢复后再次尝试投递之前已被处理过的消息, 之后消费者会收到与上条消息内容相同并且MessageID也相同的消息.
3. 负债均衡时消息重复.
当消息队列RocketMQ的broker或者客户端重启或者扩容时, 会触发ReBalance操作, 此时消费者可能会收到重复的消息.
处理方式
因为MessageID存在重复的可能, 所以在发送消息时, 最好以业务唯一标识作为关键依据(message.setKey("业务Id");). 消费者在进行消费时, 根据业务标识进行去重, 确保不会发送重复消费的问题.
7.2 消息可靠性
在使用MQ时, 有一个原则就是数据不能多一条, 也不能少一条. 不能多, 就是前面说的重复消费问题. 不能少, 就是说数据不能丢失.
一条消息从发送到消费完成, 一共可以分为三个部分, 其中任何一个阶段都可能丢失消息.

生产阶段
无论是使用同步还是异步方式发送消息, 最后都会获取一个状态值或者异常, 只要处理好返回值与异常, 一般都能保证消息发送时不丢失. 另外可以设置合理的重试次数, 保证消息发送的可靠性.
存储阶段
在默认情况下(异步刷盘), 消息只要到达broker端, 会先存入内存中, 然后立即返回响应给生产者, 之后broker定期批量的将一个或多个消息持久化到磁盘中. 在这种方式下, 如果服务器宕机会导致消息无法及时持久化, 内存中的部分数据就会丢失. 此时如果希望broker端不丢消息, 保证消息的可靠性, 可以将刷盘机制改为同步刷盘, 即消息到达broker之后, 需要成功持久化才能返回响应给生产者.
需要注意的是, 在同步刷盘模式下, 如果broker在同步刷盘时间内(默认5s)未能成功持久化, 会返回FLUSH_DISK_TIMEOUT给生产者. 这种情况下, 可以选择直接忽视. 但一般情况下, 建议重新发送该消息. 虽然可能会导致消息重复, 不过只要消费端做好消息重复的处理就没问题.
在集群部署时, 为了保证消息不丢失, 消息还需要从master节点复制到slave节点.
在同步复制的情况下, 消息到达master之后, master需要将消息发送到slave上, 等到salve返回响应之后master才将响应返回给生产者. 这种情况下, 由于需要等到slave的响应会造成一定的性能损失.
在异步复制的情况下, 消息到达master之后, master直接返回响应, 之后在特定的时间将数据发送到slave上. 这种情况下, 如果master宕机, 在slave上会暂时找不到未完成同步的数据.
一般情况下, 建议使用同步刷盘 + 异步复制的组合, 这种情况下, 可以保证只要消息到达broker并且成功持久化, 即使master宕机导致消息无法到达slave, 只要在master重启后就可以再次进行复制将消息发送到slave上, 从而保证消息不丢失同时保证一定的性能. 如果需要严格的保证消息不丢失, 可以采用同步刷盘 + 同步复制的组合. 不过无论哪种组合, 都需要生产者的配合, 根据返回的状态值和捕获的异常来判断是否需要补偿重试.
消费阶段
消费者从broker拉取消息, 然后执行相应的业务逻辑. 一旦执行成功, 将会返回ConsumeConcurrentlyStatus.CONSUME_SUCCESS状态给 Broker. 如果Broker未收到消费确认响应或收到其他状态, 消费者下次还会再次拉取到该条消息进行重试. 这样的方式有效避免了消费者消费过程发生异常, 或者消息在网络传输中丢失的情况.
在消费阶段最重要的就是返回的消息状态, 只有在业务真正处理成功后, 才返回ConsumeConcurrentlyStatus.CONSUME_SUCCESS, 否则需要返回ConsumeConcurrentlyStatus.RECONSUME_LATER或者null或者异常. 只要处理好返回值, 那么在消费阶段也不太可能出现消息丢失的状况.
7.3 消息有序性
消息有序性指的是可以按照消息的发送顺序来进行消费, 可以分为分区有序和全局有序. 一个topic内所有的消息按照先进先出的顺序进行发布和消费, 这种称为全局顺序. 一个queue内所有的消息按照先进先出的顺序进行发布和消费, 这种称为分区有序. 一般情况下, 如果要保证消息的有序性, 只需要保证分区有序性即可, 全局有序性可以忽略.
RocketMQ在默认情况下消息发送会采用轮询的方式把消息发送到不同的分区队列, 而消费消息的时候从多个queue上拉取消息. 这种情况发送和消费是不能保证顺序的. 但是如果控制发送的消息按照顺序依次发送到同一个queue中, 消费的时候只从这个queue上拉取, 就可以保证有序性.
producer端
在producer端中只要确保消息顺序唯一要做的事情就是将消息路由到特定的分区上, 在RocketMQ中可以通过MessageQueueSelector来实现分区的选择.
public interface MessageQueueSelector {
MessageQueue select(final List<MessageQueue> mqs, final Message msg, final Object arg);
}
consumer端
一个consumer对应一个或者多个queue. 因此只要queue中能保证有序性, 那么consumer端无论使用pull方式还是push方式, 从同一个queue中获取到的消息都是可以保证有序性的.
7.4 消息积压
如果消费端出现问题导致不消费或者消费速度极其慢. 比如消费端在消费完消息后需要将结果写入到MySQL中, 而此时MySQL正好宕机, 导致消费端无法消费. 这种情况下, 就会导致消息积压, 验证影响MQ的性能.
这种情况下, 并不能只修复消费端就结束了. 此时需要增加消费者数量进行快速消费. 但是由于堆积的topic中queue数量固定, 能增加消费者数量也是有限的. 这种情况下, 可以添加一个用于分发的消费者, 将堆积的消息分发到另外一个临时的topic中, 并根据实际需要指定queue数量, 进而加速消费者消费.
浙公网安备 33010602011771号