从零到一实现一个简单的动态线程池(2)

0 阅读14分钟

上一篇文章我们解决了动态线程池的核心设计,包括怎么管理动态线程池, 怎么解决一些原始JDK带来的常见问题,类似无法扩容阻塞队列, 拒绝策略需要增强等 有兴趣可以看一下上一篇的内容 juejin.cn/post/759586…

那么到现在为止 其实我们的动态线程池已经初具规模 可以实现nacos修改配置后线上环境实时更新 但是我们还缺少一个关键的实现 ----可视化

比如说市面上的消息队列都有设计一些可视化页面来实现这种动态观测 以及操作 这篇文章来分享一些设置可视化页面以及日志打印的经验:

首先来聊一下日志打印 其实很简单就是定时采集动态线程池的参数 打印到控制台 来实现一个可观测化 我们可以使用原生JDK自带的 ScheduledExecutorService 来实现:

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 动态线程池监控守护进程
 */
public class DyThreadPoolMonitor {

    // 1. 存储所有被管理的线程池 (Key: 线程池名称/ID)
    private static final Map<String, ThreadPoolExecutor> POOL_REGISTRY = new ConcurrentHashMap<>();

    // 2. 定时调度器 (单线程即可,因为只是做配置检查,很快)
    private final ScheduledExecutorService scheduler;

    public DyThreadPoolMonitor() {
        // 使用自定义 ThreadFactory 命名线程,方便 jstack 排查问题
        this.scheduler = new ScheduledThreadPoolExecutor(1, new ThreadFactory() {
            private final AtomicInteger seq = new AtomicInteger(0);
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r, "dy-thread-monitor-" + seq.getAndIncrement());
                t.setDaemon(true); // 设置为守护线程,随主应用关闭
                return t;
            }
        });
    }

    /**
     * 启动监控任务
     * @param initialDelay 初始延迟
     * @param period 执行间隔
     * @param unit 时间单位
     */
    public void start(long initialDelay, long period, TimeUnit unit) {
        // 使用 scheduleWithFixedDelay 而不是 AtFixedRate
        // 确保上一次任务执行完,歇一会儿再执行下一次,防止堆积
        scheduler.scheduleWithFixedDelay(this::checkAndUpdateConfigs, initialDelay, period, unit);
        System.out.println(">>> DyThread 监控守护线程已启动...");
    }

    /**
     * 注册线程池到监控列表
     */
    public static void register(String poolName, ThreadPoolExecutor executor) {
        POOL_REGISTRY.put(poolName, executor);
    }

    /**
     * 核心逻辑:轮询检查并更新
     */
    private void checkAndUpdateConfigs() {
        try {
            for (Map.Entry<String, ThreadPoolExecutor> entry : POOL_REGISTRY.entrySet()) {
                String poolName = entry.getKey();
                ThreadPoolExecutor executor = entry.getValue();

                // 1. 模拟从配置中心 (Redis/Nacos/DB) 获取最新配置
                // 在实际项目中,这里替换为 configService.getConfig(poolName)
                ThreadPoolConfig newConfig = mockGetConfigFromRemote(poolName);

                // 2. 检查是否需要更新
                if (isChanged(executor, newConfig)) {
                    System.out.printf("[%s] 检测到配置变更,准备刷新参数...%n", poolName);
                    resizeThreadPool(executor, newConfig);
                }
            }
        } catch (Throwable t) {
            // 🌟 重点:必须捕获所有异常!
            // 如果 Runnable 抛出未捕获异常,ScheduledExecutorService 会静默停止后续调度,任务就挂了。
            System.err.println("DyThread 监控任务发生异常,但不影响下次调度: " + t.getMessage());
            t.printStackTrace();
        }
    }

    /**
     * 安全地更新线程池参数
     * 注意:JDK 的 setCorePoolSize 和 setMaximumPoolSize 有校验逻辑,顺序错了会报错
     */
    private void resizeThreadPool(ThreadPoolExecutor executor, ThreadPoolConfig config) {
        // 核心思路:如果要调大,先调 Max 再调 Core;如果要调小,先调 Core 再调 Max
        // 这里为了简单通用,采用了比较保守的写法,也可以直接 set,但在极端并发下可能会报 IllegalArgumentException

        int oldCore = executor.getCorePoolSize();
        int oldMax = executor.getMaximumPoolSize();
        int newCore = config.corePoolSize;
        int newMax = config.maxPoolSize;

        // 1. 修改 Core 和 Max
        // 如果新 Core > 旧 Max,必须先提升 Max,否则 setCorePoolSize 会报错
        if (newMax > oldMax) {
            executor.setMaximumPoolSize(newMax);
            executor.setCorePoolSize(newCore);
        } else {
            // 如果新 Max < 旧 Core,必须先降低 Core,否则 setMaximumPoolSize 会报错
            executor.setCorePoolSize(newCore);
            executor.setMaximumPoolSize(newMax);
        }
        
        // 2. 修改队列容量 (需使用自定义的可调整容量的 Queue,JDK 默认的 LinkedBlockingQueue capacity 是 final 的)
        // executor.getQueue().setCapacity(config.queueCapacity); 
        
        // 3. 修改存活时间
        if (config.keepAliveTime != executor.getKeepAliveTime(TimeUnit.SECONDS)) {
            executor.setKeepAliveTime(config.keepAliveTime, TimeUnit.SECONDS);
        }

        System.out.printf(">>> 更新成功. Old:[%d-%d], New:[%d-%d]%n", 
                oldCore, oldMax, executor.getCorePoolSize(), executor.getMaximumPoolSize());
    }

    // --- 辅助方法 ---

    private boolean isChanged(ThreadPoolExecutor executor, ThreadPoolConfig config) {
        return executor.getCorePoolSize() != config.corePoolSize ||
               executor.getMaximumPoolSize() != config.maxPoolSize ||
               executor.getKeepAliveTime(TimeUnit.SECONDS) != config.keepAliveTime;
    }

    // 模拟配置类
    static class ThreadPoolConfig {
        int corePoolSize;
        int maxPoolSize;
        long keepAliveTime;

        public ThreadPoolConfig(int core, int max, long keep) {
            this.corePoolSize = core;
            this.maxPoolSize = max;
            this.keepAliveTime = keep;
        }
    }

    // 模拟远程获取配置
    private ThreadPoolConfig mockGetConfigFromRemote(String poolName) {
        // 模拟:让配置动态变化
        // 实际开发中,这里是 return restTemplate.getForObject("http://config-center/...", ...);
        if (System.currentTimeMillis() % 10000 < 5000) {
            return new ThreadPoolConfig(5, 10, 60);
        } else {
            return new ThreadPoolConfig(10, 20, 120);
        }
    }
}

这样就实现了简单的本地日志打印

虽然日志监控在问题排查时很有用,但在生产环境中,我们更需要的是专业的监控体系集成

现在列举一个具体的案例帮助你理解动态线程池的可观测优势

你的手机突然响起告警铃声——Grafana 监控面板显示某个核心业务线程池的活跃线程数持续飙升,队列堆积严重。你立即打开监控大盘,通过时间序列图表清晰地看到:该线程池的 active.size 指标从正常的 5-10 逐步攀升到 50,同时 queue.size 也从 0 增长到 500+。更关键的是,通过多维度标签筛选,你快速定位到是 order-service 应用的 payment-processor 线程池出现了异常

面对这种突发流量,传统的静态配置束手无策,这正是引入动态线程池的必要时刻。

所以我们需要一种可视化的动态的检测系统 恰好actuator能做到监听数据这一点

在 Spring Boot 生态中,Actuator 是一个非常强大的监控和管理组件。如果说 Spring Boot 帮你快速搭建了应用的“身体”,那么 Actuator 就是给这个身体安装了一套“仪表盘”和“体检系统

我们可以引入它的依赖然后在项目启动的时候通过端口访问来接收参数并展示 值得一提的是Micrometer在Spring Boot 2.x 之后,两者深度集成,形成了“门面模式”的关系。以下是它们关系的详细拆解:- Micrometer (类比:SLF4J) 它是指标收集的门面(Facade) 。它定义了一套标准的 API(如 Counter, Timer, Gauge),让开发者可以统一编写监控代码,而不需要关心底层的监控系统是 Prometheus、Datadog 还是 Graphite。

  • Spring Boot Actuator (类比:管理中心) 它是 Spring Boot 的一个子项目,旨在提供“生产就绪”的特性。它不仅包含指标(Metrics),还包含健康检查(Health)、环境信息(Env)、线程堆栈(Thread Dump)等功能。

下面是实现简单检测线程池参数的Demo 主要利用了Metrics.gauge这个方法 我们需要创建一个单例 Bean,专门负责将线程池注册到 MeterRegistry

import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tags;
import org.springframework.stereotype.Component;

import java.util.concurrent.ThreadPoolExecutor;

/**
 * 动态线程池监控采集器
 * 核心作用:将线程池的各项指标绑定到 Micrometer
 */
@Component
public class DyThreadMetrics {

    private final MeterRegistry registry;

    public DyThreadMetrics(MeterRegistry registry) {
        this.registry = registry;
    }

    /**
     * 将某个线程池注册到监控系统
     * @param threadPoolId 线程池ID (作为 Label/Tag)
     * @param executor 线程池实例
     */
    public void bind(String threadPoolId, ThreadPoolExecutor executor) {
        // 定义通用的标签:pool_name = "order-service"
        Tags tags = Tags.of("pool_name", threadPoolId);

        // 1. 监控核心线程数 (动态变化的,所以用 Gauge)
        Gauge.builder("dythread.core.size", executor, ThreadPoolExecutor::getCorePoolSize)
                .tags(tags)
                .description("动态线程池-核心线程数")
                .register(registry);

        // 2. 监控最大线程数
        Gauge.builder("dythread.max.size", executor, ThreadPoolExecutor::getMaximumPoolSize)
                .tags(tags)
                .description("动态线程池-最大线程数")
                .register(registry);

        // 3. 监控当前活跃线程数
        Gauge.builder("dythread.active.count", executor, ThreadPoolExecutor::getActiveCount)
                .tags(tags)
                .description("动态线程池-活跃线程数")
                .register(registry);

        // 4. 监控队列积压数量
        Gauge.builder("dythread.queue.size", executor, e -> e.getQueue().size())
                .tags(tags)
                .description("动态线程池-队列积压数")
                .register(registry);
        
        // 5. 监控队列剩余容量 (非常重要,用于判断是否快满了)
        Gauge.builder("dythread.queue.remaining", executor, e -> e.getQueue().remainingCapacity())
                .tags(tags)
                .description("动态线程池-队列剩余容量")
                .register(registry);

        // 6. 历史拒绝任务数 (需要你之前的 SupportThreadPoolExecutor 提供 getRejectCount 方法)
        // 假设 executor 已经是增强过的类型
        /*
        if (executor instanceof SupportThreadPoolExecutor) {
            Gauge.builder("dythread.reject.count", (SupportThreadPoolExecutor) executor, SupportThreadPoolExecutor::getRejectCount)
                    .tags(tags)
                    .description("动态线程池-累计拒绝次数")
                    .register(registry);
        }
        */
    }
}

启动项目后,访问浏览器或使用 curl: http://localhost:8080/actuator/prometheus

你应该能看到类似下面的输出

HELP dythread_active_count 动态线程池-活跃线程数 # TYPE dythread_active_count gauge dythread_active_count{application="link-ops",pool_name="order-service",} 2.0 # HELP dythread_core_size 动态线程池-核心线程数 # TYPE dythread_core_size gauge dythread_core_size{application="link-ops",pool_name="order-service",} 4.0 # HELP dythread_queue_size 动态线程池-队列积压数 # TYPE dythread_queue_size gauge dythread_queue_size{application="link-ops",pool_name="order-service",} 15.0

值得注意的一点是你需要在yaml文件中配置暴露端点

  endpoints:
    web:
      exposure:
        include: "prometheus" # 开启 /actuator/prometheus 端点
  metrics:
    tags:
      application: ${spring.application.name} # 给所有指标加个应用名的标签,方便区分

“数据采集只是第一步。虽然我们现在能通过 API 获取线程池状态,但单纯的数字无法让我们在故障发生时快速定位问题。我们需要一种更直观的方式来‘看见’线程池的脉搏。

幸运的是,业界早已有成熟的解决方案。我们将引入 Prometheus 进行指标收集,并搭配 Grafana 进行数据可视化,只需几步即可实现毫秒级的动态观测页面。”

下面我通过一个例子来向你展示Prometheus的强大之处

周一早上 9 点,刚把咖啡泡好,运维同事的紧急消息就弹了出来:“订单服务的响应时间飙升,线程池好像扛不住了!”

在传统的监控体系下,这往往意味着一场手忙脚乱的硬仗:你需要 SSH 登录服务器,在海量日志中 grep 关键字,甚至使用 Arthas 等工具临时挂载来查看内存状态。这不仅效率低下,而且当你还在排查第一台机器时,可能故障已经扩散到了整个集群。

但如果你接入了 Prometheus,一切将截然不同。你只需优雅地打开控制台,输入一行简单的 PromQL:dynamic_thread_pool_active_size{application_name="order-service"}。瞬息之间,整个集群所有节点的线程池活跃度趋势便呈现在眼前——从“盲人摸象”到“上帝视角”,往往只需要这一行查询。

这就是 Prometheus 成为云原生时代监控事实标准的原因。它不仅仅是一个数据收集器,更是一个带有“时间维度”的系统分析师。相比于传统的 Zabbix 或 Graphite,Prometheus 在处理动态线程池这种高并发、易波动的指标时,展现出了压倒性的优势:

  • Pull(拉取)模式采集【核心优势】

    • 传统痛点:Push(推送)模式下,当业务系统负载过高(如线程池已满)时,负责推送监控数据的线程往往也会被阻塞,导致监控数据丢失,出现“由于故障而看不到故障”的盲区。
    • Prometheus 优势主动拉取将控制权掌握在监控服务端。即使应用濒临崩溃,Prometheus 也能通过拉取超时立即发现“实例存活异常”,这对于高并发下的故障感知至关重要。
  • 多维数据模型与 PromQL

    • 它不只是存储数字,而是存储“带标签的时间序列”。你可以轻松通过 pool_name(线程池名称)、instance(实例IP)、env(环境)等标签进行聚合。比如一键查询“生产环境所有订单服务核心线程池的平均利用率”,这是传统监控难以做到的。
  • 高效的时间序列存储 (TSDB)

    • 专为高频监控数据设计。动态线程池的指标(活跃数、队列大小)是秒级变化的,Prometheus 的 TSDB 引擎能以极高的压缩比存储这些海量数据,保证查询秒级响应。
  • 无缝的服务发现 (Service Discovery)

    • 在 K8s 或微服务架构中,服务实例是动态伸缩的。Prometheus 能自动感知新上线的服务节点并开始监控,无需人工修改配置,完美契合我们的“动态”主题。

Prometheus 介绍

在微服务和容器化技术(Kubernetes)大行其道的今天,Prometheus 已经成为了监控领域的“瑞士军刀”。它最初由 SoundCloud 开发,后来捐赠给了 CNCF(云原生计算基金会),是继 Kubernetes 之后的第二个毕业项目。

简单来说,Prometheus 是一个开源的系统监控和报警工具包。它不依赖分布式存储,单个节点就能处理每秒数百万次的数据点采集,非常适合监控高动态的云环境

1. 基于 Pull(拉取)的数据采集模式 这是 Prometheus 与传统监控(如 Zabbix、Graphite)最大的不同。

  • 传统 Push 模式:应用主动把数据“推”给监控服务器。如果应用挂了,数据推不过去;如果应用负载过高,推送动作可能会阻塞业务。

  • Prometheus Pull 模式:应用只需要暴露一个 HTTP 接口(如 /actuator/prometheus),Prometheus 服务器会按照设定的时间间隔(如每 15秒)主动来“拉”数据。

    • 优势:监控系统不会影响业务系统的性能。即使业务系统忙不过来,Prometheus 拉取超时也只是丢一个点的数据,不会导致业务崩盘。

2. 多维数据模型(Labels 标签机制) 这是 Prometheus 最强大的地方。它不再只是存储简单的 Key-Value(如 cpu_usage: 80%),而是支持通过**标签(Labels)**给数据打上各种维度的标记。

  • 在我们的动态线程池场景中,这显得尤为重要。我们可以定义如下指标:

    Plaintext

    dythread_active_count{app="order-service", pool_name="io-dense-pool", env="prod"} 20
    

    这样,你既可以查询“整个生产环境的线程活跃总数”,也可以精确查询“订单服务中 IO 密集型线程池的活跃数”。

3. 强大的 PromQL 查询语言 有了多维数据,就需要强大的查询语言。PromQL 允许你进行复杂的聚合、计算和预测。

  • 例子:你想知道过去 5 分钟内,线程池拒绝任务的速率增长了多少? rate(dythread_reject_count_total[5m]) 一行代码就能搞定,而不需要自己在代码里写复杂的滑动窗口算法。

4. 完善的生态系统

  • Exporter:对于数据库(MySQL)、中间件(Redis、Kafka)等无法直接修改代码的组件,Prometheus 提供了丰富的 Exporter 来“翻译”指标。
  • Service Discovery:在 K8s 等环境中,服务实例的 IP 是动态变化的。Prometheus 可以自动发现新上线的服务实例并开始监控,无需人工修改配置。

下面回到我们的动态线程池项目,Prometheus 在其中扮演了数据仓库的角色:

  1. 应用层:我们的 DyThreadMetrics 通过 Micrometer 将线程池的实时状态(活跃数、队列大小等)暂存在内存中。

  2. 暴露层:Spring Boot Actuator 将这些数据格式化为 Prometheus 可读的文本格式,暴露在 /actuator/prometheus 端点。

  3. 采集层:Prometheus 服务器定时(Scrape)访问这个端点,将数据抓取回来并写入自带的时序数据库(TSDB)。

  4. 展示层:最后,由 Grafana 连接 Prometheus 数据源,将这些枯燥的时间序列数据渲染成炫酷的动态仪表盘。

    下面给出大概代码流程 如果你要实现的话可能需要部署自己的Prometheus 和 Grafana 这里我用我自己的虚拟机举例 并给出构建的docker语句

首先我们需要拉一下Prometheus的镜像:

  --name prometheus \
  -p 9090:9090 \
  --entrypoint sh \
  -e TZ=Asia/Shanghai \
  prom/prometheus:v2.51.1 \
  -c 'echo "global:
  scrape_interval: 15s
  evaluation_interval: 15s
scrape_configs:
  - job_name: '\''prometheus'\''
    static_configs:
      - targets: ['\''localhost:9090'\'']
    
  - job_name: '\''dythread-app'\''
    metrics_path: '\''/actuator/prometheus'\''
    static_configs:
      # ⚠️ 注意这里:替换为外部机器的真实 IP的服务
      - targets: ['\''192.168.1.50:18080'\'']
    scrape_interval: 10s
    scrape_timeout: 5s
" > /etc/prometheus/prometheus.yml && /bin/prometheus --config.file=/etc/prometheus/prometheus.yml'

然后可以访问(http://192.168.159.135:9090/targets?search=)来进行查看看板 具体是这样的(记得把端口改成自己的)

image.png

UP是健康状态 由于我这里没有开启应用所以最上面为Down

然后是# Grafana的安装

docker run -d \  --name grafana \  --network monitoring \  -p 3000:3000 \ grafana/grafana:9.0.5

直接安装即可 注意我这里创建了自己的网络 这是为了方便grafana 与 Prometheus之间的通信

   docker network connect monitoring prometheus

访问对应端口即可

image.png

接下来点击右下角添加数据源

image.png

HTTP URL 处填写 http://prometheus:9090 即可,通过 Docker 内部网络进行通信,会自动将 prometheus 解析为对应的 IP 地址

接下来可以自己写一个模板JSON可视化页面 这里可以参考15755这个id的模板文件设计 最后大概是这样的页面

image.png

“回顾整个构建过程,我们完成了一次从理论到落地的全链路实践

  1. 破局:针对原生 JDK 的局限性,自定义队列与拒绝策略,实现内核级的动态化;
  2. 集成:利用 Spring Boot Starter 实现零侵入的自动装配,解决工程化难题;
  3. 洞察:接入 Prometheus 监控体系,实现毫秒级的数据检测与可视化。

这样一个麻雀虽小五脏俱全的框架,不仅解决了实际业务中的痛点,更构成了一个真正意义上的动态治理闭环。”