看麻了呀,线程池日志错乱问题

一、背景

由于推送业务的历史问题,推送业务代码比较散乱,推送日志也不方便查看,推送模型也不够统一,因此专门设计一个核心模块来逐步替换掉之前的推送业务代码,进行统一管理

二、问题描述

通过线程池抓取数据后进行组装然后推送给业务方,在推送过程中出现整个日志上下文打印错乱的问题, 如A线程持有的上下文参数对象,在B线程的日志中被打印出来了。在测试case中,有三类数据要被推送,不同类型的数据被分到同一个线程池的不同线程处理, 如下(正常情况):A类数据—>T1线程处理–>T1线程日志展示A类数据的上下文信息(如类型信息,list大小,查询条件内容等等) B类数据—>T2线程处理–>T2线程日志展示B类数据的上下文信息(如类型信息,list大小,查询条件内容等等) C类数据—>T3线程处理–>T3线程日志展示C类数据的上下文信息(如类型信息,list大小,查询条件内容等等) 但是在日志显示上是错乱的,如下:A类数据—>T1线程处理–>T1线程日志展示A类数据的上下文信息(如类型信息,list大小,查询条件内容等等)+部分B类数据的上下文信息(如list大小,查询条件内容) B类数据—>T2线程处理–>T2线程日志展示B类数据的上下文信息(如类型信息,list大小,查询条件内容等等)+部分A类数据的上下文信息(如list大小,查询条件内容) C类数据—>T3线程处理–>T3线程日志展示C类数据的上下文信息(如类型信息,list大小,查询条件内容等等)

上面的是打印日志方面的问题,但是实际上在测试过程中整个数据推送流程没有问题,因此当时第一阶段上线前并没有太在意,上线之后也没有问题,但是至于为啥会错乱,肯定需要排查的。

三、排查思路

3.1 并发问题

第一印象是觉得可能出现了并发问题,因此在关键方法上加了锁,重新发了一版之后生效了,但是后面又出现了,所以这个方向不对。

3.2 trace问题

由于是xxljob触发调度,在调度过程中没有统一的trace信息,因此加上了,最初也是怀疑是因为aop+MDC trace导致的,另外之前也看过trace多线程日志错乱问题,但是明显跟我这个场景不一样,在业务代码中注释了引入的trace代码之后还是存在问题,所以这个方向也排除了。

3.3 其他问题

  1. 配置问题
  2. 嵌套调用
  3. …..

另外也通过上面的方向排查了,没啥思路。

3.4 问题场景回归

每个线程只处理一类数据,上下文是隔离的,但是当前日志的内容却打印了别的类型的数据,明显出现错乱,所以可能是组装数据的上下文不是纯隔离的。如下代码:

   /**
     * 推送
     * @param pushDataContext
     */
    private void exePush(PushDataContext pushDataContext){
        Map<Long, List<PushTaskMain>> pushTaskGroupMap = pushDataContext.getPushTaskList().stream().collect(Collectors.groupingBy(PushTaskMain::getPushConfigId));
        try {
            for (Map.Entry<Long,List<PushTaskMain>> pushTaskGroup : pushTaskGroupMap.entrySet()){
                PushConfig pushConfig = pushConfigService.getCacheById(pushTaskGroup.getKey());
                if(pushConfig == null){
                    log.debug("orgId={},找不到pushConfig id= {},不再推送数据任务",pushDataContext.getOrgId(), pushTaskGroup.getKey());
                    continue;
                }
                pushThreadExecutor.execute(()->{
                    try {
                        pushProcess(pushConfig, pushTaskGroup.getValue(), pushDataContext);
                    }catch (Exception e) {
                        log.error("推送任务执行异常",e);
                      
                    }
                });
            }
        }catch (Exception e) {
            log.error("推送任务执行异常",e);
        }
    }
    /**
     * 执行入口
     * @param pushConfig
     * @param pushTaskList
     * @param pushDataContext
     */
    public void pushProcess(PushConfig pushConfig, List<PushTaskMain> pushTaskList, PushDataContext pushDataContext){
        //这里进行线程隔离,每个线程池持有一个新的PushDataContext对象
        PushDataContext newPushDataContext = new PushDataContext();
        newPushDataContext.setPushConfig(pushConfig);
        newPushDataContext.setRound(pushDataContext.getRound());
        newPushDataContext.setOrgId(pushConfig.getOrgId());
        Map<String,PushTaskMain> pushTaskMainMap = pushTaskList.stream().collect(Collectors.toMap(PushTaskMain::getDataId,o->o,(o1,o2)->o1));
        newPushDataContext.setPushTaskList(pushTaskList);
        newPushDataContext.setPushTaskMap(pushTaskMainMap);
        newPushDataContext.setQueryParams(pushDataContext.getQueryParams());
     //调用数据抓取和组装的processorBean,然后后面继续调用推送的processorBean
        PushDataProcessor pushDataProcessor = (PushDataProcessor)ApplicationContextUtils.getBean(pushConfig.getDataProcessorBeanName());
        pushDataProcessor.processData(newPushDataContext);
    }

初步怀疑是对象引用问题,导致 PushDataContext newPushDataContext虽然是新对象,但是其值却不是,由于排查过程中并不太方便打印sql,所以仅仅针对pushTaskList做了一次深拷贝,然后测试,结果也不对,另外也在多个地方打印了newPushDataContext对象的地址,印证了每个线程只会持有自己的对象,另外也排除掉了嵌套调用的情况。后面因为整个推送流程涉及到多个服务类的调用,所以在多个关键节点上加了对推送类型,查询参数,查询sql和数据返回方面的日志,在多次反复测试过程中发现了线索,日志如下:T1线程处理A类数据,打印T1持有的A类数据的相关内容,打印A类数据的上下文的queryParams的内容 T2线程处理B类数据,打印T2持有的B类数据的相关内容,打印B类数据的上下文的queryParams的内容(但是此时这个map里的kv却包含了A类上下文才持有的查询参数kv) T3线程处理C类数据,打印T3持有的C类数据的相关内容,打印C类数据的上下文的queryParams的内容(但是此时这个map里的kv却包含了A类上下文才持有的查询参数kv) 由此可见确定了是因为newPushDataContext.setQueryParams的数据是被多线程共享了,导致后面往里塞查询条件kv的时候被其他线程看到了,然后打印了其他线程的数据。

四、解决方案

由于已经知道问题的发生原因了,所以也很好解决,就是在pushProcess()方法里对queryParams进行深拷贝,保证每个线程只持有自己的查询kv数据。这个问题很有意思,当然排查过程也比较艰难,耗时两天,问题跨度两个月,还好问题场景特殊,所以并没有对业务造成影响。


原文始发于微信公众号(神帅的架构实战):看麻了呀,线程池日志错乱问题

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/241328.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!