事故总结集锦-FutureTask子线程超时不生效导致的接口响应慢事故7(一周一更)

859 阅读2分钟

【问题描述】

APP首页接口响应慢,加载不出来楼层。

【影响范围】

APP首页

【事故级别】

P0

【处理过程】

9:14 群里反馈app响应慢,首页楼层加载不出来 9:20 定位昨天首页上过线,紧急重启,重启后依然无效; 9:30 评估后进行版本回滚,回滚后首页加载正常; 10:00 根据监控定位首页接口响应变慢; 10:30 review代码,发现新版本上了几个新楼层,首页网关设置了多线程超时的时间,但是并未生效; 10:50 定位到原因,修改代码上线;

【故障原因】

网关多线程批量调用下游接口,每个楼层都设置了超时时间,如果有的楼层超时则默认抛弃不展示,但是实际场景中,超时时间并未生效,而是等所有楼层接口执行加载后才结束,问题代码如下所示:

image.png

超时时间设置的是 380ms,执行第一次 for 循环时,假如任务执行的时间是 300ms,则可以正常展示,接着下一个任务执行时间是 500ms,这个楼层仍然能够显示出来,因为它是在第一个任务等待 300ms 的基础之上又等待 380ms 超时,两个时间加起来是 680ms,500ms 的任务是可以拿到结果的,依次类推,越往后的任务能够允许等待的超时时间会特别久。

我们通过代码进行复现:

public class FuatureTaskDemo {
    static ExecutorService mExecutor = Executors.newFixedThreadPool(3);
    private class QuoteTask implements Callable<Long> {
        public final Long time;

        public QuoteTask(Long time) {
            this.time = time;
        }

        @Override
        public Long call() throws Exception {
            Thread.sleep(time);
            return time;
        }
    }

    /**
     * @return
     */
    public void getWorker() throws Exception {
        List<FutureTask> features = new ArrayList<>();
        FutureTask<Long> task = new FutureTask<>(new QuoteTask(300L));
        FutureTask<Long> task1 = new FutureTask<>(new QuoteTask(680L));
        FutureTask<Long> task2 = new FutureTask<>(new QuoteTask(800L));
        features.add(task);
        features.add(task1);
        features.add(task2);
        features.forEach(futureTask -> {
            mExecutor.execute(futureTask);
        });
        int timeOut = 380;
        long start = System.currentTimeMillis();
        for (FutureTask future : features) {
            try {
                System.out.println("执行任务的结果:" + (future.get(timeOut, TimeUnit.MILLISECONDS)));
//                Long excuteTime = System.currentTimeMillis() - start;
//                timeOut = timeOut - excuteTime.intValue();
//                if(timeOut<=0){
//                    timeOut=1;
//                }
            } catch (CancellationException e) {
                System.out.println("任务超时异常:" + e.getMessage());
            } catch (Exception e) {
                System.out.println("任务异常:" + e.getMessage());
            }
        }
        long end = System.currentTimeMillis();
        System.out.println("总共执行任务的时间:" + (end - start));
        mExecutor.shutdown();
    }


    public static void main(String[] args) {
        FuatureTaskDemo it = new FuatureTaskDemo();
        try {
            it.getWorker();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

我们把task1设置为680ms,执行结果如下所示:

image.png

我们把task1设置为690ms,执行结果如下所示:

image.png

如何解决?

验证了我们上述的事故原因。那么我们如何解决这个问题呢?

方式一:

累减的方式来定义每个线程的超时时间,最终线程时间归1;(上述代码里的注释去掉) 我们可以依次动态设置每个任务的超时时间,拿设置的允许任务执行的最大超时时间,减去获取任务结果已经执行的时间,作为下一个任务获取结果超时时间(最小超时时间设置为 1)即可;

方式二:

使用 invokeAll 实现设置所有任务的超时时间,改造代码如下

public class FuatureTaskDemo {
    static ExecutorService mExecutor = Executors.newFixedThreadPool(3);

    private static class QuoteTask implements Callable<Long> {
        public final Long time;

        public QuoteTask(Long time, ConcurrentHashMap<Long, Long> map) {
            this.time = time;
            map.put(time,time);
        }

        @Override
        public Long call() throws Exception {
            Thread.sleep(time);
            return time;
        }
    }

    /**
     * @return
     */
    public void getWorker() throws Exception {
        int timeOut = 380;
        long start = System.currentTimeMillis();
        ConcurrentHashMap<Long, Long> map = new ConcurrentHashMap<>();
        List<QuoteTask> lists = new ArrayList<>();
        lists.add(new QuoteTask(300L,map));
        lists.add(new QuoteTask(500L,map));
        lists.add(new QuoteTask(800L,map));
        List<Future<Long>> futuresList = mExecutor.invokeAll(lists, timeOut, TimeUnit.MILLISECONDS);
        for (Future<Long> future : futuresList) {
            try {
                System.out.println("执行任务的结果:" + (future.get(timeOut, TimeUnit.MILLISECONDS)));
            } catch (CancellationException e) {
                System.out.println("任务超时异常:" + e.getMessage());
            } catch (Exception e) {
                System.out.println("任务异常:" + e.getMessage());
            }
        }
        long end = System.currentTimeMillis();
        System.out.println("执行map结果" + JSON.toJSONString(map));
        System.out.println("总共执行任务的时间:" + (end - start));
        mExecutor.shutdown();
    }

}

invokeAll 底层也是按任务的获取超时时间递减实现

image.png

总结:

  • 我们在使用FutureTask多线程开发的时候,一定注意子线程的超时时间问题
  • 在业务场景下一定要考虑是否整体设置一个超时时间
  • 如果业务核心的话,也可以考虑采用线程编排的方式,隔离线程池的方式 进行并行。