1.整体设计
整体分为业务服务与管理端两大部分,包括了三个主要功能点:
(1)简化线程池配置:corePoolSize、maximumPoolSize,workQueue,它们最大程度地决定了线程池的任务分配和线程分配策略。考虑到在实际应用中我们获取并发性的场景主要是两种:(1)并行执行子任务,提高响应速度。这种情况下,应该使用同步队列,没有什么任务应该被缓存下来,而是应该立即执行。(2)并行执行大批次任务,提升吞吐量。这种情况下,应该使用有界队列,使用队列去缓冲大批量的任务,队列容量必须声明,防止任务无限制堆积。所以线程池只需要提供这三个关键参数的配置,并且提供两种队列的选择,就可以满足绝大多数的业务需求
(2)参数可动态配置: 在java线程池留有的基础上,封装线程池、允许线程池根据外部配置变化,进行修改配置,并在管理端开发控制界面方便开发;
(3)增加线程池监控:在线程池执行任务的整个生命周期添加监控能力,如当前活跃任务数、执行任务发生的异常数、任务队列大小等指标,方便相关开发了解线程池状态;
2.总体架构设计
动态调参:提供管理界面,支持线程池参数动态调整,包括线程池核心线程数大小,最大线程数大小,缓冲队列长度等;参数修改后及时生效
任务监控:支持应用粒度、线程池粒度、任务粒度的事务监控,可以查看线程池的任务执行情况,最大任务执行时间,平均任务执行时间
负载告警:支持告警规则配置,当超过阈值时会通知相关的开发负责人
操作监控与日志:管理段配置操作接入审计日志
权限校验:不同应用用户
3.主要业务流程
主要可分为应用(客户端)、管理端两大部分,使用mysql、es数据库,其中监控页面使用kibana可视化工具配置,线程池的配置页面使用简单前端完成;
4.部分核心代码
4.1 客户端实现
4.1.1 动态线程池定义与实现
由于ThreadPoolExecutor只开放了maxCorePoolSize/corePoolSize/
只能实现BlockingQueue接口实现阻塞队列,并开放队列容量改变的方法,这样set方法就可以动态修改,其他部分直接拷贝ArrayBlockingQueue的实现即可实现动态阻塞队列;
//自定义链表阻塞队列
public class PhxResizeLinkedBlockingQueue extends AbstractQueue
implements BlockingQueue, java.io.Serializable {
private volatile int capacity;
//增加设置队列容量接口
public void setCapacity(int capacity) {
this.capacity = capacity;
}
}
除了动态修改后,还需要支持状态上报、过载异常信息的上报、任务运行耗时统计等,所以还需要对重写afterExecute方法对任务运行状态监控,自定义RejectHandler监控拒绝动作,如下所示。
//重写方法,监听任务运行状态
public class PhxThreadPool extends ThreadPoolExecutor {
@Override
protected void afterExecute(Runnable r, Throwable throwable) {
LocalDateTime startTime = LocalDateTime.now();
super.afterExecute(r, throwable);
LocalDateTime endTime = LocalDateTime.now();
if (throwable == null && r instanceof Future) {
try {
((Future) r).get();
} catch (CancellationException ce) {
throwable = ce;
} catch (ExecutionException ee) {
throwable = ee.getCause();
} catch(InterruptedException ie){
Thread.currentThread().interrupt();
}
}
try {
if (throwable != null) {
publishEvent(EventEnums.POOL_RUNNABLE_EXECUTE_ERROR.name(), null);
} else {
Duration duration = Duration.between(startTime, endTime);
Integer costTime = Integer.parseInt(String.valueOf(duration.getSeconds()));
publishEvent(EventEnums.POOL_RUNTIME_STATISTICS.name(), costTime);
}
} catch (Exception ex){
}
}
//自定义拒绝策略,监听并上报
public class PhxRejectHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
PhxThreadPool threadPool = (PhxThreadPool)executor;
List metaDataDTOS = new ArrayList<>();
...
publisher.publishEvent(metaDataDTOS);
}
}
4.1.2 动态线程池注册
为了方便管理线程池实例,定义了线程池容器
public class ThreadPoolContainer {
private ConcurrentMap container = new ConcurrentHashMap<>();
public void put(String poolName, PhxThreadPool threadPool) {
container.putIfAbsent(poolName, threadPool);
}
public PhxThreadPool get(String poolName) {
return container.getOrDefault(poolName, null);
}
}
扫描与注册线程时使用了bean的后置处理器处理器,对spring容器启动时逐个扫描定义好的线程池实例,为了不阻塞启动过程,并另起一条线程专门用来注册。还需要将自定义的拒绝策略设置到线程池实例中。
public class PhxThreadPoolBeanPostProcessor implements BeanPostProcessor {
private final String url;
private PhxThreadPoolConfig config;
private ThreadPoolContainer container;
private RejectedExecutionHandler rejectHandler;
private ExecutorService executorService = Executors.newSingleThreadExecutor();
public PhxThreadPoolBeanPostProcessor(PhxThreadPoolConfig config, ThreadPoolContainer container) {
this.config = config;
url = config.getAdminUrl() + "/meta/register";
this.container = container;
}
@Override
public Object postProcessBeforeInitialization(final Object bean, final String beanName) throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(final Object poolBean, final String beanName) throws BeansException {
if (poolBean instanceof PhxThreadPool) {
executorService.execute(() -> handler((PhxThreadPool) poolBean));
}
return poolBean;
}
private void handler(final PhxThreadPool poolBean) {
String poolName = poolBean.getName();
poolBean.setRejectedExecutionHandler(rejectHandler);
container.put(poolName, poolBean);
post(buildJsonParams(poolBean));
}
}
4.1.3 线程池运行状态信息上报
为了加大消息吞吐量,最大程度节省应用的资源,在客户端组件中使用了消息队列进行缓冲,其中包括了事件/事件发布器/消息队列/事件处理器几大部分,
主要实现代码如下:
//监控事件包装类
public class MonitorEvent implements Serializable {
private List eventDTOs;
public void clear () {
eventDTOs = null; //使用后让jvm主动gc回收
}
}
//事件
public class MonitorEventDTO {
private String appName;
private String ip;
private String poolName;
private Integer corePoolSize;
private Integer maxPoolSize;
private Integer queueSize;
private Integer queueCapacity;
}
//事件生成工厂类
public class MonitorEventFactory implements EventFactory {
@Override
public MonitorEvent newInstance() {
return new MonitorEvent();
}
}
//事件处理器
public class MonitorEventHandler implements EventHandler {
private PhxThreadPoolConfig config;
public MonitorEventHandler(PhxThreadPoolConfig config) {
this.config = config;
}
@Override
public void onEvent(MonitorEvent monitorEvent, long l, boolean b) throws Exception {
if (monitorEvent == null || monitorEvent.getEventDTOs() == null
|| monitorEvent.getEventDTOs().size() == 0) {
return;
}
String path = config.getAdminUrl() + "/meta/upload";
List eventDTOs = monitorEvent.getEventDTOs();
PhxSender.upload(path, eventDTOs);
monitorEvent.clear();
}
}
//消息发布器
public class MonitorEventPublisher {
private Disruptor disruptor;
private MonitorEventHandler eventHandler;
private PhxThreadPoolConfig config;
public void publishEvent(final List events) {
final RingBuffer ringBuffer = disruptor.getRingBuffer();
ringBuffer.publishEvent(new EventTranslator(), events);
}
public void destroy() {
disruptor.shutdown();
}
}
//消息转换
public class EventTranslator implements EventTranslatorOneArg> {
@Override
public void translateTo(MonitorEvent event, long l, List monitorEventDTOS) {
event.setEventDTOs(monitorEventDTOS);
}
}
//发送
public class PhxSender {
public static void upload(String path, List metaDataDTOS) throws IOException {
if (metaDataDTOS != null && metaDataDTOS.size() > 0) {
String jsonBody = OkHttpTools.getInstance().getGosn().toJson(metaDataDTOS);
OkHttpTools.getInstance().post(path, jsonBody);
}
}
}
4.1.4 线程池动态配置
实现线程池的配置动态修改可以有两种方式,第一种方式是使用apollo客户端监听控制端发出的配置修改事件,然后根据事件响应,类似于“推模式“;第二种方式是在客户端起一条线程,不停地请求控制端服务,然后更新本地的线程池配置,类似于“拉模式“。这里采用第二种方式。
public class PhxClient {
public void startPullPoolConfig() {
pullConfigThread = new Thread(() -> {
while(!toStop) {
try {
String path = config.getAdminUrl() + "/meta/" + config.getAppName();
String response = OkHttpTools.getInstance().get(path);
//解析控制端返回的线程池配置信息
List pools = JSON.parseArray(response, ThreadPoolConfigDto.class);
if (pools != null && pools.size() > 0) {
pools.forEach(pool -> {
PhxThreadPool threadPool = container.get(pool.getPoolName());
threadPool.setCorePoolSize(pool.getCorePoolSize());
threadPool.setMaximumPoolSize(pool.getMaxPoolSize());
PhxResizeLinkedBlockingQueue queue = (PhxResizeLinkedBlockingQueue)threadPool.getQueue();
queue.setCapacity(pool.getQueueCapacity());
container.put(pool.getPoolName(), threadPool);
});
}
} catch (Exception ex) {
log.error("【线程池控制客户端-拉取配置线程发生异常】exception:", ex);
if (!toStop) {
}
}
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}