【小家Java】一次Java线程池误用(newFixedThreadPool)引发的线上血案和总结
相关阅读
【小家java】java5新特性(简述十大新特性) 重要一跃
【小家java】java6新特性(简述十大新特性) 鸡肋升级
【小家java】java7新特性(简述八大新特性) 不温不火
【小家java】java8新特性(简述十大新特性) 饱受赞誉
【小家java】java9新特性(简述十大新特性) 褒贬不一
【小家java】java10新特性(简述十大新特性) 小步迭代
【小家java】java11新特性(简述八大新特性) 首个重磅LTS版本
这是一个十分严重的线上问题
自从最近的某年某月某天起,线上服务开始变得不那么稳定(软病)。在高峰期,时常有几台机器的内存持续飙升,并且无法回收,导致服务不可用。
给出监控中GC的采样曲线:
内存使用曲线如下:
如上两张图显示: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的内存相关使用情况: