应公司需求我们对一个项目进行了线上压力测试,结果发现,三台服务器一共只有59TPS,结果惨不忍睹。
那么针对这样的场景,我们利用一周时间进行专注性的优化,寻找性能的瓶颈点。
第一步:我们针对线上的环境进行模拟,尽量真实的在测试环境中再现,采用数据库连接池为咱们默认的C3P0。
那么当压测到二万批,100个用户同时访问的时候,并发量突然降为零!报错如下:
- Could not get JDBC Connection; nested exception is java.sql.SQLException: An attempt by a client to checkout a Connection has timed out.
针对以上错误跟踪C3P0源码,以及在网上搜索资料(http://blog.sina.com.cn/s/blog_53923f940100g6as.html)发现,C3P0在大并发下表现的性能不佳。
第二步:针对这个问题进行数据库连接池优化,更换了BoneCPDataSource,以及Apache BasicDataSource后,发现报错如下:
- java.lang.OutOfMemoryError: unable to create new native thread, dubbo version: 2.5.4, current host: 192.168.122.1
- java.lang.OutOfMemoryError: unable to create new native thread
第三步:由此可以判断,问题不在于连接池的问题,于是在压测的时候,将DUMP日志导出进行分析发现,项目中启动了一万多个线程,而且每个线程都极为忙碌,彻底将资源耗尽。
于是迅速定位到代码,发现如下代码:
- private static final ExecutorService executorService = Executors.newCachedThreadPool();
- /**
- * 异步执行短频快的任务
- * @param task
- */
- public static void asynShortTask(Runnable task){
- executorService.submit(task);
- //task.run();
- }
- CommonUtils.asynShortTask(new Runnable() {
- @Override
- public void run() {
- String sms = sr.getSmsContent();
- sms = sms.replaceAll(finalCode, AES.encryptToBase64(finalCode, ConstantUtils.getDB_AES_KEY()));
- sr.setSmsContent(sms);
- smsManageService.addSmsRecord(sr);
- }
- });
那么问题到底在哪里呢???就在这一行!
在并发的情况下,无限制的申请线程资源造成性能严重下降,在图表中显抛物线形状的元凶就是它!!!那么采用这种方式最大可以产生多少个线程呢??答案是:Integer的最大值!看如下源码:
- public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
- return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
- 60L, TimeUnit.SECONDS,
- new SynchronousQueue<Runnable>(),
- threadFactory);
- }
那么尝试修改成如下代码:
修改完成以后,并发量重新上升到100以上TPS,但是当并发量非常大的时候,项目GC(垃圾回收能力下降),分析原因还是因为 Executors.newFixedThreadPool(50)这一行,虽然解决了产生无限线程的问题,但是
当并发量非常大的时候,采用newFixedThreadPool这种方式,会造成大量对象堆积到队列中无法及时消费,看源码如下:
- public static ExecutorService newFixedThreadPool(int nThreads) {
- return new ThreadPoolExecutor(nThreads, nThreads,
- 0L, TimeUnit.MILLISECONDS,
- new LinkedBlockingQueue<Runnable>());
- }
可以看到采用的是无界队列,也就是说队列是可以无限的存放可执行的线程,造成大量对象无法释放和回收。
结论:
目前我们的项目还在持续优化中,还没有最终优化完成,目标是要把项目优化完善,但此次事件,再次提醒我们,在使用线程池的时候,一定要把握其细节,深入了解其原理再使用,不要随意使用,任何线程池的使用方式都有不同的使用场景,并不是只要使用了线程池就万事大吉,还有很多工作需要我们去注意。
********************************学习笔记开始********************************
看完了还是很有感触的,因为我就是这么使用线程池的,只不过业务关系没有那么大量,触发不了文章的场景。
背景:
线程池主要是解决复用跟流控(防止系统资源耗尽)
参数:
如果是使用简单实现如下。
ExecutorService executorService = Executors.newFixedThreadPool(50);
当然还可以根据自己业务需求通过ThreadPoolExecutor来创建一个线程池,先把线程池的参数列一下:
- corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
- runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列。
- ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
- LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
- SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
- PriorityBlockingQueue:一个具有优先级得无限阻塞队列。
- maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。
- ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字,Debug和定位问题时非常又帮助。
RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK1.5提供的四种策略。n AbortPolicy:直接抛出异常。
- CallerRunsPolicy:只用调用者所在线程来运行任务。
- DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
- DiscardPolicy:不处理,丢弃掉。
- 当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。
- keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
- TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
demo代码,:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ThreadPoolExecutor.AbortPolicy;
public class ThreadPoolTest {
public static void main(String[] args) throws InterruptedException {
// TODO Auto-generated method stub
ArrayBlockingQueue queue = new ArrayBlockingQueue(2);
ThreadFactory threadFactory = Executors.defaultThreadFactory();
RejectedExecutionHandler rej = new RejectedExecutionHandlerImpl();
//create ThreadPoolExecutor
ThreadPoolExecutor executorPool = new ThreadPoolExecutor(2, 4, 10, TimeUnit.MILLISECONDS,
queue, threadFactory, rej);
//monitor
MyMonitorThread monitor = new MyMonitorThread(executorPool, 3);
monitor.start();
//execute no return
for(int i=0; i<10; i++){
executorPool.execute(new WorkerThread("cmd"+i));
}
Thread.sleep(10000);
//shut down the pool
executorPool.shutdown();
//shut down the monitor thread
Thread.sleep(5000);
monitor.shutdown();
}
}
线程池处理任务的优先级为:
核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。我们初始线程池大小设为2、最大值设为4、工作队列大小设为2。所以,如果当前有4个任务正在运行而此时又有新任务提交,工作队列将只存储2个任务和其他任务将交由RejectedExecutionHandlerImpl (demo中只是打印信息)处理。
运行结果如下:
***********************总结*********************
如何配置线程池,需要根据任务角度去考虑,CPU密集型,还是IO密集型,是否根据优先级使用PriorityBlockingQueue等等。还有一点,就是考虑业务的波峰、波谷去调整最新,最大,保存时间等参数。通常还是推荐使用有界阻塞队列。
对于上文程老师的这个场景,异步执行短频快的需求,代码就是下发短信。单个节点已经不能满足。能够采用MQ这种方式进行解耦处理,即把任务推送到mq中。有多个节点并发的去获取后处理。毕竟每个 节点的资源有限。这样根据压测设置合理线程池大小,解决了单个节点资源不足的情况。
参考:
http://www.importnew.com/8542.html
http://ifeve.com/java-threadpool/