阈值触发告警规则

告警规则

告警策略如下所示:

维度 触发条件 检测含义
活跃度 activeCount / maximumPoolSize 连续高于阈值(默认 80%) 线程资源已逼近瓶颈,需扩容或对入口流量做限流
队列负载 queueSize / queueCapacity 超过阈值 排队任务激增,处理能力被入口流量压制,易引发大面积超时
拒绝异常 监控到新的 RejectedExecutionException 线程池已无法接收新任务,属于阻断场景,应立刻介入

实现告警检查器

1. 告警定时检查

线程池状态监控通常采用定时任务方式进行,以延迟换取业务稳定性。此类定时检查无需引入额外框架,JDK 提供的 ScheduledExecutorService 已能满足稳定的调度需求

ThreadPoolAlarmChecker 利用一个单线程的调度器,定期扫描系统中所有已注册线程池的运行状态,并针对启用了告警的线程池执行各类运行指标检测,及时触发相关告警处理

2. 活跃度告警

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 检查线程活跃度,当活跃线程数占最大线程数的比例超过阈值时,触发告警。
*/
private void checkActiveRate(ThreadPoolExecutorHolder holder) {
ThreadPoolExecutor executor = holder.getExecutor();
ThreadPoolExecutorProperties properties = holder.getExecutorProperties();
// 获取当前活跃线程数 (注意: getActiveCount() 内部有锁,高频率调用可能有性能影响)
int activeCount = executor.getActiveCount();
int maximumPoolSize = executor.getMaximumPoolSize();
if (maximumPoolSize == 0) {
return;
}
// 计算活跃度(百分比)
int activeRate = (int) Math.round((activeCount * 100.0) / maximumPoolSize);
// 获取活跃度告警阈值
int threshold = properties.getAlarm().getActiveThreshold();
// 如果活跃度超过阈值,则发送告警
if (activeRate >= threshold) {
sendAlarmMessage("Activity", holder);
}
}

3. 容量告警

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 检查队列使用率,当队列中任务数占总容量的比例超过阈值时,触发告警。
*/
private void checkQueueUsage(ThreadPoolExecutorHolder holder) {
ThreadPoolExecutor executor = holder.getExecutor();
ThreadPoolExecutorProperties properties = holder.getExecutorProperties();
BlockingQueue<?> queue = executor.getQueue();
// 当前队列中的任务数
int queueSize = queue.size();
// 队列总容量 = 当前任务数 + 剩余容量
int capacity = queueSize + queue.remainingCapacity();
// 如果容量为0(例如SynchronousQueue),则不进行此项检查
if (capacity == 0) {
return;
}
// 计算队列使用率(百分比)
int usageRate = (int) Math.round((queueSize * 100.0) / capacity);
// 获取队列使用率告警阈值
int threshold = properties.getAlarm().getQueueThreshold();
// 如果使用率超过阈值,则发送告警
if (usageRate >= threshold) {
sendAlarmMessage("Capacity", holder);
}
}

4. 拒绝策略告警

见后面

5. 告警参数组装

对公共的参数封装逻辑进行了抽象,提取成一个统一的方法

常见问题

1. 谁来调用 start() 方法启动检查?

需要 显式调用 start() 方法,以启动定时检查任务

常见做法包括在 SpringBoot 项目的启动回调(如 ApplicationRunnerInitializingBean)中调用;这样可以确保告警逻辑启动时,系统中已经存在可检查的线程池实例。实际上,我也是采用了相同的方式来管理线程池告警检查的生命周期

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class YsyThreadBaseConfiguration {
// ......
/**
* 注册 线程池运行状态报警检查器
*/
@Bean(initMethod = "start", destroyMethod = "stop")
public ThreadPoolAlarmChecker threadPoolAlarmChecker(NotifierDispatcher notifierDispatcher) {
return new ThreadPoolAlarmChecker(notifierDispatcher);
}
// ......
}

利用 initMethod = "start"destroyMethod = "stop",分别在 Bean 初始化与销毁阶段启动和停止定时检查任务

2. 如果上一次检查没有结束,下一次检查又来了怎么办?

ThreadPoolAlarmChecker 使用的是 JDK 自带的 ScheduledExecutorService,采用了 scheduleWithFixedDelay具体调度方式如下:

1
scheduler.scheduleWithFixedDelay(this::checkAlarm, 0, 5, TimeUnit.SECONDS);
  • 每次检查结束后再等待 5 秒,才会触发下一次执行

  • 若某次检查耗时较长,不会并发触发下一次,而是顺延执行,避免了重叠和堆积

3. 如果项目中动态线程池过多,是否会有影响?

当线程池数量较多时(例如数十个甚至上百个),确实可能对告警检查性能提出一定挑战。

  • 使用单线程定时调度器执行所有告警逻辑,避免在高并发场景中对业务线程产生干扰
  • 每次检查本质上是一次遍历 + 统计计算,即使线程池较多,也不会产生明显的 CPU 或内存压力

优化方法

对于性能和准确性要求更高的场景,还可以进一步引入优化手段,例如:

  • 将告警处理逻辑 异步化,避免阻塞检查线程
  • 引入 专用线程池,对需要检查的线程池的状态并发检查与告警发送
  • 配合 监控系统 进行异步指标上报和阈值告警判断

4. 运行过程中定时检查抛异常了怎么办?

在定时执行 checkAlarm() 的过程中,如果某个线程池实例状态异常、配置错误,或内部检查逻辑抛出未捕获异常,很可能会导致本次检查任务中断甚至整个定时调度器崩溃退出

为避免这种情况,ThreadPoolAlarmChecker 内部 可以采用 将所有检查逻辑包裹在统一的异常保护块中,确保单次任务失败不会影响调度器的存活性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void checkAlarm() {
   try {
       Collection<ThreadPoolExecutorHolder> holders = OneThreadRegistry.getAllHolders();
       for (ThreadPoolExecutorHolder holder : holders) {
           if (holder.getExecutorProperties().getAlarm().getEnable()) {
               checkQueueUsage(holder);
               checkActiveRate(holder);
               // ...
          }
      }
  } catch (Throwable t) {
       log.error("[oneThread] 线程池告警检查异常", t);
  }
}

使用 try-catch 捕获 Throwable,可以防止包括运行时异常和错误在内的所有异常中断调度线程

拒绝策略告警

代理模式

代理模式 是一种在 不修改原始类代码的前提下 ,通过 引入代理对象 对其行为进行增强的设计手段,非常适合用于 功能增强、权限控制、延迟加载 等场景

静态代理

但静态代理真的足够优雅吗?不妨冷静分析一下其局限性:

  • 类爆炸问题 :每一种 拒绝策略 都需要手动创建对应的代理类来实现增强逻辑。以 JDK 提供的 4 种默认策略为例,若都需要扩展,就得创建 4 个额外类,显然不符合开闭原则 ,也不利于维护
  • 侵入性较高,增加系统复杂度 :所有线程池都 必须显式使用 这些增强后的拒绝策略,一旦项目规模扩大或开发人员更替,容易遗漏代理逻辑或出现不一致实现,造成潜在的系统风险

Lambda 轻量级静态代理

Lambda 实现了一个更轻量级 的方案,不依赖反射,也不需要额外的代理类,仅通过一层静态包装就完成了功能增强

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public void setRejectedExecutionHandler(RejectedExecutionHandler handler) {
// 1. 创建一个“包装器” (Wrapper)
RejectedExecutionHandler handlerWrapper = new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// [核心动作 A]:偷摸记录一下拒绝次数
rejectCount.incrementAndGet();
// [核心动作 B]:继续执行原来的拒绝策略
handler.rejectedExecution(r, executor);
}
// 解决告警时获取拒绝策略名称的问题
@Override
public String toString(){
// 伪装到底:打印名字时,还是显示原来那个策略的名字
return handler.getClass().getSimpleName();
}
};
// 2. 将这个“加了料”的包装器,真正设置给线程池
super.setRejectedExecutionHandler(handlerWrapper);
}
对比维度 传统静态代理 Lambda 静态包装
是否需要代理类 ✅ 是(要写一个实现类) ❌ 否(用闭包或匿名函数直接实现)
可复用性 ✅ 高(代理类可用于多处) 限于作用域(适合局部包装)
可读性 ❌ 容易产生样板代码 ✅ 简洁清晰
接口要求 可代理多个方法 仅适用于函数式接口(如 RejectedExecutionHandler

可以这么理解:Lambda 实现本质上是一种“轻量级的静态代理”,特别适用于函数式接口的包装增强场景

可以动态代理,但是不需要搞得这么复杂