消息的持久化和签收机制等都是为了保证消息的可靠性,但仅保证可靠性是不够的,有时我们还需要保证高可用性,这时就需要搭建ActiveMQ集群,以避免单点故障提升可用性。官方为我们介绍了三种集群搭建的方式:详参
5.6版本之后推出了LevelDB的持久化引擎,它使用自定义的索引代替常用的BTree索引,持久化性能高于KahaDB。虽然目前默认的持久化方式还是KahaDB,但是LevelDB会是趋势。5.9版本还提供了基于Zookeeper和LevelDB的数据主从复制方式,是主从数据复制方案的首选。
下面以Zookeeper + Replicated-LevelDB-Store进行集群环境的搭建。
一、原理说明
使用Zookeeper集群注册所有的ActiveMQ Broker,但只有其中的一个Broker可以提供服务,这个提供服务的Broker被视为Master,其他的Broker处于待机状态,被视为Slave。客户端只能连接Master,不能连接Slave。
如果Master因故障不能提供服务,Zookeeper会从Slave中选举出一个Broker充当Master。
Slave连接Master并同步它的存储状态,Slave不接受客户端连接。所有的存储操作都将被复制到连接至Master的Slaves。如果Master宕机则得到了最新更新的Slave会成为Master,故障节点在恢复后会重新加入到集群中并连接Master进入Slave模式。
所有需要同步的消息操作都将等待存储状态被复制到其他法定节点的操作完成才算完成。所以,如果你配置了replicas=3,那么法定大小是(3/2)+1=2。Master将会存储并更新然后等待(2-1)=1个Slave存储和更新完成才汇报success。
有一个node要作为观察者存在,当一个新的Master被选中,你需要至少保障一个法定node在线以能够找到拥有最新状态的node,这个node才可以成为新的Master。因此推荐运行至少3个replica nodes以防止一个node失败后导致服务中断。
二、环境搭建
环境准备:
主机 | Zookeeper集群端口 | AMQ集群bind端口 | AMQ消息tcp端口 | 管理控制台端口 | AMQ节点安装目录 |
---|---|---|---|---|---|
192.168.2.107 | 2191 | bind=“tcp://0.0.0.0:63631” | 61616 | 8161 | /mq_cluster/mq_node01 |
192.168.2.107 | 2192 | bind=“tcp://0.0.0.0:63632” | 61617 | 8162 | /mq_cluster/mq_node02 |
192.168.2.107 | 2193 | bind=“tcp://0.0.0.0:63633” | 61618 | 8163 | /mq_cluster/mq_node03 |
注
:此处是做测试,将所有服务都部署在了一台机器上,用端口进行服务区分,生产上应该部署在多台机器上
1、启动Zookeeper集群服务:此处采用的是docker的方式,具体请参考:使用Docker进行ZK集群搭建 和 Linux安装docker-compose,也可以自己部署Zookeeper服务集群
2、创建MQ集群目录
mkdir /mq_cluster/ #创建集群目录
cp -r /opt/apache-activemq-5.15.9 mq_node01 #创建第1个node节点
cp -r mq_node01 mq_node02 #创建第2个node节点
cp -r mq_node01 mq_node03 #创建第3个node节点
3、编辑每个节点下的conf/jetty.xml配置文件:修改管理控制台的端口,node01节点可以不修改
4、编辑/etc/hosts文件进行域名映射:该配置文件和Windows下的hosts文件类似
192.168.2.107 mq-server
5、修改三个节点的brokerName,使它们完全一样
6、修改三个节点的持久化方式为LevelDB:
node01:
<persistenceAdapter>
<replicatedLevelDB
directory="${activemq.data}/leveldb"
replicas="3"
bind="tcp://0.0.0.0:63631"
zkAddress="localhost:2191,localhost:2192,localhost:2193"
hostname="mq-server"
sync="local_disk"
zkPath="/activemq/leveldb-stores"/>
</persistenceAdapter>
node02:
<persistenceAdapter>
<replicatedLevelDB
directory="${activemq.data}/leveldb"
replicas="3"
bind="tcp://0.0.0.0:63632"
zkAddress="localhost:2191,localhost:2192,localhost:2193"
hostname="mq-server"
sync="local_disk"
zkPath="/activemq/leveldb-stores"/>
</persistenceAdapter>
node03:
<persistenceAdapter>
<replicatedLevelDB
directory="${activemq.data}/leveldb"
replicas="3"
bind="tcp://0.0.0.0:63633"
zkAddress="localhost:2191,localhost:2192,localhost:2193"
hostname="mq-server"
sync="local_disk"
zkPath="/activemq/leveldb-stores"/>
</persistenceAdapter>
注意
:
1️⃣zkAddress的值即为上面的zk集群的地址
2️⃣hostName的值为我们做的域名映射的域名
3️⃣repicas的值为节点的个数
7、修改各个节点的消息端口:第1个节点可不做改动
8、按顺序启动这3个MQ节点,可以采用如下的Shell脚本(mq_start.sh)进行启动:启动之前务必保证ZK集群已经正常启动
#!/bin/sh
cd /mq_cluster/mq_node01/bin
./activemq start
cd /mq_cluster/mq_node02/bin
./activemq start
cd /mq_cluster/mq_node03/bin
./activemq start
9、访问:http://192.168.2.107:8161/admin/
注意
:
1️⃣需要关闭防火墙保证Windows可以ping通ActiveMQ所在的服务器
2️⃣此时只能访问8161这个端口的服务,因为Shell脚本中它是最先启动的,是此时的Master,剩下的两个Slave是不能被访问的,只有当Master宕机后被推选为Master的Slave才可以被访问。总而言之,只有Master可以被访问
3️⃣如果是在虚拟机上使用docker的方式搭建集群,在访问管理控制台时很可能会报错,查看后台日志的错误信息是:no space left on device,此时可以尝试清理下docker的空间
docker system prune
三、ZK集群的节点状态查看
通过zkCli连接到zk集群中任意一个服务(也可以在Windows下使用zkCli客户端连接,前提是网络相通),可以查看到集群中的每个mq服务的状态:因为我在Linux虚拟机上采用docker的方式安装的zk,没有安装zk客户端,因此此处在Windows环境下进行查看
1、连接到ZK集群中的任意一个服务
2、查看根节点下的节点:会看到activemq
3、查看activemq节点下的节点:看到leveldb-stores,其实就是我们配置持久化时zkPath的值
4、在leveldb-stores下会看到注册的3个mq的服务
5、可以查看每个服务的状态
node01:可以看出来被选中为master,即elected值不为null
node02:
node03:
四、客户端连接MQ集群
ActiveMQ的客户端只能访问Master的Broker,其他处于Slave的Broker不能访问,所以客户端连接的Broker应该使用failover协议在发生连接失败时进行转移。在我们上面配置的集群环境中,ZK和MQ各有3个服务,当一个ActiveMQ节点挂掉或者一个ZK节点挂掉,ActiveMQ服务依然能够正常运转,但如果仅剩下一个ActiveMQ节点,由于不能选举Master,则ActiveMQ不能正常运转。同样地,如果ZK仅剩一个节点活动,不管ActiveMQ各节点的存活情况,ActiveMQ都不能正常提供服务,也就是说此时ActiveMQ集群的高可用依赖于Zookeeper集群的高可用。
在代码中需要做的改变是我们在填写ACTIVE_URL时需要使用failover协议:生产者和消费者都是如此
public static final String ACTIVEMQ_URL = "failover:(tcp://192.168.2.107:61616,tcp://192.168.2.107:61617,tcp://192.168.2.107:61618)?randomize=false";