# 22 | 支付订单信息:如何高效解决for循环产生的内存溢出? 你好,我是高楼。 今天,我们来优化支付订单接口。通过这个接口,我们来看看怎么高效解决for循环产生的内存溢出问题。 对于JVM内存溢出或泄露来说,通常性能人员都能定位到一个应用hang住了。但是,要想进一步判断出应用hang住的原因,并没有那么容易做到。因为内存大时做堆Dump比较费时,更重要的一点是,要想把堆里面的对象和栈关联起来是需要足够的经验和时间的。这也是其中的难点之一。 这节课我就带你来看看怎么解决这个难点。 不过,在此之前,我们会先处理一个熟悉的问题,就是数据库表加索引。很显然,我们在测试这个接口时又遇到它了。虽然我在[第16讲](https://time.geekbang.org/column/article/367285)中给你重点讲过这个问题,但是这一次的每秒全表扫描比之前要高得多。通过这次的讲解,我希望你能明白只要存在全表扫描,CPU消耗很快就会达到100%。同时,也希望你能借此看清楚全表扫描对CPU消耗的影响。 ## 场景运行数据 首先,我们来运行一下场景: ![](https://static001.geekbang.org/resource/image/02/e1/02f5facd8d0196ae07c223b8dc1e4ae1.png) 这是一个典型的TPS太低、响应时间不断上升的性能瓶颈,对于这种瓶颈的分析逻辑,我在前面的课程里已经写过很多次了,相信你已经掌握。下面我们来看一下具体的问题是什么。 ## 架构图 ![](https://static001.geekbang.org/resource/image/72/8f/72b7b04b1e2323d4fae9fcc30cb3be8f.png) 这个接口的链路比较简单:User - Gateway - Order - MySQL,我们大概记在脑子里就好。 ## 第一阶段 在这里我就不拆分时间了,我们直接来看全局监控。因为第一阶段的问题相对来说比较简单,只是性能瓶颈的表现形式和之前不太一样。 ### 全局监控分析 全局监控的数据如下: ![](https://static001.geekbang.org/resource/image/0a/3a/0a2471dca622df4a813e8c06d31e5b3a.png) 看到这张图,你是不是有一种终于见到典型性能瓶颈的感觉?CPU使用率这么高!那还不简单?打栈看代码呀! 不过,我们得先查一下是什么东西导致k8s-worker-1的CPU使用率这么高的。这个worker上运行的服务如下: ``` [root@k8s-master-2 ~]# kubectl get pods -o wide | grep worker-1 mysql-min-6685c9ff76-4m5xr 1/1 Running 0 4d23h 10.100.230.14 k8s-worker-1 skywalking-es-init-ls7j5 0/1 Completed 0 4d11h 10.100.230.18 k8s-worker-1 [root@k8s-master-2 ~]# ``` 可以看到,有两个服务在这个worker上跑着,一个是初始化容器,另一个是MySQL。初始化容器已经是完成的状态,那CPU使用率高肯定是因为MySQL了。因此,我们就进到容器中,执行下top,看看资源消耗到什么程度。 ![](https://static001.geekbang.org/resource/image/a4/5f/a420ef3454ee13f4f37834eyy0e5045f.png) 什么情况?CPU使用率这么高吗?既然这样,我们就得来查一下MySQL的全局监控了。 在这里,**查看全局监控其实是分析MySQL的必要步骤**。如果直接查看trx表,中间其实是有些跳跃的,因为查看MySQL的全局监控数据,才是承上启下的一步。我现在把这个过程写全一些,以免你产生困惑。 于是,我们执行一个mysqlreport命令,看看mysql的全局监控数据是怎样的。这里我截取了其中的一些重要信息: ``` __ Questions ___________________________________________________________ Total 307.93M 912.0/s +Unknown 201.91M 598.0/s %Total: 65.57 DMS 43.20M 128.0/s 14.03 Com_ 32.90M 97.5/s 10.69 QC Hits 29.91M 88.6/s 9.71 COM_QUIT 389 0.0/s 0.00 Slow 20 ms 273.66k 0.8/s 0.09 %DMS: 0.63 Log: DMS 43.20M 128.0/s 14.03 SELECT 32.39M 95.9/s 10.52 74.98 INSERT 10.64M 31.5/s 3.46 24.63 UPDATE 170.15k 0.5/s 0.06 0.39 REPLACE 0 0/s 0.00 0.00 DELETE 0 0/s 0.00 0.00 Com_ 32.90M 97.5/s 10.69 set_option 21.98M 65.1/s 7.14 commit 10.70M 31.7/s 3.48 admin_comma 137.68k 0.4/s 0.04 __ SELECT and Sort _____________________________________________________ Scan 20.04M 59.4/s %SELECT: 61.88 Range 0 0/s 0.00 Full join 32 0.0/s 0.00 Range check 0 0/s 0.00 Full rng join 0 0/s 0.00 Sort scan 120 0.0/s Sort range 2.41k 0.0/s Sort mrg pass 0 0/s ``` 你看,DMS中的select占比比较大。其实,如果只是select的占比比较大的话,倒不是什么大事,关键是在下面的数据中还有一个Scan(全表扫描),而全表扫描是典型的性能问题点。 看到这里,我想你应该非常清楚我接下来的套路了吧,就是找SQL,看执行计划,然后确定优化方案。如果你不太清楚,可以再看一下[第](https://time.geekbang.org/column/article/367285)[16](https://time.geekbang.org/column/article/367285)[讲](https://time.geekbang.org/column/article/367285)或[第](https://time.geekbang.org/column/article/370723)[20](https://time.geekbang.org/column/article/370723)[讲](https://time.geekbang.org/column/article/370723),其中都有描述。 ### 定向监控分析 于是,我们现在进入到了定向监控分析阶段。通过查看innodb\_trx表,我们看到了SQL中执行慢的语句,它的执行计划如下: ![](https://static001.geekbang.org/resource/image/82/4c/82c5c3ba11f32184972ef2ce32127f4c.png) 这是很典型的全表扫描,虽然数据量并不大,但是我们也要添加索引。添加索引的语句如下: ![](https://static001.geekbang.org/resource/image/e5/c7/e510e1a94a6d59c11204d062da9ed4c7.png) 这里你要注意一下,在创建索引的时候,如果数据量太大,创建索引可能会卡住很长时间,这要取决于机器单CPU的能力。 ### 优化效果 添加索引之后,我们直接来看一下优化效果: ![](https://static001.geekbang.org/resource/image/5d/1a/5d245afc0a324280d9d93c0102a0f81a.png) 你看,TPS要上千了! 其实,对于SQL的优化,如果我们只是加一个索引,那就算是非常简单的步骤了,并且效果也会非常好,TPS增加上千倍、甚至上万倍都有可能。可是,如果优化涉及到了业务逻辑,那就麻烦一些了。 如果你觉得这节课只是为了给你讲一个加索引的案例,那你就有些单纯了。下面,我们来看一个复杂的问题。 ## 第二阶段 在我接着执行压力的时候,看到了一个非常有意思的情况,我们来一起折腾折腾! ### 场景运行数据 ![](https://static001.geekbang.org/resource/image/29/a4/291ee16344ffbdd94cd0ae1yy62a11a4.png) 你看这张图,在压力持续大概6分钟之后,TPS不稳定也就算了,居然还掉下来了!你掉下来也就算了,居然还断开了!你断开了也就算了,响应时间居然也不增加! 这可怎么分析呢?想要拆分时间都没有一个适合的理由呀! 这时候,就得用上哥的性能分析决策树了。我们把相关的全局监控计数器都看一看,一层层查下去,还好这个接口的逻辑链路也不怎么长。 ### 全局监控分析 按照高老师的习惯,我们首先来看全局监控: ![](https://static001.geekbang.org/resource/image/62/78/62412b8c7159df58cbdcce7b196yya78.png) 从这张图上,我们什么也没看出来。所以,我们接着查性能分析决策树,一层层地看下去。当看到Order的GC健康状态时,我们看到了下面这些数据: ``` [root@svc-mall-order-568bd9b79-twhcw /]# jstat -gcutil 1 1s ​S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 100.00 0.00 100.00 100.00 95.06 92.82 1182 34.966 495 3279.704 3314.670 100.00 0.00 100.00 100.00 95.06 92.82 1182 34.966 495 3279.704 3314.670 100.00 0.00 100.00 100.00 95.06 92.82 1182 34.966 495 3279.704 3314.670 100.00 0.00 100.00 100.00 95.06 92.82 1182 34.966 495 3279.704 3314.670 ​90.88 0.00 100.00 100.00 95.08 92.82 1182 34.966 495 3286.621 3321.58 100.00 0.00 100.00 100.00 95.08 92.82 1182 34.966 496 3286.621 3321.586 100.00 0.00 100.00 100.00 95.08 92.82 1182 34.966 496 3286.621 3321.586 100.00 0.00 100.00 100.00 95.08 92.82 1182 34.966 496 3286.621 3321.586 100.00 0.00 100.00 100.00 95.08 92.82 1182 34.966 496 3286.621 3321.586 100.00 0.00 100.00 100.00 95.08 92.82 1182 34.966 496 3286.621 3321.586 100.00 0.00 100.00 100.00 95.08 92.82 1182 34.966 496 3286.621 3321.586 ........................... ``` 有没有感到绝望?内存泄漏了!年轻代和年老代的内存都用到100%了!即便是FullGC之后,内存也没回收下来,可怜呀。 ### 定向监控分析 既然是内存被用完了,那我们自然要查一下是什么样的对象把内存用完了。所以,我们进到容器里面,执行jmap -histo 1|more看一眼: ``` num #instances #bytes class name ---------------------------------------------- ​1: 49727866 1397691896 [ ​2: 12426103 795269200 [[ ​3: 12426038 397633216 com.mysql.cj.protocol.a.result.ByteArrayRo ​4: 2002596 384498432 com.dunshan.mall.order.domain.OmsOrderDetai ​5: 12426082 198817312 com.mysql.cj.protocol.a.MysqlTextValueDecode ​6: 2070085 182840264 [Ljava.lang.Object ​7: 6008660 144207840 java.lang.Lon ​8: 2207452 132116320 [ ​9: 4072895 97749480 java.util.ArrayLis ​10: 2002690 80107600 org.apache.ibatis.cache.CacheKe ​11: 2039613 65267616 java.util.HashMap$Nod ​12: 2197616 52742784 java.lang.Strin ​13: 14736 23246672 [Ljava.util.HashMap$Node ​14: 36862 3243856 java.lang.reflect.Metho ​15: 97195 3110240 java.util.concurrent.ConcurrentHashMap$Nod ​16: 62224 2986752 java.util.HashMa ​17: 19238 2452264 [ ​18: 21482 2360328 java.lang.Clas ​19: 26958 1078320 java.util.LinkedHashMap$Entr ........................... ``` 从中我们似乎找到了问题点。你看,这里面有一个MySQL的result占的内存还挺大,同时,在它的下面我们也看到了OmsOrderDetail类,这个类是用来在数据库中查询订单的详细信息的。 从逻辑上来讲,我们看订单的详细信息,实际上是想查询数据库中的信息,进而把查询出来的数据放到应用的内存中。所以,MySQL的result查的数据越多,就会导致应用的JVM内存消耗越大。 你也许会想,接下来是不是直接去看OmsOrderDetail的代码就可以了?你可以去看,但是,我们这个案例并没有那么直接。因为我们已经知道代码了,逻辑也梳理清楚了,所以,再去查看代码其实也看不出什么问题来。 那为什么JVM内存消耗会高呢?这里我们就要查一下线程在做什么动作了: ``` -- top [root@k8s-worker-3 ~]# docker exec -it 66d3639cf4a8 /bin/bash [root@svc-mall-order-568bd9b79-twhcw /]# top top - 16:10:50 up 11 days, 2:37, 0 users, load average: 3.92, 4.77, 3.35 Tasks: 4 total, 1 running, 3 sleeping, 0 stopped, 0 zombie %Cpu0 : 46.7 us, 8.6 sy, 0.0 ni, 43.6 id, 0.0 wa, 0.0 hi, 0.7 si, 0.3 st %Cpu1 : 23.3 us, 9.2 sy, 0.0 ni, 66.1 id, 0.0 wa, 0.0 hi, 1.0 si, 0.3 st %Cpu2 : 50.0 us, 7.2 sy, 0.3 ni, 41.4 id, 0.0 wa, 0.0 hi, 0.7 si, 0.3 st %Cpu3 : 46.4 us, 8.5 sy, 0.0 ni, 43.7 id, 0.0 wa, 0.0 hi, 1.0 si, 0.3 st %Cpu4 : 50.5 us, 8.0 sy, 0.0 ni, 41.5 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st %Cpu5 : 50.2 us, 3.1 sy, 0.0 ni, 46.1 id, 0.0 wa, 0.0 hi, 0.7 si, 0.0 st KiB Mem : 16265992 total, 171760 free, 9077080 used, 7017152 buff/cache KiB Swap: 0 total, 0 free, 0 used. 6676508 avail Mem ​PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND ​1 root 20 0 8788300 4.2g 13860 S 127.9 27.1 115:17.15 java ​575 root 20 0 11828 1776 1328 S 0.0 0.0 0:00.01 sh ​789 root 20 0 11964 1980 1484 S 0.0 0.0 0:00.02 bash ​802 root 20 0 56232 2012 1432 R 0.0 0.0 0:00.05 to -- top -Hp 1 top - 16:11:39 up 11 days, 2:38, 0 users, load average: 8.87, 6.09, 3.87 Threads: 85 total, 1 running, 84 sleeping, 0 stopped, 0 zombie %Cpu0 : 55.6 us, 7.1 sy, 0.0 ni, 36.6 id, 0.0 wa, 0.0 hi, 0.3 si, 0.3 st %Cpu1 : 41.3 us, 3.8 sy, 0.0 ni, 54.9 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st %Cpu2 : 30.4 us, 9.9 sy, 0.0 ni, 59.4 id, 0.0 wa, 0.0 hi, 0.3 si, 0.0 st %Cpu3 : 60.3 us, 6.7 sy, 0.0 ni, 32.7 id, 0.0 wa, 0.0 hi, 0.3 si, 0.0 st %Cpu4 : 21.2 us, 9.2 sy, 0.0 ni, 69.6 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st %Cpu5 : 45.6 us, 10.1 sy, 0.3 ni, 43.6 id, 0.0 wa, 0.0 hi, 0.0 si, 0.3 st KiB Mem : 16265992 total, 197656 free, 9071444 used, 6996892 buff/cache KiB Swap: 0 total, 0 free, 0 used. 6681848 avail Mem ​PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND ​7 root 20 0 8788300 4.2g 13836 R 96.0 27.1 70:13.42 VM Thread ​26 root 20 0 8788300 4.2g 13836 S 0.7 27.1 0:05.70 VM Periodic Tas ``` 执行了上面两个命令之后,你有没有注意到只有一个线程在消耗CPU?根据我们前面查看的GC状态,这个线程应该在忙着做FullGC。我们打印栈信息来确认一下,果然是它! ``` "VM Thread" os_prio=0 tid=0x00007fb18c0f4800 nid=0x7 runnable ``` 到这里,我们下一步该怎么做就非常清晰了,那就是打印一个堆Dump,来看看对象在内存中的消耗比例。 所以,我们现在执行下面这个命令,来生成堆Dump。 ``` jmap -dump:format=b,file=文件名 pid ``` 然后,我们再把生成的堆Dump下载下来,用MAT打开。 在我打开堆文件的过程中出现了一个小插曲,这也是你需要留意的地方,那就是如果堆Dump的内存太大的话,我们打开堆Dump就会报这样的错: ![](https://static001.geekbang.org/resource/image/be/85/be3dc22bf50e7cdab4c3ea474a050d85.png) 这个时候我们就要到MemoryAnalyzer.ini文件中,把JVM的最大值参数-Xmx给调大。-Xmx的默认值是1G,至于要调到多大就要看你想打开多大的文件了。如果-Xmx调整得不够大,还会出现下面这样的错误: ![](https://static001.geekbang.org/resource/image/da/0e/dac4692b9da8b5680131aa4ea16b470e.png) 在我们费了九牛二虎之力,终于打开堆文件后,看到这样的信息: ![](https://static001.geekbang.org/resource/image/1d/7f/1d6964cyy56e5934b8657234ea2cf47f.png) 有一段内存居然消耗了2.6G!这个可疑的内存点在上图中也出现了(用MAT或jmap来看对象的内存消耗,是两种不同的手段,你可以自己选择。只是在MAT上,我们可以看到可疑的内存消耗点的提醒,而在jmap中是不会给出这样的提醒的,需要我们自己判断),我们点进去看一眼,看看可疑的内存点里具体的对象是什么: ![](https://static001.geekbang.org/resource/image/02/bc/02cb9099ec17332ab42e2564c2a106bc.png) 你看,确实是SQL返回的数据量比较大,在上图的列表中居然有1千多万条记录。我们再把相应的栈展开看看: ![](https://static001.geekbang.org/resource/image/4e/94/4e2dea48f6969a4a6f3919bddd4c3594.png) 看到OmsOrderDetail这个类了没?你可能会想,是支付订单信息这个接口有问题!但是证明还不足,我们要确定OmsOrderDetail是不是在这个接口中调用或生成的,才能判断出是不是这个接口的问题。 由于当前使用的接口是paySuccess,我们看一下paySuccess的调用逻辑,发现paySuccess有一个getDetail函数。看到这个“getDetail”,还有其中这个“Detail”字符,你是不是感觉和OmsOrderDetail可以对应上?那我们就来查看一下getDetail对应的代码,看看它和OmsOrderDetail之间是什么关系: ![](https://static001.geekbang.org/resource/image/51/4a/5133a446f6e72549a40daace912f794a.png) 不难发现,getDetail是一个OmsOrderDetail类。这么看来,我们的接口确实用到了OmsOrderDetail类,你是不是有一种抓住元凶的兴奋感?别着急,让人无奈的事情总是会出现的,下面我们看一下这段代码对应的SQL语句是一个什么样的逻辑。 通过查看代码,可以看到这个接口中有两个update和一个select,这三个语句分别是: ``` ​update oms_orde ​........................ ​where id = #{id,jdbcType=BIGINT ​ ​update pms_sku_stoc ​........................ ​where id = #{id,jdbcType=BIGINT ​