【小家Java】一次Java线程池误用(newFixedThreadPool)引发的线上血案和总结

相关阅读

【小家java】java5新特性(简述十大新特性) 重要一跃
【小家java】java6新特性(简述十大新特性) 鸡肋升级
【小家java】java7新特性(简述八大新特性) 不温不火
【小家java】java8新特性(简述十大新特性) 饱受赞誉
【小家java】java9新特性(简述十大新特性) 褒贬不一
【小家java】java10新特性(简述十大新特性) 小步迭代
【小家java】java11新特性(简述八大新特性) 首个重磅LTS版本


这是一个十分严重的线上问题

自从最近的某年某月某天起,线上服务开始变得不那么稳定(软病)。在高峰期,时常有几台机器的内存持续飙升,并且无法回收,导致服务不可用。

给出监控中GC的采样曲线:
【小家Java】一次Java线程池误用(newFixedThreadPool)引发的线上血案和总结
内存使用曲线如下:
【小家Java】一次Java线程池误用(newFixedThreadPool)引发的线上血案和总结

如上两张图显示:18:50-19:00的这10分钟阶段里,服务已经处于不可用的状态了。这就导致了:上游服务的超时异常会增加,该台机器会触发熔断。

熔断触发后,这台机器的流量会打到其他机器,其他机器发生类似的情况的可能性会提高,极端情况会引起所有服务宕机,造成雪崩,曲线掉底。

问题分析和猜想

结合我们的业务情况,我们监控到在那段时间里,访问量是最高的,属于一个高峰情况,因此我们初步断定,这个和流量高并发有密不可分个的关系。

1、因为线上内存过大,如果采用 jmap dump的方式,这个任务可能需要很久才可以执行完,同时把这么大的文件存放起来导入工具也是一件很难的事情

2、再看JVM启动参数,也很久没有变更过 Xms, Xmx, -XX:NewRatio, -XX:SurvivorRatio, 虽然没有仔细分析程序使用内存情况,但看起来也无大碍。

3、于是开始找代码,某年某天某月~ 嗯,注意到一段这样的代码提交:

private static ExecutorService executor = Executors.newFixedThreadPool(15);
public static void push2Kafka(Object msg) {
    executor.execute(new WriteTask(msg,  false));    
}

这段代码的功能是:每次线上调用,都会把计算结果的日志打到 Kafka,Kafka消费方再继续后续的逻辑。

看这块代码的问题:咋一看,好像没什么问题,但深入分析,问题就出现在
Executors.newFixedThreadPool(15)这段代码上。

因为使用了 newFixedThreadPool 线程池,而它的工作机制是,固定了N个线程,而提交给线程池的任务队列是不限制大小的,如果Kafka发消息被阻塞或者变慢,那么显然队列里面的内容会越来越多,也就会导致这样的问题。

验证猜想

为了验证这个想法,做了个小实验,把 newFixedThreadPool 线程池的线程个数调小一点,然后自己模拟压测一下:
测试代码如下:

    //创建一个固定线程池
    private static ExecutorService executor = Executors.newFixedThreadPool(1);


    //向kafka里推送消费
    public static void push2Kafka(Object msg) {
        executor.execute(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + "-->任务放到线程池:" + msg);
                //模拟此任务的耗时 为了希望问题尽快出现,这里模拟处理此任务需要1分钟
                TimeUnit.MINUTES.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }


    public static void main(String[] args) {

        //模拟高并发环境下  一直向线程池里面不停的塞任务
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            System.out.println("塞任务start..." + i);
            push2Kafka(i);
            System.out.println("塞任务end..." + i);
        }

    }

打开JConsole查看JVM的内存相关使用情况: