(篇一)线程池管理-线程池核心指标监控

525 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情

前言

为了提升系统的性能运行,节省资源开销,对于一些任务处理,大家习惯用线程池来实现任务的异步化。

但是线程池到底应该配置多大,各种参数(如:corePoolSize, maximumPoolSize, workQueue等)应该是多少,在不同的场景下,参数肯定不同。

常见的一些配置建议,如:

* 1. IO密集型的任务,并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数*2, 或者CPU核数 /(1 - 阻系数)

* 2. CPU密集型的任务,线程数等于CPU数为最大效率(因为频繁的切换线程也要消耗时间)

但有没有更好的配置的建议呢?

答案是有,而且可以更加准确合理的对系统中线程池去进行调配?

1. 核心思路

要想对线程池实现动态管理,第一步就是实现对线程池的状态监控。 你要知道当前线程池负载的运行状况,才能更好的进行调配。

1.1 应该监控线程池什么参数?

我们只关心线程池的关键参数,在ThreadPoolExecutor中有很多参数。 但是对我们更重要的就是以下几点:

  • 线程池是不是满负载
  • 线程池核心线程数是不是足够(如果不够,那么线程池就会创建新的线程)
  • 队列是否充足(反映了任务堆积情况)
  • 常驻有效线程(反映了系统的常规运行负载) 所以一些核心的参数还是我们需要关注的,比如:corePoolSize, queue.size

1.2 如何获取线程池的监控指标?

要想监控到线程池的状态,首先要将线程池注册到我们的系统中去。 即创建线程池时要显式的进行注册。示例如下:

// 以线程池名称为Key,以线程池对象为Value,进行注册,如果本级用,可以使用ConcurrentHashMap等进行管理
public void register(String threadPoolName, ThreadPoolExecutor executor);

1.3 如何监控?

所谓监控,其实简单理解,就是如何将我们关心的数据,能够定时定量的,把当前的状态反馈给我们。 可以是通过taskScheduledThreadPoolExecutor, 也可以写脚本运行,甚至通过延时MQ都可以。

2 案例实践

2.1 注册

/**
 * 存储了所有的管理线程信息
 */
public static final Map<String, ThreadPoolExecutor> MONITOR_INFO = Maps.newConcurrentMap();

/**
 * 监控线程池 核心参数
 * @param monitorKey 线程池名称
 * @param threadPoolExecutor 具体的线程池参数信息
 */
public void register(String monitorKey, ThreadPoolExecutor threadPoolExecutor) {
    try {
        Preconditions.checkNotNull(monitorKey, "监控Key不能为空");
        Preconditions.checkNotNull(threadPoolExecutor, "监控线程池不能为空");

        // 拼接线程池监控指数前缀[这里按照业务,可以指定业务前缀,我这里就采用了直接赋值]
        final String fullMonitorKey = monitorKey;

        MONITOR_INFO.put(fullMonitorKey, threadPoolExecutor);

    } catch (Exception exception) {
        LOGGER.error("线程池运行参数监控失败, monitorKey={}", monitorKey, exception);
    }
}

2.2 监控


/**
 * 开始进行监控计数
 * 【注意:这里我是通过一个ScheduledThreadPoolExecutor进行触发的,每隔30s执行一次】
 */
public void startMonitor() {
    // 这里通过unmodifiableMap方法,保证统计过程中的数据是不变的
    final Map<String, ThreadPoolExecutor> threadPoolExecutorMap = Collections.unmodifiableMap(MONITOR_INFO);
    if (threadPoolExecutorMap.isEmpty()) {
        return;
    }

    for (Map.Entry<String, ThreadPoolExecutor> entry : threadPoolExecutorMap.entrySet()) {
        THREAD_COUNTER.clear();

        String monitorKey = entry.getKey();

        final ThreadPoolExecutor threadPoolExecutor = entry.getValue();
        if (threadPoolExecutor.isShutdown()) {
            // 防止内存泄露
            MONITOR_INFO.keySet().removeIf(key -> key.equals(monitorKey));
            continue;
        }

        // 系统默认监控指标【这里也就是我们要统计的核心参数了】
        THREAD_COUNTER.put(join(monitorKey, MonitorProperty.CORE_POOL_SIZE), threadPoolExecutor.getCorePoolSize());
        THREAD_COUNTER.put(join(monitorKey, MonitorProperty.MAXIMUM_POOL_SIZE), threadPoolExecutor.getMaximumPoolSize());
        THREAD_COUNTER.put(join(monitorKey, MonitorProperty.POOL_SIZE), threadPoolExecutor.getPoolSize());
        THREAD_COUNTER.put(join(monitorKey, MonitorProperty.TASK_COUNT), threadPoolExecutor.getTaskCount());
        THREAD_COUNTER.put(join(monitorKey, MonitorProperty.QUEUE_SIZE), threadPoolExecutor.getQueue().size());
        THREAD_COUNTER.put(join(monitorKey, MonitorProperty.ACTIVE_COUNT), threadPoolExecutor.getActiveCount());
        THREAD_COUNTER.put(join(monitorKey, MonitorProperty.LARGEST_POOL_SIZE), threadPoolExecutor.getLargestPoolSize());
    }
}

// 拼接Key
private String join(String... keys) {
    return StringUtils.join(keys, "_"); // commons-lang3.jar中的StringUtils
}

2.3 统计及上报

上一步中,相当于把所有的监控指标可以拿到了。 这里其实就可以根据公司的组件不同来制定不同的统计策略了。 这里举例说明一下:

2.3.1 数据库

上一步中的THREAD_COUNTER其实就是一些核心的数据。我们可以直接按照这种数据格式的维度,存储到常见的数据库中,然后有另外的展示面板进行展示,或者定时导出excel进行离线分析。

2.3.2 实时监控平台

如果你们公司有自己的数据看板,可以直接将这些数据转义公司指定的格式进行上报,就可以实时监测数据看板了

2.3.3 单机写入,离线分析

可以直接将数据写到一个文件服务器的文件中,后续手动进行分析。

3 总结

其实这一步就是通过量化的数据,观测到线程池中的线程状态。从而判断当前的线程池参数配置是否合理。

下一篇会从另一个维度继续和大家探讨对线程池状态的管理,以及一个JDK比较核心的类ManagementFactory

当前,前言中提到的如何做到动态管理,后续也会进行说明。

附:

如果文中有描述失误内容,或者没有描述清楚的,可以将问题发我邮箱,harveytuan@163.com, 如果有其他问题,也可以联系我,大家一起共同讨论。

  • 愿大家共同进步,共同成长。