应区分拒绝原因:shutdown导致的归为运维事件,队列满且线程达上限才判过载;注入TracedRunnable等上下文提升告警可追溯性;通过异步ring buffer分级告警,防反压。

实现标准的 RejectedExecutionHandler 接口,核心是重写 rejectedExecution(Runnable r, ThreadPoolExecutor executor) 方法,在线程池拒绝任务时触发自定义逻辑。团队专属告警策略的关键在于:**区分拒绝原因、记录可追溯上下文、联动现有告警通道、避免二次阻塞**。
识别真实拒绝原因,避免误判
ThreadPoolExecutor 的拒绝可能发生在两种场景:队列已满 且 线程数已达 maximumPoolSize(典型饱和),或调用 shutdown() 后继续提交任务(生命周期异常)。不能一概视为“系统过载”。
- 通过
executor.isShutdown()和executor.isTerminated()判断是否处于关闭流程中,这类拒绝应归类为“运维事件”,走变更告警通道 - 若队列使用有界队列(如
ArrayBlockingQueue),检查executor.getQueue().remainingCapacity() == 0且executor.getActiveCount() >= executor.getMaximumPoolSize(),才确认为真实过载 - 对无界队列(如
LinkedBlockingQueue),拒绝只会在 shutdown 后发生,此时不应触发容量类告警
注入业务上下文,让告警可定位
原始 Runnable 通常不携带请求 ID、接口名、来源服务等信息,直接告警价值低。建议在提交任务前统一包装:
- 定义轻量级上下文持有类(如
TracedRunnable),构造时传入traceId、endpoint、callerService等字段,并委托执行原任务 - 在
rejectedExecution中尝试向下转型:if (r instanceof TracedRunnable) { ... },提取上下文并写入告警消息 - 若无法改造提交方,可结合
MDC(如 Logback)或ThreadLocal在任务执行前快照关键变量,但需注意内存泄漏风险
分级告警 + 异步上报,防止反压
拒绝本身是高危信号,但告警动作不能拖慢线程池主流程,更不能因告警失败导致任务丢失。
- 拒绝后立即记录本地 ring buffer 或 disruptor 队列(如
Log4j2 AsyncLogger),由独立线程批量消费并上报 - 设置分级阈值:单分钟内拒绝数 ≤ 3 → 记录审计日志;> 3 且
- 上报失败时本地落盘(如
RejectedTaskRecord序列化到磁盘文件),启动守护线程定时重试,避免依赖外部服务不可用导致告警丢失
提供快速止血能力,不止于通知
高级策略可嵌入自动干预能力,缩短 MTTR:
- 集成配置中心(如 Nacos/Apollo),在告警触发后动态降低该线程池的
corePoolSize或切换至降级队列(如仅允许内部管理任务) - 暴露 JMX 或 HTTP Endpoint(如
/actuator/reject-control),支持手动清空队列、暂停接收新任务、或切换拒绝策略为CALLER_RUNS临时缓解 - 拒绝时自动 dump 当前线程池状态(活跃线程数、队列大小、拒绝累计数、最近 10 条拒绝任务的上下文摘要),附加在告警消息中
不复杂但容易忽略的是:所有自定义逻辑必须是无锁、无阻塞、无异常抛出的。在 rejectedExecution 方法内 catch 所有异常并记录 error 日志,绝不能让 handler 自身失败导致线程池拒绝逻辑中断。

















