RocketMQ源码系列(1) — 基础架构

1,262 阅读12分钟

专栏:RocketMQ 源码系列

消息中间件

国内一般常用的消息中间件(MQ)有 Kafka、RabbitMQ、RocketMQ、ActiveMQ,其中 ActiveMQ 以前用的比较多,现在国内使用的公司很少了。下面简单对 Kafka、RocketMQ、RabbitMQ 做一些对比。

Kafka

优点:

  • 高吞吐量,单机可以达到每秒十几万QPS
  • 高性能,消息发送/接收毫秒级响应
  • 高可用,支持集群部署,部分机器宕机可以继续提供服务

缺点:

  • Kafka 可能会丢失数据,因为 Kafka 收到消息会写入一个磁盘缓冲区,没有直接落地磁盘,所以如果机器故障可能会丢失一部分数据。
  • 功能单一,主要支持发送消息和消费消息,适用场景有限。

场景:

  • Kafka 一般用在数据量特别大,要求吞吐量高,适当丢失数据也没影响的场景,比如大数据领域、日志采集和传输等。

RabbitMQ

优点:

  • 保证数据不丢失
  • 高可用,支持集群部署,部分机器宕机可以继续提供服务
  • 支持部分高级功能,比如死信队列、消息重试

缺点:

  • 吞吐量低,一般单机每秒几万QPS
  • 集群扩展较为麻烦
  • 开发语言是 erlang,无法阅读源码,不利于做二次开发。

RocketMQ

RocketMQ 是阿里开源的消息中间件,它几乎同时解决了 Kafka 和 RabbitMQ 的缺陷,国内很多互联网大厂也在使用 RocketMQ。

优点:

  • 高吞吐量,单机可达到10万以上QPS
  • 高性能,消息发送/接收毫秒级响应
  • 高可用性,支持大规模集群部署
  • 可保证数据绝对不丢失
  • 支持多种高级功能:延迟消息、事务消息、消息回溯、死信队列、消息积压、消息重试等
  • 开发语言是 Java,很容易阅读源码和二次开发

RocketMQ

RocketMQ 非常适合用在 Java 业务系统架构中,它的高性能、高级功能 可以帮助我们解决各种业务问题。并且 RocketMQ 是 Java 语言开发的,学习和阅读它的源码也比较容易。本系列就来深入研究和学习 RocketMQ 的源码,看下它是如何实现各种高级功能的,便于我们更好的使用 RocketMQ。同时,通过学习 RocketMQ 的架构设计、源码,掌握这类分布式中间件的设计开发能力。

基础架构

我们先通过下面这张图来整体的对 RocketMQ 有一个基础架构上的认识。

RocketMQ 主要有四个角色:NameServer、Broker、Producer、Consumer。

image.png

Broker

每台机器上部署的 RocketMQ 进程一般称之为 Broker,生产者向 Broker 发送消息,Broker 收到生产者的消息后存储到本地磁盘文件中,消费者从 Broker 读取消息来消费。

高并发机制

RocketMQ 单机一般 10万+ QPS,为了支撑高并发访问,例如几十万QPS,Broker 支持集群部署,这样可以把请求分散到多台 Broker 上,RocketMQ 就是通过集群化部署来抗下高并发。

海量数据存储

Broker 会收到大量消息,这些消息并不会立马被消费者消费,所以 RocketMQ 会把消息存储到本地磁盘,然后等待消费者来消费消息。MQ 可能会存储大量的消息,可能是几百万、上亿的消息,这么多消息在一台机器上肯定无法存储。

基于 Broker 的集群化部署,所有的消息会分散到多台 Broker 上,然后 Broker 存储到自己机器的本地磁盘上,这样 RocketMQ 就实现了海量数据存储,其本质上是一种分布式的存储。

主从架构

如果 Broker 宕机,为了保证数据不丢失,RocketMQ 支持主从架构模式。

Broker 有 Master 和 Slave 两种角色,Master Broker 收到消息后会同步给 Slave Broker,同步数据采用 Pull 模式,即 Slave Broker 不断向 Master Broker 拉取数据。

部署主从后,这时如果 Master 宕机了,Slave 上有数据副本,可以保证数据不丢失,继续对外提供服务,保证了 MQ 的可靠性和高可用性。

部署 Master-Slave 后,生产者将消息写入 Master Broker,然后 Slave Broker 从 Master Broker 拉取消息进行同步;而消费者则可以从 Master Broker 或者 Slave Broker 拉取消息来消费。这里面的详细机制后面再深入研究。

在 RocketMQ 4.5 版本之前,Slave Broker 只是用来同步数据,尽量保证数据不丢失,但在 Master Broker 宕机后,无法自动切换为 Master。在 4.5 版本之后,RocketMQ 提供了一种 Dledger 的机制,基于 Dledger 可以实现主从自动切换的效果。Dledger 技术要求一个 Master 至少有两个 Slave,在 Master 宕机后,就从 Slave 中选举一个出来,切换成 Master。

NameServer

Broker 支持集群部署,对于生产者来说,它要将消息发送到哪个 Broker,消费者又要从哪个 Broker 获取消息呢?这就需要 NameServer 了,NameServer 是一个路由注册中心,支持 Broker 的动态注册与发现。

高可用性

要部署 RocketMQ,首先就要部署 NameServer。NameServer 也支持集群化部署,主要就是为了保证 NameServer 的高可用性。

NameServer 采用 Peer-to-Peer 的模式,集群中每台 NameServer 都是独立运行,互不通信。每台 NameServer 都会有集群中所有的 Broker 信息,只要有一台 NameServer 还在运行,就可以保证 MQ 系统的稳定性。

心跳检查

Broker 注册到 NameServer 时,会与 NameServer 建立一个 TCP 长连接,然后每隔 30秒 给 NameServer 发送一个心跳告诉它自己还活着。

NameServer 收到心跳后,就会更新这个 Broker 最近一次心跳的时间。NameServer 会每隔 10秒 去检查各个 Broker 最近一次心跳的时间,如果某个 Broker 超过 120秒 都没发送心跳了,就认为这个 Broker 宕掉了,然后就会从路由信息中摘除这个 Broker。

工作机制

Broker(包括 Master 和 Slave)启动时会把自己注册到所有 NameServer 上,也就是说,每个 NameServer 都会有一份集群中所有 Broker 的信息。

生产者和消费者会跟 NameServer 集群中的一个 NameServer 建立 TCP 长连接,定时从 NameServer 拉取最新的路由信息,这时就可以根据要访问的 Topic,找出这个 Topic 关联的几台 Broker,再根据一定的负载均衡策略,选出其中一个 Broker。选出 Broker 后,生产者或消费者就会跟这个 Broker 建立 TCP 长连接,然后就可以通过这个 TCP 长连接来访问这个 Broker 了。

数据模型

RocketMQ 中的核心数据模型叫 Topic,表示数据集合的意思,不同类型的数据需要放到不同的 Topic 中,可以根据不同的业务场景创建不同的 Topic。

一个 Topic 可能会包含很多消息,每个 Topic 的消息可以分布式存储到多台 Broker 上。

每个 Broker 在发送心跳给 NameServer 时,就会上报自己包含哪些 Topic。这样生产者和消费者就可以根据要访问的 Topic 找到 Broker。

部署测试

环境说明:

快速搭建

下面用RocketMQ提供的 Binary 版本来快速构建和部署基于 DLedger 的可以自动容灾切换的 RocketMQ 集群。

创建rocketmq目录:

# mkdir /rocketmq

# cd /rocketmq

下载Binary版本并解压:

# wget https://github.com/apache/rocketmq/archive/refs/tags/rocketmq-all-4.9.3.zip

# unzip rocketmq-all-4.9.3.zip

# cd rocketmq-4.9.3

可以看到有如下一些目录:

  • LICENSE、NOTICE、README.md 包括一些版权声明和功能说明;
  • benchmark 里包括运行 benchmark 程序的 shell 脚本;
  • bin 里包含各种使用 RocketMQ 的脚本,比如启动 NameServer 和 Broker 的脚本;
  • conf 里包含一些示例配置文件、logback 日志配置文件等;
  • lib 里包括 RocketMQ 各个模块编译成的 jar 包以及依赖的 jar 包。
[root@0a8f0d2f8ac1 rocketmq-4.9.3]# ls
benchmark  bin  conf  lib  LICENSE  nohup.out  NOTICE  README.md

启动集群:

# sh bin/dledger/fast-try.sh start

这个脚本会在本地启动一个 NameServer + 一组 Broker(一主两从),三个Broker的配置文件在 conf/dledger 下。

image.png

以 broker-no.conf 配置文件为例看看 Broker 的配置:

# broker 集群名称
brokerClusterName = RaftCluster

# 一组broker的名称,比如 一主两从 作为一组 broker,它们的 brokerName 必须一样;
# 另外一组就需要取别的名称,比如 RaftNode01
brokerName=RaftNode00

# broker 监听的端口
listenPort=30911

# NameServer 的地址,默认端口为 9876;多个 NameServer 可用分号隔开
namesrvAddr=127.0.0.1:9876

# 存放 broker 数据的位置
storePathRootDir=/tmp/rmqstore/node00
storePathCommitLog=/tmp/rmqstore/node00/commitlog

# 是否启动 DLeger 技术,必须设置为 true
enableDLegerCommitLog=true

# DLedger Raft Group 的名字,建议和 brokerName 保持一致
dLegerGroup=RaftNode00

# DLedger Group 内各节点的端口信息,同一个 Group 内的各个节点配置必须要保证一致 
dLegerPeers=n0-127.0.0.1:40911;n1-127.0.0.1:40912;n2-127.0.0.1:40913

# 当前DLedger节点 id, 必须属于 dLegerPeers 中的一个;同 Group 内各个节点要唯一
dLegerSelfId=n0

# 发送消息的线程数,建议配置成 Cpu 核数
sendMessageThreadPoolNums=16

启动完成后检查集群状态:

# sh bin/mqadmin clusterList -n 127.0.0.1:9876

image.png

其中,BID0 的表示 Master,大于 0 的都是 Slave。

接下来将 Master Kill 掉,过一会可以看到其中一个 Slave 已经自动切换为 Master:

image.png

停止集群使用如下命令:

# sh bin/dledger/fast-try.sh stop

集群部署

NameServer 是核心的路由服务,首先要保证 NameServer 的高可用,一般可以部署三台 NameServer 组成集群就可以保证高可用。NameServer 主要是承载 Broker 注册和心跳,以及生产者和消费者系统拉取路由表等请求,负载其实很低,因此不需要特别高的机器配置,生产环境一般使用 8C 16G 500GB 的机器足矣。

Broker 是负载最高的,要承载高并发写入和海量数据存储,因此机器配置要高,例如 24C 48G 1TB 的机器配置。为了保证高可用,基于 DLeger 技术,采用一主两从的部署模式。根据集群的负载,再扩展集群Broker组的数量。

本地部署测试为了简便,使用三台虚拟机来部署,模式为三个 NameServer,一组Broker(一主两从)。

部署架构如下:

image.png

按前面的步骤,先在三台机器上创建 rocketmq 目录,下载 Binary 版本并解压。

RocketMQ中的启动脚本中JVM内存设置得比较大,建议调整脚本中的JVM内存参数:

# vim bin/runserver.sh
# vim bin/runbroker.sh

# 修改如下参数
-Xms2g -Xmx2g -Xmn1g

修改脚本启动器,注释掉这几行代码,需要在环境变量中设置好 JAVA_HOME:

image.png

接下来先在三台机器上分别部署 NameServer:

# sh bin/mqnamesrv &

在三台机器上分别修改对应的 broker-n?.conf 配置文件,主要修改如下几项:

# Master 节点设置 brokerId=0,另外两个节点设置 1、2
brokerId=0
# 监听端口都修改为 30911
listenPort=30911
# NameServer 地址
namesrvAddr=172.17.0.2:9876;172.17.0.3:9876;172.17.0.4:9876
# dLeger 节点
dLegerPeers=n0-172.17.0.2:40911;n1-172.17.0.3:40911;n2-172.17.0.4:40911
# 当前节点ID
dLegerSelfId=n0

在每台机器上分别启动 Broker,注意指定对应的配置文件:

# sh bin/mqbroker -c conf/dledger/broker-n0.conf &

检查集群状态:

# sh bin/mqadmin clusterList -n 172.17.0.2:9876

image.png

可视化界面

RocketMQ 最大的优势之一就是提供了一个可视化的仪表盘管理界面,使得我们可以去监控和查看集群的信息和状态等。下面就来安装下 RocketMQ 的仪表盘。

随便找一台机器,先拉取 RocketMQ dashboard 工程源码:

# git clone https://github.com/apache/rocketmq-dashboard.git

# cd rocketmq-dashboard

修改下配置:

# vim src/main/resources/application.yml

修改 NameServer 进群的地址:

rocketmq:
  config:
    namesrvAddrs:
      - 172.17.0.2:9876
      - 172.17.0.3:9876
      - 172.17.0.4:9876

执行打包命令:

# mvn clean package -Dmaven.test.skip=true

运行仪表盘:

# nohup java -jar target/rocketmq-dashboard-1.0.1-SNAPSHOT.jar --server.port=40933 >dashboard.log &

启动好之后,就可以在浏览器访问仪表盘:

image.png

整个集群部署成功后,可以开发一些Demo程序来测试,代码示例可参考官方文档:样例 Example

RocketMQ 源码

源码结构

后续将深入研究RocketMQ的源码,本节先从Github将源码克隆到本地,对RocketMQ工程有个初步认识。

下载源码后切换到 4.9.3,然后执行构建命令:

# git checkout -b rocketmq-all-4.9.3`

# mvn clean install -DskipTests

RocketMQ 源码结构如下图所示:

image.png

工程模块说明如下:

  • rocketmq-acl:集群权限控制
  • rocketmq-broker:Broker 模块,实现消息队列的主体
  • rocketmq-client:消息客户端,包括生产者和消费者
  • rocketmq-common:包括一些公共的功能类的实现
  • dev:开发者信息
  • rocketmq-distribution:一些运行脚本、配置文件、日志配置等
  • docs:文档
  • rocketmq-example:使用示例
  • rocketmq-filter:消息过滤相关基础类
  • rocketmq-logging:日志相关
  • rocketmq-namesrv:NameServer 模块,管理集群元数据
  • rocketmq-openmessaging:对 OpenMessaging 的支持,面向云的分布式消息
  • rocketmq-remoting:基于 Netty 的远程通信模块
  • rocketmq-srvutil:服务器工具类
  • rocketmq-store:消息存储相关
  • style:checkstyle 相关实现
  • rocketmq-test:测试相关
  • rocketmq-tools:管理工具,监控命令相关实现类

本地测试

RocketMQ 在 distribution 模块下提供了启动脚本和一些部署模式的配置文件。

image.png

其中提供了如下几种部署模式,根据需求选择对应的模式配置文件去部署即可:

  • broker.conf:单机部署
  • 2m-2s-async:两主两从,主从异步复制
  • 2m-2s-sync:两主两从,主从同步复制
  • 2m-noslave:两主无从
  • dledger:基于 DLedger 多副本高可用

在 rocketmq 工程下新建两个目录用于存放测试数据:

  • conf:配置目录
  • data:数据目录

本地测试先部署单机模式,将 conf/broker.conf 文件拷贝到 ~/conf 下,并将日志配置文件拷贝过去。

修改 ~/broker.conf 中的如下配置:

# broker IP
brokerIP1=127.0.0.1

# 存储根目录
storePathRootDir=/Users/bojiangzhou/repo/repo-open-source/rocketmq/data

先启动 NameServer,运行 namesrv 下的 NamesrvStartup 启动类,注意在环境变量中添加 ROCKETMQ_HOME,程序中会使用到这个环境变量。

image.png

再启动 Broker,需在参数中指定 broker.conf 配置文件,在环境变量中添加 ROCKETMQ_HOME

image.png

最后再启动 rocketmq-example 下的生产者和消费者示例程序来测试,启动类如下:

// 生产者
org.apache.rocketmq.example.quickstart.Producer
// 消费者
org.apache.rocketmq.example.quickstart.Consumer

启动前先在 Producer 和 Consumer 的 main 方法中添加如下两行代码。

// 初始化一些配置
DefaultChannelId.newInstance();
// 设置 NameServer 地址
comdumer/producer.setNamesrvAddr("127.0.0.1:9876");

程序运行起来后,可以在 data 目录下看到生成了很多文件,这些就是 broker 消息存储相关的文件,我们后面再来深入分析。

image.png

RocketMQ 的日志文件默认在 ${user.home}/logs/rocketmqlogs 下,遇到问题可以查看相关日志文件。

image.png