一个DefaultMessageListenerContainer可以开启多个 (concurrent)AsyncMessageListenerInvoker并发 收消息
两种模式:
递增监听线程并调度,监听线程轮询监听消息。这种模式初始化为concurrent个AsyncMessageListenerInvoker线程然后调度执行,并在接受到消息后适度新增监听线程,直到达到最大值maxConcurrentConsumers。这种模式在高峰期过后线程数量不会主动下降,会一直保持高峰值运行。
动态调度监听线程(递增/递减),有限轮询+重新触发调度。这种模式初始化为concurrent个AsyncMessageListenerInvoker线程然后调度执行,并在接受到消息后适度新增监听线程,直到达到最大值maxConcurrentConsumers。这种模式在高峰期过后会主动下降到concurrent数量个线程。
每个AsyncMessageListenerInvoker在执行了maxMessagesPerTask轮(不区分有没有收到消息)后就会结束本线程,然后交给container确定是否继续调度本线程。
如果使用xml(jms:listener-container)配置, prefetch就是maxMessagesPerTask。如果某个参数没起作用可以看看spring-jmx.jar (org.springframework.jms.config.AbstractListenerContainerParser)源码。
缓存(cacheLevel)级别:
通过分析AsyncMessageListenerInvoker#run源码发现这种模式每次轮询收消息会从连接池获取连接并创建session和consumer, 即1个连接–1个session–1个consumer。每个container对应[0, concurrent]个connnection/session/consumer,每次轮询都会创建/连接池获取和关闭/回收。
一个container永久(可以这么说)持有一个connection, 该container下的所有AsyncMessageListenerInvoker共享这同一个连接。每次轮询都会动态创建和关闭session/consumer。
连接异常处理:
假设在轮询的过程中connection中断了,也就是不能正常使用了,怎么办?如果缓存级别是NONE,连接异常了只会影响本次轮询,下次轮询会重新冲连接池获取,连接池来处理异常的连接。看看AsyncMessageListenerInvoker#run的catch部分,有个recoverAfterListenerSetupFailure()#refreshConnectionUntilSuccessful(), 即如果中途异常了会进入恢复模式,只要container不关闭就会一直重试连接服务器,直到连接成功。
一个container永久(可以这么说)持有一个connection, 该container下的所有AsyncMessageListenerInvoker共享这同一个连接。每个AsyncMessageListenerInvoker执行期内各自保持一个session,创建后不关闭。
每次轮询都会动态创建和关闭consumer。
一个container永久(可以这么说)持有一个connection, 该container下的所有AsyncMessageListenerInvoker共享这同一个连接。每个AsyncMessageListenerInvoker执行期内各自保持一个session和一个consumer,创建后不关闭。
进过查看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, 否则适当设置缓存级别。
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。另外可能还需要其他的连接,所及建议设置的比这个值要大一些。
如:
"queue" container-type="default"
connection-factory="jmsFactory" transaction-manager="jmsTransactionManager"
receive-timeout="60000" cache="none" acknowledge="transacted"
concurrency="${mq.concurrency}" prefetch="20">
"${app.name}-SEND_SMS" id="MSTContainer_SEND_SMS" ref="activeMQ" />
"${app.name}-LOGIN" ref="activeMQ" />
"${app.name}-REGISTER" ref="activeMQ" />
"${app.name}-SEND_VOICE_VERIFY_CODE" id="MSTContainer_SEND_VOICE_VERIFY_CODE" ref="activeMQ" />
"${app.name}-WEICHAT_TEMPLATE_MESSAGE" id="MSTContainer_WEICHAT_TEMPLATE_MESSAGE" ref="activeMQ" />
</jms:listener-container>
如果使用tcp连接要慎重设置 ActiveMQConnectionFactory.maxThreadPoolSize(connection的session线程池最大值) 和 PooledConnectionFactory.maxConnections, 假设
那么在满载的情况下线程峰值至少是 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