您现在的位置是:电脑教程 >>正文
JVM 内存大对象监控和优化实践
电脑教程83223人已围观
简介一、问题描音乐业务中,core服务主要提供歌曲、歌手等元数据与用户资产查询。随着元数据与用户资产查询量的增长,一些JVM内存问题也逐渐显露,例如GC频繁、耗时长,在高峰期RPC调用超时等问题,导致业务 ...

一 、对象问题描
音乐业务中,监控践core服务主要提供歌曲 、和优化实歌手等元数据与用户资产查询 。对象随着元数据与用户资产查询量的监控践增长,一些JVM内存问题也逐渐显露,和优化实例如GC频繁 、对象耗时长,监控践在高峰期RPC调用超时等问题,和优化实导致业务核心功能受损。对象

图1 业务异常数量变化
二、监控践分析与解决
通过对日志 ,和优化实机器CPU 、对象内存等监控数据分析发现:
YGC平均每分钟次数12次 ,监控践峰值为24次,和优化实平均每次的亿华云耗时在327毫秒 。FGC平均每10分钟0.08次,峰值1次,平均耗时30秒 。可以看到GC问题较为突出。
在问题期间,机器的CPU并没有明显的变化,但是堆内存出现较大异常。图2,黄色圆圈处 ,内存使用急速上升,FGC变的频繁,服务器租用释放的内存越来越少 。

图2 老年代内存使用异常
因此,我们认为业务功能异常是机器的内存问题导致的,需要对服务的内存做一次专项优化。
步骤1 JVM优化以下是默认的JVM参数:
复制-Xms4096M -Xmx4096M -Xmn1024M -XX:MetaspaceSize=256M -Djava.security.egd=file:/dev/./urandom -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/{ runuser}/logs/other1.如果不指定垃圾收集器,那么JDK 8默认采用的是Parallel Scavenge(新生代) +Parallel Old(老年代),这种组合在多核CPU上充分利用多线程并行的高防服务器优势,提高垃圾回收的效率和吞吐量 。但是,由于采用多线程并行方式 ,会造成一定的停顿时间 ,不适合对响应时间要求较高的应用程序。然而 ,core这类的服务特点是对象数量多 ,生命周期短。在系统特点上 ,香港云服务器吞吐量较低,要求时延低。因此,默认的JVM参数并不适合core服务。
根据业务的特点和多次对照实验,选择了如下参数进行JVM优化(4核8G的机器)。该参数将young区设为原来的1.5倍,减少了进入老年代的对象数量。将垃圾回收器换成ParNew+CMS ,可以减少YGC的次数,模板下载降低停顿时间。此外还开启了CMSScavengeBeforeRemark ,在CMS的重新标记阶段进行一次YGC ,以减少重新标记的时间。
复制-Xms4096M -Xmx4096M -Xmn1536M -XX:MetaspaceSize=256M -XX:+UseConcMarkSweepGC -XX:+CMSScavengeBeforeRemark -Djava.security.egd=file:/dev/./urandom -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/{ runuser}/logs/other1.
图3 JVM优化前后的堆内存对比
优化后效果如图3,堆内存的使用明显降低 ,但是Dubbo超时仍然存在 。
我们推断,在业务高峰期,该节点出现了大对象晋升到了老年代 ,导致内存使用迅速上升 ,源码下载并且大对象没有被及时回收 。那如何找到这个大对象及其产生的原因呢 ?为了降低问题排查期间业务的损失 ,提出了临时的故障转移策略,尽量降低异常数量 。
步骤2 故障转移策略在api服务调用core服务出现异常时 ,将出现异常的机器ip上报给监控平台 。然后利用监控平台的统计与告警能力,配置相应的告警规则与回调函数。当异常触发告警 ,通过配置的回调函数将告警ip传递给api服务,此时api服务可以将core服务下的该ip对应的机器视为“故障”,进而通过自定义的故障转移策略(实现Dubbo的AbstractLoadBalance抽象类,并且配置在项目),自动将该ip从提供者集群中剔除 ,从而达到不去调用问题机器。图 4 是整个措施的流程 。在该措施上线前,每当有机器内存告警时 ,将会人工重启该机器。

图4 故障转移策略
步骤3 大对象优化大对象占用了较多的内存,导致内存空间无法被有效利用 ,甚至造成OOM(Out Of Memory)异常。在优化过程中,先是查看了异常期间的线程信息 ,然后对堆内存进行了分析,最终确定了大对象身份以及产生的接口 。
(1) Dump Stack 查看线程
从监控平台上Dump Stack文件,发现一定数量的如下线程调用 。
复制Thread 5612: (state = IN_JAVA) - org.apache.dubbo.remoting.exchange.codec.ExchangeCodec.encodeResponse(org.apache.dubbo.remoting.Channel, org.apache.dubbo.remoting.buffer.ChannelBuffer, org.apache.dubbo.remoting.exchange.Response) @bci=11, line=282 (Compiled frame; information may be imprecise) - org.apache.dubbo.remoting.exchange.codec.ExchangeCodec.encode(org.apache.dubbo.remoting.Channel, org.apache.dubbo.remoting.buffer.ChannelBuffer, java.lang.Object) @bci=34, line=73 (Compiled frame) - org.apache.dubbo.rpc.protocol.dubbo.DubboCountCodec.encode(org.apache.dubbo.remoting.Channel, org.apache.dubbo.remoting.buffer.ChannelBuffer, java.lang.Object) @bci=7, line=40 (Compiled frame) - org.apache.dubbo.remoting.transport.netty4.NettyCodecAdapter$InternalEncoder.encode(io.netty.channel.ChannelHandlerContext, java.lang.Object, io.netty.buffer.ByteBuf) @bci=51, line=69 (Compiled frame) - io.netty.handler.codec.MessageToByteEncoder.write(io.netty.channel.ChannelHandlerContext, java.lang.Object, io.netty.channel.ChannelPromise) @bci=33, line=107 (Compiled frame) - io.netty.channel.AbstractChannelHandlerContext.invokeWrite0(java.lang.Object, io.netty.channel.ChannelPromise) @bci=10, line=717 (Compiled frame) - io.netty.channel.AbstractChannelHandlerContext.invokeWrite(java.lang.Object, io.netty.channel.ChannelPromise) @bci=10, line=709 (Compiled frame) ...1.2.3.4.5.6.7.8.9.state = IN_JAVA 表示Java虚拟机正在执行Java程序。从线程调用信息可以看到,Dubbo正在调用Netty,将输出写入到缓冲区。此时的响应可能是一个大对象 ,因而在对响应进行编码 、写缓冲区时,需要耗费较长的时间,导致抓取到的此类线程较多 。另外耗时长 ,也即是大对象存活时间长,导致full gc 释放的内存越来越小 ,空闲的堆内存变小,这又会加剧full gc 次数。
这一系列的连锁反应与图2相吻合 ,那么接下来的任务就是找到这个大对象 。
(2)Dump Heap 查看内存
对core服务的堆内存进行了多次查看,其中比较有代表性的一次快照的大对象列表如下,

图5 core服务的堆内存快照
整个Netty的taskQueue有258MB 。并且从图中绿色方框处可以发现 ,单个的Response竟达到了9M,红色方框处 ,显示了调用方的服务名以及URI。
进一步排查,发现该接口会通过core服务查询大量信息 ,至此基本排查清楚了大对象的身份以及产生原因。
(3)优化结果
在对接口进行优化后 ,整个core服务也出现了非常明显的改进。YGC全天总次数降低了76.5% ,高峰期累计耗时降低了75.5% 。FGC三天才会发生一次,并且高峰期累计耗时降低了90.1%。

图6 大对象优化后的core服务GC情况
尽管优化后,因内部异常导致获取核心业务失败的异常请求数显著减少,但是依然存在。为了找到最后这一点异常产生的原因,我们打算对core服务内存中的对象大小进行监控。

图7 系统内部异常导致核心业务失败的异常请求数
步骤4 无侵入式内存对象监控Debug Dubbo 源码的过程中 ,发现在网络层,Dubbo通过encodeResponse方法对响应进行编码并写入缓冲区 ,通过checkPayload方法去检查响应的大小,当超过payload时 ,会抛出ExceedPayloadLimitException异常。在外层对异常进行了捕获,重置buffer位置 ,而且如果是ExceedPayloadLimitException异常,重新发送一个空响应,这里需要注意 ,空响应没有原始的响应结果信息