关于 Druid 与 MySQL-Connector8.x 引发的 boot.loader 包对象无法回收的坑

问题背景

  1. 项目架构: Springboot2.x架构,微服务框架SpringCloud Greenwich.SR6,数据库MySQL5.7
  2. 近期生产环境某服务经常发生系统卡顿、响应慢现象,甚至出现服务自动挂掉
  3. 环境部署在自建的容器云上,通过监控初步判断为应用占用内存过高所致,最终导致OOM
  4. 由于日志没有持久化,系统在挂死后,容器示例无法进入,导致最终挂死之前原因不能100%确认
  5. 出现系统慢的时间基本为早上8点至9点,下午5点左右时间段,该时间段内,正值用户访问量略高
  6. 在对容器实例进行重启后,可运行正常一段时间,在用户访问量上来后,依然可能出现上述问题
  7. 过程中,观察容器占用内存,重启后不久,内存就持续飙升,一直较高,且没有回落现象

问题排查过程

排查过程使用的工具总结(干货)

  1. 命令:jstat -gc pid 1000 100 查看gc情况
  2. 命令:jmap -histo pid > server.log 查看堆内存对象实例数情况
  3. 命令:jstack pid 查看线程栈信息
  4. 命令:top,由于容器云 top -Hp 无法执行,故无法定位高CPU的线程,智能看整个进程占用情况
  5. 实时日志:错误日志与正常日志,容器云提供
  6. 监控:容器云提供,可观察CPU、内存、网络IO进出等
  7. 本地排查:jconsole、jvisualvm工具
  8. 命令:jmap -dump:format=b,file=server.hprof pid,导出堆内存转储文件
  9. 工具:MAT,堆内存可视化分析工具
  10. 注意:容器云内,较多 jvm 命令不支持,也为排查问题增加了难度;

排查思路分析

  1. 从CPU占用情况来看,并不高,由于线上也不支持定位到具体线程CPU占用情况,故先不予考虑;
  2. 核心排查内存占用高的原因,从 jmap -histo 的结果入手

在这里插入图片描述

  1. 对比正常环境,观察到了几个异常对象,java.net.URL、StringSequence、JarURLConnection、bonree包下的对象
  2. 首先是怀疑到了bonree,这个东西网上查了下,是一个博睿公司的监控组件,系统架构并没有显示集成,初步判断应该是在部署的容器云环境有自动集成
  3. 虽然bonree这个对象高,不过生产环境有些实例又没有bonree,但表现出来的现象依然是内存占用较高
  4. 然后就去查GC情况,发现在系统低峰期,最多的时候有上百kb的对象在年轻代产生,并且看得出,通过YGC,也回收不了太多
    在这里插入图片描述
  5. 于是把问题焦点放在 java.net.URL 对象上,结果跟踪错误日志,正好看见了较多系统调用外部HTTP接口报错现象

在这里插入图片描述
8. 跟着报错堆栈,找到了关键代码,发现确实有一处代码存在瑕疵,在调用外部HTTP接口出现异常时,系统报出异常后,没有正常释放连接,

在这里插入图片描述
9. 于是将 releaseConnection() 放在 finally 里,以为问题定位到了,后来发到线上环境,问题依然存在
10. 就这样折腾了几天,都没有太大收获,于是只能倒堆栈文件分析了

堆栈文件分析

  1. 考虑设置:-XX:+HeapDumpOnOutOfMemoryError 参数,但一想,如果服务挂了,容器重启,该转储文件就没了
  2. 只能手动线上导出,而且得在内存占用高水位时倒,这样的堆内存文件才有较高参考价值;
  3. 使用:jmap -dump:format=b,file=server.hprof pid,导出了以后,还不好拿出来,因为是容器云,层层服务器管理都掌握在客户手里,需要提申请、打报告,经过一天,才把一3台实例、共计6个多G的文件弄了出来
  4. 一开始想图省事,用本地jvisualvm打开,想看个什么名堂出来,结果没有什么收获
  5. 于是切换到更专业的工具,MAT

在这里插入图片描述
6. 这个工具果然挺强的,自带一些问题分析和建议

在这里插入图片描述
7. 之前没注意到线程这块数量,只是导出了线程栈,大概看了下,并没有太多运行中的线程,也就放过了这一方面线索
8. 现在看到MAT提示这个,于是一点点对照Thread面板看,发现了一个点,别的太抽象,不过看到sleuth关键字,这个我认识,貌似和sleuth有关

在这里插入图片描述
9. 于是检查了一番sleuth的配置,因为之前印象中是关闭掉的
在这里插入图片描述
10. 为了排除掉这个问题,干脆将spring-cloud-starter-zipkin依赖都排除掉算了
11. 继续排查,发现logstash 、MybatisPlus、boot.Loader比较高
在这里插入图片描述
12. logstash 这个跟踪到里面内容看,确实积压了不少日志信息,有些内容还比较大,经过了解,发现生产环境都没有部署 logstash,logstash配置的为生产环境地址,网络上根本联不通
在这里插入图片描述
13. 通过翻阅logback.xml 配置文件,发现确实存在系统操作日志有写入logstash,于是将此处,appender引用注释掉,最后干脆把 logstash-logback-encoder 从工程里去掉了,以便排除这些干扰因素
在这里插入图片描述
在这里插入图片描述
14. 经过这一番折腾,中途还优化了redis、Redisson线程数、关闭了swagger文档等等方面;
15. 然后就是 MybatisConfiguration 这个大对象了,足足有19M回收不掉,当时脑子里想到的就是可能存在慢SQL所致
16. 确实,在代码中发现了较多类似这样的大SQL,主要是关于审批流相关功能,这种SQL看着都慢,而且与客户反馈的现象很符合,说是业务提交审批流过程非常慢。

在这里插入图片描述
17. 不过经过后来其他同事排查,发现这些SQL执行频次都不高,执行高频的那几个SQL也都非常快,还有一些慢SQL,通过MySQL服务端的日志,已经找到了,在最近已经都优化掉了。
18. 于是感觉这个问题应该还是没有找到原因,MybatisConfiguration之所以大,经过冷静的分析,是因为这个模块里确实有很多mapper文件,服务启动到时候,应该会将这些mapper.xml都解析到内存中,理论上来说,是属于大而不会过于增长的对象。另外考虑,这个对象不管里面的SQL慢不慢,至少不会导致占用大量内存,因为数据查出来一个,返回给前端,这个线程就结束了,里面的对象一定会被GC给回收掉,不可能常驻内存里面。
20. 这一下,又让问题陷入了迷雾之中,于是很无奈,把上面修改的内容发布上去先看看,虽然心里觉得问题应该还是没找到,但是还是想看看,是否有惊喜。
21. 果然,发布上去后,虽然线程数暂时没上来,但是内存占用依然较高,这个背后的罪魁祸首,依然还没定位到

定位到问题关键(占大头)

焦点回到boot.loader

  1. 于是,再把视线拉回到这两个东西
    在这里插入图片描述
  2. 经过了前期对这两个东西的研究,发现他并不是工程里直接依赖的class,而是springboot打包插件,在打好的jar包里植入的

在这里插入图片描述
3. 分析其作用,大概为springboot的自定义类扫描和加载器等功能吧,正是因为他并不在工程里直接又,idea搜索不到,所以一直一头雾水
4. 但是这个两个东西,相比正常的环境,不管我调整了什么,他们两都不下去,问题一定和他们有关系
5. 加上想到了必须是jar运行才有,于是本地将代码打包,然后java -jar 运行其来
6. 激动的时刻到了,本地居然复现了,气死了,因为之前就想在本地复现这个现象,一直没有,原因就是直接通过IDEA,掉main方法启动的,无语……
在这里插入图片描述
7. 想着本地都能复现了,这个问题解决起来就不难了。谁知道又遇到一些比较棘手的问题,这个怎么调试?
8. 于是抱着尝试的心态,我在项目里面显示的加入了spring-boot-loader依赖
在这里插入图片描述

  1. 然后配置IDEA,抱着试试的心态,看看是否能进断点

在这里插入图片描述
10. 根据经验,我把断点断在如下位置,观察url都是些什么东西
在这里插入图片描述
11. 结果如之前猜想一样,断点确实进来了,而且确实是在服务启动到时候,大量调用get 方法,在服务启动完成后,也会有一些情况调用,比如第一次访问一个API地址,会有调用,还有访问swagger文档,会每次都有一些调用,如果放着不动,也就每隔5秒钟,被调用一次
12. 这就有点不科学了,根据现象,该对象增长的如此之快,应该不可能以这样的频次创建,一定是非常快的

想办法debug,观察线程调用栈

  1. 于是折腾半天,想了一个这样的招,将端点断在这个对象的构造方法里,因为创建了这么多对象,那肯定要调这个构造方法来创建嘛,然后在里面把url都打印出来,看看都扫描的是些什么jar或class

在这里插入图片描述
14. 结果这个端点,导致服务都起不来。于是只能在服务启动阶段把这个端点先失效,待服务基本启动完毕了,再启用;
15. 这样,根据访问的几个请求来看,url基本都是jar包下面的classes目录,然后调用频次也不高。
16. 同时使用jvisualvm,观察这服务的堆内存里,那几个重点对象,发现增长不快;下图主要表示怎么观察,实际截图是已经正常了

在这里插入图片描述

  1. 这个时候,灵光一现,想着之前访问都是很简单的API,没有访问数据库查询的,再试试一个有查数据库的请求试试

  2. 于是找到了一个很合适的接口,就是用户登录,这个接口不需要token,同时还需要查库验用户,然后随便填写了用户密码,点了一下,哇,一大波创建JarURLConnection

  3. 这还不能说明什么问题,可能第一次访问嘛对吧,然后我接着来登录几次,第2次,第3次……,出了需要输入验证码,这个很讨厌

  4. 想办法绕过验证码后,再次测试,1次2次3次4次,……,JarURLConnection实例数不断飙升,啊,找了快一礼拜的问题了, 终于到了!

  5. 有了这个突破口,思路就来了,在断点里,一个个往回重溯 调用 堆栈(栈桢),看到到底是哪个地方调来的?
    在这里插入图片描述

  6. 最后,把焦点锁在了com.alibaba.druid.util.MySqlUtils#getLastPacketReceivedTimeMs,这个方法居然写死了MySQL的连接驱动类,而且是不带cj.包的这个,这个就比较明显了,MySQL8.x的客户端连接包,我们都配置的是 com.mysql.cj.MysqlConnection 这个呀,怎么这个地方还写死找不带cj包的驱动类呢?

  7. 于是对比正常系统的代码一看,这里做了两次,Utils.loadClass
    在这里插入图片描述

  8. 仔细一分析,就比较好理解了,druid这个地方,每次都去找老的那个MySQLConnection,每次找libs目录下的所有jar都找不到,结果还每次都要找

  9. 找一次,没找到,内存里就创建了一堆JarURLConnection等对象,这个有恐怖了,每一次的数据库查询,都会干这个事情啊,这当并发多一点的时候,系统能快?能扛得住?

  10. 虽然后面发现GC可以回收掉,但是架不住操作数据库在线上太频繁了,回收后立马就能上去

  11. 最后解决这个问题可以说就是1s,把druid版本从1.1.22升级到1.1.24即可,再次测试,问题解决

  12. 至于更详细的分析,最后看了网上博客,搜索了下这个版本,好多博主都写了这个问题,发现其实还有正好是因为testOnBorrow这个开关,我们开着的,才正好采上这个坑在这里插入图片描述https://blog.csdn.net/weixin_44990626/article/details/118057115
    https://blog.csdn.net/BTBO_/article/details/117632093

  13. 如果不升级,可临时先把参数修改成false,也暂可避免

spring:
  datasource:
    druid:
      validationQuery: SELECT 1 FROM DUAL
      # 指明连接是否被空闲连接回收器(如果有)进行检验.如果检测失败,则连接将被从池中去除
      testWhileIdle: false
      # 指明是否在从池中取出连接前进行检验,如果检验失败,则从池中去除连接并尝试取出另一个,true对数据库操作有一定性能影响
      testOnBorrow: false
      # 指明是否在归还到池中前进行检验
      testOnReturn: false

最后遗留的bonree监控疑点

  1. 由于这块对象时而高,时而不高,非必现,不好排查,加上该类问题排查不属于我们所能掌控
  2. 再者,根据网上已有案例反馈,此类问题确实可能存在
  3. 所以需要与客户反馈,寻求帮助,让客户重视该问题
  4. 另外,需要在近期持续跟踪和监控,需要时,把一些关键的堆栈保留导出,作为后续问题排查线索
  5. 参考博文:https://blog.csdn.net/qq_30035133/article/details/103969802

总结经验教训

第一条:阿里开源的东西使用要谨慎

  1. 回想我们用过的 Druid、FastJson、nacos、sentinel
  2. 这些产品,架构设计的思想和理念都非常优秀,但落地实现后,经常报bug、漏洞等等
  3. 另外,目前的这个seata,在我们系统里用的就是很不舒服
  4. 阿里也就是Dubbo这个产品,还是稳点

第二条:日志收集很重要

  1. 生产环境由于没有完整的日志,这其实也让我们走了很多弯路
  2. 其实还是建议至少部署一个logstah把日志打到文件里也行,ELK觉得重的话
  3. 或者如果是这种容器云,平台应该有能力收集日志
  4. 最新用Loki + Grafanal也挺好呀
  5. 还有,实时日志最好不要输出debug,dao也不要,这样其实也及其影响服务器性能

第三条:环境有限的情况下要物尽其用

  1. 现在这种容器环境,相比传统物理机、虚拟机,有很多linux命令、jvm命令没有
  2. 这时候要考虑如何应对这种异常问题,其实是很困难的
  3. 这里比如在启动命令里加上OOM时导出堆内存转储文件,也是一种方式
  4. 在能将日志、堆栈等信息拿出来的时候,一定要想办法拿出来,用专业工具分析
  5. 专业的工具比你人眼去看,要省事很多,当然专业的工具得出来的结果,也需要你很多经验去判断

第四条:善于协调沟通

  1. 类似这种情况,服务器的资源及权限都掌控在客户手中,很多排查问题手段受限
  2. 过程中,则需要更多的找到关键线索,并及时把需要客户信息化部门人员配合和支持的事情梳理出来
  3. 最后就是团队内部自己加强沟通、特别是在没有排查思路的时候,大家要多交换信息
  4. 最后,该像客户提申请时,必须要提出,否则可能延误问题解决时间
  5. 例如从容器云里拿出一些日志文件,开一些网络访问端口等等
  6. 最后,若发现与本身项目无关的一些关键线索,也应该及时与客户相关技术人员沟通,寻求帮助
  7. 例如博睿监控,这块不受我们系统所控制,且无源码可看,遇到问题,必须向客户反馈,协调其他组织一起处理

第五条:连蒙带猜+善于总结+耐心

  1. 当你实在没有思路的时候,一定要发散下自己的思维
  2. 比如找其他同事说说,聊聊这个问题,看看他有什么看法和想法
  3. 然后结合实际情况,多联想、把大概率出现的一些猜想优先排查
  4. 如果还是没有思路,就要回头把问题在总结,把所有线索再梳理下
  5. 因为有很多问题,可能都是复合型问题,综合性问题,就是说问题原因不是某一方面原因所致
  6. 其实这种问题犹如破案,有时候更需要的是坚持和耐心

这次总结了1w字+,深刻的一次总结!深刻的一次教训,也是深刻的一次学习过程~

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值