上个月线上服务突然卡死,排查半天发现是线程池用错了。记录一下,避免后人踩坑。
现象
早上 9 点用户高峰期,服务响应越来越慢,最后直接无响应。重启后恢复,过半小时又卡死。
日志里没有报错,CPU 占用不高,内存也正常。就是所有请求都超时。
排查
dump 线程栈一看,所有线程都阻塞在提交任务的地方。
代码简化后是这样:

ExecutorService executor = Executors.newFixedThreadPool(10);

public void handleRequest(Request req) {
    Future<Result> future = executor.submit(() -> process(req));
    Result result = future.get(); // 阻塞等待
}

看起来没问题?问题在 process 方法里。
process 内部又调了另一个服务,那个服务超时了。线程池里的 10 个线程全被占满,都在等外部响应。
新请求进来,往线程池提交任务,队列塞满,最后拒绝。但 future.get() 还在阻塞等结果,整个链路卡死。
根本原因
线程池里的线程不是越多越好,但也不是固定就够用。
这里两个错误:

  1. 任务里嵌套阻塞调用
    线程池里的线程应该快速处理完释放,而不是长时间挂起等 IO。
  2. 没有超时控制
    future.get() 没有设超时,无限等下去。
    修复
    改成异步 + 超时:
Future<Result> future = executor.submit(() -> process(req));

try {
    Result result = future.get(2, TimeUnit.SECONDS); // 最多等2秒
} catch (TimeoutException e) {
    future.cancel(true); // 取消任务,释放线程
    return fallbackResult(req);
}

process 内部也加超时,不依赖外部服务良心:

public Result process(Request req) {
    HttpClient client = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(1))
        .build();
    
    // 请求外部服务,最多等1秒
    Response resp = client.send(request, BodyHandlers.ofString());
    return parse(resp);
}

更深的教训
Executors.newFixedThreadPool 底层用的是无界队列 LinkedBlockingQueue 。
任务提交速度超过处理速度,队列无限增长,最后内存溢出。
线上改用自定义线程池,有界队列 + 拒绝策略:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    4,                      // 核心线程数
    8,                      // 最大线程数
    60L, TimeUnit.SECONDS,  // 空闲线程存活时间
    new ArrayBlockingQueue<>(100), // 有界队列,最多100个排队
    new ThreadPoolExecutor.CallerRunsPolicy() // 满了让提交者自己执行
);

这样队列满的时候,主线程自己跑任务,自然降速,不会压垮系统。
总结

线程池里的任务尽量别阻塞,快速处理完释放

future.get() 必须设超时,不然就是埋雷

别用 Executors 的便捷方法,生产环境自己配 ThreadPoolExecutor ,队列要有界

拒绝策略想清楚,是丢弃、抛异常、还是让调用方自己执行
你们线程池踩过什么坑?评论区交流。

标签: none

添加新评论