故障现场 | 这个死锁出奇的诡异

2024年 1月 29日 35.3k 0

1. 问题&分析

线程池用多了总会出现些诡异问题,特别是当任务间的关系比较复杂时,经常会出现让你想象不到问题,比如这次出现的这个问题。

1.1. 案例

突然间,系统出现大量报警,具体信息如下:

图片图片

从抛出的异常可知,提交量较大导致线程池资源被耗尽,从而触发了线程池的拒绝策略,直接抛出了 RejectedExecutionException。

开始的时候,小艾认为等高峰流量过去后,系统便能恢复正常。可出乎意料的是,系统一直没有恢复,那么流量已经将至个位数,请求也是 100% 失败,同时该节点的大量后台任务都出现异常。没有办法,为了快速止损,不得已对异常节点进行重启,系统随之恢复正常,日志输入如下:

图片图片

其他的后台任务也恢复正常。

惊魂初定的小艾找到出问题的代码如下:

@GetMapping("syncSubmit")
public RestResult syncSubmit(String taskName){
    this.executeService.submit(new ParentTask());
    return RestResult.success("提交成功");
}

class ParentTask implements Callable{

    @Override
    public Boolean call() throws Exception {
        Future aFuture = executeService.submit(new FetchAChildTask());
        doSomeThing(500);
        Future bFuture = executeService.submit(new FetchBChildTask());
        doSomeThing(500);
        C c = buildC(aFuture.get(), bFuture.get());
        Future cFuture = executeService.submit(new SaveCChildTask(c));
        return cFuture.get();
    }
}

代码的逻辑非常简单,核心流程如下图所示:

图片图片

核心流程为:

  • 提交异步任务,并行获取 A 和 B
  • 线程同步处理一下耗时操作
  • 获取 A 和 B 结果后,构建新对象 C
  • 将 C 保存到数据库
  • 逻辑非常简单,唯一的复杂点在于:==多处任务提交使用了统一线程池。==

    【背景】考虑到线程池是系统中最宝贵的资源,公司“大牛”封装了一个全局的 GlobalExecuteService 服务,并制定规范要求所有异步任务统一使用 GlobalExecuteService 来完成。如果需要构建自己的线程池,需要向他提交审批,只有在审批后才能创建新的线程池。

    1.2. 问题分析

    线程池处于什么状态?为什么所有异步任务都无法提交?

    这是一个比较烧脑的问题,单盘逻辑没有什么头绪,没有办法只能将现场 dump 下来进行分析。

    第一个问题:线程池线程都处于什么状态,线程栈信息如下:

    图片图片

    从日志中可知:

  • 线程池中的线程全部处于 WAITING,也就是等待状态;
  • 展开 Thread-1 栈信息,发现线程再调用 future.get 操作时出现阻塞
  • 实际等待对象为 FutrueTask#10
  • 接下来,需要进一步确认 FutureTask#10 具体处于什么状态,从内存堆中找到 FutureTask#10 对象,详细信息如下:

    图片图片

    从日志中可以看出:

  • FutureTask#10 是 LinkedBlockingQueue 的 Node 节点持有,也就是 FutureTask#10 处于等待队列中
  • 该阻塞队列属于 GlobalExecuteService 所有
  • 排查到这里,真相浮出水面:GlobalExecuteService 中的线程,正在等待 GlobalExecuteService 阻塞队列的任务完成。

    具体如下图所示:

    图片图片

    线程池中的所有工作线程都在等待阻塞队列的任务完成,由于没有可用的工作线程,阻塞队列中的任务永远都不会被执行。

    这就是典型的死锁!!!

    2. 解决方案

    费了老大劲终于定位问题,解决思路也就变的明了:不要向自己运行的线程池提交任务。

    图解如下:

    图片图片

    线程池不会向自己提交任务,而是将任务提交到其他线程池。

    2.1. 手工拆分线程池

    问题修复变的简单,我们需要:

  • 先建一个子线程池服务
  • 父线程池向子线程池提交任务
  • 具体代码如下:

    @Autowired
    private GlobalExecuteService executeService;
    
    // 创建新的线程池服务
    @Autowired
    private SubExecuteService subExecuteService;
    
    @GetMapping("syncSubmit")
    public RestResult syncSubmit(String taskName){
        this.executeService.submit(new ParentTask());
        return RestResult.success("提交成功");
    }
    
    class ParentTask implements Callable{
    
        @Override
        public Boolean call() throws Exception {
            log.info("Begin to Run Parent Task");
    
            // 向新的线程池服务提交任务
            Future aFuture = subExecuteService.submit(new FetchAChildTask());
            doSomeThing(500);
    
            // 向新的线程池服务提交任务
            Future bFuture = subExecuteService.submit(new FetchBChildTask());
            doSomeThing(500);
            C c = buildC(aFuture.get(), bFuture.get());
    
            // 向新的线程池服务提交任务
            Future cFuture = subExecuteService.submit(new SaveCChildTask(c));
            Boolean result = cFuture.get();
            log.info("End to Run Parent Task");
            return result;
        }
    }

    2.2. 多级任务管理

    手工拆分线程池确实能解决这个场景的问题,但由于 GlobalExecuteService 服务已经使用很长时间,任务间的关系错综复杂,很难一次性排查并修复所有问题,同时随着逻辑的变化未来仍旧会出现类似的问题。

    那最佳方案是什么?

    让 GlobalExecuteService 具备多级管理能力。核心代码如下:

    @Service
    public class GlobalExecuteServiceV2 {
        // 记录当前线程运行级别,默认 0,表示当前线程非该类管理的线程池线程
        private static final ThreadLocal LEVEL_HOLDER = ThreadLocal.withInitial(()->0);
        // 一级线程池
        private ExecutorService executorServiceLeve1;
        // 二级线程池
        private ExecutorService executorServiceLeve2;
        // 默认线程池
        private ExecutorService defExecutorService;
    
        @PostConstruct
        public void init() {
           // 省略线程池初始化逻辑
        }
    
        public  Future submit(Callable callable){
            // 获取当前线程的运行级别
            Integer level = LEVEL_HOLDER.get();
            // 根据当前运行级别,计算子任务所使用的线程池
            ExecutorService executorService = getNextExecutorServiceByLevel(level);
            // 为子任务分配运行级别
            CallableWrapper callableWrapper = new CallableWrapper(level + 1, callable);
            // 提交任务
            return executorService.submit(callableWrapper);
        }
    
        private ExecutorService getNextExecutorServiceByLevel(Integer level) {
            if (level == 0){
                return executorServiceLeve1;
            }
            if (level == 1){
                return executorServiceLeve2;
            }
            return defExecutorService;
        }
    
        class CallableWrapper implements Callable{
            private final Integer level;
            private final Callable callable;
    
            CallableWrapper(Integer level, Callable callable) {
                this.level = level;
                this.callable = callable;
            }
    
            @Override
            public T call() throws Exception {
                try {
                    // 为线程池绑定运行级别
                    LEVEL_HOLDER.set(level);
                    return callable.call();
                }finally {
                    // 清理线程池运行级别
                    LEVEL_HOLDER.remove();
                }
            }
        }
    }

    核心设计如下:

  • 使用 ThreadLocal LEVEL_HOLDER 记录当前线程运行的级别,默认为 0 表示任务未在线程池中运行
  • 提交任务时,通过当前运行级别计算下一级别的线程池
    • 当前级别为0,返回 Level1 线程池
    • 当前级别为1,返回 Level2 线程池
    • 其他基本,返回 默认 线程池
  • 通过 CallableWrapper 自动将任务运行的 Level 绑定到当前线程上下文
  • 任务执行前,使用 LEVEL_HOLDER.set(level) 完成运行 level 的设置
  • 任务执行后,使用 LEVEL_HOLDER.remove() 完成运行 level 的清理
  • 为了演示方便,仅定义了 3 级线程池,通常情况下足够业务使用,但需要注意:

    • 超过三级提交,仍旧有可以出现死锁的情况,可以通过日志方式及时暴露问题
    • 如不放心,可以升级为 “无限极” 设计,及使用 List 对线程池进行统一管理,并根据 Level 完成线程池的动态创建

    3. 示例&源码

    代码仓库:

    https://gitee.com/litao851025/learnFromBug

    代码地址:

    https://gitee.com/litao851025/learnFromBug/tree/master/src/main/java/com/geekhalo/demo/thread/deadlock

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论