0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

JVM内存大对象监控和优化问题描述及解决办法

OSC开源社区 来源:vivo互联网技术 2023-08-28 11:39 次阅读

服务器内存问题是影响应用程序性能和稳定性的重要因素之一,需要及时排查和优化。本文介绍了某核心服务内存问题排查与解决过程。首先在JVM与大对象优化上进行了有效的实践,其次在故障转移与大对象监控上提出了可靠的落地方案。最后,总结了内存优化需要考虑的其他问题。

一、问题描述

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

9ccc3132-4337-11ee-a2ef-92fbcf53809c.png

图1 业务异常数量变化

二、分析与解决

通过对日志,机器CPU、内存等监控数据分析发现:

YGC平均每分钟次数12次,峰值为24次,平均每次的耗时在327毫秒。FGC平均每10分钟0.08次,峰值1次,平均耗时30秒。可以看到GC问题较为突出。

在问题期间,机器的CPU并没有明显的变化,但是堆内存出现较大异常。图2,黄色圆圈处,内存使用急速上升,FGC变的频繁,释放的内存越来越少。

9cd45902-4337-11ee-a2ef-92fbcf53809c.png

图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

9ce03704-4337-11ee-a2ef-92fbcf53809c.png

图3 JVM优化前后的堆内存对比

优化后效果如图3,堆内存的使用明显降低,但是Dubbo超时仍然存在。

我们推断,在业务高峰期,该节点出现了大对象晋升到了老年代,导致内存使用迅速上升,并且大对象没有被及时回收。那如何找到这个大对象及其产生的原因呢?为了降低问题排查期间业务的损失,提出了临时的故障转移策略,尽量降低异常数量。

步骤2 故障转移策略

在api服务调用core服务出现异常时,将出现异常的机器ip上报给监控平台。然后利用监控平台的统计与告警能力,配置相应的告警规则与回调函数。当异常触发告警,通过配置的回调函数将告警ip传递给api服务,此时api服务可以将core服务下的该ip对应的机器视为“故障”,进而通过自定义的故障转移策略(实现Dubbo的AbstractLoadBalance抽象类,并且配置在项目),自动将该ip从提供者集群中剔除,从而达到不去调用问题机器。图 4 是整个措施的流程。在该措施上线前,每当有机器内存告警时,将会人工重启该机器。

9d09b1ba-4337-11ee-a2ef-92fbcf53809c.jpg

图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服务的堆内存进行了多次查看,其中比较有代表性的一次快照的大对象列表如下,

9d229766-4337-11ee-a2ef-92fbcf53809c.png

图5 core服务的堆内存快照

整个Netty的taskQueue有258MB。并且从图中绿色方框处可以发现,单个的Response竟达到了9M,红色方框处,显示了调用方的服务名以及URI。

进一步排查,发现该接口会通过core服务查询大量信息,至此基本排查清楚了大对象的身份以及产生原因。

(3)优化结果

在对接口进行优化后,整个core服务也出现了非常明显的改进。YGC全天总次数降低了76.5%,高峰期累计耗时降低了75.5%。FGC三天才会发生一次,并且高峰期累计耗时降低了90.1%。

9d2cff62-4337-11ee-a2ef-92fbcf53809c.png

图6大对象优化后的core服务GC情况

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

9d4ae2c0-4337-11ee-a2ef-92fbcf53809c.png

图7系统内部异常导致核心业务失败的异常请求数

步骤4 无侵入式内存对象监控

Debug Dubbo 源码的过程中,发现在网络层,Dubbo通过encodeResponse方法对响应进行编码并写入缓冲区,通过checkPayload方法去检查响应的大小,当超过payload时,会抛出ExceedPayloadLimitException异常。在外层对异常进行了捕获,重置buffer位置,而且如果是ExceedPayloadLimitException异常,重新发送一个空响应,这里需要注意,空响应没有原始的响应结果信息,源码如下。

//org.apache.dubbo.remoting.exchange.codec.ExchangeCodec#encodeResponse
protected 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#checkPayload
protected 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调参,而是一个全方面的持续过程。





审核编辑:刘清

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 编码器
    +关注

    关注

    45

    文章

    3679

    浏览量

    135396
  • RPC
    RPC
    +关注

    关注

    0

    文章

    111

    浏览量

    11587
  • cms
    cms
    +关注

    关注

    0

    文章

    60

    浏览量

    11013
  • JVM
    JVM
    +关注

    关注

    0

    文章

    158

    浏览量

    12273
  • 回调函数
    +关注

    关注

    0

    文章

    87

    浏览量

    11636

原文标题:JVM 内存大对象监控和优化实践

文章出处:【微信号:OSC开源社区,微信公众号:OSC开源社区】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    hyper 内存,Hyper内存:如何监控优化hyper-v虚拟机的内存使用

    :如何监控优化hyper-v虚拟机的内存使用。    在虚拟化环境中,合理监控优化Hyper-V虚拟机的
    的头像 发表于 01-24 14:15 231次阅读
    hyper <b class='flag-5'>内存</b>,Hyper<b class='flag-5'>内存</b>:如何<b class='flag-5'>监控</b>与<b class='flag-5'>优化</b>hyper-v虚拟机的<b class='flag-5'>内存</b>使用

    常见垫圈故障及解决办法 防漏垫圈的设计与应用

    常见垫圈故障及解决办法 1. 垫圈老化 故障现象: 垫圈因长时间使用而老化,失去弹性,导致密封性能下降。 解决办法: 定期检查垫圈的老化情况,及时更换新的垫圈。 2. 垫圈变形 故障现象: 由于安装
    的头像 发表于 12-12 15:31 510次阅读

    雷达探测器常见故障及解决办法

    雷达探测器,特别是用于车辆泊车辅助的雷达系统,常见故障及其解决办法可以归纳如下: 一、无报警提示故障 故障现象 : 在挂入倒挡或者按下雷达开关后,雷达系统毫无反应。 可能原因 : 倒车雷达系统工作
    的头像 发表于 11-24 09:46 961次阅读

    PCBA板常见故障及解决办法

    和使用过程中可能会出现各种故障,以下是一些常见的故障及其解决办法: 1. 焊接不良 故障现象: 焊点不光滑、有空洞、焊料不足或过多、焊点开裂等。 解决办法: 检查焊接设备(如回流焊机)的温度曲线是否正确。 确保焊膏和焊料的质量符合要求。 检查焊接过程中是否有污染或氧化。
    的头像 发表于 11-18 10:20 747次阅读

    谷景科普电感磁芯发热的解决办法

    谷景科普电感磁芯发热的解决办法编辑:谷景电子电感磁芯发热是电感产品中的常见问题,它可能影响电感的的性能和可靠性。解决电感磁芯发热的方法多种多样,下面给大家介绍几种常用的方法:优化电感设计:通过调整
    发表于 11-13 22:58 0次下载

    常见RAM内存故障及解决办法

    随机存取存储器(RAM)是计算机中不可或缺的组件之一,它负责存储计算机运行时的数据和程序。然而,由于各种原因,RAM可能会出现故障,导致计算机性能下降甚至无法启动。 常见RAM内存故障 无法识别内存
    的头像 发表于 11-11 10:01 2439次阅读

    温控器常见故障及解决办法

    温控器是维持室内温度舒适的必备设备。然而,像所有电子设备一样,它们也可能出现故障。了解常见故障及其解决办法可以帮助您快速解决问题,避免不必要的维修费用。 一、温控器不工作 故障现象: 温控器显示正常
    的头像 发表于 11-06 13:51 7609次阅读

    常见MCU故障及解决办法

    微控制器单元(MCU)是现代电子设备中的核心组件,负责处理和控制各种功能。然而,由于各种原因,MCU可能会出现故障。以下是一些常见的MCU故障及其解决办法: 1. 电源问题 故障现象: MCU无法
    的头像 发表于 11-01 13:41 3052次阅读

    常见元器件故障及解决办法

    在电子设备的日常使用和维护中,元器件的故障是不可避免的。了解常见元器件的故障类型及其解决办法,对于快速定位问题并进行修复至关重要。 1. 电阻器故障 故障现象: 电阻值变化:电阻器老化或损坏导致其实
    的头像 发表于 10-29 16:21 966次阅读

    JVM xmx, xms等内存相关参数合理性设置

    的,提高内存占用(Memory Footprint)就有可能同时优化这两个标的,这篇文章就来聊聊内存相关内容。 内存占用一般指应用运行需要的所有内存
    的头像 发表于 10-10 14:42 694次阅读

    海外大带宽服务器连接失败解决办法

     海外大带宽服务器连接失败可能由多种原因引起,以下是一些常见的故障排除步骤和可能的解决方案。Rak小编为您整理发布海外大带宽服务器连接失败解决办法
    的头像 发表于 09-29 09:53 283次阅读

    从原理聊JVM(一):染色标记和垃圾回收算法

    更好地优化自己的代码,并解决一些潜在的性能问题。 本文及后续文章将从原理聊起,对JVM内存分配、GC、编译等知识进行分析和总结。 1 JVM运行时
    的头像 发表于 08-20 15:25 308次阅读
    从原理聊<b class='flag-5'>JVM</b>(一):染色标记和垃圾回收算法

    聊聊JVM如何优化

    进行优化。 1.JVM内存模型 针对JAVA8的模型进行讨论,JVM内存模型主要分为几个关键区域:堆、方法区、程序计数器、虚拟机栈和本地方
    的头像 发表于 08-05 17:49 549次阅读
    聊聊<b class='flag-5'>JVM</b>如何<b class='flag-5'>优化</b>

    深入理解Java 8内存管理机制及故障排查实战指南

    Java的自动内存管理机制是由 JVM 中的垃圾收集器来实现的,垃圾收集器会定期扫描堆内存中的对象,检测并清除不再使用的对象,以释放
    的头像 发表于 04-04 08:10 1078次阅读
    深入理解Java 8<b class='flag-5'>内存</b>管理机制及故障排查实战指南

    Profinet IO通信故障的解决办法

    Profinet IO通信故障可能由多种原因引起,以下是一些常见的通信故障及其解决办法
    的头像 发表于 03-08 11:27 1266次阅读