在生产环境中,发现我们Tomcat的应用导致超高的CPU(170%)及负载(90+)[Intel® Xeon® CPU E31230 @ 3.20GHz 四核心,8线程].

原因

经过排查发现,有些线程,占用超高的CPU:

SimpleAsyncTaskExecutor-N,平均每个线程,上下文切换的次数都超过4W+,系统CPU占用率一直在60+。

查找原因途径

查看上下文切换情况:

pidstat -t -w -p PID 1

查看CPU情况:

pidstat -t -u -p PID 1

查看内存情况:

pidstat -t -r -p PID 1

查看IO情况:

pidstat -t -d -p PID 1

找到对应的线程PID后,然后

jstatck -l Java主进程PID > /tmp/Java主进程pid.log

导出线程栈情况,然后根据上面的pidstat工具查找对应占用资源情况的线程PID,与导出的Java线程情况的文件里/tmp/Java主进程pid.log查找与之对应的线程的详细情况. 注意:pidstat得出的线程PID是十进制的,而jstatck导出的线程PID是十六进制的,这要转换成十六进制后,再在导出的文件/tmp/Java主进程pid.log里查找.

可以使用以下shell命令来进行转换

转换的方法:

➜  Desktop  cat hello.txt 
03:25:21 HKT         -      8765   2902.00     26.00  |__java
03:25:21 HKT         -      8767   3049.00     20.00  |__java
03:25:21 HKT         -      8852   2896.00     41.00  |__java
03:25:21 HKT         -      8853   3056.00     36.00  |__java
03:25:21 HKT         -      8854   2841.00     27.00  |__java
03:25:21 HKT         -      8855   2738.00     29.00  |__java
03:25:21 HKT         -      8871   2823.00    124.00  |__java
03:25:21 HKT         -      8872   2865.00      9.00  |__java
03:25:21 HKT         -      8875   2905.00     24.00  |__java
03:25:21 HKT         -      8876   3090.00     27.00  |__java

➜  Desktop  cat hello.txt | awk '{print $4}' | xargs -I {} printf "{}=%x\n" {} 
8765=223d
8767=223f
8852=2294
8853=2295
8854=2296
8855=2297
8871=22a7
8872=22a8
8875=22ab
8876=22ac
➜  Desktop  

这时,我发现,是由10多条以SimpleAsynctaskExecutor开头的线程占用了超高的CPU。

找出对应的线程后,再根据文件里/tmp/Java主进程pid.log对应线程的调用栈,类似如下内容:

"SimpleAsyncTaskExecutor-1" prio=10 tid=0x00007fe354e56000 nid=0x5440 runnable [0x00007fe359acd000]
   java.lang.Thread.State: TIMED_WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for  <0x00000007a1d79ed0> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
	at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:226)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2082)
	at java.util.concurrent.LinkedBlockingQueue.poll(LinkedBlockingQueue.java:467)
	at org.springframework.amqp.rabbit.listener.BlockingQueueConsumer.nextMessage(BlockingQueueConsumer.java:188)
	at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.doReceiveAndExecute(SimpleMessageListenerContainer.java:466)
	at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.receiveAndExecute(SimpleMessageListenerContainer.java:455)
	at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.access$300(SimpleMessageListenerContainer.java:58)
	at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$AsyncMessageProcessingConsumer.run(SimpleMessageListenerContainer.java:548)
	at java.lang.Thread.run(Thread.java:722)

   Locked ownable synchronizers:
	- None

这时,我们查看类源码org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer,发现到一个问题:

private volatile Executor taskExecutor = new SimpleAsyncTaskExecutor();

这一行代码,暴露出了它使用了SimpleAsyncTaskExecutor来执行我们的任务.

SimpleAsyncTaskExecutor 说明

通过Spring官方文档SimpleAsynctaskExecutor可知: 1. 每执行一个task,都会创建一个新的线程来执行任务 2. 默认情况下,创建的线程数量是无限的(可以通过 concurrencyLimit 属性来限制) 3. 这个实现,并不会重用任何线程的!

项目自身的原因

由于我们使用了Spring Rabbit,配置如下:

<rabbit:listener-container connection-factory="rabbitConnectionFactory" error-handler="MessageErrorHandler">
</rabbit:listener-container>

这样子,默认的情况下,就是使用了private volatile Executor taskExecutor = new SimpleAsyncTaskExecutor();

我们改为

    <task:annotation-driven executor="myExecutor" scheduler="myScheduler"/>
	<task:executor id="myExecutor" pool-size="10-20" />
	<task:scheduler id="myScheduler" pool-size="5" />
    <rabbit:listener-container connection-factory="rabbitConnectionFactory" error-handler="MessageErrorHandler" concurrency="10" task-executor="myExecutor">
    </rabbit:listener-container>

这样子定义<task:executor>后,就会Spring就会创建一个ThreadPoolTaskExecutor,包含有线程池了.

Spring Task

*注意,使用Spring Task也要留意这个问题,都要记得指定executor*,不然Spring又创建了SimpleAsyncTaskExecutor这种线程执行器。不过,SimpleAsyncTaskExecutor比较适合于那种临时性,执行时间非常短的任务。不过,还是线程池使用的比较安全点.

性能

如果担心创建的线程池太多占用资源,可以使用pool-size="10-20"这种范围式声明线程池的大小,有个动态范围.

技巧

要多点看看Spring所在版本的 XSD 文件里的说明,那里有非常详细的说明文档,这样子在对应的版本里,就会有对应的executor行为了

参考资料

  1. http://www.baeldung.com/spring-async

  2. http://stackoverflow.com/questions/13401558/spring-simpleasynctaskexecutor-and-threadpooltaskexecutor-with-async-annotation

  3. http://blog.csdn.net/yangjun2/article/details/8363750

  4. http://www.7-sun.com/doc/spring2.5_doc_cn/org/springframework/scheduling/backportconcurrent/ThreadPoolTaskExecutor.html

  5. http://my.oschina.net/never/blog/140368?fromerr=PEzwhJbo