dubbo 线程池介绍

506 阅读4分钟

1. 背景

组内同事的项目报了个故障,QPS太高导致dubbo的线程池满了,这个时候我们该怎么办呢,另外在游览同事周报的时候,另外一个组的兄弟也遇到了线程池满了的情况,所以有必要研究一下dubbo线程池

2. 线程池分类

2.1 FixedThreadPool,固定线程数量的线程池

// org.apache.dubbo.common.threadpool.support.fixed.FixedThreadPool
// 固定线程数量的线程池
public class FixedThreadPool implements ThreadPool {

    @Override
    public Executor getExecutor(URL url) {
        // 获取一些参数变量
        String name = url.getParameter("threadname", (String) url.getAttribute("threadname", "Dubbo"));
        int threads = url.getParameter("threads", 200);
        int queues = url.getParameter("queues", 0);
        
        // 调用创建线程池的构造方法
        return new ThreadPoolExecutor(
              // 核心线程数量:threads
              threads, 
              // 最大线程数量:threads
              threads, 
              // 非核心线程空闲时的存活时间等于0
              0, 
              // 非核心线程空闲时的存活时间等于0,单位:毫秒
              TimeUnit.MILLISECONDS,
              // 存放任务的阻塞队列
                      queues == 0 ? new SynchronousQueue<Runnable>() :
                              (queues < 0 ? new LinkedBlockingQueue<Runnable>()
                                      : new LinkedBlockingQueue<Runnable>(queues)),
              // 创建线程的工厂
              new NamedInternalThreadFactory(name, true), 
              // 带有导出线程堆栈的拒绝策略,内部继承了 AbortPolicy 抛异常策略
              new AbortPolicyWithReport(name, url)
        );
    }
}

固定数量的默认值是 200,也就是说在某时刻最大能并行处理 200 个任务,假设每个任务耗时 1 秒,相当于这台机器的单机 QPS = 200,假设每个任务耗时 5 秒,那这台机器的单机 QPS = 40。

2.2 LimitedThreadPool, 有限制数量的线程池

// org.apache.dubbo.common.threadpool.support.limited.LimitedThreadPool
// 有限制数量的线程池
public class LimitedThreadPool implements ThreadPool {

    @Override
    public Executor getExecutor(URL url) {
        // 获取一些参数变量
        String name = url.getParameter("threadname", (String) url.getAttribute("threadname", "Dubbo"));
        int cores = url.getParameter("corethreads", 0);
        int threads = url.getParameter("threads", 200);
        int queues = url.getParameter("queues", 0);
        
        // 调用创建线程池的构造方法
        return new ThreadPoolExecutor(
              // 核心线程数量:cores
              cores, 
              // 最大线程数量:threads
              threads, 
              // 非核心线程空闲时的永久存活
              Long.MAX_VALUE, 
              // 非核心线程空闲时的存活时间,单位:毫秒
              TimeUnit.MILLISECONDS,
              // 存放任务的阻塞队列
                      queues == 0 ? new SynchronousQueue<Runnable>() :
                              (queues < 0 ? new LinkedBlockingQueue<Runnable>()
                                      : new LinkedBlockingQueue<Runnable>(queues)),
              // 创建线程的工厂
              new NamedInternalThreadFactory(name, true), 
              // 带有导出线程堆栈的拒绝策略,内部继承了 AbortPolicy 抛异常策略
              new AbortPolicyWithReport(name, url)
        );
    }
}

2.3 CachedThreadPool 缓存线程池

// org.apache.dubbo.common.threadpool.support.cached.CachedThreadPool
// 缓存一定数量的线程池
public class CachedThreadPool implements ThreadPool {

    @Override
    public Executor getExecutor(URL url) {
        // 获取一些参数变量
        String name = url.getParameter("threadname", (String) url.getAttribute("threadname", "Dubbo"));
        int cores = url.getParameter("corethreads", 0);
        int threads = url.getParameter("threads", Integer.MAX_VALUE);
        int queues = url.getParameter("queues", 0);
        int alive = url.getParameter("alive", 60 * 1000);
        
        // 调用创建线程池的构造方法
        return new ThreadPoolExecutor(
              // 核心线程数量:cores
              cores, 
              // 最大线程数量:threads
              threads, 
              // 非核心线程空闲时的存活时间
              alive, 
              // 非核心线程空闲时的存活时间,单位:毫秒
              TimeUnit.MILLISECONDS,
              // 存放任务的阻塞队列
                      queues == 0 ? new SynchronousQueue<Runnable>() :
                              (queues < 0 ? new LinkedBlockingQueue<Runnable>()
                                      : new LinkedBlockingQueue<Runnable>(queues)),
              // 创建线程的工厂
              new NamedInternalThreadFactory(name, true), 
              // 带有导出线程堆栈的拒绝策略,内部继承了 AbortPolicy 抛异常策略
              new AbortPolicyWithReport(name, url)
        );
    }
}

2.4 EagerThreadPool渴望数量的线程池

// org.apache.dubbo.common.threadpool.support.eager.EagerThreadPool
// 渴望数量的线程池
public class EagerThreadPool implements ThreadPool {

    @Override
    public Executor getExecutor(URL url) {
        // 获取一些参数变量
        String name = url.getParameter("threadname", (String) url.getAttribute("threadname", "Dubbo"));
        int cores = url.getParameter("corethreads", 0);
        int threads = url.getParameter("threads", Integer.MAX_VALUE);
        int queues = url.getParameter("queues", 0);
        int alive = url.getParameter("alive", 60 * 1000);

        // 初始化队列和线程池
        TaskQueue<Runnable> taskQueue = new TaskQueue<Runnable>(queues <= 0 ? 1 : queues);
        EagerThreadPoolExecutor executor = new EagerThreadPoolExecutor(
                // 核心线程数量:cores
                cores,
                // 最大线程数量:threads
                threads,
                // 非核心线程空闲时的存活时间
                alive,
                // 非核心线程空闲时的存活时间,单位:毫秒
                TimeUnit.MILLISECONDS,
                // 存放任务的阻塞队列
                taskQueue,
                // 创建线程的工厂
                new NamedInternalThreadFactory(name, true),
                // 带有导出线程堆栈的拒绝策略,内部继承了 AbortPolicy 抛异常策略
                new AbortPolicyWithReport(name, url));
        // 将队列和线程池建立联系
        taskQueue.setExecutor(executor);
        return executor;
    }
}
                  ↓
// org.apache.dubbo.common.threadpool.support.eager.TaskQueue#offer
// 尝试添加任务至队列
                  
@Override
public boolean offer(Runnable runnable) {
    // 参数必要性检查,若线程池对象为 null 则抛出异常
    if (executor == null) {
        throw new RejectedExecutionException("The task queue does not have executor!");
    }
    
    // 获取线程池中工作线程 worker 的数量
    int currentPoolThreadSize = executor.getPoolSize();
    // have free worker. put task into queue to let the worker deal with task.
    // 若线程池中活跃的数量小于 worker 的数量,
    // 说明有些 worker 是闲置状态,没有活干
    // 因此把任务添加到队列后,线程就有机会被分派到任务继续干活了
    if (executor.getActiveCount() < currentPoolThreadSize) {
        return super.offer(runnable);
    }
    
    // return false to let executor create new worker.
    // 还能来到这里,说明目前所有的 worker 都在处于工作状态
    // 那么继续看 worker 的数量和最大线程数量想比,若偏小的话
    // 那么就返回 false 表示需要继续创建 worker 来干活
    // 至于为什么返回 false 就能创建 worker 来继续干活,请看下面的 execute 方法
    if (currentPoolThreadSize < executor.getMaximumPoolSize()) {
        return false;
    }
    
    // currentPoolThreadSize >= max
    // 还能来到这里,说明已经达到了最大线程数量了,
    // 那该放队列就放队列,队列放不下的的话,又没有非核心线程了,那就走拒绝策略了
    return super.offer(runnable);
}
// java.util.concurrent.ThreadPoolExecutor#execute
// 线程池添加任务的方法
// 解释:currentPoolThreadSize < executor.getMaximumPoolSize() 这行代码
//      为什么返回 false 就能创建 worker 来继续干活
// 原理:在 workQueue.offer(command) 返回 false 后继续走下面的
//      else if (!addWorker(command, false)) 尝试添加 worker 工作线程,
//      添加成功了,那就执行任务,添加不成功了,说明已达到了最大线程数量,走拒绝策略
  
public void execute(Runnable command) {
    // 若任务 command 对象为 null 的话,是不合法的,直接抛出 NPE 异常
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    
    // 若工作线程的数量小于核心线程的数量的话
    if (workerCountOf(c) < corePoolSize) {
        // 则添加核心线程,addWorker(command, true) 中的 true 表示创建核心线程
        // 添加成功就结束该 execute 方法流程了
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    
    // 若还能来到这里,说明工作线程数量已经达到了核心线程的数量了
    // 再来的任务就只能尝试添加至任务阻塞队列了
    // 调用队列的 offer 方法尝试看看能否添加至任务队列
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        // 若添加成功了,但是呢,线程池不处于运行状态,还得将任务移除,
        // 然后执行拒绝策略
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    
    // 还能来到这里,说明线程池处于运行状态,但是尝试添加至队列 offer 失败了
    // 那么就再次尝试调用 addWorker(command, false) 来创建非核心线程来执行任务
    // 尝试添加失败的话,再走拒绝策略
    else if (!addWorker(command, false))
        reject(command);
}

3. 线程池监控改造

Dubbo 线程池什么时候耗尽,可能是无法预测的,但是我们可以监控,提前预防,比如当活跃线程数量与最大线程满足多少百分比时,记录一个打点


// 自定义监控固定数量的线程池
@Slf4j
public class MonitorFixedThreadPool extends FixedThreadPool implements Runnable {

    private static final Set<ThreadPoolExecutor> EXECUTOR_SET = new HashSet<>();
    
    /** <h2>高水位线阈值</h2> **/
    private static final double HIGH_WATER_MARK = 0.85;
    
    // 默认的构造方法,借用该构造方法创建一个带有轮询机制的单线程池
    public MonitorFixedThreadPool() {
        Executors.newSingleThreadScheduledExecutor()
                .scheduleWithFixedDelay(
                        // 当前的 MonitorFixedThreadPool 对象自己
                        this,
                        // 启动后 0 秒执行一次
                        0,
                        // 每间隔 30 秒轮询检测一次
                        30,
                        // 单位:秒
                        TimeUnit.SECONDS
                );
    }
    
    // 重写了父类的 FixedThreadPool 的 getExecutor 方法
    // 然后择机将返回值 executor 存储起来了
    @Override
    public Executor getExecutor(URL url) {
        // 通过 super 直接调用父类的方法,拿到结果
        Executor executor = super.getExecutor(url);
        
        // 针对结果进行缓存处理
        if (executor instanceof ThreadPoolExecutor) {
            EXECUTOR_SET.add((ThreadPoolExecutor) executor);
        }
        return executor;
    }
    
    @Override
    public void run() {
        // 每隔 30 秒,这个 run 方法被触发执行一次    
        for (ThreadPoolExecutor executor : EXECUTOR_SET) {
            // 循环检测每隔线程池是否超越高水位线        
            doCheck(executor);
        }
    }    
    
    // 检测方法
    private void doCheck(ThreadPoolExecutor executor) {
        final int activeCount = executor.getActiveCount();
        int maximumPoolSize = executor.getMaximumPoolSize();
        double percent = activeCount / (maximumPoolSize * 1.0);
        
        // 判断计算出来的值,是否大于高水位线
        if (percent > HIGH_WATER_MARK) {
            log.info("溢出高水位线:activeCount={}, maximumPoolSize={}, percent={}",
                    activeCount, maximumPoolSize, percent);
                    
            // 记录打点,将该信息同步值 Cat 监控平台
            CatUtils.logEvent("线程池溢出高水位线",
                    executor.getClass().getName(),
                    "1", buildCatLogDetails(executor));
        }
    }    
}

// 资源目录文件
// 路径为:/META-INF/dubbo/org.apache.dubbo.common.threadpool.ThreadPool
monitorfixed=com.hmilyylimh.cloud.threadpool.config.MonitorFixedThreadPool


// 修改 Java 代码配置类指定使用该监控线程池
// 或
// dubbo.provider.threadpool=monitorfixed
@Bean
public ProtocolConfig protocolConfig(){
    ProtocolConfig protocolConfig = new ProtocolConfig("dubbo", 28260);
    protocolConfig.setThreadpool("monitorfixed");
    return protocolConfig;
}