使用Spring/Spring Boot集成JMS的陷阱

609 查看

JmsTemplate性能问题

Spring提供了JmsTemplate用来简化JMS的操作,但是JmsTemplate的性能是有很大问题的,主要体现在以下几个方面:

频繁创建connection,session,producer

根据HornetQ官方文档的说法,JmsTemplate是一种anti-pattern,如果在使用JmsTemplate的情况下觉得很慢,那么就不要怪HornetQ。

并且ConnectionSessionProducerConsumer这些对象本身设计上是可以复用的。因此JmsTemplate的做法会大大降低性能,并且将大部分的时间都花在反复重建这些对象上。

频繁创建临时Queue

JmsTemplate#sendAndReceive方法可以用来模拟RPC调用过程,内部原理是:

  1. A程序创建一个临时Queue作为接受相应的Queue

  2. send一个Messaeg到Queue,并在这个临时Queue上等待结果

  3. B程序consume了这个Message把结果send到那个临时Queue

  4. A接受到结果,把这个临时Queue干掉

当然整个过程中还包括前面提到的创建ConnectionSessionProducerConsumer的动作。

不出所料,HornetQ官方文档又说了,频繁创建临时Queue是一种anti-pattern,会大大影响性能。

@JmsListener性能问题

使用sync方法consume消息

使用@JmsListener注解可以很方便的用来consume消息,但是它的性能非常低下,因为它是synchronous得consume消息,而不是asynchronous得consume消息,这个和官方文档所说的恰恰相反(关于sync/async consume消息的资料)。

Spring Boot的JmsAnnotationDrivenConfiguration默认使用DefaultJmsListenerContainerFactory生成DefaultMessageListenerContainer ,而它的内部原理是使用TaskExecutor发起多个线程同时从Queue中拉取消息,这也就是为什么Spring官方文档里说如果监听的是Topicconcurrency > 1,那么可能会收到重复消息的原因。

DefaultMessageListenerContainer的javadoc中说道:

Actual MessageListener execution happens in asynchronous work units which are created through Spring's TaskExecutor abstraction

这里就有个矛盾,如果使用async的方式consume消息,那么只需给consumer设置MessageListener就行了,何必使用TaskExecutor呢?

一看代码果然不出所料:

  1. DefaultMessageListenerContainer#runcallinvokeListener

  2. 然后callAbstractPollingMessageListenerContainer#receiveAndExecute

  3. 然后calldoReceiveAndExecute

  4. 然后callreceiveMessage

  5. 然后callMessageConsumer#consume

也就是说Spring只是使用一个线程在不停的sync的consumer消息而已。

虽然可以使用concurrency参数提高并发,但是多线程从Queue/Topic中consume消息的性能比javax.jms.MessageConsumer#setMessageListener的方法要低上很多。

有兴趣的同学可以使用HornetQ Example(下载地址)里的JMS Perf代码做一下测试,它的测试代码用的是javax.jms.MessageConsumer#setMessageListener的方式来consume消息的,在配置正确的情况下可以达到接近10w/s。至于使用@JmsListener的性能测试则留给同学自己做了。

为@JmsListener创建的session默认transacted=true

还是之前提到的JmsAnnotationDrivenConfiguration,使用的DefaultJmsListenerContainerFactoryConfigurer默认是把session设置为transacted的。

根据测算,当一个session是transacted的时候其性能会相差20%,有兴趣的同学可以使用HornetQ Example(下载地址)里的JMS Perf代码做一下测试。

下载之后找到examples/jms/perf目录,看这个目录下的readme.html获得执行方法,在执行之前修改src/main/resources/perf.properties文件,下面是部分参数的解释:

  1. num-messages,测试多少个消息

  2. num-warmup-messages,热身用的消息数,热身过之后性能会提升

  3. durable,对应DeliveryMode

  4. transacted,是否transacted

  5. batch-size,批量commit的大小

  6. drain-queue,是否测试前先把队列里已有的消息都清空

可以使用以下两套配置对比transacted的性能差别:

配置一,transacted=false:

num-messages=100000
num-warmup-messages=1000
message-size=1024
durable=false
transacted=false
batch-size=1
drain-queue=true
destination-lookup=perfQueue
connection-factory-lookup=/ConnectionFactory
throttle-rate=-1
dups-ok-acknowledge=false
disable-message-id=true
disable-message-timestamp=true

配置二,transacted=true:

num-messages=100000
num-warmup-messages=1000
message-size=1024
durable=false
transacted=true
batch-size=1
drain-queue=true
destination-lookup=perfQueue
connection-factory-lookup=/ConnectionFactory
throttle-rate=-1
dups-ok-acknowledge=false
disable-message-id=true
disable-message-timestamp=true

@JmsListener创建的session默认加入了事务控制

关于加入事务控制是否会有性能问题没有实际测试过,不过值得注意的这是Spring Boot的默认行为。

相关代码:代码1, 代码2代码3Javadoc

Spring Boot配置

ConnectionFactory全局只有一个实例

Spring将JMS的集成变得非常简单,只需提供几个配置参数就可以了,但是只能提供一种默认配置,不管业务场景如何都使用同一种配置是非常有问题的。

spring-boot提供了以下几种方式(文档)集成JMS:

  1. JNDI

  2. HornetQ, native模式和embedded模式

  3. ActiveMQ

我只用过HornetQ,因此就只讲讲前两种模式的缺点:

  1. JNDI方式,根据HornetQ的官方文档,HornetQ里可以配置多个不一样的ConnectionFactory绑定到JNDI,各个ConnectionFactory的差别可以是性能的差别,也可能是是否支持XA的差别,总之可以根据不同场景配置不一样的ConnectionFactory。但是Spring的只能配置使用某一种,这也就意味着整个Spring application只能使用一种ConnectionFactory,而不能根据不同场景使用不同的ConnectionFactory

  2. HornetQ方式,和JNDI的方式一样,整个Spring application只能使用一种,而且提供的参数过少,HornetQ本身的ConnectionFactory有更为丰富的的参数可以使用。

JmsListener的默认配置也就只提供一种

相关代码见DefaultJmsListenerContainerFactoryConfigurer
JmsAnnotationDrivenConfiguration就明白了。