文章目录

  • 架构
  • 启动
  • 集群
  • Netty
  • 主从复制
  • 刷盘机制
  • 事务消息
  • 顺序消息
  • 消息清理
  • 顺序写和零拷贝
  • 基于Dledger的主从复制


架构

rocketmq springboot版本对应 rocketmq底层_客户端

rocketmq springboot版本对应 rocketmq底层_java_02


Producer:就是消息生产者,可以集群部署。它会先和 NameServer 集群中的随机一台建立长连接,得知当前要发送的 Topic 存在哪台 Broker Master上,然后再与其建立长连接,支持多种负载平衡模式发送消息。

Consumer:消息消费者,也可以集群部署。它也会先和 NameServer 集群中的随机一台建立长连接,得知当前要消息的 Topic 存在哪台 Broker Master、Slave上,然后它们建立长连接,支持集群消费和广播消费消息。

Broker:主要负责消息的存储、查询消费,支持主从部署,一个 Master 可以对应多个 Slave,Master 支持读写,Slave 只支持读。Broker 会向集群中的每一台 NameServer 注册自己的路由信息。

NameServer:是一个很简单的 Topic 路由注册中心,支持 Broker 的动态注册和发现,保存 Topic 和 Borker 之间的关系。通常也是集群部署,但是各 NameServer 之间不会互相通信, 各 NameServer 都有完整的路由信息,即无状态。

启动

先启动 NameServer 集群,各 NameServer 之间无任何数据交互,Broker 启动之后会向所有 NameServer 定期(每 30s)发送心跳包,包括:IP、Port、TopicInfo,NameServer 会定期扫描 Broker 存活列表,如果超过 120s 没有心跳则移除此 Broker 相关信息,代表下线。

这样每个 NameServer 就知道集群所有 Broker 的相关信息,此时 Producer 上线从 NameServer 就可以得知它要发送的某 Topic 消息在哪个 Broker 上,和对应的 Broker (Master 角色的)建立长连接,发送消息。

Consumer 上线也可以从 NameServer 得知它所要接收的 Topic 是哪个 Broker ,和对应的 Master、Slave 建立连接,接收消息。

集群

rocketmq springboot版本对应 rocketmq底层_java_03

1 NameServer 集群
提供轻量级的服务发现及路由,每个 NameServer 记录完整的路由信息,提供相应的读写服务,支持快速存储扩展。

NameServer是一个功能齐全的服务器,主要包含两个功能:

Broker 管理,接收来自 Broker 集群的注册请求,提供心跳机制检测 Broker 是否存活
路由管理,每个 NameServer 持有全部有关 Broker 集群和客户端请求队列的路由信息
2 Broker 集群
通过提供轻量级的 Topic 和Queue 机制处理消息存储。同时支持推(Push)和拉(Pull)两种模型,包含容错机制。提供强大的峰值填充和以原始时间顺序累积数千亿条消息的能力。此外还提供灾难恢复,丰富的指标统计数据和警报机制,这些都是传统的消息系统缺乏的。

Broker 有几个重要的子模块:

远程处理模块,Broker 入口,处理来自客户端的请求
客户端管理,管理客户端(包括消息生产者和消费者),维护消费者的主题订阅
存储服务,提供在物理硬盘上存储和查询消息的简单 API
HA 服务,提供主从 Broker 间数据同步
索引服务,通过指定键为消息建立索引并提供快速消息查询
3 Producer 集群
消息生产者支持分布式部署,分布式生产者通过多种负载均衡模式向 Broker 集群发送消息。

.4 Consumer 集群
消息消费者也支持 Push 和 Pull 模型的分布式部署,还支持集群消费和消息广播。提供了实时的消息订阅机制,可以满足大多数消费者的需求。

Netty

Java提供的基于时间驱动的NIO模型

NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区),Selector(选择器)。

Netty 主要基于主从 Reactors 多线程模型(如下图)做了一定的修改,其中主从 Reactor 多线程模型有多个 Reactor:

MainReactor 负责客户端的连接请求,并将请求转交给 SubReactor。

SubReactor 负责相应通道的 IO 读写请求。

非 IO 请求(具体逻辑处理)的任务则会直接写入队列,等待 worker threads 进行处理。

这里引用 Doug Lee 大神的 Reactor 介绍:Scalable IO in Java 里面关于主从 Reactor 多线程模型的图:

rocketmq springboot版本对应 rocketmq底层_java_04


主从 Reactor 多线程模式让 Reactor 在多个线程中运行(分成 MainReactor 线程与 SubReactor 线程)。这种模式的基本工作流程为:

1)Reactor 主线程 MainReactor 对象通过 select 监听客户端连接事件,收到事件后,通过 Acceptor 处理客户端连接事件。

2)当 Acceptor 处理完客户端连接事件之后(与客户端建立好 Socket 连接),MainReactor 将连接分配给 SubReactor。(即:MainReactor 只负责监听客户端连接请求,和客户端建立连接之后将连接交由 SubReactor 监听后面的 IO 事件。)

3)SubReactor 将连接加入到自己的连接队列进行监听,并创建 Handler 对各种事件进行处理。

4)当连接上有新事件发生的时候,SubReactor 就会调用对应的 Handler 处理。

5)Handler 通过 read 从连接上读取请求数据,将请求数据分发给 Worker 线程池进行业务处理。

6)Worker 线程池会分配独立线程来完成真正的业务处理,并将处理结果返回给 Handler。Handler 通过 send 向客户端发送响应数据。

7)一个 MainReactor 可以对应多个 SubReactor,即一个 MainReactor 线程可以对应多个 SubReactor 线程。

netty架构:

rocketmq springboot版本对应 rocketmq底层_java_05

关于这张图,作以下几点说明:

1)Netty 抽象出两组线程池:BossGroup 和 WorkerGroup,也可以叫做 BossNioEventLoopGroup 和 WorkerNioEventLoopGroup。每个线程池中都有 NioEventLoop 线程。BossGroup 中的线程专门负责和客户端建立连接,WorkerGroup 中的线程专门负责处理连接上的读写。BossGroup 和 WorkerGroup 的类型都是 NioEventLoopGroup。

2)NioEventLoopGroup 相当于一个事件循环组,这个组中含有多个事件循环,每个事件循环就是一个 NioEventLoop。

3)NioEventLoop 表示一个不断循环的执行事件处理的线程,每个 NioEventLoop 都包含一个 Selector,用于监听注册在其上的 Socket 网络连接(Channel)。

4)NioEventLoopGroup 可以含有多个线程,即可以含有多个 NioEventLoop。

5)每个 BossNioEventLoop 中循环执行以下三个步骤:

5.1)select:轮训注册在其上的 ServerSocketChannel 的 accept 事件(OP_ACCEPT 事件)

5.2)processSelectedKeys:处理 accept 事件,与客户端建立连接,生成一个 NioSocketChannel,并将其注册到某个 WorkerNioEventLoop 上的 Selector 上

5.3)runAllTasks:再去以此循环处理任务队列中的其他任务

6)每个 WorkerNioEventLoop 中循环执行以下三个步骤:

6.1)select:轮训注册在其上的 NioSocketChannel 的 read/write 事件(OP_READ/OP_WRITE 事件)

6.2)processSelectedKeys:在对应的 NioSocketChannel 上处理 read/write 事件

6.3)runAllTasks:再去以此循环处理任务队列中的其他任务

7)在以上两个processSelectedKeys步骤中,会使用 Pipeline(管道),Pipeline 中引用了 Channel,即通过 Pipeline 可以获取到对应的 Channel,Pipeline 中维护了很多的处理器(拦截处理器、过滤处理器、自定义处理器等)。这里暂时不详细展开讲解 Pipeline。

rocketmq springboot版本对应 rocketmq底层_数据_06

主从复制

1 配置:

rocketmq springboot版本对应 rocketmq底层_数据_07


基本上,master与slave差别不大,各broker需要的功能,都会具有的。比如都会开启各服务端口,都会进行文件清理动作,都会向nameserver注册自身等等。唯一的差别在于,slave会另外开启一个同步的定时任务,每10秒向master发送一次同步请求,即 syncAll(); 那么,所谓的同步,到底是同步个啥?即其如何实现同步?

所有的主从同步的实现都在这里了:syncAll(); 同步4种数据:

topic信息、
消费偏移信息、
延迟信息、
订阅组信息;

同步的及时性如何?每10秒发起一步同步请求,即延迟是10秒级的。
步骤:
1 HAService的开启 同步服务的开启,是在messageStore初始化时做的。它会读取一个单独的端口配置,开启HA同步服务。HAService作为rocketmq中的一个小型服务,运行在后台线程中,为了简单起见或者资源隔离,它使用一些单独的端口和通信实现处理。也可谓麻雀虽小,五脏俱全。下面我就分三个单独的部分讲解下如何实现数据同步。
2. 从节点同步实现
从节点负责主动拉取主节点数据,是一个比较重要的步骤。它的实现是在 HAClient 中的,该client启动起来之后,会一直不停地向master请求新的数据,然后同步到自己的commitlog中。

我们来看org.apache.rocketmq.store.ha.HAService.HAClient 函数都干了什么:

// 使用原生nio, 尝试连接至master
// 隔一段时间向master汇报一次本slave的同步信息
// 如果连接无效,则关闭,下次再循环周期将会重新发起连接
// 核心逻辑:处理获取到的消息数据
// processReadEvent() 即是在收到master的新数据后,实现如何同步到本broker的commitlog中。其实现主要还是依赖于commitlogService.

// 按协议读取数据
// 数据读取完成,则立即添加到存储中
// 添加到commitlog中,并生成后续的consumeQueue,index等相关信息

写:将meaasge写进commmitlog,更新consummerqueue中的offset,size,tag,hash值,更新index文件
读:先从consummerqueue中读取offset,size,tag,hash值,再从对应的offset读取commitlog,

rocketmq springboot版本对应 rocketmq底层_java-rocketmq_08

主从复制源码解读

刷盘机制

rocketmq springboot版本对应 rocketmq底层_数据_09

rocketmq springboot版本对应 rocketmq底层_rocketmq_10


刷盘源码解读1刷盘源码解读2

事务消息

rocketmq springboot版本对应 rocketmq底层_java-rocketmq_11


(1)正常情况:在事务主动方服务正常,没有发生故障的情况下,发消息流程如下:

步骤①:MQ 发送方向 MQ Server 发送 half 消息,MQ Server 标记消息状态为 prepared,此时该消息 MQ 订阅方是无法消费到的

步骤②:MQ Server 将消息持久化成功之后,向发送方 ACK 确认消息已经成功接收

步骤③:发送方开始执行本地事务逻辑

步骤④:发送方根据本地事务执行结果向 MQ Server 提交二次确认,commit 或 rollback

最终步骤:MQ Server 如果收到的是 commit 操作,则将半消息标记为可投递,MQ订阅方最终将收到该消息;若收到的是 rollback 操作则删除 half 半消息,订阅方将不会接受该消息;如果本地事务执行结果没响应或者超时,则 MQ Server 回查事务状态,具体见步骤(2)的异常情况说明。

(2)异常情况:在断网或者应用重启等异常情况下,图中的步骤④提交的二次确认超时未到达 MQ Server,此时的处理逻辑如下:

步骤⑤:MQ Server 对该消息进行消息回查

步骤⑥:发送方收到消息回查后,检查该消息的本地事务执行结果

步骤⑦:发送方根据检查得到的本地事务的最终状态再次提交二次确认。

最终步骤:MQ Server基于 commit/rollback 对消息进行投递或者删除

rocketmq springboot版本对应 rocketmq底层_java_12

顺序消息

rocketmq springboot版本对应 rocketmq底层_客户端_13

消息清理

1 以一个log文件为单位清理

消息是被顺序存储在 commitlog 文件的,且消息大小不定长,所以消息的清理是不可能以消息为单位进 行清理的,而是以commitlog 文件为单位进行清理的。否则会急剧下降清理效率,并实现逻辑复杂。

2 手动清理和自动清理

2.1 自动清理策略

commitlog 文件存在一个 过期时间 ,默认为 72 小时,即三天。除了用户手动清理外,在以下情况下也 会被自动清理,无论文件中的消息是否被消费过:
(1)文件过期,且到达 清理时间点 (默认为凌晨 4 点)后,自动清理过期文件
(2)文件过期,且磁盘空间占用率已达 过期清理警戒线 (默认 75% )后,无论是否达到清理时间点,都会自动清理过期文件
(3)磁盘占用率达到 清理警戒线 (默认 85% )后,开始按照设定好的规则清理文件,无论是否过期。 默认会从最老的文件开始清理
(4)磁盘占用率达到 系统危险警戒线 (默认 90% )后, Broker 将拒绝消息写入

需要注意以下几点:

1 )对于 RocketMQ 系统来说,删除一个 1G 大小的文件,是一个压力巨大的 IO 操作。在删除过程
中,系统性能会骤然下降。所以,其默认清理时间点为凌晨 4 点,访问量最小的时间。也正因如
果,我们要保障磁盘空间的空闲率,不要使系统出现在其它时间点删除 commitlog 文件的情况。

2 )官方建议 RocketMQ 服务的 Linux 文件系统采用 ext4 。因为对于文件删除操作, ext4 要比 ext3 性
能更好

顺序写和零拷贝

mappedfilequeue

rocketmq springboot版本对应 rocketmq底层_客户端_14


mappedfile属性:

rocketmq springboot版本对应 rocketmq底层_客户端_15


rocketmq springboot版本对应 rocketmq底层_java-rocketmq_16

rocketmq springboot版本对应 rocketmq底层_客户端_17

mmap+DMA+sendfile

基于Dledger的主从复制

RocketMQ集群部署可以分为两种方式master-slave和dledger,虽然master-slave方式提供了一定的高可用性,但是如果集群中的master节点挂了,这时需要运维人员手动进行重启或者切换操作,即不能自动在集群的剩余节点中选出一个master,dledger解决了这个问题。本文将从以下角度来分析dledger。这里需要先说明一点,在RocketMQ中集群选举及数据复制都封装在Dledger项目中,它是一个基于raft协议的Java库,用于构建高可用性、高持久性、强一致性的commitlog,

Dledger在broker中配置:

rocketmq springboot版本对应 rocketmq底层_java_18

rocketmq springboot版本对应 rocketmq底层_java_19


选举:

在Dledger中节点的角色分为三种:leader、follower和candidate,

其初始状态是Candidate,所以当一个节点启动后执行到这里时会先执行maintainAsCandidate()方法。

maintainState()的整体逻辑是:

  • 如果节点当前角色是candidate则会发起投票;
  • 如果节点当前角色是leader则发送心跳信息给follower并且在没有收到n/2+1个节点的响应情况下节点状态变成candidate;
  • 如果节点当前角色是follower则接收leader的心跳信息并且在没有收到leader的心跳信息的情况下节点角色变成candidate;

rocketmq springboot版本对应 rocketmq底层_java-rocketmq_20

rocketmq springboot版本对应 rocketmq底层_java_21


选举详细过程:

rocketmq springboot版本对应 rocketmq底层_java-rocketmq_22


rocketmq springboot版本对应 rocketmq底层_数据_23

必须得到集群内超过半数节点认可,即最终选举出来的主节点的当前复制进度一定是比绝大多数的从节点要大,并且也会等于承偌给客户端的已提交偏移量。故得出的结论是不会丢消息。

rocketmq springboot版本对应 rocketmq底层_数据_24

dledger的主从复制:

leader处理写请求及日志复制流程

leader处理写请求
  当producer发送消息到集群时,master在收到请求后会使用SendMessageProcessor来处理请求,从这里入手不难发现:Dledger模式最终是调用DledgerCommitLog中的asyncPutMessage方法来完成数据写入,在该方法中有两个关键点需要注意:一个是调用serialize方法来完成消息的组装,一个是在构建完AppendEntryRequest请求后调用DledgerServer的handleAppend方法将数据写入master节点。

handleAppend接收producer数据
该方法是用来处理数据追加请求的,其主要完成以下操作:
  (1)将数据追加到本地
  (2)确认follower节点数据复制的结果
  (3)判断被pending的AppendEntryRequest请求是否超过10000,如果超过则拒绝请求并返回

DLedgerEntryPusher数据转发
  在Dledger中数据复制是在DLedgerEntryPusher中实现的,在集群选举出leader的情况下,集群中的leader负责接收客户端的请求,follower负责从leader同步数据,不会处理读写请求。在DLedgerEntryPusher中有三个重要的线程分别是:
  (1)EntryDispatcher:日志转发线程(leader使用)
  (2)EntryHandler:日志接收线程(follower使用)
  (3)QuorumAckChecker:处理日志复制返回结果的线程(leader使用)

EntryDispatcher数据分发
  EntryDispatcher是日志转发线程,由leader节点激活,该线程会将日志条目发送到follower,所以该线程应该是集群中除leader外每个节点一个。leader向follower发送日志复制请求分为4中类型:
  (1)APPEND:将日志条目append到follower
  (2)COMPARE:集群中的leader如果发生变化则新的leader首先会比较自己与follower的日志条目
  (3)TRUNCATE:leader节点完成日志条目的对比后则会发送该请求给follower,follower在接收请求会对其数据进行调整
  (4)COMMIT:通常情况下leader会将已经commit的日志条目的index附加到APPEND请求,如果APPEND请求很少,leader会单独发送请求通知follower已经提交的index

EntryHandler 从节点数据接收
  EntryHandler是由follower节点激活,它是用来接收日志复制请求并按照日志条目的index进行排序

QuorumAckChecker结果检查
  在Dledger中只有集群中超过半数的节点将数据复制成功后leader才会将响应返回给客户端。QuorumAckChecker线程就是在leader端检查收取到的成功响应是否超过了半数。这里需要注意:该线程会更新peerWaterMarksByTerm