线上内存泄漏!如何破案( 二 )


不得不说,阿里在java技术积淀远超我司.因为之前已在镜像里面集成了阿尔萨斯,我们立即启动cpu profiler采样生成火焰图.

线上内存泄漏!如何破案

文章插图
果然除了日志打印以外,我们又发现了一处CPU热点
线上内存泄漏!如何破案

文章插图
这两个地方合计占掉了60%以上的性能.
CPU高的原因
首先分析一下日志占用过高,这是一个使用log4j2的问题,涉及日志打印参数调优,我们之前已经优化过一轮的参数
#RingBuffer大小 AsyncLogger.RingBufferSize=524288#日志等待策略sleepAsyncLogger.WaitStrategy=SLEEP#Ringbuffer满了后直接丢弃log4j2.AsyncQueueFullPolicy=Discard
理论上这已经是性能最好的日志策略,为什么会出现占用CPU负载的问题?
问题出在LockSupport.parkNanos上,简单来说当日志消费速度赶不上生产速度的时候,日志线程会调用这个方法自旋等待若干纳秒,在线程数少的时候性能影响不明显,但是在高并发的情况下会造成大量线程在短时间内频繁唤醒/等待,从而影响业务性能.解决方案:是把自旋的时间间隔调大,如下
AsyncLogger.RingBufferSize=524288AsyncLogger.WaitStrategy=SLEEPlog4j2.AsyncQueueFullPolicy=DiscardAsyncLogger.SleepTimeNs=500
分析下第二个CPU热点,这个问题没那么复杂
线上内存泄漏!如何破案

文章插图
是一个对象深拷贝的性能问题,每次请求来的时候都会将一个大对象先序列化在反序列化,这个在请求量低的时候影响较小,但是在我们每天几千万的请求量冲击下,性能瓶颈非常明显.
讲一下对象拷贝的四种解决方案:
JSON : 非常规,吃CPU
Apache BeanUtils :性能最差,不建议使用
Spring BeanUtils: 性能稍好
MapStruct MapStruct – Java bean mAppings, the easy way!,性能无损,推荐!!
具体各自的拷贝原理不再深入分析,大家可以搜资料查看
热点问题解决了
给一下优化前后的CPU对比,以下优化结果是在请求量翻倍同时pod数减半的CPU表现:
优化前: 45%+
线上内存泄漏!如何破案

文章插图
优化后: 11%
线上内存泄漏!如何破案

文章插图
2.3 最终定位
随着一步步的分析,我们也越来越接近问题的真相: CPU虽然有点高,但仍不足以解释缓慢和重启的现象,另外问题是在线上请求量增大以及随时间推移逐渐暴漏的,几乎可以断定网关存在内存泄漏.于是我们把调查重点投向JVM内存,布下天罗地网,静等凶手再次犯案.
功夫不负有心人,在部署约一天后,几台服务器又开始重启,我们迅速登录还未重启的机器,执行以下操作
  • 首先查看jvm内存已经逼近100%
  • GC非常活跃且无效,大量的内存无法回收
  • 通过火焰图查看的CPU绝大部分在执行GC
  • jmap -dump:format=b,file=heapdump.phrof pid 生成内存dump并上传cos
内存杀器Jprofile这个时候就要请出排查内存问题的另一大杀器Jprofile,具体资料可以在网上搜,这里主要介绍定位过程.
首先查看内存分布,有1000W+的ImmutableTag,不是我们的业务对象...非常意外
其次查看大对象,SimpleMeterRegistry占用了80%的内存空间!!!
这两个类均属于io.micrometer的核心组件,用来暴漏服务参数供监控使用.Micrometer中包含的SimpleMeterRegistry,它在内存中维护每个meter的最新值.
再一次分析内存中保留的对象内容
线上内存泄漏!如何破案

文章插图
可以看出1000W+的对象中,全部记录的是我们每次请求的RouteUri Method 耗时等信息,通过关键字定位,这些对象生产的源头是GateWay的 GatewayMetricsFilter组件,这里会记录所有路由请求的信息apply到micrometer中.而这个GatewayMetricsFilter的启用条件是存在Spring Boot Actuator组件.我们业务中刚好引用了该组件.
至此凶手归案,我们下线Actuator,并手动将GatewayMetricsFilter启动设置为False后,问题彻底解决.
三 内存泄漏原因
但是为什么呢?一个Spring官方提供的监控组件会导致内存泄漏?为什么对象持续无法回收?直觉告诉我们一定是哪个地方不太对劲.珍贵的食材往往需要最简单的烹饪方式,最复杂的场景往往用最朴素的手段抽丝剥茧
3.1 DEBUG过程
幸好我们有一套完整可用的开发环境,足够做场景复现,打好断点触发请求.经过几轮分析,犯案原因也随之浮出水面.
首先还是从SimpleMeterRegistry的引用链开始,(过程比较无聊,不再赘述)


推荐阅读