Skip to main content

18小节:通过本地日志打印实现动态线程池监控

作者:程序员马丁

在线博客:https://nageoffer.com

note

热门项目实战社群,收获国内众多知名公司面试青睐,近千名同学面试成功!助力你在校招或社招上拿个offer。

通过本地日志打印实现动态线程池监控,元数据信息:

©版权所有 - 拿个offer-开源&项目实战星球专属学习项目,依据《中华人民共和国著作权法实施条例》《知识星球产权保护》,严禁未经本项目原作者明确书面授权擅自分享至 GitHub、Gitee 等任何开放平台。违者将面临法律追究。


内容摘要:本文详细介绍 oneThread 动态线程池框架的监控功能设计与实现,重点阐述本地日志监控方式的架构设计。通过定时任务调度运行时信息采集多维度指标监控,实现了线程池运行状态的全面可观测性。

课程目录如下所示:

  • 前言
  • 监控架构设计
  • 本地日志监控实现
  • 运行时信息采集优化
  • 文末总结

前言

在日常开发中,虽然 JDK 自带的线程池够用,但要说配置灵活性和监控能力,确实有点拉垮,很多时候我们连它是“健康”还是“爆了”都不清楚。

举个例子:

某天晚上 10 点,支付系统突然收到告警,说下游消息堆积严重,消费速度明显下降。但你打开日志一看,线程池既没报错、也没异常栈,表面上一切正常。你开始排查网络、排查 MQ、甚至重启服务,最后才发现——是线程池的处理能力(比如队列堆积)到了瓶颈,导致任务消费变慢。

虽然现在我们可以通过拒绝策略告警、线程活跃度告警、队列使用率告警等机制及时发现部分异常,但如果缺乏对线程池运行状态的持续观测与数据分析能力,很多问题依然难以深入理解。

比如:

  • 为什么这个线程池在某些时段会频繁打满?是核心线程数设置不合理?还是任务突发增长?
  • 某个接口偶发超时,是否跟线程池的排队时间过长有关?
  • 同一个线程池服务多个任务,是否存在“某类任务占满资源、其他任务被饿死”的情况?
  • 系统扩容之后线程池是否真的起效?性能瓶颈有没有缓解?

这些都需要依赖细粒度的指标监控和趋势分析,仅靠“事发时的告警”远远不够。没有监控,开发人员对线程池的理解就是一片黑盒,排查问题只能靠猜,做性能调优也没底。

所以我们强调,线程池监控不是只为了报警,它更重要的价值在于:

  • 辅助定位问题:出故障时,能看到是线程数打满了,还是队列堆积了;
  • 支持容量规划:通过长期趋势判断线程池配置是否合理;
  • 洞察系统瓶颈:比如是否存在某些任务执行时间异常拉长,影响整体调度效率。

这些能力,才是一个成熟线程池监控体系真正应该具备的。本文就带大家看看日志监控Micrometer 两套机制的设计思路。

监控架构设计

oneThread 的监控功能采用了分层架构设计,主要包括以下几个核心组件:

在设计监控这块的时候,我们主要坚持了几个原则:

  • 职责清晰:谁负责啥一目了然,每个模块只干自己的事。
  • 易扩展:以后想加新的监控方式,直接扩展就行,不用改老代码。
  • 性能优先:监控再重要,也不能拖慢业务,监控所需要执行的代码的都尽量轻量。
  • 配置控制:所有监控行为都靠配置说了算,开关灵活,调整方便。

本地日志监控实现

1. 定时任务调度机制

本地日志监控的核心是定时任务调度机制,通过ScheduledExecutorService实现周期性监控:

@Slf4j
public class ThreadPoolMonitor {

private ScheduledExecutorService scheduler;
private Map<String, ThreadPoolRuntimeInfo> micrometerMonitorCache;

/**
* 启动定时检查任务
*/
public void start() {
BootstrapConfigProperties.MonitorConfig monitorConfig =
BootstrapConfigProperties.getInstance().getMonitorConfig();

if (!monitorConfig.getEnable()) {
return;
}

// 初始化监控相关资源
micrometerMonitorCache = new ConcurrentHashMap<>();
scheduler = Executors.newScheduledThreadPool(
1,
ThreadFactoryBuilder.builder()
.namePrefix("scheduler_thread-pool_monitor")
.build()
);

// 每指定时间检查一次,初始延迟0秒
scheduler.scheduleWithFixedDelay(() -> {
Collection<ThreadPoolExecutorHolder> holders = OneThreadRegistry.getAllHolders();
for (ThreadPoolExecutorHolder holder : holders) {
ThreadPoolRuntimeInfo runtimeInfo = buildThreadPoolRuntimeInfo(holder);

// 根据采集类型判断
if (Objects.equals(monitorConfig.getCollectType(), "log")) {
logMonitor(runtimeInfo);
} else if (Objects.equals(monitorConfig.getCollectType(), "micrometer")) {
micrometerMonitor(runtimeInfo);
}
}
}, 0, monitorConfig.getCollectInterval(), TimeUnit.SECONDS);
}
}

2. 本地日志输出实现

本地日志监控的实现相对简单,但需要确保日志格式的规范性和可读性:

解锁付费内容,👉 戳