VIP03-RocketMQ高级原理.pdf

1 消息模型(Message Model)

2 消息生产者(Producer)

3 消息消费者(Consumer)

4 主题(Topic)

同一个Topic下的数据,会分片保存到不同的Broker上,而每一个分片单位,就叫做MessageQueue。
MessageQueue是生产者发送消息与消费者消费消息的最小单位。

5 代理服务器(Broker Server)

5.1、负责存储消息、转发消息。
5.2、代理服务器在RocketMQ系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备。代理服务器也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等。
5.3、Broker Server包含了多个重要的子模块:
5.3.1、Remoting Module:整个Broker的实体,负责处理来自clients端的请求。
5.3.2、Client Manager:负责管理客户端(Producer/Consumer)和维护Consumer的Topic订阅信息
5.3.3、Store Service:提供方便简单的API接口处理消息存储到物理硬盘和查询功能。
5.3.4、HA Service:高可用服务,提供Master Broker 和 Slave Broker之间的数据同步功能。
5.3.5、Index Service:根据特定的Message key对投递到Broker的消息进行索引服务,以提供消息的快
速查询。
而Broker Server要保证高可用需要搭建主从集群架构.

RocketMQ两种Broker架构模式

普通集群

这种集群模式下会给每个节点分配一个固定的角色,master负责响应客户端的请求,并存储消息。
slave则只负责对master的消息进行同步保存,并响应部分客户端的读请求。消息同步方式分为同步同
步和异步同步。
缺点:一旦master节点挂了,需要手动把从节点升级为master节点。

Dledger高可用集群(4.5版本以后)

这个模式下的集群会随机选出一个节点作为master,而当master节点挂了后,会从slave中自动选出一个节点升级成为master。
Dledger技术做的事情:1、接管Broker的CommitLog消息存储 ,也就是CommitLog不再由broker去写了,而 是由Dledger去写
2、从集群中选举出master节点
3、完成master节点往slave节点的消息同步。
Dledger是使用Raft算法来进行节点选举的。

Raft算法

image.png
首先:每个节点有三个状态,Leader,follower和candidate(候选人)。正常运行的情况下,集群
中会有一个leader,其他都是follower,follower只响应Leader和Candidate的请求,而客户端的
请求全部由Leader处理,即使有客户端请求到了一个follower,也会将请求转发到leader。
集群刚启动时,每个节点都是follower状态,之后集群内部会发送一个timeout信号,所有
follower就转成candidate去拉取选票,获得大多数选票的节点选为leader,其他候选人转为
follower。如果一个timeout信号发出时,没有选出leader,将会重新开始一次新的选举。而
Leader节点会往其他节点发送心跳信号,确认他的leader状态。
然后会启动定时器,如果在指定时间内没有收到Leader的心跳,就会转为Candidate状态,然
后向其他成员发起投票请求,如果收到半数以上成员的投票,则Candidate会晋升为Leader。然
后leader也有可能会退化成follower。
然后,在Raft协议中,会将时间分为一些任意时间长度的时间片段,叫做term。term会使用一
个全局唯一,连续递增的编号作为标识,也就是起到了一个逻辑时钟的作用。
在每一个term时间片里,都会进行新的选举,每一个Candidate都会努力争取成为leader。获
得票数最多的节点就会被选举为Leader。被选为Leader的这个节点,在一个term时间片里就会保
持leader状态。这样,就会保证在同一时间段内,集群中只会有一个Leader。在某些情况下,选
票可能会被各个节点瓜分,形成不了多数派,那这个term可能直到结束都没有leader,直到下一
个term再重新发起选举,这也就没有了Zookeeper中的脑裂问题。而在每次重新选举的过程中,
leader也有可能会退化成为follower。也就是说,在这个集群中, leader节点是会不断变化的。
然后,每次选举的过程中,每个节点都会存储当前term编号,并在节点之间进行交流时,都会
带上自己的term编号。如果一个节点发现他的编号比另外一个小,那么他就会将自己的编号更新
为较大的那一个。而如果leader或者candidate发现自己的编号不是最新的,他就会自动转成
follower。如果接收到的请求term编号小于自己的编号,term将会拒绝执行。
在选举过程中,Raft协议会通过心跳机制发起leader选举。节点都是从follower状态开始的,如
果收到了来自leader或者candidate的心跳RPC请求,那他就会保持follower状态,避免争抢成为
candidate。而leader会往其他节点发送心跳信号,来确认自己的地位。如果follower一段时间(两
个timeout信号)内没有收到Leader的心跳信号,他就会认为leader挂了,发起新一轮选举。
选举开始后,每个follower会增加自己当前的term,并将自己转为candidate。然后向其他节点
发起投票请求,请求时会带上自己的编号和term,也就是说都会默认投自己一票。之后
candidate状态可能会发生以下三种变化:
赢得选举,成为leader:如果它在一个term内收到了大多数的选票,将会在接下的剩余
term时间内称为leader,然后就可以通过发送心跳确立自己的地位。(每一个server在一个
term内只能投一张选票,并且按照先到先得的原则投出)
其他节点成为leader:在等待投票时,可能会收到其他server发出心跳信号,说明其他
leader已经产生了。这时通过比较自己的term编号和RPC过来的term编号,如果比对方大,
说明leader的term过期了,就会拒绝该RPC,并继续保持候选人身份; 如果对方编号不比自己
小,则承认对方的地位,转为follower。
选票被瓜分,选举失败: 如果没有candidate获取大多数选票, 则没有leader产生, candidate们
等待超时后发起另一轮选举. 为了防止下一次选票还被瓜分,必须采取一些额外的措施, raft采
用随机election timeout(随机休眠时间)的机制防止选票被持续瓜分。通过将timeout随机设
为一段区间上的某个值, 因此很大概率会有某个candidate率先超时然后赢得大部分选票。

选举过程

所以以三个节点的集群为例,选举过程会是这样的:
1. 集群启动时,三个节点都是follower,发起投票后,三个节点都会给自己投票。这样一轮投
票下来,三个节点的term都是1,是一样的,这样是选举不出Leader的。
2. 当一轮投票选举不出Leader后,三个节点会进入随机休眠,例如A休眠1秒,B休眠3秒,C休
眠2秒。
3. 一秒后,A节点醒来,会把自己的term加一票,投为2。然后2秒时,C节点醒来,发现A的
term已经是2,比自己的1大,就会承认A是Leader,把自己的term也更新为2。实际上这个
时候,A已经获得了集群中的多数票,2票,A就会被选举成Leader。这样,一般经过很短的
几轮选举,就会选举出一个Leader来。
4. 到3秒时,B节点会醒来,他也同样会承认A的term最大,他是Leader,自己的term也会更
新为2。这样集群中的所有Candidate就都确定成了leader和follower.
5. 然后在一个任期内,A会不断发心跳给另外两个节点。当A挂了后,另外的节点没有收到A的
心跳,就会都转化成Candidate状态,重新发起选举。

Dledger还会采用Raft协议

进行多副本的消息同步

6 名字服务(Name Server)

Broker Server会在启动时向所有的Name Server注册自己的服务信息,并且后续通过心跳请求的方式保证这个服务信息的实时性。生产者或消费者能够通过名字服务查找各主题相应的Broker IP列表。多个Namesrv实例组成集群,但相互独立,没有信息交换。
只要有一台服务节点正常,整个路由服务就不会有影响。

7 消息(Message)

二、消息存储

1、何时存储消息

文件默认保存48小时,凌晨4点删除

2、消息存储介质(磁盘顺序写)

2.1磁盘保存文件慢吗?

RocketMQ的消息用顺序写,保证了消息存储的速度。

2.2零拷贝技术加速文件读写

关于零拷贝,JAVA的NIO中提供了两种实现方式,mmap和sendfile,其中mmap适合比较小的文件(1-2G),而sendfile适合传递比较大的文件。
mmap机制在Java中是通过NIO包中的MappedByteBuffer实现的。RocketMQ充分利用了上述特性,也就是所谓的“零拷贝”技术,提高消息存盘和网络发送的速度。
这里需要注意的是,采用MappedByteBuffer这种内存映射的方式有几个限制,其中之一是一次只能映射1.5~2G 的文件至用户态的虚拟内存,这也是为何RocketMQ默认设置单个CommitLog日志数据文件为1G的原因了

3 、消息存储结构

RocketMQ消息的存储分为三个部分:
CommitLog:所有消息都会顺序存入到CommitLog文件当中。
ConsumerQueue:存储消息在CommitLog的索引。一个MessageQueue一个文件
IndexFile
image.png

4、 刷盘机制(可配置)

同步刷盘

消息写入内存的PAGECACHE后,立刻通知刷盘线程刷盘, 然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,返回消息写 成功的状态。

异步刷盘

在返回写成功状态时,消息可能只是被写入了内存的PAGECACHE,写操作的返回快,吞吐量大;
当内存里的消息量积累到一定程度时,统一触发写磁盘动作,快速写入。

5、消息主从复制(可配置)

如果Broker以一个集群的方式部署,会有一个master节点和多个slave节点。

同步复制

同步复制是等Master和Slave都写入消息成功后才反馈给客户端写入成功的状态。
在同步复制下,如果Master节点故障,Slave上有全部的数据备份,这样容易恢复数据。但是同步复制
会增大数据写入的延迟,降低系统的吞吐量。

异步复制

异步复制是只要master写入消息成功,就反馈给客户端写入成功的状态。然后再异步的将消息复制给
Slave节点。
在异步复制下,系统拥有较低的延迟和较高的吞吐量。但是如果master节点故障,而有些数据没有完成
复制,就会造成数据丢失。

6、负载均衡

6.1Producer负载均衡

Producer发送消息时,默认会轮询目标Topic下的所有MessageQueue,并采用递增取模的方式往不同
的MessageQueue上发送消息,以达到让消息平均落在不同的queue上的目的。
image.png
可以指定一个MessageQueueSelector,这样可以保证消息局部有序.

6.2、Consumer负载均衡

Consumer也是以MessageQueue为单位来进行负载均衡。分为集群模式和广播模式。

6.2.1、集群模式

在集群消费模式下,每条消息只需要投递到订阅这个topic的Consumer Group下的一个实例即可。
RocketMQ采用主动拉取的方式拉取并消费消息,在拉取的时候需要明确指定拉取哪一条message
queue。
而每当实例的数量有变更,都会触发一次所有实例的负载均衡,这时候会按照queue的数量和实例的数
量平均分配queue给每个实例。

6.2.2、分配策略

6.2.2.1、平均分配策略

image.png

6.2.3、广播模式

广播模式下,每一条消息都会投递给订阅了Topic的所有消费者实例,所以也就没有消息分配这一说。
而在实现上,就是在Consumer分配Queue时,所有Consumer都分到所有的Queue。

7、消息重试

首先对于广播模式的消息, 是不存在消息重试的机制的,即消息消费失败后,不会再重新进行发送,而
只是继续消费新的消息。
而对于普通的消息,当消费者消费消息失败后,你可以通过设置返回状态达到消息重试的结果。

1、如何让消息进行重试

集群消费方式下,消息消费失败后期望消息重试,需要在消息监听器接口的实现中明确进行配置。可以
有三种配置方式:
返回Action.ReconsumeLater-推荐
返回null
抛出异常
如果希望消费失败后不重试,可以直接返回Action.CommitMessage。

2、重试消息如何处理

重试的消息会进入一个 “%RETRY%”+ConsumeGroup 的队列中。

8、死信队列(人工处理)

当一条消息消费失败,RocketMQ就会自动进行消息重试。而如果消息超过最大重试次数,RocketMQ
就会认为这个消息有问题。但是此时,RocketMQ不会立刻将这个有问题的消息丢弃,而会将其发送到
这个消费者组对应的一种特殊队列:死信队列。
特征:
一个死信队列对应一个ConsumGroup,而不是对应某个消费者实例。
如果一个ConsumeGroup没有产生死信队列,RocketMQ就不会为其创建相应的死信队列。
一个死信队列包含了这个ConsumeGroup里的所有死信消息,而不区分该消息属于哪个Topic。
死信队列中的消息不会再被消费者正常消费。
死信队列的有效期跟正常消息相同。默认3天,对应broker.conf中的fileReservedTime属性。超过
这个最长时间的消息都会被删除,而不管消息是否消费过。

9、消息幂等

1、幂等的概念

在MQ系统中,对于消息幂等有三种实现语义:
at most once 最多一次:每条消息最多只会被消费一次
at least once 至少一次:每条消息至少会被消费一次
exactly once 刚刚好一次:每条消息都只会确定的消费一次
RocketMQ只能保证at least once,使用RocketMQ时,需要由业务系统自行保证

2、消息幂等的必要性

在互联网应用中,尤其在网络不稳定的情况下,消息队列 RocketMQ 的消息有可能会出现重复,这个
重复简单可以概括为以下情况:

发送时消息重复

当一条消息已被成功发送到服务端并完成持久化,此时出现了网络闪断或者客户端宕机,导致服务
端对客户端应答失败。 如果此时生产者意识到消息发送失败并尝试再次发送消息,消费者后续会
收到两条内容相同并且 Message ID 也相同的消息。

投递时消息重复

负载均衡时消息重复(包括但不限于网络抖动、Broker 重启以及订阅方应用重启)

3、处理方式

从上面的分析中,我们知道,在RocketMQ中,是无法保证每个消息只被投递一次的,所以要在业务上
自行来保证消息消费的幂等性。
而要处理这个问题,RocketMQ的每条消息都有一个唯一的MessageId,这个参数在多次投递的过程中
是不会改变的,所以业务上可以用这个MessageId来作为判断幂等的关键依据。
但是,这个MessageId是无法保证全局唯一的,也会有冲突的情况。所以在一些对幂等性要求严格的场
景,最好是使用业务上唯一的一个标识比较靠谱。例如订单ID。而这个业务标识可以使用Message的
Key来进行传递。