上一篇 ActiveMQ(5)-基于ZooKeeper的HA方案介绍了通过zookeeper管理节点,实现activemq的master-slave高可用架构。存在一定的问题:
1)占用的节点数过多,1个zookeeper集群至少3个节点,1个activemq集群也至少得3个节点,但其实正常运行时,只有一个master节点在对外响应,换句话说,花6个节点的成本只为了保证1个master节点的高可用,太浪费资源了;
2)一个master无法支撑大的数据量,所以master需要水平扩展,做成负责均衡;
3)性能下降太明显,比起单节点的activemq,性能下降了近1个数量级。
本篇介绍基于networks of brokers的HA方案,不需要借助zookeeper等第3方组件,只需要2个activemq节点就能达到类似效果。
ip | 主机名 | activemq | 消息端口 | 控制台端口 |
---|---|---|---|---|
192.168.72.133 | hadoop1 | 1 | 61616 | 8161 |
192.168.72.129 | hadoop2 | 1 | 61617 | 8161 |
Broker一词的原意是『经纪人、中间人』,用在ActiveMQ的构架中,即:Broker作为Producer与Consumer的中间人(或代理人),生产者不用知道消费者在哪里、如何消费这些细节,只要将消息扔给中间人Broker即可,类似的,消费者也不用关心消息是从哪个生产者过来的,它只知道这是从Broker那里拿来的,如果画一张图来描述,就是下面这样(引用自本文最后参考文章中的图片)
那么,当生产者将消息发给Broker时,会发生什么?下图描述的就是这个过程:
1) 生产者将消息发给Broker
2) Broker将消息落地存储
3) 然后给生产者反馈:事情我已经办妥了!
继续,再来看看消费者又是如何跟Broker打交道的:
1) Broker将接收到的消息,从db中取出
2) 然后发送给消费者
3) 如果消费者使用的是自动确认模式(即:Session.AUTO_ACKNOWLEDGE),则Consumer会马上告诉Broker:ok,消息我已经收到了。
4) 然后进行自己的业务处理
5) Broker一旦收到确认,将会马上更新消息的状态为已消费(或直接删除,取决于持久化的实现机制)(注:虽然图中步骤5排在步骤4之后,但是步骤4、5几乎是同时发生的)
在一些大型应用中,如果一个Broker出现性能瓶颈抗不住压力,可能会拆分成多个Broker,如下图所示:
(注:上图中箭头的方法并非数据流向,而应该理解成调用关系,即:Producer调用Broker1,Consumer调用Broker2…)
Producer将消息发送给Broker1,而Consumer从另一个Broker2接收消息,有点类似数据库读写分离的意思,这样系统的性能可以提升一定程度的提升,但是问题来了,Broker1上的消息,如何”同步”(见下面的注释)到Broker2呢,这就依赖networkConnector的配置。
注:同步这个词用在这里可能不太准确,但也找不到一个更精确的词来描述,实际上,二个broker用上述机制组建成小集群后,如果生产者连接到broker1,消费者连接到broker2,当消息发送到broker1后,broker1不会将该消息复制一份到broker2,而是等消费者从broker2上消费该消息时,这条消息才从broker1取到broker2上,相当于此时broker2是消费者,从broker1消费了一条消息,然后broker2上就有这条消息了,最终消费者才能broker2上拿到这条消息。
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://activemq.apache.org/schema/core http://activemq.apache.org/schema/core/activemq-core.xsd">
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<value>file:${activemq.conf}/credentials.propertiesvalue>
property>
bean>
<bean id="logQuery" class="io.fabric8.insight.log.log4j.Log4jLogQuery"
lazy-init="false" scope="singleton"
init-method="start" destroy-method="stop">
bean>
<broker xmlns="http://activemq.apache.org/schema/core" brokerName="activemq-1" dataDirectory="${activemq.data}">
<networkConnectors>
<networkConnector uri="static:(tcp://hadoop2:61617)" duplex="true"/>
networkConnectors>
<destinationPolicy>
<policyMap>
<policyEntries>
<policyEntry topic=">" >
<pendingMessageLimitStrategy>
<constantPendingMessageLimitStrategy limit="1000"/>
pendingMessageLimitStrategy>
policyEntry>
policyEntries>
policyMap>
destinationPolicy>
<managementContext>
<managementContext createConnector="false"/>
managementContext>
<persistenceAdapter>
<kahaDB directory="${activemq.data}/kahadb"/>
persistenceAdapter>
<systemUsage>
<systemUsage>
<memoryUsage>
<memoryUsage percentOfJvmHeap="70" />
memoryUsage>
<storeUsage>
<storeUsage limit="100 gb"/>
storeUsage>
<tempUsage>
<tempUsage limit="50 gb"/>
tempUsage>
systemUsage>
systemUsage>
<transportConnectors>
<transportConnector name="openwire" uri="tcp://0.0.0.0:61616?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="amqp" uri="amqp://0.0.0.0:5672?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="stomp" uri="stomp://0.0.0.0:61613?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="mqtt" uri="mqtt://0.0.0.0:1883?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="ws" uri="ws://0.0.0.0:61614?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
transportConnectors>
<shutdownHooks>
<bean xmlns="http://www.springframework.org/schema/beans" class="org.apache.activemq.hooks.SpringContextHook" />
shutdownHooks>
broker>
<import resource="jetty.xml"/>
beans>
注意:14-16行及21-22行,该Broker对外暴露61616端口,同时”连接”到主机名为hadoop2的activemq服务的61617端口(即另1个broker),最终的效果相当于,如果有producer把消息发到61616(broker1),则从另一个broker(61617端口)上也能消费这条消息。
明白这些基本原理后,在61617对应的activemq上,也做类似的配置,只不过”连接方向”正好相反,参考以下配置:
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://activemq.apache.org/schema/core http://activemq.apache.org/schema/core/activemq-core.xsd">
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<value>file:${activemq.conf}/credentials.propertiesvalue>
property>
bean>
<bean id="logQuery" class="io.fabric8.insight.log.log4j.Log4jLogQuery"
lazy-init="false" scope="singleton"
init-method="start" destroy-method="stop">
bean>
<broker xmlns="http://activemq.apache.org/schema/core" brokerName="activemq-2" dataDirectory="${activemq.data}">
<networkConnectors>
<networkConnector uri="static:(tcp://hadoop1:61616)" duplex="true"/>
networkConnectors>
<destinationPolicy>
<policyMap>
<policyEntries>
<policyEntry topic=">" >
<pendingMessageLimitStrategy>
<constantPendingMessageLimitStrategy limit="1000"/>
pendingMessageLimitStrategy>
policyEntry>
policyEntries>
policyMap>
destinationPolicy>
<managementContext>
<managementContext createConnector="false"/>
managementContext>
<persistenceAdapter>
<kahaDB directory="${activemq.data}/kahadb"/>
persistenceAdapter>
<systemUsage>
<systemUsage>
<memoryUsage>
<memoryUsage percentOfJvmHeap="70" />
memoryUsage>
<storeUsage>
<storeUsage limit="100 gb"/>
storeUsage>
<tempUsage>
<tempUsage limit="50 gb"/>
tempUsage>
systemUsage>
systemUsage>
<transportConnectors>
<transportConnector name="openwire" uri="tcp://0.0.0.0:61617?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="amqp" uri="amqp://0.0.0.0:5672?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="stomp" uri="stomp://0.0.0.0:61613?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="mqtt" uri="mqtt://0.0.0.0:1883?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="ws" uri="ws://0.0.0.0:61614?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
transportConnectors>
<shutdownHooks>
<bean xmlns="http://www.springframework.org/schema/beans" class="org.apache.activemq.hooks.SpringContextHook" />
shutdownHooks>
broker>
<import resource="jetty.xml"/>
beans>
这样,activemq-1与activemq-2这二个broker就互为主备,发给你的消息会同步到我,发给我的消息也会同步到你,实现了HA,示意图如下:
sender
package com.mq.activemq_01.brokercluster1;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.DeliveryMode;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.jms.TextMessage;
import org.apache.activemq.ActiveMQConnectionFactory;
public class Sender {
public static void main(String[] args) throws JMSException, InterruptedException {
//1.建立ConnectionFactory工厂对象,连接地址tcp://localhost:61616,在activemq.xml文件中配置
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(ActiveMQConnectionFactory.DEFAULT_USER,
ActiveMQConnectionFactory.DEFAULT_PASSWORD,
"tcp://192.168.72.133:61616");
// "failover:(tcp://192.168.72.133:61616,tcp://192.168.72.129:61617)?Randomize=false");
//2.通过ConnectionFactory创建Connection连接,并通过start开启连接
Connection connection = connectionFactory.createConnection();
connection.start();
//3.通过Connection创建Session会话,参数1表示是否启动事务,参数2表示签收模式封装在Session中
Session session = connection.createSession(Boolean.FALSE, Session.AUTO_ACKNOWLEDGE);
//4.通过Session创建Destination目的地,参数生产者和消费者的约束名字,消费者需要知道该名字
Destination destination = session.createQueue("queue2");
//5.创建消息发送或者接收对象
MessageProducer messageProducer = session.createProducer(destination);
//6.设置持久化和非持久化对象,这里设置是非持久化
messageProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
//7.通过session创建message,JMS定义了五种正文格式
for (int i = 1; i <= 50000; i++) {
TextMessage textMessage = session.createTextMessage();
textMessage.setText("我是消息内容,id="+i);
messageProducer.send(textMessage);
System.out.println("生产者:生产了"+textMessage.getText());
Thread.sleep(1000);
}
//8.释放连接
if(connection != null){
connection.close();
}
}
}
receiver
package com.mq.activemq_01.brokercluster1;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.DeliveryMode;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.jms.TextMessage;
import org.apache.activemq.ActiveMQConnectionFactory;
public class Receiver {
public static void main(String[] args) throws JMSException, InterruptedException {
//1.建立ConnectionFactory工厂对象,连接地址tcp://localhost:61616,在activemq.xml文件中配置
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(ActiveMQConnectionFactory.DEFAULT_USER,
ActiveMQConnectionFactory.DEFAULT_PASSWORD,
"tcp://192.168.72.129:61617");
// "failover:(tcp://192.168.72.133:61616,tcp://192.168.72.129:61617)?Randomize=false");
//2.通过ConnectionFactory创建Connection连接,并通过start开启连接
Connection connection = connectionFactory.createConnection();
connection.start();
//3.通过Connection创建Session会话,参数1表示是否启动事务,参数2表示签收模式封装在Session中
Session session = connection.createSession(Boolean.FALSE, Session.AUTO_ACKNOWLEDGE);
//4.通过Session创建Destination目的地,参数生产者和消费者的约束名字,消费者需要知道该名字
Destination destination = session.createQueue("queue2");
//5.创建消息发送或者接收对象
MessageConsumer messageConsumer = session.createConsumer(destination);
//6.设置持久化和非持久化对象,这里设置是非持久化
while (true) {
TextMessage receive = (TextMessage)messageConsumer.receive();
if(receive == null){
break;
}
System.out.println("消费者:收到消息"+receive.getText());
Thread.sleep(1000);
}
//7.释放连接
if(connection != null){
connection.close();
}
}
}
从上面的代码中可以看到我这里故意设置了发送者和接收者的通信地址不一致
发送者发送的地址为192.168.72.133:61616
接收者接收的地址为192.168.72.129:61617
执行结果:receiver
消费者:收到消息我是消息内容,id=1
消费者:收到消息我是消息内容,id=2
消费者:收到消息我是消息内容,id=3
消费者:收到消息我是消息内容,id=4
消费者:收到消息我是消息内容,id=5
消费者:收到消息我是消息内容,id=6
消费者:收到消息我是消息内容,id=7
消费者:收到消息我是消息内容,id=8
执行结果:sender
生产者:生产了我是消息内容,id=1
生产者:生产了我是消息内容,id=2
生产者:生产了我是消息内容,id=3
生产者:生产了我是消息内容,id=4
生产者:生产了我是消息内容,id=5
生产者:生产了我是消息内容,id=6
生产者:生产了我是消息内容,id=7
生产者:生产了我是消息内容,id=8
说明两个broker之间已经建立起了通信,同时数据做到了同步。
将上面的代码做微调,使得发送和接收的通信地址都为两个,如下
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(ActiveMQConnectionFactory.DEFAULT_USER,
ActiveMQConnectionFactory.DEFAULT_PASSWORD, "failover:(tcp://192.168.72.133:61616,tcp://192.168.72.129:61617)?Randomize=false");
启动receiver和sender,服务自动连接的是192.168.72.133:61616,执行一段时间后关闭61616服务
sender自动切换到61617服务
receiver也自动切换到61617服务
服务间自动切换,表明实现了高可用的效果
这里的duplex=”true”很重要。
如果将该选项都去掉,不能采用测试1那种单通道的连接,虽然可以同步数据,但是如果出现单点故障问题,应用服务无法使用。需要采用测试2中连接方式,仍然可以实现高可用。
本文参考的文章中说相互通信不能两个都配置duplex=”true”,笔者在测试的过程中将两台服务器都配置了duplex=”true”,并没有出现任何异常。阅读者可以放心的配置。
到此,ActiveMQ基于networkConnector实现的完成。具体一些细节待续..
参考文章:
http://www.cnblogs.com/leihenqianshang/articles/5623841.html