关于内存:JVM-内存大对象监控和优化实践
作者:vivo 互联网服务器团队 - Liu Zhen、Ye Wenhao服务器内存问题是影响应用程序性能和稳定性的重要因素之一,须要及时排查和优化。本文介绍了某外围服务内存问题排查与解决过程。首先在JVM与大对象优化上进行了无效的实际,其次在故障转移与大对象监控上提出了牢靠的落地计划。最初,总结了内存优化须要思考的其余问题。 一、问题形容音乐业务中,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/other如果不指定垃圾收集器,那么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/other 图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)...state = IN_JAVA 示意Java虚拟机正在执行Java程序。从线程调用信息能够看到,Dubbo正在调用Netty,将输入写入到缓冲区。此时的响应可能是一个大对象,因此在对响应进行编码、写缓冲区时,须要消耗较长的工夫,导致抓取到的此类线程较多。另外耗时长,也即是大对象存活工夫长,导致full gc 开释的内存越来越小,闲暇的堆内存变小,这又会加剧full gc 次数。 ...