线上服务日志出现大量RejectedExecutionException,CPU使用率100%。如何定位?如何解决?

106 阅读9分钟

线上服务出现 大量 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)、PrometheusQPS 峰值是平时的 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% 的核心是「任务提交速率 > 处理能力」,解决思路是:

  1. 紧急止血:扩容线程池、降级非核心任务,快速恢复服务;
  2. 根因定位:通过监控 + 工具排查线程池配置、任务耗时、流量 / 资源问题;
  3. 彻底解决:优化线程池配置、任务耗时、架构隔离,从源头提升承载能力;
  4. 长效预防:完善监控、定期压测、规范使用,避免问题复发。