大家好,我是周伟。
今天给大家带来一宗热乎乎的**“5.1惨案“**。
场景
领导给我打电话,反馈同事系统无法登录了。
当下第一反应就是:我没改代码啊,怎么会有问题。
抱怨归抱怨,身体还是很诚实,立马乖乖的下载日志,查看日志~
给大家看看报错信息,看大家能不能定位个大概问题。
1 | API处理错误:Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: unable to create new native thread |
OOM了,无法再创建新的处理线程。
收到领导反馈问题的时间是上午10:48分,而我开始查问题的时候已经是中午12:15左右了,这个时候系统已经自动恢复功能了,这说明释放出了空间能够让代码继续运行;
最开始我是导出的当天的运行日志,没有查看出端倪,继续找前一天的,我们需要找到第一次发生错误的日志。

找到上边这段日志,基本上也知道触发OOM的原因是什么了。
业务场景:
在我的任务(禅道)系统里面,每天早上9点,有一个未完成任务任务的微信推送,因为推送的内容与每个同事信息相关,所以不能采取批量推送的方式,就循环了需要推送的用户(用户总数为183个),调用推送系统的推送消息接口。
理论上是没有一点压力的,本质就是调用微信的推送接口183次,微信这点推送还是能扛住的。
但是看这个日志,就是这个推送把JVM给弄OOM了。

找到触发OOM的原因,就继续跟踪业务代码,从禅道任务系统跟踪到微信推送系统(不是连边写的代码,但是秉承的解决问题的初衷,屎山也要看。)。首先脑海里进行一波分析,禅道任务系统是用PHP写的,他没有做任何的操作,就是发送一个http请求,出问题的概率应该不大,把重点的心思放在微信推送系统里边。
直到看到这段代码,我内心都崩溃了,我不知道是哪个想秀技术的程序员,秀出来的代码。
看一段代码

大家看出什么问题了吗?
我们看一段代码,首先不要陷入到细枝末节里面去看,要看这段代码、这个函数是要解决一个什么问题。
异步批量发送微信消息模版是这个函数要解决的问题。
看到了2个引起警觉的代码newFixedThreadPool和CompletableFuture.supplyAsync。
从上面这段代码,大家看出什么问题了吗?欢迎在留言区和我谈论。
没看出问题的也没事,我们接着来补充并发编程的基础。
补充基础
newFixedThreadPool
创建固定数量的线程池。
其实在阿里手册里边就有一条,不要使用封装好的这些创建线程池的方法。
我们来看它具体的实现

这里面又夹杂了一道常见面试题。
创建线程池ThreadPoolExecutor有几个参数,你是怎么理解这几个参数的?在工作中,你是怎么设置的?
这里先不散开来讲了,还是针对性的讲newFixedThreadPool。
ThreadPoolExecutor继承自AbstractExecutorService,而AbstractExecutorService实现了ExecutorService接口。

7个参数:
corePoolSize:线程池中核心线程数的最大值;maximumPoolSize:线程池中能拥有最多线程数;keepAliveTime:表示空闲线程的存活时间;TimeUnit unit:表示keepAliveTime的单位;workQueue:它决定了缓存任务的排队策略。对于不同的应用场景我们可能会采取不同的排队策略,这就需要不同类型的队列。这个队列需要一个实现了BlockingQueue接口的任务等待队列;threadFactory:指定创建线程的工厂;handler:表示当workQueue已满,且池中的线程数达到maximumPoolSize时,线程池拒绝添加新任务时采取的策略。
回顾newFixedThreadPool的具体实现:

它的核心线程数和最大线程数是一样,都是nThreads的变量的值,该变量由用户自己决定,所以说是固定大小线程池。此外,它每隔0毫秒回收一次线程,换句话说就是不回收线程,因为它的核心线程和最大线程数是一样,回收没有任何意义,此外,使用了LinkedBlockingQueue队列,该队列其实是有界队列,很多人误解了,只是它的初始值比较大,是Interger的最大值,Integer.MAX_VALUE。
回到最开始,我被秀的那段代码中,其实就是我调用一次微信模版推送方法,就会创建包含2个线程的线程池。
先继续补充第二个知识点。
CompletableFuture.supplyAsync
用某个线程池,去异步执行我的业务代码。

输出:

因为没有调用ExecutorService的shutdown方法,启动的方法不会停止,会一直在运行状态。
复现问题
通过上面的讲解,其实我们已经知道了问题所在,就是创建线程没有调用线程池的shutdown,导致线程池一直等待任务,而这个任务永远不会到来了,等到达一个临界值之后,就会把系统拉垮。
为了证实这个理论,我们来写一段代码来验证下。

输出:

增加shutdown方法:

结合前面的文章,这里留给大家一个思考题,加了shutdown方法的代码,真的能一直跑吗?pool到多少,会停止呢?
最后我的解决方案
可能大家都会以为我会直接增加shutdown方法来解决这个问题,但是我最后没有用shutdown来直接处理,而是把代码直接改成了

没有用线程池,没有用多线程的玩意儿,直接改成了for循环来修复了这个问题,因为从具体的业务出发,userIdList最多不过300个,我们是内部系统;还有就是充分相信微信的处理能力,如果这点有担忧的同学,可以改成异步的方法,我想着最后的结果反正就是推送不出去,就不去用复杂的异步调度了。
总结
- 问题在你这里来了,不管是不是你的问题,先要解决问题,owner精神;
- 这里说一点题外话,恢复系统正常使用永远是第一要务,遇到问题,首先第一时间要预估恢复系统的时间,然后果断的做出判断,重启应用能够解决问题,就先重启应用,恢复系统之后,再去排查问题;
- 能用简单的技术解决问题,就用简单的技术,不要动不动就用牛刀杀鸡;
- 理解并注意前人提出的那些规范原理,有时候真的少些一行代码,能把线上服务器搞崩;