少一行代码,把线上环境搞崩了

大家好,我是周伟。

今天给大家带来一宗热乎乎的**“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个引起警觉的代码newFixedThreadPoolCompletableFuture.supplyAsync

从上面这段代码,大家看出什么问题了吗?欢迎在留言区和我谈论。

没看出问题的也没事,我们接着来补充并发编程的基础。

补充基础

newFixedThreadPool

创建固定数量的线程池。

其实在阿里手册里边就有一条,不要使用封装好的这些创建线程池的方法。

我们来看它具体的实现

newFixedThreadPool的具体实现

这里面又夹杂了一道常见面试题。

创建线程池ThreadPoolExecutor有几个参数,你是怎么理解这几个参数的?在工作中,你是怎么设置的?

这里先不散开来讲了,还是针对性的讲newFixedThreadPool。

ThreadPoolExecutor继承自AbstractExecutorService,而AbstractExecutorService实现了ExecutorService接口。

ThreadPoolExecutor方法

7个参数:

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

回顾newFixedThreadPool的具体实现:

newFixedThreadPool的具体实现

它的核心线程数和最大线程数是一样,都是nThreads的变量的值,该变量由用户自己决定,所以说是固定大小线程池。此外,它每隔0毫秒回收一次线程,换句话说就是不回收线程,因为它的核心线程和最大线程数是一样,回收没有任何意义,此外,使用了LinkedBlockingQueue队列,该队列其实是有界队列,很多人误解了,只是它的初始值比较大,是Interger的最大值,Integer.MAX_VALUE。

回到最开始,我被秀的那段代码中,其实就是我调用一次微信模版推送方法,就会创建包含2个线程的线程池。

先继续补充第二个知识点。

CompletableFuture.supplyAsync

用某个线程池,去异步执行我的业务代码。

模拟实现

输出:

输出效果

因为没有调用ExecutorServiceshutdown方法,启动的方法不会停止,会一直在运行状态。

复现问题

通过上面的讲解,其实我们已经知道了问题所在,就是创建线程没有调用线程池的shutdown,导致线程池一直等待任务,而这个任务永远不会到来了,等到达一个临界值之后,就会把系统拉垮。

为了证实这个理论,我们来写一段代码来验证下。

伪代码验证问题

输出:

输出

增加shutdown方法:

增加shutdown方法

结合前面的文章,这里留给大家一个思考题,加了shutdown方法的代码,真的能一直跑吗?pool到多少,会停止呢?

最后我的解决方案

可能大家都会以为我会直接增加shutdown方法来解决这个问题,但是我最后没有用shutdown来直接处理,而是把代码直接改成了

直接for循环

没有用线程池,没有用多线程的玩意儿,直接改成了for循环来修复了这个问题,因为从具体的业务出发,userIdList最多不过300个,我们是内部系统;还有就是充分相信微信的处理能力,如果这点有担忧的同学,可以改成异步的方法,我想着最后的结果反正就是推送不出去,就不去用复杂的异步调度了。

总结

  1. 问题在你这里来了,不管是不是你的问题,先要解决问题,owner精神;
  2. 这里说一点题外话,恢复系统正常使用永远是第一要务,遇到问题,首先第一时间要预估恢复系统的时间,然后果断的做出判断,重启应用能够解决问题,就先重启应用,恢复系统之后,再去排查问题;
  3. 能用简单的技术解决问题,就用简单的技术,不要动不动就用牛刀杀鸡;
  4. 理解并注意前人提出的那些规范原理,有时候真的少些一行代码,能把线上服务器搞崩;