Spring里的Executor使用

  • Post author:
  • Post category:其他



在生产环境中,发现我们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行为了


转载于:https://my.oschina.net/u/3070368/blog/818718