图17 动态化线程池整体设计 3.3.2 功能架构
动态化线程池提供如下功能:
-
动态调参:支持线程池参数动态调整、界面化操作;包括修改线程池核心大小、最大核心大小、队列长度等;参数修改后及时生效。
-
任务监控:支持应用粒度、线程池粒度、任务粒度的Transaction监控;可以看到线程池的任务执行情况、最大任务执行时间、平均任务执行时间、95/99线等。
-
负载告警:线程池队列任务积压到一定值的时候会通过大象(美团内部通讯工具)告知应用开发负责人;当线程池负载数达到一定阈值的时候会通过大象告知应用开发负责人。
-
操作监控:创建/修改和删除线程池都会通知到应用的开发负责人。
-
操作日志:可以查看线程池参数的修改记录,谁在什么时候修改了线程池参数、修改前的参数值是什么。
-
权限校验:只有应用开发负责人才能够修改应用的线程池参数。
图18 动态化线程池功能架构 参数动态化
JDK原生线程池ThreadPoolExecutor提供了如下几个public的setter方法,如下图所示:
图19 JDK 线程池参数设置接口 JDK允许线程池使用方通过ThreadPoolExecutor的实例来动态设置线程池的核心策略,以setCorePoolSize为方法例,在运行期线程池使用方调用此方法设置corePoolSize之后,线程池会直接覆盖原来的corePoolSize值,并且基于当前值和原始值的比较结果采取不同的处理策略。对于当前值小于当前工作线程数的情况,说明有多余的worker线程,此时会向当前idle的worker线程发起中断请求以实现回收,多余的worker在下次idle的时候也会被回收;对于当前值大于原始值且当前队列中有待执行任务,则线程池会创建新的worker线程来执行队列任务,setCorePoolSize具体流程如下:
图20 setCorePoolSize方法执行流程 线程池内部会处理好当前状态做到平滑修改,其他几个方法限于篇幅,这里不一一介绍。重点是基于这几个public方法,我们只需要维护ThreadPoolExecutor的实例,并且在需要修改的时候拿到实例修改其参数即可。基于以上的思路,我们实现了线程池参数的动态化、线程池参数在管理平台可配置可修改,其效果图如下图所示:
图21 可动态修改线程池参数 用户可以在管理平台上通过线程池的名字找到指定的线程池,然后对其参数进行修改,保存后会实时生效。目前支持的动态参数包括核心数、最大值、队列长度等。除此之外,在界面中,我们还能看到用户可以配置是否开启告警、队列等待任务告警阈值、活跃度告警等等。关于监控和告警,我们下面一节会对齐进行介绍。
线程池监控
除了参数动态化之外,为了更好地使用线程池,我们需要对线程池的运行状况有感知,比如当前线程池的负载是怎么样的?分配的资源够不够用?任务的执行情况是怎么样的?是长任务还是短任务?
基于对这些问题的思考,动态化线程池提供了多个维度的监控和告警能力,包括:线程池活跃度、任务的执行Transaction(频率、耗时)、Reject异常、线程池内部统计信息等等,既能帮助用户从多个维度分析线程池的使用情况,又能在出现问题第一时间通知到用户,从而避免故障或加速故障恢复。
1. 负载监控和告警
线程池负载关注的核心问题是:基于当前线程池参数分配的资源够不够。对于这个问题,我们可以从事前和事中两个角度来看。事前,线程池定义了“活跃度”这个概念,来让用户在发生Reject异常之前能够感知线程池负载问题,线程池活跃度计算公式为:线程池活跃度 = activeCount/maximumPoolSize。这个公式代表当活跃线程数趋向于maximumPoolSize的时候,代表线程负载趋高。
事中,也可以从两方面来看线程池的过载判定条件,一个是发生了Reject异常,一个是队列中有等待任务(支持定制阈值)。以上两种情况发生了都会触发告警,告警信息会通过大象推送给服务所关联的负责人。
图22 大象告警通知 2. 任务级精细化监控
在传统的线程池应用场景中,线程池中的任务执行情况对于用户来说是透明的。比如在一个具体的业务场景中,业务开发申请了一个线程池同时用于执行两种任务,一个是发消息任务、一个是发短信任务,这两类任务实际执行的频率和时长对于用户来说没有一个直观的感受,很可能这两类任务不适合共享一个线程池,但是由于用户无法感知,因此也无从优化。动态化线程池内部实现了任务级别的埋点,且允许为不同的业务任务指定具有业务含义的名称,线程池内部基于这个名称做Transaction打点,基于这个功能,用户可以看到线程池内部任务级别的执行情况,且区分业务,任务监控示意图如下图所示:
图23 线程池任务执行监控 3. 运行时状态实时查看
用户基于JDK原生线程池ThreadPoolExecutor提供的几个public的getter方法,可以读取到当前线程池的运行状态以及参数,如下图所示:
图24 线程池实时运行情况 动态化线程池基于这几个接口封装了运行时状态实时查看的功能,用户基于这个功能可以了解线程池的实时状态,比如当前有多少个工作线程,执行了多少个任务,队列中等待的任务数等等。效果如下图所示:
图25 线程池实时运行情况 3.4 实践总结
面对业务中使用线程池遇到的实际问题,我们曾回到支持并发性问题本身来思考有没有取代线程池的方案,也曾尝试着去追求线程池参数设置的合理性,但面对业界方案具体落地的复杂性、可维护性以及真实运行环境的不确定性,我们在前两个方向上可谓“举步维艰”。
最终,我们回到线程池参数动态化方向上探索,得出一个且可以解决业务问题的方案,虽然本质上还是没有逃离使用线程池的范畴,但是在成本和收益之间,算是取得了一个很好的平衡。成本在于实现动态化以及监控成本不高,收益在于:在不颠覆原有线程池使用方式的基础之上,从降低线程池参数修改的成本以及多维度监控这两个方面降低了故障发生的概率。希望本文提供的动态化线程池思路能对大家有帮助。
四、参考资料
[1]JDK 1.8 源码
[2] 维基百科-线程池
[3] 更好的使用Java线程池
[4] 维基百科Pooling(Resource Management)
[5] 深入理解Java线程池:ThreadPoolExecutor
[6]《Java并发编程实践》
五、作者简介
致远,2018年加入美团点评,美团到店综合研发中心后台开发工程师。
陆晨,2015年加入美团点评,美团到店综合研发中心后台技术专家。
---------- END ----------
招聘信息
美团到店综合研发中心长期招聘前端、后端、数据仓库、机器学习/数据挖掘算法工程师,坐标上海,欢迎感兴趣的同学发送简历到:[email protected](邮件标题注明:美团到店综合研发中心—上海)
也许你还想看
Java 动态调试技术原理及实践
Java字节码增强探秘
Java魔法类:Unsafe应用解析
Java动态追踪技术探究
线程池你用过吧,线程数是怎么设置的呢?
Hunter心想,这不难啊,曾经在《Java并发编程》一书中有看到过线程池中线程数目设置的讲述,于是张口就来:
线程数的设置需要考虑三方面的因素,服务器的配置、服务器资源的预算和任务自身的特性。具体来说就是服务器有多少个CPU,多少内存,IO支持的最大QPS是多少,任务主要执行的是计算、IO还是一些混合操作,任务中是否包含数据库连接等的稀缺资源。线程池的线程数设置主要取决于这些因素。
面试官追问来了:
那具体是怎么设置呢?
Hunter略一思忖,整理了下思路,娓娓道来:
假设机器有N个CPU,那么对于计算密集型的任务,应该设置线程数为N+1;对于IO密集型的任务,应该设置线程数为2N;对于同时有计算工作和IO工作的任务,应该考虑使用两个线程池,一个处理计算任务,一个处理IO任务,分别对两个线程池按照计算密集型和IO密集型来设置线程数。
面试官表情毫无变化,接着发问:
N+1和2N是怎么来的?
Hunter张口就来:
是个经验值。
面试官:
经验值吗?那为什么不是N+2或者N+3,而非得是N+1呢?
Hunter被驳得稍有点懵,脑子里努力在回想学习过的那些技术点,竟一时语塞。
看得出来面试官略有不满,于是提示道:
那假如在一个请求中,计算操作需要5ms,DB操作需要100ms,对于一台8个CPU的服务器,怎么设置线程数呢?
Hunter努力平复心情,紧接着最开始的思路,说到:
这是一个计算和IO混合型的任务,可以将其分解为两个线程池来处理。一个线程池处理计算操作,设置N+1=9个线程,一个线程处理IO操作,设置2N=16个线程。
面试官:
如果一个任务同时包含了一个计算操作和DB操作呢,不能拆分怎么设置?你能讲一下具体的计算过程吗?
Hunter略有点慌,心里不断给自己暗示:这个问题不难不难。然后不断回想看过的《Java并发编程实战》和《Java虚拟机并发编程》中关于线程池设置的章节,并试图将自己对这个问题的分析思路也表达出来。
首先这个任务整体上是一个IO密集型的任务。在处理一个请求的过程中,总共耗时100+5=105ms,而其中只有5ms是用于计算操作的,CPU利用率为5/(100+5)。使用线程池是为了尽量提高CPU的利用率,减少对CPU资源的浪费,假设以100%的CPU利用率来说,要达到100%的CPU利用率,对于一个CPU就要设置其利用率的倒数个数的线程数,也即1/(5/(100+5)),8个CPU的话就乘以8。那么算下来的话,就是……168,对,这个线程池要设置168个线程数。
面试官表情略有缓和,嘴角微微一笑:
如果实际的任务差异较大,不同任务实际的CPU操作耗时和IO操作耗时有所不同,那么怎么设置线程数呢?
经过刚才的分析过程,Hunter心里已经回忆起了这块的知识点,已然不慌了。
那对所有任务的CPU操作耗时和IO操作耗时求个平均值就好了。
Hunter心里渐渐恢复了自信,大脑的利用率瞬间提高好几十个百分点。
面试官轻轻“嗯”了一声,表示认可。
那如果现在这个IO操作是DB操作,而DB的QPS上限是1000,这个线程池又该设置为多大呢?
经过刚才的心理调整,对问题完整的分析过程,以及面试官的略微认可,Hunter已经知道如何去更好地回答面试官的问题了。
按比例来减少就可以了,按照之前的计算过程,可以计算出来当线程数设置为168的时候,DB操作的QPS为,168(1000/(100+5))=1600,如果现在DB的QPS最大为1000,那么对应的,最大只能设置168(1000/1600)=105个线程。
面试官这次是真的满意了,给这个回答给了一个正面的评价:
思路挺清晰的。那设置线程池的时候除了考虑这些,还需要考虑哪些内容呢?
Hunter此时已经完全找回自信了,不惧任何问题。
除了考虑任务CPU操作耗时、IO操作耗时之外,还需要服务器的内存资源、硬盘资源、网络带宽等等的。
面试官点点头,看起来Hunter已经获得了面试官的正式认可了。面试官告诉Hunter,表现不错,等接下来的面试安排吧。
面试后总结
Hunter内心异常激动,这真算是一次“死里逃生”的经历了。面试结束后,Hunter压抑兴奋,马上去找到《Java并发编程实战》和《Java虚拟机并发编程》两本书,翻到对应的章节,想确认下自己的回答。
果然,压力除了会造成紧张之外,也能提高大脑利用率。Hunter在调整状态后的回答完全正确。附上两本书中对线程池设置的理论。
线程数的第一种计算方法
在《Java并发编程实践》中,是这样来计算线程池的线程数目的:
在一个基准负载下,使用 几种不同大小的线程池运行你的应用程序,并观察CPU利用率的水平。
给定下列定义:
Ncpu = CPU的数量
Ucpu = 目标CPU的使用率, 0 <= Ucpu <= 1
W/C = 等待时间与计算时间的比率
为保持处理器达到期望的使用率,最优的池的大小等于:
Nthreads = Ncpu x Ucpu x (1 + W/C)
这种计算方式,我们需要知道上面定义的几个数值,才能计算出来线程池需要设置的线程数。其中,CPU数量是确定的,CPU使用率是目标值也是确定的,W/C也是可以通过基准程序测试得出的。
线程数的第二种计算方法
而在《Java虚拟机并发编程》中,则是这样来计算线程池的线程数目的:
线程数 = CPU可用核心数/(1 - 阻塞系数),其中阻塞系数的取值在0和1之间。
计算密集型任务的阻塞系数为0,而IO密集型任务的阻塞系数则接近1。一个完全阻塞的任务是注定要挂掉的,所以我们无须担心阻塞系数会达到1。
这种计算方式,我们需要知道CPU可用核心数和阻塞系数,才能计算出来线程池需要设置的线程数目。其中,CPU可用核心数是确定的,阻塞系数可以通过公式:阻塞系数=阻塞时间/(阻塞时间+计算时间),其实也就是上一种算法中的W/C的方式来计算,所以阻塞系数也是可以通过基准程序计算得出的。
所谓的经验值怎么来的
那么我们再来看所谓的N+1与2N的经验值的来源。
计算密集型应用
以第一种计算方式来看,对于计算密集型应用,假定等待时间趋近于0,是的CPU利用率达到100%,那么线程数就是CPU核心数,那这个+1意义何在呢?
《Java并发编程实践》这么说:
计算密集型的线程恰好在某时因为发生一个页错误或者因其他原因而暂停,刚好有一个“额外”的线程,可以确保在这种情况下CPU周期不会中断工作。
所以N+1确实是一个经验值。
IO密集型应用
同样以第一种方式来看,对于IO密集型应用,假定所有的操作时间几乎都是IO操作耗时,那么W/C的值就为1,那么对应的线程数确实为2N。
监控二:
摘要:背景在开发中,我们经常要使用Executors类创建线程池来执行大量的任务,使用线程池的并发特性提高系统的吞吐量。但是,线程池使用不当也会使服务器资源枯竭,导致异常情况的发生,比如固定线程池的阻塞队列任务数量过多、缓存线程池创建的线程过多导致内存溢出、系统假死等问题。因此,我们需要一种简单的监控方案来监控线程池的使用情况,比如完成任务数量、未完成任务数量、线程大小等信息。ExecutorsUtil工具类以下是我们开发的一个线程池工具类,该工具类扩展ThreadPoolExec
背景
在开发中,我们经常要使用Executors类创建线程池来执行大量的任务,使用线程池的并发特性提高系统的吞吐量。但是,线程池使用不当也会使服务器资源枯竭,导致异常情况的发生,比如固定线程池的阻塞队列任务数量过多、缓存线程池创建的线程过多导致内存溢出、系统假死等问题。因此,我们需要一种简单的监控方案来监控线程池的使用情况,比如完成任务数量、未完成任务数量、线程大小等信息。
ExecutorsUtil工具类
以下是我们开发的一个线程池工具类,该工具类扩展ThreadPoolExecutor实现了线程池监控功能,能实时将线程池使用信息打印到日志中,方便我们进行问题排查、系统调优。具体代码如下:
package com.concurrent.monitor;
import java.util.Date;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 该类继承ThreadPoolExecutor类,覆盖了shutdown(), shutdownNow(), beforeExecute() 和 afterExecute()
* 方法来统计线程池的执行情况
*
*/
public class ExecutorsUtil extends ThreadPoolExecutor {
private static final Logger LOGGER = LoggerFactory.getLogger(ExecutorsUtil.class);
// 保存任务开始执行的时间,当任务结束时,用任务结束时间减去开始时间计算任务执行时间
private ConcurrentHashMap startTimes;
// 线程池名称,一般以业务名称命名,方便区分
private String poolName;
/**
* 调用父类的构造方法,并初始化HashMap和线程池名称
*
* @param corePoolSize
* 线程池核心线程数
* @param maximumPoolSize
* 线程池最大线程数
* @param keepAliveTime
* 线程的最大空闲时间
* @param unit
* 空闲时间的单位
* @param workQueue
* 保存被提交任务的队列
* @param poolName
* 线程池名称
*/
public ExecutorsUtil(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue,
String poolName) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, new EventThreadFactory(poolName));
this.startTimes = new ConcurrentHashMap<>();
this.poolName = poolName;
}
/**
* 线程池延迟关闭时(等待线程池里的任务都执行完毕),统计线程池情况
*/
@Override
public void shutdown() {
// 统计已执行任务、正在执行任务、未执行任务数量
LOGGER.info(String.format(this.poolName + " Going to shutdown. Executed tasks: %d, Running tasks: %d, Pending tasks: %d",
this.getCompletedTaskCount(), this.getActiveCount(), this.getQueue().size()));
super.shutdown();
}
/**
* 线程池立即关闭时,统计线程池情况
*/
@Override
public List shutdownNow() {
// 统计已执行任务、正在执行任务、未执行任务数量
LOGGER.info(
String.format(this.poolName + " Going to immediately shutdown. Executed tasks: %d, Running tasks: %d, Pending tasks: %d",
this.getCompletedTaskCount(), this.getActiveCount(), this.getQueue().size()));
return super.shutdownNow();
}
/**
* 任务执行之前,记录任务开始时间
*/
@Override
protected void beforeExecute(Thread t, Runnable r) {
startTimes.put(String.valueOf(r.hashCode()), new Date());
}
/**
* 任务执行之后,计算任务结束时间
*/
@Override
protected void afterExecute(Runnable r, Throwable t) {
Date startDate = startTimes.remove(String.valueOf(r.hashCode()));
Date finishDate = new Date();
long diff = finishDate.getTime() - startDate.getTime();
// 统计任务耗时、初始线程数、核心线程数、正在执行的任务数量、已完成任务数量、任务总数、队列里缓存的任务数量、池中存在的最大线程数、最大允许的线程数、线程空闲时间、线程池是否关闭、线程池是否终止
LOGGER.info(String.format(this.poolName
+ "-pool-monitor: Duration: %d ms, PoolSize: %d, CorePoolSize: %d, Active: %d, Completed: %d, Task: %d, Queue: %d, LargestPoolSize: %d, MaximumPoolSize: %d,KeepAliveTime: %d, isShutdown: %s, isTerminated: %s",
diff, this.getPoolSize(), this.getCorePoolSize(), this.getActiveCount(), this.getCompletedTaskCount(), this.getTaskCount(),
this.getQueue().size(), this.getLargestPoolSize(), this.getMaximumPoolSize(), this.getKeepAliveTime(TimeUnit.MILLISECONDS),
this.isShutdown(), this.isTerminated()));
}
/**
* 创建固定线程池,代码源于Executors.newFixedThreadPool方法,这里增加了poolName
*
* @param nThreads
* 线程数量
* @param poolName
* 线程池名称
* @return ExecutorService对象
*/
public static ExecutorService newFixedThreadPool(int nThreads, String poolName) {
return new ExecutorsUtil(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue (), poolName);
}
/**
* 创建缓存型线程池,代码源于Executors.newCachedThreadPool方法,这里增加了poolName
*
* @param poolName
* 线程池名称
* @return ExecutorService对象
*/
public static ExecutorService newCachedThreadPool(String poolName) {
return new ExecutorsUtil(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue (), poolName);
}
/**
* 生成线程池所用的线程,只是改写了线程池默认的线程工厂,传入线程池名称,便于问题追踪
*/
static class EventThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
/**
* 初始化线程工厂
*
* @param poolName
* 线程池名称
*/
EventThreadFactory(String poolName) {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
namePrefix = poolName + "-pool-" + poolNumber.getAndIncrement() + "-thread-";
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
}
以上的ExecutorsUtil类继承了ThreadPoolExecutor类,重写了shutdown()、shutdownNow()、beforeExecute() 和 afterExecute() 方法来统计线程池的执行情况,这四个方法是ThreadPoolExecutor类预留给开发者进行扩展的方法,具体如下:
shutdown():线程池延迟关闭时(等待线程池里的任务都执行完毕),统计已执行任务、正在执行任务、未执行任务数量
shutdownNow():线程池立即关闭时,统计已执行任务、正在执行任务、未执行任务数量
beforeExecute(Thread t, Runnable r):任务执行之前,记录任务开始时间,startTimes这个HashMap以任务的hashCode为key,开始时间为值
afterExecute(Runnable r, Throwable t):任务执行之后,计算任务结束时间。统计任务耗时、初始线程数、核心线程数、正在执行的任务数量、已完成任务数量、任务总数、队列里缓存的任务数量、池中存在的最大线程数、最大允许的线程数、线程空闲时间、线程池是否关闭、线程池是否终止信息
监控到的记录如下:
momentspush-pool-2-thread-90 ExecutorsUtil.java:91 momentspush_monitor: Duration: 599 ms, PoolSize: 200, CorePoolSize: 200, Active: 200, Completed: 334924, Task: 417702, Queue: 82578, LargestPoolSize: 200, MaximumPoolSize: 200,KeepAliveTime: 0, isShutdown: false, isTerminated: false
一般我们会依赖beforeExecute和afterExecute这两个方法统计的信息,具体原因请参考需要注意部分的最后一项。有了这些信息之后,我们可以根据业务情况和统计的线程池信息合理调整线程池大小,根据任务耗时长短对自身服务和依赖的其他服务进行调优,提高服务的可用性。
需要注意的
在afterExecute方法中需要注意,需要调用ConcurrentHashMap的remove方法移除并返回任务的开始时间信息,而不是调用get方法,因为在高并发情况下,线程池里要执行的任务很多,如果只获取值不移除的话,会使ConcurrentHashMap越来越大,引发内存泄漏或溢出问题。该行代码如下:
Date startDate = startTimes.remove(String.valueOf(r.hashCode()));
有了ExecutorsUtil类之后,我们可以通过newFixedThreadPool(int nThreads, String poolName)和newCachedThreadPool(String poolName)方法创建两个日常我们使用最多的线程池,跟默认的Executors里的方法不同的是,这里需要传入poolName参数,该参数主要是用来给线程池定义一个与业务相关并有具体意义的线程池名字,方便我们排查线上问题。具体可参考《Java线程池扩展之关联线程池与业务》一文
在生产环境中,谨慎调用shutdown()和shutdownNow()方法,因为调用这两个方法之后,线程池会被关闭,不再接收新的任务,如果有新任务提交到一个被关闭的线程池,会抛出java.util.concurrent.RejectedExecutionException异常,具体可参考《一步一步分析RejectedExecutionException异常》一文。其实在使用Spring等框架来管理类的生命周期的条件下,也没有必要调用这两个方法来关闭线程池,线程池的生命周期完全由该线程池所属的Spring管理的类决定
转者实践代码:
import java.util.Date;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import org.slf4.LoggerFactory;
import org.slf4j.Logger;
/**
* 参考 https://www.aliyun.com/jiaocheng/778174.html
* 该类继承ThreadPoolExecutor类,覆盖了shutdown(), shutdownNow(), beforeExecute() 和 afterExecute()
* 方法来统计线程池的执行情况
*/
public class ThreadPoolExecutorWrapper extends ThreadPoolExecutor {
private static final Logger THREAD_POOL_MONITOR_LOGGER = LoggerFactory.getLogger("THREAD_POOL_MONITOR_LOGGER");
// 保存任务开始执行的时间,当任务结束时,用任务结束时间减去开始时间计算任务执行时间
private ConcurrentHashMap startTimes;
// 线程池名称,一般以业务名称命名,方便区分
private String poolName;
/**
* 调用父类的构造方法,并初始化HashMap和线程池名称
*
* @param corePoolSize 线程池核心线程数
* @param maximumPoolSize 线程池最大线程数
* @param keepAliveTime 线程的最大空闲时间
* @param unit 空闲时间的单位
* @param queueCapacity 保存被提交任务的队列
* @param poolName 线程池名称
*/
public ThreadPoolExecutorWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, int queueCapacity,
String poolName, RejectedExecutionHandler rejectedExecutionHandler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, new LinkedBlockingQueue(queueCapacity), new EventThreadFactory(poolName),rejectedExecutionHandler);
this.startTimes = new ConcurrentHashMap<>();
this.poolName = poolName;
}
/**
* 线程池延迟关闭时(等待线程池里的任务都执行完毕),统计线程池情况
*/
@Override
public void shutdown() {
// 统计已执行任务、正在执行任务、未执行任务数量
THREAD_POOL_MONITOR_LOGGER.info(String.format(this.poolName + " Going to shutdown. Executed tasks: %d, Running tasks: %d, Pending tasks: %d",
this.getCompletedTaskCount(), this.getActiveCount(), this.getQueue().size()));
super.shutdown();
}
/**
* 线程池立即关闭时,统计线程池情况
*/
@Override
public List shutdownNow() {
// 统计已执行任务、正在执行任务、未执行任务数量
THREAD_POOL_MONITOR_LOGGER.info(
String.format(this.poolName + " Going to immediately shutdown. Executed tasks: %d, Running tasks: %d, Pending tasks: %d",
this.getCompletedTaskCount(), this.getActiveCount(), this.getQueue().size()));
return super.shutdownNow();
}
/**
* 任务执行之前,记录任务开始时间
*/
@Override
protected void beforeExecute(Thread t, Runnable r) {
startTimes.put(String.valueOf(r.hashCode()), new Date());
}
/**
* 任务执行之后,计算任务结束时间
*/
@Override
protected void afterExecute(Runnable r, Throwable t) {
Date startDate = startTimes.remove(String.valueOf(r.hashCode()));
if(startDate == null){
startDate = new Date(0L);
}
Date finishDate = new Date();
long diff = finishDate.getTime() - startDate.getTime();
// 统计任务耗时、初始线程数、核心线程数、正在执行的任务数量、已完成任务数量、任务总数、队列里缓存的任务数量、池中存在的最大线程数、最大允许的线程数、线程空闲时间、线程池是否关闭、线程池是否终止
THREAD_POOL_MONITOR_LOGGER.info(String.format(this.poolName
+ "-pool-monitor: Duration: %d ms, PoolSize: %d, CorePoolSize: %d, Active: %d, Completed: %d, Task: %d, Queue: %d, LargestPoolSize: %d, MaximumPoolSize: %d,KeepAliveTime: %d, isShutdown: %s, isTerminated: %s",
diff, this.getPoolSize(), this.getCorePoolSize(), this.getActiveCount(), this.getCompletedTaskCount(), this.getTaskCount(),
this.getQueue().size(), this.getLargestPoolSize(), this.getMaximumPoolSize(), this.getKeepAliveTime(TimeUnit.MILLISECONDS),
this.isShutdown(), this.isTerminated()));
}
/**
* 生成线程池所用的线程,只是改写了线程池默认的线程工厂,传入线程池名称,便于问题追踪
*/
static class EventThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
/**
* 初始化线程工厂
*
* @param poolName 线程池名称
*/
EventThreadFactory(String poolName) {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
namePrefix = poolName + "-pool-" + poolNumber.getAndIncrement() + "-thread-";
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
}
spring配置一个线程池实例
logger4j配置
日志
2018-10-16 20:51:02 my_task_pool-pool-monitor: Duration: 12 ms, PoolSize: 8, CorePoolSize: 8, Active: 2, Completed: 2998, Task: 3000, Queue: 0, LargestPoolSize: 8, MaximumPoolSize: 20,KeepAliveTime: 60000, isShutdown: false, isTerminated: false
2018-10-16 20:51:02 my_task_pool-pool-monitor: Duration: 12 ms, PoolSize: 8, CorePoolSize: 8, Active: 2, Completed: 2998, Task: 3000, Queue: 0, LargestPoolSize: 8, MaximumPoolSize: 20,KeepAliveTime: 60000, isShutdown: false, isTerminated: false
转自 https://www.aliyun.com/jiaocheng/778174.html
监控三*
美团点评 Cat监控 线程池监控
mrjyng 2020-03-14 16:38:38 654 已收藏
分类专栏: 并发 分布式 java 文章标签: java 监控类
版权
在日常项目开发中,我们经常会遇到需要异步处理的任务,例如日志服务,监控服务等。有一定开发经验的同学首先就会想到使用线程池,因为“在线程池中执行任务”比“为每个任务创建一个线程”更有优势,通过重用现有的线程,可以避免线程在不断创建、销毁过程中产生的开销。在java开发中,一般做法就是基于ThreadPoolExecutor类,自定义corePoolSize, maxPoolSize, ThreadFactory等参数来创建一个线程池工具类。但是,线程池也不是没有缺点的,对于开发者而言,我们很难在项目上线之前准确地预估业务的规模,所以,如何合理地为线程池设置corePoolSize,maxPoolSize是一个比较难把握的事情,设置不合理会导致不能达到预计的性能,甚至会引发线上故障。不过,利用ThreadPoolExecutor为我们提供的一些监控api,我们可以做到对线程池进行实时监控和调优。本文将利用如下几个API,并结合Cat提供的拓展功能,实现对线程池的持续监控。
1.自定义线程池
如下代码是我用来测试的一个线程池工具类
public class ThreadPoolManager {
/**
* 根据cpu的数量动态的配置核心线程数和最大线程数
*/
//private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
/**
* 核心线程数 = CPU核心数 + 1
*/
private static final int CORE_POOL_SIZE = 20;
/**
* 线程池最大线程数 = CPU核心数 * 2 + 1
*/
private static final int MAXIMUM_POOL_SIZE = 25;
private static final int QUEUE_SIZE = 1000;
/**
* 非核心线程闲置时超时1s
*/
private static final int KEEP_ALIVE = 3;
/**
* 线程池的对象
*/
private final ThreadPoolExecutor executor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE,
KEEP_ALIVE, TimeUnit.SECONDS, new ArrayBlockingQueue<>(QUEUE_SIZE),
Executors.defaultThreadFactory(), new DiscardPolicyWithLog());
/**
* 要确保该类只有一个实例对象,避免产生过多对象消费资源,所以采用单例模式
*/
private ThreadPoolManager() {
}
private static ThreadPoolManager sInstance;
public static ThreadPoolManager getsInstance() {
if (sInstance == null) {
synchronized (ThreadPoolManager.class) {
if (sInstance == null) {
sInstance = new ThreadPoolManager();
}
}
}
return sInstance;
}
/**
* 开启一个无返回结果的线程
* @param r
*/
public void execute(Runnable r) {
executor.execute(r);
}
/**
* 开启一个有返回结果的线程
*
* @param r
* @return
*/
public Future submit(Callable r) {
// 把一个任务丢到了线程池中
return executor.submit(r);
}
/**
* 把任务移除等待队列
*
* @param r
*/
public void cancel(Runnable r) {
if (r != null) {
executor.getQueue().remove(r);
}
}
}
2.加入Cat监控
假设你的项目已经集成了cat监控,那么,利用cat提供的SPI拓展能力,我们可以监控任何需要关注的系统运行时指标。我们现在想要监控线程池运行时的一些指标,帮助我们更好的优化线程池配置,提高系统性能。在以上线程池工具类的构造方法里加入如下代码,就可以实现。
private ThreadPoolManager() {
StatusExtensionRegister.getInstance().register(new StatusExtension() {
@Override
public String getId() {
return "mqtt_msg_pool_monitor";
}
@Override
public String getDescription() {
return "mqtt消息处理线程池监控";
}
@Override
public Map getProperties() {
Map map = new HashMap<>();
//线程池曾经创建过的最大线程数量
map.put("largest-pool-size", String.valueOf(executor.getLargestPoolSize()));
map.put("max-pool-size", String.valueOf(executor.getMaximumPoolSize()));
map.put("core-pool-size", String.valueOf(executor.getCorePoolSize()));
map.put("current-pool-size", String.valueOf(executor.getPoolSize()));
map.put("queue-size", String.valueOf(executor.getQueue().size()));
return map;
}
});
}
- 效果观察
*四 计算时间
具体实践
通过公式,我们了解到需要 3 个具体数值
一个请求所消耗的时间 (线程 IO time + 线程 CPU time)
该请求计算时间 (线程 CPU time)
CPU 数目
请求消耗时间
Web 服务容器中,可以通过 Filter 来拦截获取该请求前后消耗的时间
public class MoniterFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(MoniterFilter.class);
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
ServletException {
long start = System.currentTimeMillis();
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String uri = httpRequest.getRequestURI();
String params = getQueryString(httpRequest);
try {
chain.doFilter(httpRequest, httpResponse);
} finally {
long cost = System.currentTimeMillis() - start;
logger.info(“access url [{}{}], cost time [{}] ms )”, uri, params, cost);
}
private String getQueryString(HttpServletRequest req) {
StringBuilder buffer = new StringBuilder("?");
Enumeration emParams = req.getParameterNames();
try {
while (emParams.hasMoreElements()) {
String sParam = emParams.nextElement();
String sValues = req.getParameter(sParam);
buffer.append(sParam).append("=").append(sValues).append("&");
}
return buffer.substring(0, buffer.length() - 1);
} catch (Exception e) {
logger.error(“get post arguments error”, buffer.toString());
}
return “”;
}
}
CPU 计算时间
CPU 计算时间 = 请求总耗时 - CPU IO time
假设该请求有一个查询 DB 的操作,只要知道这个查询 DB 的耗时(CPU IO time),计算的时间不就出来了嘛,我们看一下怎么才能简洁,明了的记录 DB 查询的耗时。
通过(JDK 动态代理/ CGLIB)的方式添加 AOP 切面,来获取线程 IO 耗时。代码如下,请参考:
public class DaoInterceptor implements MethodInterceptor {
private static final Logger logger = LoggerFactory.getLogger(DaoInterceptor.class);
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
StopWatch watch = new StopWatch();
watch.start();
Object result = null;
Throwable t = null;
try {
result = invocation.proceed();
} catch (Throwable e) {
t = e == null ? null : e.getCause();
throw e;
} finally {
watch.stop();
logger.info("({}ms)", watch.getTotalTimeMillis());
}
return result;
}
}
CPU 数目
逻辑 CPU 个数 ,设置线程池大小的时候参考的 CPU 个数
cat /proc/cpuinfo| grep “processor”| wc -l
总结
合适的配置线程池大小其实很不容易,但是通过上述的公式和具体代码,我们就能快速、落地的算出这个线程池该设置的多大。
不过最后的最后,我们还是需要通过压力测试来进行微调,只有经过压测测试的检验,我们才能最终保证的配置大小是准确的。*
五监控
springboot实现异步线程池并实现实时监控
文章标签: springboot异步线程池 并实现线程监控
最后发布:2019-07-03 11:37:10 首次发布:2019-07-03 11:37:10
版权
背景
:因为我要对接京东订单服务 拉取订单的时候需要100个商户同时拉取订单服务,必须是异步的。
首先要在springboot 启动处加入
@EnableAsync
@Configuration
class TaskPoolConfig {
@Bean("taskExecutor")
public Executor taskExecutor() {
//注意这一行日志:2. do submit,taskCount [101], completedTaskCount [87], activeCount [5], queueSize [9]
//这说明提交任务到线程池的时候,调用的是submit(Callable task)这个方法,当前已经提交了101个任务,完成了87个,当前有5个线程在处理任务,还剩9个任务在队列中等待,线程池的基本情况一路了然;
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//核心线程数10:线程池创建时候初始化的线程数
executor.setCorePoolSize(10);
//最大线程数20:线程池最大的线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程
//maxPoolSize 当系统负载大道最大值时,核心线程数已无法按时处理完所有任务,这是就需要增加线程.每秒200个任务需要20个线程,那么当每秒1000个任务时,则需要(1000-queueCapacity)*(20/200),即60个线程,可将maxPoolSize设置为60;
executor.setMaxPoolSize(30);
//缓冲队列200:用来缓冲执行任务的队列
executor.setQueueCapacity(400);
//允许线程的空闲时间60秒:当超过了核心线程出之外的线程在空闲时间到达之后会被销毁
executor.setKeepAliveSeconds(60);
//线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
executor.setThreadNamePrefix("taskExecutor");
//理线程池对拒绝任务的处策略:这里采用了CallerRunsPolicy策略,当线程池没有处理能力的时候,该策略会直接在 execute 方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务
/*CallerRunsPolicy:线程调用运行该任务的 execute 本身。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。
这个策略显然不想放弃执行任务。但是由于池中已经没有任何资源了,那么就直接使用调用该execute的线程本身来执行。(开始我总不想丢弃任务的执行,但是对某些应用场景来讲,很有可能造成当前线程也被阻塞。如果所有线程都是不能执行的,很可能导致程序没法继续跑了。需要视业务情景而定吧。)
AbortPolicy:处理程序遭到拒绝将抛出运行时 RejectedExecutionException
这种策略直接抛出异常,丢弃任务。(jdk默认策略,队列满并线程满时直接拒绝添加新任务,并抛出异常,所以说有时候放弃也是一种勇气,为了保证后续任务的正常进行,丢弃一些也是可以接收的,记得做好记录)
DiscardPolicy:不能执行的任务将被删除
这种策略和AbortPolicy几乎一样,也是丢弃任务,只不过他不抛出异常。
DiscardOldestPolicy:如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程)
该策略就稍微复杂一些,在pool没有关闭的前提下首先丢掉缓存在队列中的最早的任务,然后重新尝试运行该任务。这个策略需要适当小心*/
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
return executor;
}
}
每个配置文件代表什么意思可以看一下
这个时候启动的时候我们异步线程池是已经创建好
我们创建一个task 类
public class Task {
public static Random random = new Random();
@Async("taskExecutor")
public void doTask(Integer i) throws Exception {
System.out.println("开始做任务");
long start = System.currentTimeMillis();
//这里写业务代码
long end = System.currentTimeMillis();
System.out.println("完成任务耗时:" + (end - start)/1000 + "秒");
}
}
这个时候我们就可以使用了我们把task 注入到 controller 层
@RestController
@RequestMapping("test/")
public class TestController {
@Autowired
private TaskService taskService;
@Autowired
private Executor taskExecutor;
private Logger logger = LogManager.getLogger(JDcontroller.class);
@PostMapping("order")
public String addOrder(@RequestBody RequestParameterDTO requestParameters){
//这里会执行你开启的任务,都是异步的,调用这个接口会立马返回 OK 然后业务是在后台运行的
taskService.doTask(requestParameters);
return "OK";
}
//这里我们可以通过接口实时观看效果 具体效果如下图
@GetMapping("order/asyncExceutor")
public Map getThreadInfo() {
Map map =new HashMap();
Object[] myThread = {taskExecutor};
for (Object thread : myThread) {
ThreadPoolTaskExecutor threadTask = (ThreadPoolTaskExecutor) thread;
ThreadPoolExecutor threadPoolExecutor =threadTask.getThreadPoolExecutor();
System.out.println("提交任务数"+threadPoolExecutor.getTaskCount());
System.out.println("完成任务数"+threadPoolExecutor.getCompletedTaskCount() );
System.out.println("当前有"+threadPoolExecutor.getActiveCount()+"个线程正在处理任务");
System.out.println("还剩"+threadPoolExecutor.getQueue().size()+"个任务");
map.put("提交任务数-->",threadPoolExecutor.getTaskCount());
map.put("完成任务数-->",threadPoolExecutor.getCompletedTaskCount());
map.put("当前有多少线程正在处理任务-->",threadPoolExecutor.getActiveCount());
map.put("还剩多少个任务未执行-->",threadPoolExecutor.getQueue().size());
map.put("当前可用队列长度-->",threadPoolExecutor.getQueue().remainingCapacity());
map.put("当前时间-->",DateFormatUtil.stringDate());
}
return map;
}
}
上图那个名字要和你在springboot启动处定义的名字要相同 这样spring 才能找到 才能监控你的线程池 ,当然这做的好处是你可以监控多个线程池 的线程,只需要在启动处 在加入 类似 的代码,名字不一样就行了