背景
故事是这样的,好像是测试在进行代扣流程的压力测试,然后群里有人@了:“怎么这个dubbo服务又调不通了?”
打开群里的截图粗略看下来好像是报的服务调用失败的错误,@了运维小哥哥说是清结算前置应用宕掉了,内存溢出了
问题定位与分析
于是赶紧找运维小哥哥将服务器上的堆dump文件弄了下来,对于堆内存溢出的分析我们还是通过jdk自带的jvisualvm来进行分析,导出来的hprof还是挺大的,花了近30-40秒才加载完毕,查看类的当时快照的情况发现应用中有几个业务model对象和数据库连接对账实例数排在前列
然后通过jvisualvm的概要发现触发OOM的入口
上图溢出的原因显示的是因为mybatis在构造解析sql的时候,这时我们接着往下看
发现导致该OOM的流程是清结算应用中有一个异步处理的流程(上层支付系统在实时处理完支付结果后有一个异步操作,将此笔支付订单通过mq队列的方式同步给清结算系统;而清结算系统侧则会有一个消费过程:从mq中取出一笔笔的支付订单然后落地到清算系统中用来做后续的清分结算)
而在消费到数据后进行入库时内存溢出正发生在此阶段,通过查看此溢出的线程名SimpleAsyncTaskExecutor-74
发现是执行队列消费的线程池
又继续翻了翻该消费队列线程池的其他线程当时的情况
发现有很多都在BLOCKED,而BLOCKED的地方则是发生在在日志写入的地方
而后又打开了应用的队列消费的配置文件,发现队列消费者的线程池数配置为
查看了下当时进行压测时的支付系统交易的并发量(高峰时140+笔/s)
紧接着又打开了应用的数据库连接池配置文件
db.initialPoolSize=10
db.minPoolSize=10
db.maxPoolSize=20
结合以上信息来看上层支付的交易发生后一直往队列中进行推送,而消费者端虽然消费入口开了300线程,但是整个消费流程中有一环就是连接数据库进行插入数据,此时应用的数据连接最大只有20,这样就会导致一个现象(有一个容量固定的桶,一个口径15cm的管子往里面灌水,而只有一个口径1cm的管子从桶里往外抽水,时间一久自然桶中水满而溢出来)
而我们又回过头去查了下桶的容量,找了运维小哥哥把当时应用堆大小的启动值发出来了
-Xms3072m
-Xmx3072m
-XX:NewSize=1024m
-XX:MaxNewSize=1024m
解决方案
so,通过以上问题定位和分析后我们列出如下几点导致OOM的原因:
- 应用机器的堆内存设置的过于小
- 队列消费的线程池数量过于大(当时是相对当前的这种情况来说)
- 应用数据库连接池设置过于小
- 应用流程中日志打印量太大(一笔支付流程中涉及到很多信息)
针对以上问题和后续的压测标准我们将对应的配置信息做出如下相应调整:
- 让运维小哥哥调整了机器堆大小(增大)
- 调整了消费者线程池大小(最大300->200),缩小并行消费流量
- 调整了数据库连接池大小(最大20->500),增大入库并行量
- 调整日志输出点(只打印核心日志)
调整过后我们commit->push->test继续压测中…
总结
堆的内存溢出在实际应用中我们可能会经常碰到,但是如何避免需要我们在日常开发中针对当时实际业务场景和需求进行综合考量后才可以进行对应的技术参数配置,从而使我们的应用能够充分利用到机器资源提供最高最稳定的服务