RocketMQ概念模型
Producer: 消息生产者,负责生产消息。
Consumer: 消息消费者,负责异步消费消息。
Push Consumer: Consumer的一种,需要向Consumer对象注册监听。
Pull Consumer: Consumer的一种,需要主动请求Broker拉取消息。
Producer Group: 生产者集合,一般用于发送一类消息。创建生产者时必须指定,用于事务消费的消息回查。
Consumer Group: 消费者集合,一般用于接受一类消息进行消费。创建消费者时必须指定。
Broker: MQ消息服务(中转角色,用于消息存储与生产消费转发)。
RocketMQ源码下载
git地址:https://github.com/apache/rocketmq/
源码结构:
rocketmq-broker:主要的业务逻辑,消息收发,主从同步,pagecache。
rocketmq-client:客户端接口,比如生产者和消费者。
rocketmq-example:示例,比如生产者和消费者。
rocketmq-common:公共数据结构等等。
rocketmq-distribution:编译模块,编译输出等。
rocketmq-filter:进行Broker过滤不感兴趣的消息传输,减小带宽压力。
rocketmq-logappender、rocketmq-logging:日志相关。
rocketmq-namesrv:Namesrv服务,用于服务协调。
rocketmq-openmessaging:对外提供服务。
rocketmq-remoting:远程调用接口,封装Netty底层通信。
rocketmq-srvutil:提供一些公用的工具方法,比如解析命令行参数。
rocketmq-store:消息存储。
rocketmq-tools:管理工具,比如有名的mqadmin工具。
RocketMQ集群方式
在Metaq1.x/2.x的版本中,分布式协调采用的是Zookeeper,而RocketMQ自己实现了一个NameServer,所以RocketMQ启动时,先启动对应的NameServer。
推荐的几种 Broker 集群部署方式,这里的Slave 不可写,但可读,类似于 Mysql主备方式。
单个Master
很显然,这种方式风险较大,一旦Broker重启或者宕机时,会导致整个服务不可用,不建议线上环境使用。
多Master模式
一个集群无Slave,全是Master,例如2个Master或者3个Master。
优点: 配置简单,单个Master宕机或重启维护对应用无影响,在磁盘配置为RAID10时,即使机器宕机不可恢复情况下,由于RAID10磁盘非常可靠,消息也不会丢(异步刷盘丢失少量消息,同步刷盘一条不丢)。性能最高。
缺点: 单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅,消息实时性会受到影响。
多Master多Slave模式,异步复制
每个Master配置一个Slave,有多对Master-Slave,HA采用异步复制方式,主备有短暂消息延迟,毫秒级。
优点: 即使磁盘损坏,消息丢失的非常少,且消息实时性不会受影响,因为Master宕机后,消费者仍然可以从Slave消费,此过程对应用透明,不需要人工干预。性能同多Master模式几乎一样。主节点上线后会自动拉去从节点的offest信息,不会重复消费到slave中已经消费的消息。
缺点: Master宕机,磁盘损坏情况,会丢失少量消息。
多Master多Slave模式,同步双写
每个Master配置一个Slave,有多对Master-Slave,HA采用同步双写方式,主备都写成功,向应用返回成功。
优点: 数据与服务都无单点,Master宕机情况下,消息无延迟,服务可用性与数据可用性都非常高。
缺点: 性能比异步复制模式略低,大约低10%左右,发送单个消息的RT会略高。
物理部署结构
以多Master多Slave模式为例,看一下RocketMQ物理部署结构:
- Name Server
NameServer是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。3.X之前的版本都是使用zk。 - Broker
Broker 部署相对复杂,Broker分为Master和Slave,一个Master可以对应多个Slave,但是一个Slave只能对应一个Master,Master与Slave的对应关系通过指定相同的BrokerName,不同的BrokerId来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个。每个Broker与NameServer集群中的所有节点建立长连接,定时注册 Topic 信息到所有 NameServer。 - Producer
Producer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer取Topic路由信息,并向提供Topic服务的Master建立长连接,且定时向Master发送心跳。Producer完全无状态,可集群部署。 - Consumer
Consumer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer取Topic路由信息,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳。Consumer既可以从Master订阅消息,也可以从Slave订阅消息,订阅规则由Broker配置决定。
双主集群部署
这里就以双主集群为例,进行搭建。(双主集群会部署搭建,多主或多主多从也自然没啥问题了)。
服务器环境
如果条件允许,NameServer和broker分别在单独的机器上部署,我是用自己的电脑建的虚拟机,在虚拟机中部署的,限于自己电脑的配置,NameServer和broker就放在一台机器上了。
序号 | IP | 角色 | 模式 |
---|---|---|---|
1 | 192.168.152.129 | nameServer1,brokerServer1 | Master1 |
2 | 192.168.152.130 | nameServer2,brokerServer2 | Master2 |
配置IP映射
在下面的文件中配置:
vi /etc/hosts
192.168.152.129 rocketmq-nameserver1
192.168.152.129 rocketmq-master1
192.168.152.130 rocketmq-nameserver2
192.168.152.130 rocketmq-master2
生成tar包并安装
参考官网文档http://rocketmq.apache.org/docs/quick-start/
git clone https://github.com/apache/incubator-rocketmq.git
cd incubator-rocketmq
mvn -Prelease-all -DskipTests clean install -U
cd distribution/target/apache-rocketmq
在distribution/target/apache-rocketmq下有编译好的tar包 和zip包等。
创建数据文件夹
mkdir /usr/local/rocketmq/store
mkdir /usr/local/rocketmq/store/commitlog
mkdir /usr/local/rocketmq/store/consumequeue
mkdir /usr/local/rocketmq/store/index
修改配置文件
两台机器执行相同的操作,并注意,配置文件中的brokerName的值跟配置文件一致:
vim /home/rocketmq/conf/2m-noslave/broker-a.properties
vim /home/rocketmq/conf/2m-noslave/broker-b.properties
这里把常用的参数配置基本都列了出来,具体意思在注释里:
#所属集群名字
brokerClusterName=rocketmq-cluster
#broker名字
brokerName=broker-a
#0 表示Master, > 0 表示slave
brokerId=0
#nameServer 地址,分号分割
namesrvAddr=rocketmq-nameserver1:9876;rocketmq-nameserver2:9876
#brokerIP1=192.168.152.129
#在发送消息时,自动创建服务器不存在的Topic,默认创建的队列数
defaultTopicQueueNums=4
#是否允许Broker 自动创建Topic,建议线下开启,线上关闭
autoCreateTopicEnable=true
#是否允许Broker自动创建订阅组,建议线下开启,线上关闭
autoCreateSubscriptionGroup=true
#Broker 对外服务的监听端口
listenPort=10911
#删除文件时间点,默认是凌晨4点
deleteWhen=04
#文件保留时间,默认48小时
fileReservedTime=120
#commitLog每个文件的大小默认1G
mapedFileSizeCommitLog=1073741824
#ConsumeQueue每个文件默认存30W条,根据业务情况调整
mapedFileSizeConsumeQueue=300000
destroyMapedFileIntervalForcibly=120000
redeleteHangedFileInterval=120000
#检测物理文件磁盘空间
diskMaxUsedSpaceRatio=88
#存储路径
storePathRootDir=/home/rocketmq/store
#commitLog存储路径
storePathCommitLog=/home/rocketmq/store/commitlog
#消费队列存储路径
storePathConsumeQueue=/home/rocketmq/store/consumequeue
#消息索引存储路径
storePathIndex=/home/rocketmq/store/index
#checkpoint 文件存储路径
storeCheckpoint=/home/rocketmq/store/checkpoint
#abort 文件存储路径
abortFile=/home/rocketmq/store/abort
#限制的消息大小
maxMessageSize=65536
#flushCommitLogLeastPages=4
#flushConsumeQueueLeastPages=2
#flushCommitLogThoroughInterval=10000
#flushConsumeQueueThoroughInterval=60000
#Broker 的角色
brokerRote=ASYNC_MASTER
#刷盘方式
flushDiskType=ASYNC_FLUSH
#发消息线程池数量
#sendMessageTreadPoolNums=128
#拉消息线程池数量
#pullMessageTreadPoolNums=128
修改启动脚本参数
调一下JVM,包括nameserver 和 broker。限于自己机器的配置,参数调小一下。但Rocketmq最少的堆是1g,否则无法启动。两台机器执行相同的操作。
打开一下runbroker配置文件:
vim /usr/local/rocketmq/bin/runbroker.sh
修改内容如下:
JAVA_OPT="${JAVA_OPT} -server -Xms1g -Xmx1g -Xmn512m -XX:PermSize=128m -XX:MaxPermSize=320m"
打开一下runserver配置文件:
vim /usr/local/rocketmq/bin/runserver.sh
修改内容如下:
JAVA_OPT="${JAVA_OPT} -server -Xms1g -Xmx1g -Xmn512m -XX:PermSize=128m -XX:MaxPermSize=320m"
启动
要先启动namerserver,再启broker,两台机器执行相同的操作。
启动nameserver
nohup sh /home/rocketmq/bin/mqnamesrv >/home/rocketmq/logs/mqnamesrv.log 2>&1 &
启动brokerserver(节点1用的是a.properties,节点2用的是b.properties)
nohup sh /home/rocketmq/bin/mqbroker -c /home/rocketmq/conf/2m-noslave/broker-a.properties >/home/rocketmq/logs/broker-a.log 2>&1 &
nohup sh /home/rocketmq/bin/mqbroker -c /home/rocketmq/conf/2m-noslave/broker-b.properties >/home/rocketmq/logs/broker-b.log 2>&1 &
用下面命令查看一下
jps
创建控制台
先将incubator-rocketmq-externals拉到本地,因为我们需要自己对rocketmq-console进行编译打包运行。
git clone https://github.com/apache/rocketmq-externals.git
修改application.properties文件
rocketmq.config.isVIPChannel=false
通过命令行进入到rocketmq-console子目录,通过maven对其进行编译打包
mvn clean -DskipTests package
此时在rocketmq-console/target目录下生成了一个叫rocketmq-console-ng-1.0.0.jar的jar包。
cd /usr/local/software/rocketmq-externals/rocketmq-console/target/
接下来运行这个jar包,我们可以直接通过java -jar的方式运行
java -jar rocketmq-console-ng-1.0.0.jar --server.port=8889 --rocketmq.config.namesrvAddr=rocketmq-nameserver1:9876;rocketmq-nameserver2:9876
这里注意需要设置两个参数:–server.port为运行的这个web应用的端口,如果不设置的话默认为8080;–rocketmq.config.namesrvAddr为RocketMQ命名服务地址,如果不设置的话默认为“”。
PS
如果要搭主从,再次重申一遍Master与Slave在配置中的区别
Broker与Slave配对是通过指定相同的brokerName参数来配对,Master的BrokerId 必须是0,Slave的BrokerId必须是大于0的数。另外一个Master下面可以挂载多个Slave,同一Master下的多个Slave 通过指定不同的BrokerId来区分。
Broker 重启对客户端的影响
Broker 重启可能会导致正在发往这台机器的的消息发送失败,RocketMQ提供了一种优雅关闭Broker的方法,通过执行以下命令会清除Broker的写权限,过40s后,所有客户端都会更新Broker路由信息,此时再关闭Broker就不会发生发送消息失败的情况,因为所有消息都发往了其他 Broker。
sh mqadmin wipeWritePerm -b brokerName -n namesrvAddr
Master 与Slave的关系
RocketMQ的开源版本,Master宕机,Slave不能切换为Master,这里的Slave不可写,但可读,类似于 Mysql 主备方式。
Docker部署
1. 安装 Namesrv
拉取镜像
docker pull rocketmqinc/rocketmq:4.4.0
启动容器
docker run -d -p 9876:9876 -v {RmHome}/data/namesrv/logs:/root/logs -v {RmHome}/data/namesrv/store:/root/store --name rmqnamesrv -e "MAX_POSSIBLE_HEAP=100000000" rocketmqinc/rocketmq:4.4.0 sh mqnamesrv
注意事项
{RmHome} 要替换成你的宿主机想保存 MQ 的日志与数据的地方,通过 docker 的 -v 参数使用 volume 功能,把你本地的目录映射到容器内的目录上。否则所有数据都默认保存在容器运行时的内存中,重启之后就又回到最初的起点。
2. 安装 broker 服务器
拉取镜像
与上步是同一个镜像,如果上步完成,此步无需拉取
创建 broker.conf 文件
- 在 {RmHome}/conf 目录下创建 broker.conf 文件
- 在 broker.conf 中写入如下内
brokerClusterName = DefaultCluster
brokerName = broker-a
brokerId = 0
deleteWhen = 04
fileReservedTime = 48
brokerRole = ASYNC_MASTER
flushDiskType = ASYNC_FLUSH
brokerIP1 = {本地外网 IP}
brokerIP1 要修改成你自己宿主机的 IP
启动容器
docker run -d -p 10911:10911 -p 10909:10909 -v {RmHome}/data/broker/logs:/root/logs -v {RmHome}/rocketmq/data/broker/store:/root/store -v {RmHome}/conf/broker.conf:/opt/rocketmq-4.4.0/conf/broker.conf --name rmqbroker --link rmqnamesrv:namesrv -e "NAMESRV_ADDR=namesrv:9876" -e "MAX_POSSIBLE_HEAP=200000000" rocketmqinc/rocketmq:4.4.0 sh mqbroker -c /opt/rocketmq-4.4.0/conf/broker.conf
注意事项
注意: {RmHome} 同上步一样,不再缀述。broker.conf 的文件中的 brokerIP1 是你的 broker 注册到 Namesrv 中的 ip。如果不指定他会默认取容器中的内网 IP。除非你的应用也同时部署在网络相通的容器中,本地或容器外就无法连接 broker 服务了,进而导致类似 RemotingTooMuchRequestException 等各种异常。
3. 安装 rocketmq 控制台
拉取镜像
docker pull pangliang/rocketmq-console-ng
启动容器
docker run -e "JAVA_OPTS=-Drocketmq.namesrv.addr={本地外网 IP}:9876 -Dcom.rocketmq.sendMessageWithVIPChannel=false" -p 8080:8080 -t pangliang/rocketmq-console-ng
生产者快速入门
RocketMQ默认会在topic不存在时自动创建,并且为topic生成4个MessageQueue。
public class Producer {
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
DefaultMQProducer producer = new DefaultMQProducer("test_quick_producer_name");
producer.setNamesrvAddr(Const.NAMESRV_ADDR);
producer.start();
for(int i=0;i<5;i++) {
//创建消息
Message message = new Message("test_quick_topic", // 主题
"TagA", // 标签
"keyA", // 用户自定义的key,用户定义的唯一表示
("Hello Rocket" + i).getBytes() // 消息内容实体(byte[])
);
//发送消息
SendResult send = producer.send(message);
System.out.println(send);
}
producer.shutdown();
}
}
消费者快速入门
消费者需要订阅topic,设置从头消费还是从尾消费,这个只是在消费者首次启动遵循,在消费消息过后会将消费进度记录到broker和nameserver中,下次会遵循记录到位置开始消费。消费者可以返回状态,处理失败返回RECONSUME_LATER后,会自动进行重试,从5s到2h逐级递增重试15次,我们可以在消息中找到重试次数做后续到处理。
public class Consumer {
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("test_quick_consumer_name");
consumer.setNamesrvAddr(Const.NAMESRV_ADDR);
//从尾部消费
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
consumer.subscribe("test_quick_topic", "*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
MessageExt msg = list.get(0);
try {
String topic = msg.getTopic();
String tags = msg.getTags();
String keys = msg.getKeys();
//模拟失败
if(keys.equals("key1")) {
System.out.println("消息消费失败。。。");
int a = 1/0;
}
String msgBody = new String(msg.getBody(), RemotingHelper.DEFAULT_CHARSET);
System.out.println("topic:" + topic + "tags:" + tags + "keys:" + keys + "msgBody:" + msgBody);
} catch (Exception e) {
e.printStackTrace();
System.out.println("重试次数:" + msg.getReconsumeTimes());
//设置超时次数
if(msg.getReconsumeTimes() == 3) {
//记录日志并且做补偿处理
}
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.out.println("consumer start...");
}
}