服务器内存问题是影响应用程序性能和稳定性的重要因素之一,需要及时排查和优化。本文介绍了某核心服务内存问题排查与解决过程。首先在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 次数。
这一系列的连锁反应与图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异常,重新发送一个空响应,这里需要注意,空响应没有原始的响应结果信息,源码如下。
//org.apache.dubbo.remoting.exchange.codec.exchangecodec#encoderesponseprotected void encoderesponse(channel channel, channelbuffer buffer, response res) throws ioexception { //...省略部分代码 try { //1、检查响应大小是否超过 payload,如果超过,则抛出exceedpayloadlimitexception异常 checkpayload(channel, len); } catch (throwable t) { //2、重置buffer buffer.writerindex(savedwriteindex); //3、捕获异常后,生成一个新的空响应 response r = new response(res.getid(), res.getversion()); r.setstatus(response.bad_response); //4、exceedpayloadlimitexception异常,将生成的空响应重新发送一遍 if (t instanceof exceedpayloadlimitexception) { r.seterrormessage(t.getmessage()); channel.send(r); return; } }} //org.apache.dubbo.remoting.transport.abstractcodec#checkpayloadprotected static void checkpayload(channel channel, long size) throws ioexception { int payload = getpayload(channel); boolean overpayload = isoverpayload(payload, size); if (overpayload) { exceedpayloadlimitexception e = new exceedpayloadlimitexception(data length too large: + size + , max payload: + payload + , channel: + channel); logger.error(e); throw e; }}
受此启发,自定义了编解码类(实现org.apache.dubbo.remoting.codec2接口,并且配置在项目),去监控超出阈值的对象,并打印请求的详细信息,方便排查问题。在具体实现中,如果特意去计算每个对象的大小,那么势必是对服务性能造成影响。经过分析,采取了和checkpayload一样的方式,根据编码前后buffer的writerindex位置去判断有没有超过设定的阈值。代码如下。
/** * 自定义dubbo编码类 **/public class musicdubbocountcodec implements codec2 { /** * 异常响应池:缓存超过payload大小的responseid */ private static cache exceed_payload_limit_cache = caffeine.newbuilder() // 缓存总条数 .maximumsize(100) // 过期时间 .expireafterwrite(300, timeunit.seconds) // 将value设置为软引用,在oom前直接淘汰 .softvalues() .build(); @override public void encode(channel channel, channelbuffer buffer, object message) throws ioexception { //1、记录数据编码前的buffer位置 int writebefore = null == buffer ? 0 : buffer.writerindex(); //2、调用原始的编码方法 dubbocountcodec.encode(channel, buffer, message); //3、检查&记录超过payload的信息 checkoverpayload(message); //4、计算对象长度 int writeafter = null == buffer ? 0 : buffer.writerindex(); int length = writeafter - writebefore; //5、超过告警阈值,进行日志打印处理 warninglengthtoolong(length, message); } //校验response是否超过payload,超过了,缓存id private void checkoverpayload(object message){ if(!(message instanceof response)){ return; } response response = (response) message; //3.1、新的发送过程:通过状态码bad_response与错误信息识别出空响应,并记录响应id if(response.bad_response == response.getstatus() && strutil.contains(response.geterrormessage(), over_payload_error_message)){ exceed_payload_limit_cache.put(response.getid(), response.geterrormessage()); return; } //3.2、原先的发送过程:通过异常池识别出超过payload的响应,打印有用的信息 if(response.ok == response.getstatus() && exceed_payload_limit_cache.getifpresent(response.getid()) != null){ string responsemessage = getresponsemessage(response); log.warn(dubbo序列化对象大小超过payload,errormsg is {},response is {}, exceed_payload_limit_cache.getifpresent(response.getid()),responsemessage); } } }
在上文中提到,当捕获到超过payload的异常时,会重新生成空响应,导致失去了原始的响应结果,此时再去打印日志,是无法获取到调用方法和入参的,但是encoderesponse方法步骤4中,重新发送这个response,给了我们机会去获取到想要的信息,因为重新发送意味着会再去走一遍自定义的编码类。
假设有一个超出payload的请求,执行到自定编码类encode方法的步骤2(dubbo源码中的编码方法),在这里会调用encoderesponse方法重置buffer,发送新的空响应。
(1)当这个新的空响应再次进入自定义encode方法,执行 checkoverpayload方法的步骤3.1时,就会记录异常响应的id到本地缓存。由于在encoderesponse中buffer被重置,无法计算对象的大小,所以步骤4、5不会起到实际作用,就此结束新的发送过程。
(2)原先的发送过程回到步骤2 继续执行,到了步骤3.2 时,发现本地缓存的异常池中有当前的响应id,这时就可以打印调用信息了。
综上,对于大小在告警阈值和payload之间的对象,由于响应信息成功写入了buffer,可以直接进行大小判断,并且打印响应中的关键信息;对于超过payload的对象,在重新发送中记录异常响应id到本地,在原始发送过程中访问异常id池识别是否是异常响应,进行关键信息打印。
在监控措施上线后,通过日志很快速的发现了一部分产生大对象的接口,当前也正在根据接口特点做针对性优化。
三、总结
在对服务jvm内存进行调优时,要充分利用日志、监控工具、堆栈信息等,分析与定位问题。尽量降低问题排查期间的业务损失,引入对象监控手段也不能影响现有业务。除此之外,还可以在定时任务、代码重构、缓存等方面进行优化。优化服务内存不仅仅是jvm调参,而是一个全方面的持续过程。
人机界面的主要功能
福禄克芯片研发热像检测应用解决方案
第七届中国机器人峰会暨智能经济人才峰会在浙江宁波余姚隆重开幕
区块链的发展中中国有怎样的话语权
采用双电源光电耦合的上管驱动电路
浅析JVM内存大对象监控和优化实践的过程
Mozilla Firefox开发将完全转向Git,放弃Mercurial
Hugging Face更改文本推理软件许可证,不再“开源”
三星盖世S8黄金版已出,彰显贵族气质!
从DDR4到DDR5高速连接器如何助一臂之力
中兴摊牌了!将推出全球首款屏下摄像头智能手机
苹果位列中国区手机品牌销量第三 2018年3月及Q1中国畅销手机市场分析报告
英特尔实感F200技术具有遮挡提示功能
基于深度学习的目标检测技术比作一场“热兵器革命”
台积电2纳米晶圆制造将采用GAAFET架构的EDA软件
canfd通信tdcv实测值受总线长度影响吗
springMVC后台接受前端上传的文件及下载文件
努比亚Z17S发布会倒计时:努比亚Z17S外观、渲染图、配置亮点提前看
医疗废物在线管理的好处
PCB布局布线的相关基本原理和设计技巧100问