线上服务出现 大量 RejectedExecutionException + CPU 使用率 100% ,是典型的「线程池资源耗尽 + 系统过载」问题 —— 核心原因是 任务提交速率远超线程池处理能力,导致队列满、线程耗尽,触发拒绝策略;同时 CPU 100% 说明系统资源已被占满,线程池无法正常调度任务,形成 “恶性循环”。
下面按「紧急止血 → 根因定位 → 彻底解决 → 长效预防」四步走,提供可直接落地的方案:
一、紧急止血:先恢复服务可用性
线上故障优先 “止损”,避免业务持续受损,步骤如下:
1. 临时扩容线程池(快速缓解拒绝问题)
通过配置中心(Apollo/Nacos)或动态调整接口,紧急调整线程池参数(无需重启应用):
- 核心线程数(
corePoolSize):IO 密集型任务临时翻倍(如从 8 调至 16),CPU 密集型任务调至CPU 核心数 + 2(避免上下文切换过载); - 最大线程数(
maximumPoolSize):IO 密集型可调至CPU 核心数 × 4(如 32),CPU 密集型不超过CPU 核心数 × 2(避免 CPU 更拥堵); - 队列容量(
queueCapacity):有界队列临时扩容(如从 1000 调至 2000),但禁止用无界队列(会导致 OOM 二次故障); - 核心线程超时回收:确保
allowCoreThreadTimeOut(true),让空闲核心线程释放资源。
代码示例(动态调整) :
// 紧急调整线程池参数(可通过接口/配置中心触发)
public void emergencyAdjustThreadPool(ThreadPoolExecutor executor) {
int cpuCores = Runtime.getRuntime().availableProcessors();
// 临时调整核心线程数(IO密集型)
executor.setCorePoolSize(cpuCores * 4);
// 临时调整最大线程数
executor.setMaximumPoolSize(cpuCores * 6);
// 临时扩容队列(若队列支持动态调整,如自定义有界队列)
if (executor.getQueue() instanceof DynamicCapacityQueue) {
((DynamicCapacityQueue<?>) executor.getQueue()).setCapacity(2000);
}
log.warn("紧急扩容线程池:core={}, max={}, queueCapacity={}",
executor.getCorePoolSize(), executor.getMaximumPoolSize(), executor.getQueue().size());
}
2. 降级非核心任务(减少任务提交压力)
- 暂停非核心任务提交(如日志上报、统计分析、非实时数据同步);
- 核心任务降级:对非必须的外部依赖(如非核心查询)返回默认值,减少任务执行耗时;
- 限流入口:通过网关(如 Nginx、Spring Cloud Gateway)对非核心接口限流,减少下游服务任务提交量。
3. 临时扩容机器(缓解 CPU 过载)
若 CPU 100% 是集群资源不足导致,紧急扩容 1~2 台机器分担流量,降低单台机器的任务压力。
二、根因定位:找到问题核心
止血后,必须定位根本原因,避免问题复发,重点从「线程池配置、任务特性、系统资源」三个维度排查:
1. 排查线程池配置是否合理
(1)核心参数是否匹配任务类型
- 若任务是 CPU 密集型(如计算、加密):核心线程数 >
CPU 核心数 + 1→ 导致上下文切换频繁,CPU 100%; - 若任务是 IO 密集型(如 DB/HTTP 调用):核心线程数过少(如 <
CPU 核心数 × 2)→ 线程不足,任务堆积,队列满触发拒绝; - 队列是否为无界队列:若用
LinkedBlockingQueue无界模式 → 任务堆积导致 OOM 或 CPU 100%(GC 频繁)。
(2)拒绝策略是否合理
- 若用默认
AbortPolicy(直接抛异常)→ 队列满后立即拒绝,无缓冲机制,导致大量RejectedExecutionException; - 未做任务兜底(如持久化)→ 拒绝的任务直接丢失,业务受损。
2. 排查任务是否存在 “异常”
(1)任务执行耗时突增
- 用监控工具(如 SkyWalking、Pinpoint)查看任务执行耗时:若平均耗时从 10ms 增至 1s → 线程池处理能力骤降,任务堆积;
- 原因:外部依赖故障(如 DB 慢查询、第三方接口超时)、任务逻辑变更(如新增复杂计算)。
(2)任务提交速率突增
- 查看网关 QPS 监控:是否有流量峰值(如秒杀、爬虫攻击)→ 任务提交速率超过线程池吞吐量;
- 查看业务日志:是否有循环提交任务、重复提交任务的 bug → 任务量异常增长。
3. 排查系统资源是否过载
(1)CPU 100% 的具体原因
-
用
top命令查看进程 CPU 占用:确认是应用进程导致,还是其他进程(如 DB、Redis); -
用
jstack <pid>导出线程栈:- 若大量线程处于
RUNNABLE状态,且堆栈是任务业务逻辑 → CPU 密集型任务过多,线程数配置不合理; - 若大量线程处于
WAITING(如Object.wait())或BLOCKED状态 → 锁竞争、资源争抢导致线程阻塞,有效执行线程不足,任务堆积;
- 若大量线程处于
-
用
jstat -gcutil <pid> 1000查看 GC 状态:若 Full GC 频繁(如每秒 1 次)→ 内存泄漏或堆内存不足,导致 CPU 100%。
(2)其他资源瓶颈
- 内存:堆内存不足 → GC 频繁占用 CPU;
- 网络 / IO:DB 连接池耗尽、Redis 响应慢 → 任务阻塞,线程池资源无法释放。
定位工具链总结
| 问题类型 | 工具 / 命令 | 关键指标 / 现象 |
|---|---|---|
| 线程池状态 | JMX(VisualVM)、自定义监控 | 活跃线程数 = 最大线程数、队列满、拒绝任务数 > 0 |
| 任务耗时突增 | SkyWalking、Pinpoint | 任务平均耗时 > 预期 2 倍 |
| CPU 100% 原因 | jstack、top、jstat | 线程 RUNNABLE 过多、GC 频繁 |
| 流量突增 | 网关监控(Nginx/APISIX)、Prometheus | QPS 峰值是平时的 3 倍以上 |
| 外部依赖故障 | 链路追踪、依赖监控(如 MySQL 慢查询日志) | DB/HTTP 响应时间 > 1s |
三、彻底解决:从配置、任务、架构三方面优化
1. 线程池配置优化(核心)
(1)按任务类型精准配置参数
-
CPU 密集型任务(计算、加密):
- 核心线程数 =
CPU 核心数 + 1(避免上下文切换); - 最大线程数 = 核心线程数(无需扩容);
- 队列容量 =
峰值 QPS × 平均执行耗时 × 1.5(如 1000 × 0.01s × 1.5 = 150);
- 核心线程数 =
-
IO 密集型任务(DB/HTTP 调用):
- 核心线程数 =
CPU 核心数 × (1 + 等待时间/执行时间)(如 CPU 8 核,等待 90ms,执行 10ms → 8 × 10 = 80); - 最大线程数 = 核心线程数 × 1.5(应对突发流量);
- 队列容量 =
峰值 QPS × 平均执行耗时 × 2(如 1000 × 0.05s × 2 = 100);
- 核心线程数 =
-
通用配置:
- 队列:必须用有界队列(
ArrayBlockingQueue),禁止无界队列; - 空闲回收:
allowCoreThreadTimeOut(true)+keepAliveTime = 30~60s; - 线程工厂:自定义线程名(含业务标识),便于排查。
- 队列:必须用有界队列(
(2)优化拒绝策略(避免任务丢失 + 缓冲)
替换默认 AbortPolicy,用 “重试 + 降级 + 持久化” 的自定义拒绝策略:
public class CustomRejectedHandler implements RejectedExecutionHandler {
private final int maxRetryCount = 2; // 最多重试 2 次
private final TaskPersistence persistence; // 任务持久化组件(DB/消息队列)
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
if (executor.isShutdown()) {
persistence.save(r); // 线程池关闭,直接持久化
return;
}
// 重试入队(带超时,避免死循环)
for (int i = 0; i < maxRetryCount; i++) {
try {
if (executor.getQueue().offer(r, 1, TimeUnit.SECONDS)) {
log.info("任务重试入队成功,重试次数:{}", i+1);
return;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
// 重试失败:核心任务降级同步执行,非核心任务持久化
if (r instanceof CoreTask) {
log.warn("核心任务拒绝,同步执行降级");
r.run(); // 提交线程同步执行,避免丢失
} else {
persistence.save(r);
log.warn("非核心任务拒绝,持久化兜底");
}
}
}
2. 任务优化(减少线程池压力)
(1)优化任务执行耗时
-
解决外部依赖问题:
- DB 慢查询:优化 SQL、添加索引、扩容 DB 连接池;
- 第三方接口超时:设置超时时间(如 3s)、加熔断(Resilience4j/Sentinel)、切换备用接口;
-
拆分大任务:将耗时久的任务(如批量同步 1000 条数据)拆分为小任务(10 个 100 条),并行执行;
-
避免任务阻塞:用非阻塞 IO(如 WebClient 替代 RestTemplate)、异步 DB 客户端(如 MyBatis-Plus 异步接口)。
(2)控制任务提交速率
- 入口限流:通过网关或业务代码对接口限流(如用 Sentinel 限制 QPS 为 2000),避免超线程池处理能力;
- 合并小任务:高频提交的微小任务(如日志上报),用批量处理(如积累 100 条后批量上报),减少线程调度开销;
- 避免重复提交:通过分布式锁、唯一任务 ID 校验,防止任务重复提交。
3. 架构优化(提升系统承载能力)
(1)线程池隔离
- 核心业务(支付、订单)与非核心业务(日志、统计)用独立线程池,避免非核心任务拥堵影响核心业务;
- 高风险任务(调用第三方不稳定接口)用独立线程池 + 熔断限流,隔离故障。
(2)动态扩缩容
- 结合配置中心(Apollo/Nacos)实现线程池参数动态调整,根据流量波动自动适配(如 QPS 峰值时扩容,低峰时缩容);
- 集群水平扩容:通过 Kubernetes 或云服务器自动扩缩容,应对流量峰值。
(3)任务持久化与重试
- 拒绝的任务、未执行的任务持久化到 DB / 消息队列(如 RocketMQ),应用重启后恢复执行;
- 任务重试用指数退避策略(如 1s、3s、5s 间隔),限制最大重试次数(如 3 次),避免压垮依赖服务。
四、长效预防:避免问题再次发生
1. 完善监控告警
- 线程池核心指标监控:活跃线程数、队列堆积数、拒绝任务数、任务执行耗时,设置阈值告警(如拒绝任务数 > 0 立即告警);
- 系统资源监控:CPU 使用率、内存使用率、GC 频率,阈值(如 CPU>80% 告警);
- 依赖监控:DB/Redis/ 第三方接口的响应时间、失败率,阈值(如响应时间 > 500ms 告警)。
2. 压测验证
- 上线前对线程池进行压测(JMeter/Gatling),模拟峰值 QPS,验证线程池参数是否合理,是否会触发拒绝;
- 定期压测(如每月 1 次),验证业务变更后线程池的承载能力。
3. 规范线程池使用
- 禁止用
Executors工具类创建线程池,必须手动创建ThreadPoolExecutor,显式指定有界队列、拒绝策略; - 核心线程池参数需文档化,明确任务类型、配置依据;
- 线程池关闭时需优雅关闭(
shutdown()+awaitTermination()),持久化未执行任务。
4. 故障演练
- 定期进行故障演练(如模拟外部依赖超时、流量突增),验证线程池的容错能力、降级策略是否生效。
总结
线上 RejectedExecutionException + CPU 100% 的核心是「任务提交速率 > 处理能力」,解决思路是:
- 紧急止血:扩容线程池、降级非核心任务,快速恢复服务;
- 根因定位:通过监控 + 工具排查线程池配置、任务耗时、流量 / 资源问题;
- 彻底解决:优化线程池配置、任务耗时、架构隔离,从源头提升承载能力;
- 长效预防:完善监控、定期压测、规范使用,避免问题复发。