血的教训:线程池定义一定要全局化,共享使用

一、发现问题

​ 最近我们上线了一个比较底层的业务服务,上线后研发也去盯着服务器看了日志,没有发现有啥问题,就将服务全量上线了。谁知道过了15分钟左右,上游业务方打电话过来了,说报调用我们的业务服务出现超时。
这时候研发赶紧登上相关的机器去看,发现日志量很少,机器很卡。再有人去登入这台机器的时候就登入不了。研发初步考虑是网络问题或者宿主机问题,于是找来运维。

运维登入进去机器通过

1
ps -ef  | wc -l

查看运行的进程数,结果为60000多。通过脚本查看linux进程的进程限制:

1
cat /etc/security/limits.conf

,结果是

1
2
root soft nofile 65535
root hard nofile 65535

​ 进程总数差不多进程总数的限制。考虑到半小时前更新了新服务,去服务的日志看了看,原来tomcat有jvm内存溢出的日志:

1
2
java 进程out of memeory
Java HotSpot(TM) 64-Bit Server VM warning: Exception java.lang.OutOfMemoryError occurred dispatching signal SIGTERM to handler- the VM may need to be forcibly terminatedy

二、查找和解决问题

​ 通过上面的一些现象,赶紧将刚才的发布的机器回滚了,避免更大的问题。观察了半个小时,上游再也没有发现我们的服务报错的问题了。于是我们终端review了刚刚上线的一个pr,
​ 发现线程池的代码竟然定义在方法体里面:

1
2
3
4
5
6
7
8
public class BizService{
......
pulic processBiz(){
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(8);
......
}
......
}

这样每次调用这个方法,就构建一个新的线程池,开启了新的8个线程。又查找了《Java线程与Linux内核线程的映射关系》[1] , 发现:JVM线程跟内核轻量级进程有一一对应的关系。线程数达到60000多,处级进程上限65535。 每开启一个进程。
​ 对于应用进程发生:out of memory,是因为多线程的开销内存超过jvm的最大堆内存限制。 没开启一个进程,都有内存开销,内存超过linux内存的上限了,怪不得机器快僵死。
​ 为了验证这个猜想,于是将有错误的代码进行压测,果然问题复现,出OutOfMemoryError和机器被拖挂。然后将代码修改为:

1
2
3
4
5
6
7
8
public class BizService{
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(8);
......
pulic processBiz(){
......
}
......
}

然后进行了压测,问题没有复现,问题解决。

三、事后review和处理方案:

​ 复盘会议上,qa和相关人一起提出了如下方案来避免类似问题:

1、完善监控指标

​ 本次线程池超限,java进程也有报错,就是报OutOfMemoryError,可是报警系统竟然没有第一时间收到。查了监控系统,在前一段项目进程了重构,日志路径修改了,原来的采集配置竟然没有同时修改。导致造成损失却没有收到警报。

2、代码review一定要做,并且认真做

​ 本次程序员的线程池代码为低级失误,review认真是可以看出来的。

3、有大的优化要进行压测

​ 本次pr,有线程池的使用,所以要进行压测

4、添加平台监控

​ 添加平台监控,当linux进程大于20000个时候就就对进程数进行报警
参考:
[1] http://blog.sina.com.cn/s/blog_605f5b4f010198b5.html

修改记录:

2019-07-18 丰富了案例的描述

感谢您的阅读,本文由 王欣的博客 版权所有。如若转载,请注明出处:王欣的博客(https://wangxin.io/2018/10/05/concurrent/analysis_of_thread_pool_error_usage/
升级到Spring Framework 5.x.须知
谈谈并发包的工具都是用在什么地方,底层是怎么实现的?