(案例)一次对ThreadPoolExecutor特殊使用

一、开场白

这是我入职公司处理的第一个事故,也是我在公司可谈的第一个故事。

项目上有个报告生成业务,开发人员把报告请求封装成任务后,放到线程池中异步生成,生成过程会向数据库写出状态,用于支持生成状态查询,前台查询到报告状态生成成功,即可下载报告。

生成过程是从数据库和接口拉取数据,填充报告模板,最后再转化成实际的报告(PDF)。报告有一块内容是绘制一个关联关系图,它通过预先定义图的节点、边的关系,并将关系集合交给一个三方工具(yed,一个强大的画图工具,不过我不太熟~)绘制,故事就由这个第三方的工具库引起。

Yed是一个非常强大的绘图工具,但有一个缺陷,绘图一旦开始,就不能取消,也不响应中断。这就给我们的绘图带来了麻烦,当关系节点、边数量非常大且存异常关系时,会偶发出现绘图进入一个无限循环,通过jstack观察,发现某个方法一直在循环调用。通过top观察发现有CPU的占用接近100%。重复提交几个相同的报告任务,会导致服务器每核都被打满,最终表现为报告生成的服务不再响应报告生成,正在生成中的报告也无任何进度信息变更。

二、尝试解决

发现问题后,项目组召集全员一同解决,大家尝试反编译源码(Yed是一个收费库,不开放源码),同时也结合数据输入做分析,希望能发现异常点,通过纠正数据输入来解决问题,最终无果。

三、最终解决

这就是今天要说的主要内容了,最后我们期望扩展线程池,把线程池中的工作线程监控起来,当然只是简单监控,当工作线程开始报告生成且时间超过一定的阈值(正常情况报告基本可以在2分钟内完成,我们将阈值设定为5分钟),则直接结束本次报告生成,然后触发一个回调,并将数据输入降一个度(绘图数据默认4度,每降一度,数据会量级减少,图形结构会更简单。后来发现4度有问题的图在3度基本可正常生成)再重新输入,再次进行生成。

基于以上考虑,便对线程池做了如下调整封装:

public class TraceThreadPoolExecutor extends ThreadPoolExecutor {
    private static final Logger LOG = LoggerFactory.getLogger(TraceThreadPoolExecutor.class);
    private static final int SHUTDOWN_TIMEOUT = 60;
    private final Map<Runnable, Thread> tracedThreads = new ConcurrentHashMap<>();
    private final ExecutorService monitor;

    /**
     * 提交一个任务到线程池中,并且监控任务的执行耗时,如果超过阈值,则停止任务
     */
    public Future<?> submit(Runnable job,
                            int timeOut,
                            TimeUnit timeUnit,
                            TimeOutHandler timeOutHandler) {
        Future<?> futureTaskJob = submit(job);
        monitor.execute(new MonitorJob(job, futureTaskJob, timeOut, timeUnit, timeOutHandler));
        return futureTaskJob;
    }

    public void stop() {
        shutdownExecutorQuietly(monitor);
        shutdownExecutorQuietly(this);
    }

    private void shutdownExecutorQuietly(ExecutorService executor) {
        if (null != executor) {
            try {
                executor.shutdown();
                if (!executor.isShutdown()) {
                    try {
                        executor.awaitTermination(SHUTDOWN_TIMEOUT, TimeUnit.SECONDS);
                    } catch (InterruptedException ignore) {
                        LOG.warn(ignore.getMessage(), ignore);
                    }
                    if (!executor.isShutdown()) {
                        executor.shutdownNow();
                    }
                }
            } catch (Exception ignore) {
                LOG.warn(ignore.getMessage(), ignore);
            }
        }
    }

    @Override
    protected void beforeExecute(Thread worker, Runnable job) {
        /**
         * 记录处理任务具体的线程
         */
        recordJobWorker(job, worker);
    }

    @Override
    protected void afterExecute(Runnable job, Throwable error) {
        /**
         * 任务执行完成,清理任务与工作现场的关联信息
         */
        clearJobWorkerRecord(job);
    }

    private class MonitorJob implements Runnable {
        private final Future<?> futureTaskJob;
        private final Runnable originJob;
        private final int timeOut;
        private final TimeUnit timeUnit;
        private final TimeOutHandler timeOutHandler;

        public MonitorJob(Runnable originJob,
                          Future<?> futureTaskJob,
                          int timeOut,
                          TimeUnit timeUnit,
                          TimeOutHandler timeOutHandler) {
            this.originJob = originJob;
            this.futureTaskJob = futureTaskJob;
            this.timeOut = timeOut;
            this.timeUnit = timeUnit;
            this.timeOutHandler = timeOutHandler;
        }

        @Override
        public void run() {
            if (null != futureTaskJob) {
                try {
                    futureTaskJob.get(timeOut, timeUnit);
                } catch (InterruptedException ignore) {
                    LOG.warn(ignore.getMessage(), ignore);
                } catch (ExecutionException ignore) {
                    LOG.warn(ignore.getMessage(), ignore);
                } catch (TimeoutException timeOut) {
                    /**
                     * Force stop worker
                     */
                    Thread worker = getTracedThread((FutureTask) futureTaskJob);
                    if (null != worker && worker.isAlive()) {
                        worker.stop();
                    }
                    /**
                     * Do callback
                     */
                    if (null != timeOutHandler) {
                        timeOutHandler.doHandle(originJob);
                    }
                }
            }
        }
    }

    private void recordJobWorker(Runnable job, Thread worker) {
        tracedThreads.put(job, worker);
    }

    private Thread getTracedThread(Runnable job) {
        return tracedThreads.get(job);
    }

    private void clearJobWorkerRecord(Runnable job) {
        tracedThreads.remove(job);
    }

    private TraceThreadPoolExecutor(int workerThreads, int monitorThreads) {
        super(workerThreads, workerThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
        monitor = Executors.newFixedThreadPool(monitorThreads);
    }

    public static TraceThreadPoolExecutor createFixedThreadPool(int workerThreads) {
        return new TraceThreadPoolExecutor(workerThreads, workerThreads);
    }
}

放个测试栗子:

public class TraceThreadPoolExecutorTutorial {

    public static void main(String[] args) throws InterruptedException {
        TraceThreadPoolExecutor.createFixedThreadPool(1).submit(() -> {
            try {
                Thread.sleep(60000);
            } catch (InterruptedException ignore) {
            }
        }, 10, TimeUnit.SECONDS, job -> System.out.println("TimeoutHandler execute!"));

        Thread.sleep(20000);

        System.out.println("Main thread exit!");
    }
}

---------------------------------------结果输出--------------------------------------
TimeoutHandler execute!
Main thread exit!

这里可能会有一个疑问,代码中使用stop来终止一个线程的执行是否有问题?当然有,但要具体情况具体分析!

我们先理解一下Thread.stop方法的原理,抛出一个ThreadDeath的error并且释放掉所有持有的监视器锁。所以如果在一个业务中依赖了锁来处理业务,并且业务处理处于中间的非一致状态的情况下,线程被stop掉,这样其它线程就可以获取到锁,并继续业务处理,很明显这种情况可能会导致一些奇葩问题的出现。

但是这些问题,在我们的故事里都不存在。回顾一下我们的问题:

  1. 工作线程调用Yed工具库绘图,因为某种原因进入一个不响应中断的无限循环中;
  2. 报告线程与其它线程没有需要做同步的地方。

所以Thread.stop对我们来说是安全的,而且可能是我们唯一的选择。

最后业务方接受我们方案,调整之后线上运行良好,我们的问题得到解决。

四、写在最后

本文的案例存有很强的特殊性,首先工作线程执行的是一个没有出口的循环任务,其次任务无法被取消也不响应中断,在这样的背景下,我们才选择了上文阐述的解决办法,当然这也依赖于对线程池、Thread.stop方法工作原理的理解。

如果有益,请关注、收藏、点赞。个人认识有限,不准确的地方欢迎指正,谢谢!

End.