一个DefaultMessageListenerContainer可以开启多个 (concurrent)AsyncMessageListenerInvoker并发 收消息
两种模式:
- 模式一:递增监听线程并调度,监听线程轮询监听消息
- 模式二:动态调度监听线程(递增/递减),有限轮询+重新触发调度
1. 模式一
递增监听线程并调度,监听线程轮询监听消息。这种模式初始化为concurrent个AsyncMessageListenerInvoker线程然后调度执行,并在接受到消息后适度新增监听线程,直到达到最大值maxConcurrentConsumers。这种模式在高峰期过后线程数量不会主动下降,会一直保持高峰值运行。
配置
- maxMessagesPerTask < 0
- concurrency=2-10
- receiveTimeout=1000*60 (consumer.receive(timeout), 默认值是1秒,如果设置不超时设置为负数即可)
2. 模式二
动态调度监听线程(递增/递减),有限轮询+重新触发调度。这种模式初始化为concurrent个AsyncMessageListenerInvoker线程然后调度执行,并在接受到消息后适度新增监听线程,直到达到最大值maxConcurrentConsumers。这种模式在高峰期过后会主动下降到concurrent数量个线程。
配置
- maxMessagesPerTask > 0
- concurrency=2-10
- receiveTimeout=1000*60 (consumer.receive(timeout), 默认值是1秒,如果设置不超时设置为负数即可)
每个AsyncMessageListenerInvoker在执行了maxMessagesPerTask轮(不区分有没有收到消息)后就会结束本线程,然后交给container确定是否继续调度本线程。
如果使用xml(jms:listener-container)配置, prefetch就是maxMessagesPerTask。如果某个参数没起作用可以看看spring-jmx.jar (org.springframework.jms.config.AbstractListenerContainerParser)源码。
3. 缓存相关配置
缓存(cacheLevel)级别:
- NONE=0
- CONNECTION=1
- SESSION=2
- CONSUMER=3
- AUTO=4
3.1 NONE 不启用缓存
通过分析AsyncMessageListenerInvoker#run源码发现这种模式每次轮询收消息会从连接池获取连接并创建session和consumer, 即1个连接–1个session–1个consumer。每个container对应[0, concurrent]个connnection/session/consumer,每次轮询都会创建/连接池获取和关闭/回收。
3.2 CONNECTION 缓存连接
一个container永久(可以这么说)持有一个connection, 该container下的所有AsyncMessageListenerInvoker共享这同一个连接。每次轮询都会动态创建和关闭session/consumer。
连接异常处理:
假设在轮询的过程中connection中断了,也就是不能正常使用了,怎么办?如果缓存级别是NONE,连接异常了只会影响本次轮询,下次轮询会重新冲连接池获取,连接池来处理异常的连接。看看AsyncMessageListenerInvoker#run的catch部分,有个recoverAfterListenerSetupFailure()#refreshConnectionUntilSuccessful(), 即如果中途异常了会进入恢复模式,只要container不关闭就会一直重试连接服务器,直到连接成功。
3.3 SESSION 缓存session
一个container永久(可以这么说)持有一个connection, 该container下的所有AsyncMessageListenerInvoker共享这同一个连接。每个AsyncMessageListenerInvoker执行期内各自保持一个session,创建后不关闭。
每次轮询都会动态创建和关闭consumer。
3.4 CONSUMER 缓存消费者
一个container永久(可以这么说)持有一个connection, 该container下的所有AsyncMessageListenerInvoker共享这同一个连接。每个AsyncMessageListenerInvoker执行期内各自保持一个session和一个consumer,创建后不关闭。
3.5 AUTO 自动选择
进过查看initialize()代码,如果有事务管理器,则cacheLevel=NONE, 否则cacheLevel=CACHE_CONSUMER
上面的分析相关代码可以看看这里
private void initResourcesIfNecessary() throws JMSException {
if (getCacheLevel() <= CACHE_CONNECTION) {
updateRecoveryMarker();
}
else {
if (this.session == null && getCacheLevel() >= CACHE_SESSION) {
updateRecoveryMarker();
this.session = createSession(getSharedConnection());
}
if (this.consumer == null && getCacheLevel() >= CACHE_CONSUMER) {
this.consumer = createListenerConsumer(this.session);
synchronized (lifecycleMonitor) {
registeredWithDestination++;
}
}
}
}
综上如果有事务管理 + 连接池建议设置为NONE, 否则适当设置缓存级别。
4. 连接数相关分析、计算连接池最大值
1个container, Invoker数量是concurrent个
缓存级别 | connection | session | consumer |
---|---|---|---|
NONE | [0,concurrent]个,每次获取和回收 | [0,concurrent]个,每次创建和关闭 | [0,concurrent]个,每次创建和关闭 |
CONNECTION | 共享1个,不回收 | [0,concurrent]个,每次创建和关闭 | [0,concurrent]个,每次创建和关闭 |
SESSION | 共享1个,不回收 | 缓存concurrent个 | [0,concurrent]个,每次创建和关闭 |
CONSUMER | 共享1个,不回收 | 缓存concurrent个 | 缓存concurrent个 |
假设本实例要消费 m 个队列,即开启了 m 个DefaultMessageListenerContainer,每个container开启的invoker数量是[concurrent,maxConcurrent], 则最多需要连接数量是: m * maxConcurrent。另外可能还需要其他的连接,所及建议设置的比这个值要大一些。
如:
<jms:listener-container destination-type="queue" container-type="default"
connection-factory="jmsFactory" transaction-manager="jmsTransactionManager"
receive-timeout="60000" cache="none" acknowledge="transacted"
concurrency="${mq.concurrency}" prefetch="20">
<jms:listener destination="${app.name}-SEND_SMS" id="MSTContainer_SEND_SMS" ref="activeMQ" />
<jms:listener destination="${app.name}-LOGIN" ref="activeMQ" />
<jms:listener destination="${app.name}-REGISTER" ref="activeMQ" />
<jms:listener destination="${app.name}-SEND_VOICE_VERIFY_CODE" id="MSTContainer_SEND_VOICE_VERIFY_CODE" ref="activeMQ" />
<jms:listener destination="${app.name}-WEICHAT_TEMPLATE_MESSAGE" id="MSTContainer_WEICHAT_TEMPLATE_MESSAGE" ref="activeMQ" />
</jms:listener-container>
5. 其他配置
- ActiveMQConnectionFactory.maxThreadPoolSize 这个值会设置到connnection中,即connnection中sessionTaskRunner线程池的最大值,如果是在cacheLeve>=CONNECTION, 该值设置为与maxConcurrent相同即可,否则设置为1。
- ActiveMQConnectionFactory.prefetchPolicy 这个是设置connnection一次行最多取多少个消息到本地,如果开启了事务建议1或者比较小的值,如果没有开启事务建议设置的大点,提高性能。
- DefaultMessageListnerContainer.taskExecutor 默认使用的是SimpleAsyncTaskExecutor,这个执行器默认是New Thread, 如果maxConcurrent总量比较大,那在高峰阶段估计会因为创建线程而卡死,应该使用线程池,如org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor。
- 使用 Advisory Message 监听连接的创建和销毁,队列的生产者和消费者连接和断开等,综合分析消息队列和消费者/订阅者健康程度。
- Broker如果使用数据库,最好缓存连接池连接,并将开启nio
如果使用tcp连接要慎重设置 ActiveMQConnectionFactory.maxThreadPoolSize(connection的session线程池最大值) 和 PooledConnectionFactory.maxConnections, 假设
- maxThreadPoolSize=10, 即:每个connection最多同时运行10个session
- maxConnections=100, 即:最多100个connection(一个connection对应一个TcpTransport,每个TcpTransport是一个线程)
那么在满载的情况下线程峰值至少是 10 * 100 = 1000 , 另外 maxThreadPoolSize 最好设置的与 DefaultMessageListenerContainer.maxConcurrency 相同
TransportThreadSupport.doStart() 会开启线程
TcpTransport extends TransportThreadSupport
protected void doStart() throws Exception {
connect();
stoppedLatch.set(new CountDownLatch(1));
super.doStart();
}
TransportThreadSupport
protected void doStart() throws Exception {
runner = new Thread(null, this, "ActiveMQ Transport: " + toString(), stackSize);
runner.setDaemon(daemon);
runner.start();
}
NIOTransport extends TcpTransport
protected void doStart() throws Exception {
connect();
selection.setInterestOps(SelectionKey.OP_READ);
selection.enable();
}
建议连接池设置:
* 生产者(TCP): 消费队列数量 * 2
* 消费者(NIO): (消费队列数量 + 订阅主题数量) * maxConcurrency
最后调到下面的参数已经满足日常的高峰处理了:
NIO
消费3个队列,每个队列5000消息量
mq.concurrency = 2-10
mq.maxThreadPoolSize=5
mq.maxConnections=60