089、案例实战:一次没有WHERE条件的SQL语句引发的OOM问题排查实践!

一次没有WHERE条件的SQL语句引发的OOM问题排查实践!

1、案例背景引入

这个案例也是我们线上曾经出现过的一个真实的生产案例,是一个年轻的工程师在使用mybatis写SQL语句的时候在某些情况下允许不加where条件就可以执行,结果导致一下子查询出来上百万条数据引发了系统的OOM。

这个案例本身是属于比较简单的那种,不涉及太多其他的技术问题,的确就是纯系统代码自身的问题导致的

所以我们拿这个案例来给大家再深入讲讲在使用MAT进行线上内存快照分析时候的一些技巧,相信对大家也是很有用的。

2、关于MAT工具对OOM故障的实践意义

这里给说一句,如果你的系统触发OOM是由于Tomcat、Jetty、RPC框架之类的底层技术,那么MAT对你来说用处并不是那么大

因为你最多就是用MAT找一找占用内存过多的对象,然后结合异常日志调用栈和MAT中的对象引用情况,初步定位一下是底层技术中的哪部分代码导致的内存溢出。

而如果要真正解决内存溢出问题,还得去仔细研究Tomcat、Jetty、RPC框架之类技术的底层源码,结合线上系统的负载情况、访问压力以及GC情况,以及底层技术的源码细节,真正分析清楚发生OOM的原因,然后才能解决。

但是如果OOM主要是由于你自己的系统代码的问题导致的,那么就容易解决的多了

只要依托MAT层层分析,瞬间就可以定位到你代码的问题所在,毕竟你自己写的代码你是最熟悉的,所以你很快就可以解决问题。

3、故障发生现场

我们先来看一下故障发生的现场,某一天突然我们收到反馈说线上一个系统崩溃不可用了,此时当然是立即登录到线上机器去查看日志了,在日志中果然发现了OOM的异常:java.lang.OutOfMemoryError,java heap space。

堆内存溢出了,那我们下一步肯定是把自动导出的内存快照拷贝到自己电脑上,用MAT去分析对应的内存快照了

今天就给大家详细说说真实的生产案例中,如何巧妙的用MAT工具迅速定位问题代码。

4、第一步:检查内存中到底是什么对象太多了

第一步,我们可以用MAT中的一个Histogram功能,去检查一下占用内存最多的对象有哪些

之前给大家介绍过内存泄漏报告的使用,那个功能也是没问题的,也很好用,但是今天给大家介绍另外一套分析的工具和思路,其实原理都是类似的。

分析内存快照,说白了,无非就是找到占用内存最大的对象,然后就是找到谁在引用这个对象,是哪个线程在引用,接着找到创建这些对象的相关代码和方法,然后你就可以一头扎到对应的源码里去分析问题了。

我们先看下面的图,在这个图里,可以点击下图中红圈处的一个按钮,那个就是Histogram按钮,点击过后就会进入另外一个界面。
在这里插入图片描述

接着我们进入Histogram界面,如下图所示:
在这里插入图片描述

在这个界面中,其实就可以瞬间看到是谁占用内存过多了,比如这里明显是Demo1$Data这个内部类占用了过多的内存。

这里说了,Demo1 D a t a 对 象 有 10000 个 , 此 时 你 有 没 有 一 种 很 好 奇 的 冲 动 , 想 要 看 看 这 些 D e m o 1 Data对象有10000个,此时你有没有一种很好奇的冲动,想要看看这些Demo1 Data10000Demo1Data对象都是什么东西?

没问题,我们继续接着分析。

5、第二步:深入看看占用内存过多的对象

此时我们可以进入第二步,你可以深入的看看占用内存过多的对象是被谁引用的,哪个线程引用的,他们里面都是什么东西

此时我们可以按照下图,点击红圈处的按钮。
在这里插入图片描述

点击上图的那个红圈处的按钮之后,就会进入一个dominator_tree的界面,他会展示出来当前你JVM中所有的线程,如下图所示:
在这里插入图片描述

在这里,你可以清晰的看到哪些线程创建了过多的对象,比如我们这里排名第一的线程就是:

java.lang.Thread @ 0x5c0020838 main Thread

就是说一个main线程创建了过多的对象

那我们就直接可以展开这个线程,到底他创建了哪些对象?看看下图。
在这里插入图片描述

我们展开main Thread之后,发现里面有一个java.util.ArrayList @ 0x5c00206a8

说明线程创建了一个巨大的ArrayList,我们继续展开这个ArrayList,里面是一个java.lang.Object[]数组,继续展开,就会看到大量的Demo1$Data对象了。

真相大白,其实从dominator_tree界面,我们很快就能找到是哪个线程创建了过多的对象,而且层层展开,就可以看到这个线程创建了哪些对象太多了,就可以跟之前的Histogram界面中占用内存最多的Demo1$Data对上了。

而且这里我们可以轻易看到每个Demo1$Data对象的详细的情况,你可以展开任何一个对象看看。

6、生产案例的追踪

当时对于我们那个线上生产案例而言,到这一步为止,大家猜猜追踪到的是什么东西?

我们当时追踪到这里,发现某个Tomcat的工作线程创建了一大堆的java.lang.HashMap,那么这些java.lang.HashMap中是什么?

我们发现全都是各种从数据库里查出来的字段,你只要展开那个HashMap就能看到你出来放入内存的所有数据。

所以看到这一步基本就很明确了,就是Tomcat的工作线程处理一个请求的时候,发起了一个SQL语句,查出来了大量的数据,每条数据是一个HashMap,就是这大量的数据导致了系统的OOM。

7、第三步:到底是哪一行代码创建了这么多的对象?

找到占用内存最大的对象之后,最后一步就是要定位一下是哪一行代码,或者是哪个方法创建了那么多的对象?

这个又需要另外一个视图了,大家看下图的红圈处。
**加粗样式**

点击上图红圈的按钮,会进入一个thread_overview界面,如下图所示,这里会展示出来JVM中所有的线程以及每个线程当时的方法方法调用栈,以及每个方法中创建了哪些对象:
在这里插入图片描述

比如上图,我们会直接看到一个main Thread,他先是执行了一个Demo1.java类中的第12行处的一个Demo1.main()方法,接着这个main方法又执行了一个java.lang.Thread类的sleep()方法,一清二楚。

任何一个线程在此时此刻都执行和调用了哪些方法,都会在这里显示出来。

我们接着展开上图中的Demo1.main()方法,你就可以看到线程调用每个方法的时候创建和引用了哪些对象,如下图所示。
在这里插入图片描述

在上图中,我们发现Demo1.main方法执行的时候创建了一个ArrayList,展开发现是一个java.lang.Object[]数组,再次展开发现就是一大堆的Demo1$Data对象,到此为止,真相大白,一清二楚。

通过上述步骤,你可以快速的定位出来占用内存过多的对象,以及到底是哪个线程创建了这些对象,到底是线程执行哪个方法的时候创建了这些对象,每个对象的细节你都可以看到是什么东西。

8、继续对生产案例进行追踪

采用上述方法,当时我们对生产案例进行追踪,立马就定位到了是系统中的一个业务方法,在执行查询操作的时候,因为没有带上WHERE条件,直接查询出来了全部的上百万的数据,导致了内存的溢出。

此时就直接对那个方法对应的SQL语句进行修改即可,要求他必须每次都带上WHERE条件。

9、经典的MAT步骤:可以套用到全部案例中去

大家应该还记得我们之前的几个案例都直接告诉大家,当时用MAT进行分析的时候,定位到了哪些对象是在哪里被引用的,其实用的都是类似这篇文章里讲解的步骤。

大家可以把这套MAT定位步骤套用到全部案例中去,也可以用到自己自己实际的工作中去。

End

狸猫技术窝专栏上新,基于真实订单系统的消息中间件(mq)实战,重磅推荐:
![image.png](https://img-blog.csdnimg.cn/img_convert/72c4c603936fdadacb9424fbbc613b8f.png#averageHue=#f5d074&clientId=u1011de75-497c-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=uf95a9ff6&margin=[object Object]&name=image.png&originHeight=709&originWidth=1063&originalType=url&ratio=1&rotation=0&showTitle=false&size=209145&status=done&style=none&taskId=uf3cf95ad-a801-4f80-8df1-ab91c9a08c1&title=)
未来3个月,我的好朋友原子弹大侠将带你一起,全程实战,360度死磕MQ
(点击下方蓝字进行试听)
从 0 开始带你成为消息中间件实战高手


重要说明:

  • 如何提问:每篇文章都有评论区,大家可以尽情在评论区留言提问,我都会逐一答疑(ps:评论区还精选了一些小伙伴对专栏每日思考题的作答,有的答案真的非常好!大家可以通过看别人的思路,启发一下自己,从而加深理解)

  • 如何加群:购买了狸猫技术窝专栏的小伙伴都可以加入狸猫技术交流群。(群里有不少一二线互联网大厂的助教,大家可以一起讨论交流各种技术)

  • 具体加群方式请参见文末。(注:以前通过其他专栏加过群的同学就不要重复加了)


1、案例背景

线上有一个数据同步系统,是专门负责从另外一个系统去同步数据的,简单来说,另外一个系统会不停的发布自己的数据到Kafka中去,然后我们有一个数据同步系统就专门从Kafka里消费数据,接着保存到自己的数据库中去,大概就是这样的一个流程。

我们看下图,就是这个系统运行的一个流程。

结果就这么一个非常简单的系统,居然时不时就报一个内存溢出的错误,然后就得重启系统,过了一段时间又会再次内存溢出一下。而且这个系统处理的数据量是越来越大,因此我们发现他内存溢出的频率越来越高,到这个情况,就必须要处理一下了。

2、经验丰富的工程师:从现象看到本质

其实一般遇到这种现象,只要是经验丰富的工程师,应该已经可以具备从现象看到本质的能力了。我们可以来分析和思考一下,他既然是每次重启过后都会在一段时间以后出现内存溢出的问题,说明肯定是每次重启过后,内存都会不断的上涨。

而且一般要搞到JVM出现内存溢出,通常的就是两种情况,要不然是并发太高,瞬间大量并发创建过多的对象,导致系统直接崩溃了。要不就是有内存泄漏之类的问题,就是很多对象都赖在内存里,无论你如何GC就是回收不掉。

那么这个场景可能是怎么回事呢?我们当时分析了一下,这个系统的负载并不是很高,随着数据量不少,但是并不是那种瞬时高并发的场景。那么很可能就是随着时间推移,有某种对象越来越多,赖在内存里了。

然后不断的触发gc,结果每次gc都回收不掉这些对象。

一直到最后,内存实在不足了,就会内存溢出,我们看看下面的图,在下图里就画出了这个问题。

3、通过jstat来确认我们的推断

接着直接在一次重启系统之后,用jstat观察了一下JVM运行的情况:

发现老年代的对象一直在增长,不停的在增长,每次Young GC过后,老年代的对象就会增长不少,而且当老年代的使用率达到100%之后,我们发现会正常触发Full GC,但是Full GC根本回收不掉任何对象。

导致老年代使用率还是100%!

然后老年代使用率维持100%一段时间过后,就会爆出内存溢出的问题,因为再有新的对象进入老年代,实在是没有空间放他了!

所以基本就确认了我们的判断,每次系统启动,不知道什么对象会一直进入堆内存,而且随着Young GC执行,对象会一直进入老年代,最后触发Full GC都无法回收老年代的对象,最终就是内存溢出。

4、通过MAT找到占用内存最大的对象!

关于MAT分析内存快照的方法,之前已经讲解的很详细了,其实在这些案例中就不用重复一些截图了,直接说出过程和结论就好!在内存快照中,我们发现了一个问题,那就是有一个队列数据结构,直接引用了大量的数据,就是这个队列数据结构占满了内存!

那么这个队列是干什么用的?简单来说,从Kafka消费出来的数据会先写入这个队列,接着从这个队列再慢慢写入数据库中,这个主要是要额外做一些中间的数据处理和转换,所以自己在中间又加了一个队列。

我们看下面的图。

那么这个队列是怎么用的?问题就出在这里了!

大家都知道,从Kafka消费数据,是可以一下子消费一批出来的,比如消费几百条数据粗来。因此当时这个写代码的同学,直接就是每次消费几百条数据出来给做成一个List,然后把这个List放入到队列里去!

最后就搞成了,一个队列比如有1000个元素,每个元素都是一个List,每个List里都有几百条数据!这种做法怎么行?会导致内存中的队列里积压几十万条,甚至百万条数据!最终一定会导致内存溢出!

而且只要你数据还停留在队列中,就是没有办法被回收的。

我们看下面的图。

其实上图就是一个典型的对生产和消费的速率没控制好的例子。从Kafka里消费出来数据放入队列的速度很快,但是从队列里消费数据进行处理然后写入存储的速度较慢,最终会导致内存队列快速积压数据,导致内存溢出。

而且这种队列每个元素都是一个List的做法,会导致内存队列能容纳的数据量大幅度膨胀。

最终解决这个问题也很简单,把上述内存队列的使用修改了一下,做成了定长的阻塞队列,比如最多1024个元素,然后每次从Kafka消费出来数据,一条一条数据写入队列,而不是做成一个List放入队列作为一个元素。

因此这样内存中最多就是1024个数据,一旦内存队列满了,此时Kafka消费线程就会停止工作,因为被队列给阻塞住了。不会说让内存队列中的数据过多。

我们看下面的图。

5、本文小结

本文是我们整个专栏的最后一个案例,其实相信大家认真学习完这个专栏之后,就会感受到我们设计这个专栏的思路。专栏的核心是通过一步一图和大白话的方式,让大家学会JVM的核心运行原理,接着学习了JVM GC优化的核心原理和OOM问题的核心原理。

接着我们给大家讲解了JVM的GC问题以及OOM的常见发生场景和解决方法。

同时我们带给了大家数十个来源于我们真实生产环境的JVM优化案例,包括GC优化案例和OOM优化案例,大量的优化案例让大家可以对各种不同场景的问题有一个了解,同时积累起来了对不同问题进行分析、排查和解决的一个思路。

在这个过程中,如果大家反复去把这些案例看几遍,吸收透彻了,本质上就会积累起来较为丰富的JVM优化实践的经验积累,当你日后真的在工作中需要解决JVM问题的时候,就会发现这些知识全部都可以派上用场了。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值