动态线程池组件-设计与实现

4,865 阅读5分钟

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();
                }
            }
        });
    }

5.应用效果 

5.1 负载监控与告警