前言
从本文中你可以了解到如下内容:
1) consumer端消息消费的模型,session的运作机制
2) 如果提升broker和consumer端消息消费的速率
3) selector,group,exclusive对消息消费的影响
4) 如何让Priority更好的运行,提高消息的顺序性
5) Slow Consumer的产生原因,以及如何调优。
Consumer作为ActiveMQ的消费端,开起来简单,不过还有很多隐藏的特性,值得我们去探索和调优。
如下为典型的Consumer端代码示例
String brokerUrl = "tcp://localhost:61616?"
String queueName = "test-queue";
ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(brokerUrl);
ActiveMQConnection connection = (ActiveMQConnection)factory.createConnection();
final Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Destination queue = session.createQueue(queueName);
MessageConsumer consumer = session.createConsumer(queue);
consumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
try{
ActiveMQMessage m = (ActiveMQMessage)message;
System.out.println(m.toString());
}catch(Exception e){
//
e.printStackTrace();
}
}
});
connection.start();
//connection.close();
Consumer端涉及到众多的特性,其中包括:消息转发机制,session机制与多线程,消息ACK策略等,我们无法详解所有的原理,如下为Consumer端消息转发机制,仅供参考,[详细请参考]:
上图非常复杂,具体的原理,我们将会在下文中逐个解释。大概原理与过程:
- 通过Connection实例创建Session之后,将会把session实例保存在本地的list中,即connection持有session列表,并在底层开启transport,侦听数据。
- Session创建Consumer之后,将会把Consumer实例添加到本地的list中,以便此后分拣消息,即session持有consumer列表;此外connection中也持有一个consumer集合(Map),其中Key为consumerId,value为session引用。
- 如果Session支持异步转发(asyncDispatch)或者使用了转发池(dispatchPool),将创建线程池用来转发消息。
- 当broker端有消息通过transport发送时,connection将会分拣消息,根据消息中指定的consumerId,从本地session列表中获取其对应的session实例;然后将消息交付session负责转发。
- 当session接受到消息后,会根据消息的consumerId,在本地consumer列表中找到对应的consumer实例;检测session的消息转发方式,如果是同步转发,则直接将消息交给consumer,即调用consumer.dispatch(message),此方法要么调用messageListener.onMessage(),要么将消息添加到本地的unconsumedMessages队列中(唤醒receive);如果是异步转发,则将消息添加到session级别的队列中并由线程池负责转发。
- 当consumer接受到消息之后,将会调用messageListener.onMessage方法或者从receive方法中返回。
- 当消息消费成功后,consumer将会根据指定的ACK_MODE负责向broker发送ACK指令,此后消息将会在broker端清除。
(broker运行期间,当Consumer加入时,broker将会根据Consumer消费的Destination和相关配置信息,创建Queue或者Topic实例(Broker端,如果已经创建,则直接将Conusmer注册在Queue的消费者列表中),每个Destination实例都会引用Store实例,且Queue和Topic本身是一个Task,将会被运行在Broker的线程池中;这意味着Queue和Topic将可以根据Consumers的能力,并发的从Store中获取属于自己的消息列表,并转发给Consumers;消息从Store中Pagin到Queue内部的buffer列表中,并逐个遍历Consumers将消息转发;如果Consumers为异步转发,则添加到Consumer内部的buffer中,并通过Consumer内部的线程从buffer中移除,通过底层Transport发送)
1. asyncDispatch
broker端是否允许使用“异步转发”。broker端处理connection、session、consumer的模型和Client端几乎一样;当通道中(Queue/Topic)有消息需要转发给consumer时,将会调用相应的Subscription的dispatch方法(在broker端负责与Consumer通讯的服务,称为Subscription,每个Consumer客户端对应broker端一个Subscription实例);如果asyncDispatch为false,那么将会阻塞转发线程(dispatchThread),直到底层的Transport将消息发送到Consumer客户端(如果底层transport正在发送消息,意味着阻塞);如果为true,则会将消息添加到transport本地的队列中(负责与Consumer客户端通信的每个transport都有一个本地的queue和线程池),并启动线程池负责发送消息,那么转发线程即可立即返回。(每个Consumer一个异步转发线程)
(参见代码:QueueStoreCursor/TopicStoreCursor --> broker端Queue/Topic(核心类)--->QueuePrefetchSubscription,QueueRegion,AbstractRegion,RoundRobinDispatchPolicy/StrictOrderDispatchPolicy)
由此可见,当前通道中所有的consumer都在brorker端开启了“异步转发”,可以大大提升broker转发消息的性能,此值默认为true。
//在brokerUrl中设置
tcp://localhost:61616?jms.dispatchAsync=true
//在destinationUri中为当前通道设定
orderQueue?consumer.dispatchAsync=true
broker“异步转发”采用的是线程池模式,线程池的缺点即使当任务单元运行时间足够短时,线程切换的成本将变得很重要,这也意味着,如果通道中(Queue/Topic)中的consumer客户端接收和消费消息的速度很快,我们可以关闭异步转发来避免线程切换。如果consumer通常为慢速(Slower Consumer),且关闭了异步转发,那就意味着每次主线程转发消息时,都需要阻塞较长的时间,这极大的影响了整体的通讯效率。无论如何,我们都没有理由关闭异步转发,异步转发应该是最佳选择。
备注:broker端Queue/Topic(例如:org.apache.activemq.broker.region.Queue)实例,负责从底层store中获取消息,并使用线程池模式转发消息,dispatchPolicy负责将消息转发给相应的subscription,当Consumer客户端创建时,会向Broker发送一个指令,这个指令中包括consumerId(参看ConsumerInfo),那么此后Region(在逻辑上相当于客户端的Session,但每个通道只有一个实例)将会为每个ConsumerId创建相应Subscription实例(持有底层Transport,buffer队列、异步发送的线程),那么此后Consumer与Broker的数据交互,就有Subscription负责。每个Region都维护了一个Subscription列表,用于dispatchPolicy转发消息。这个设计模型,和Consumer客户端 + Session非常相似。
2. alwaysSessionAsync
客户端session是否使用异步转发,它和(1)中的参数没有任何关系,当底层Transport接收到Broker发送的消息后,会交付给session,那么session是否采用异步的方式将消息传递给Consumer!此值默认为true。
如果为false,表示使用同步。当consumer使用receive()获取消息时,那么session将会把消息添加到consumer的本地queue,然后唤醒receive等待;当consumer使用messageListener异步侦听消息时,将会调用其onMessage()方法直到方法执行完毕,然后返回。
如果为true,表示使用异步。session接受的消息,将会首先放入session buffer中(队列),那么此后的线程池将会负责移除buffer中的消息,并转发给相应的consumer。
当session中有多个consumer时,或者Transport中消息量比较密集,异步方式是最佳的。如果session中只有一个consumer,或者transport中消息量很少,使用异步并不能明显的提升性能。
//在brokerUrl中设置
tcp://localhost:61616?jms.alwaysSessionAsync=true
3. maxThreadPoolSize
在alwaysSessionAsync为true的情况下,会引入一个线程池,那么这个线程池中线程的个数,默认为Integer.MAX_VALUE,不过我们首先需要清楚,这个线程池是Connection内所有的session共享的。你可以指定合适的值来决定线程池的大小。
//在brokerUrl中设置
tcp://localhost:61616?jms.maxThreadPoolSize=64
4. useDedicatedTaskRunner
当前connetion下的每个session是否使用单独的线程。在上述介绍中,我知道了异步转发是使用了线程池的,而且这个线程池是当前Connection下所有的session共享,尽管这不会带来任何问题,可是有时候我们可能期望在threadLocal或者基于当前线程绑定一些特殊的数据,那么我们就可以使用useDedicatedTaskRunner=true;这就意味着,每个session都将创建一个单独的线程,整个生命周期中都使用它来转发消息。
当每个session只有一个Consumer,或者有多个consumer但它们的处理业务类似(比如selector具有一定逻辑关系),或者这些consumer消费速度都很快,或者我们期望在Consumer中使用类似于Thread.sleep(long)这样的方式来阻塞session转发消息时,这个参数可以帮助我们。
//brokerUrl中,默认为false
tcp://localhost:61616?jms.useDedicatedTaskRunner=true
5. redeliveryPolicy
consumer使用的重发策略,当消息在client端处理失败(比如onMessage方法抛出异常,事务回滚等),将会触发消息重发。对于Brok