再也不怕面试官问 OOM了,一次生产环境 Metaspace OOM 排查流程实操!

news2024/12/25 9:32:40

问题背景

小奎公司的运维同时今天反映核心业务一个服务目前 CPU 的使用率、堆内存、非堆内存的使用率有点高。刚反映没有过多久该服务就直接 OOM 了,以下是生产监控平台监控信息。

CPU 使用率监控
在这里插入图片描述
堆内存和非堆内存使用率
在这里插入图片描述
OOM 产生的日志报错信息
在这里插入图片描述

问题分析

根据的日志报错信息基本可以确定是元空间一致无法释导致 OOM(额外说明一下生产环境使用 JDK 版本是 1.8)查看近几天元空间的情况,元空间内存基本一致在递增。
在这里插入图片描述
服务在疯狂的 GC 并且 GC 的停顿时间非常长。
在这里插入图片描述
进而导致堆内存使用率也非常高。
在这里插入图片描述
线上有大量 blocked 线程,因为有大量的响应超时。
在这里插入图片描述
在这里插入图片描述

根据上述的现象引发 3 个问题:

第一个问题是:为什么元空间内存大小配置为什么没有生效,因为从 4月3号到 4月 9号内存一直到递增并且达到了 1.8G ?

查看了一下线上的 JVM 配置使用的如下配置:

-XX:PermSize=128M -XX:MaxPermSize=256M

这个配置在JDK 1.8 下已经失效,应该使用 MetaspaceSize 和 MaxMetaspaceSize 。

第二个问题是:什么情况下会导致元空间内存无法释放?

MetaSpace 内存管理: 类和其元数据的生命周期与其对应的类加载器相同,只要类的类加载器是存活的,在 Metaspace 中的类元数据也是存活的,不能被回收。

一般情况下我们会把 -XX:MetaSpaceSize 和 -XX:MaxMetaSpaceSize 两个值设置为固定的,但是这样也会导致在空间不够的时候无法扩容,然后频繁地触发 GC,最终 OOM。所以关键原因就是 ClassLoader 不停地在内存中 load 了新的 Class ,一般这种问题都发生在动态类加载等情况上。

通过生产的类加载监控发现确实有大量的类在加载:
在这里插入图片描述

第三个问题:如何判断代码中有哪些类在大量加载?

通过 MAT 对 OOM 堆快照进行分析。具体分析流程如下:

第一步:打开 MAT 选择堆内存快照查看泄露报告。

[图片]
[图片]
具体报告内容如下:
在这里插入图片描述

发现有大量的 ma.glasnost.orika.impl.generator.JavassistCompilerStrategy 对象产生导致,单单从这个信息暂时无法定位到具体的代码位置。

第二步:选择看看大对象的排名

在这里插入图片描述
在这里插入图片描述
根据上图分析发现有大量类似
ma.glasnost.orika.generated.Orika_HisReserveServiceTypeInternalResp_HisReserveServiceTypeDO_Mapper8862841705394371$15942 Class 对象

第三步:查看线程调用链情况

在这里插入图片描述

一般情况下线程查找不一定能找到具体调用代码,一根通过 dominator_tree
查看大对象可以结合最近上线的代码分析出可以大概定位到代码位置。

根据上图的线程调用链基本和第二步对应上了,然后根据调用链路定位到了具体的代码位置:
cn.medcloud.ufh.hospital.controller.internal.HisReserveServiceTypeInternalController#getByHisHospitalRowIdAndDepartmentIdAndHisServiceTypeId

在这里插入图片描述
在这里插入图片描述
核心问题就是红色标注的代码的问题,MQ 消费者会大量调用有使用 Orika 拷贝对象的方法。正常使用的时候映射类是可以重复使用的,但是这里是每次调用都执行如下代码逻辑:

mapperFactory.classMap(HisReserveServiceTypeDO.class, HisReserveServiceTypeInternalResp.class)
                .exclude("allowReservePatientTypes").byDefault().register(); 

这样会导致每次使用此拷贝方式的时候都会动态重新创建一个映射类,因为每天会有大量的mq 消息消费调用到上述代码逻辑,导致会创建大量的映射类的元数据存储到元空间中,将元空间的内存打爆。

为了验证我的想法我简单写了一个 demo 具体的代码如下:

package cn.medcloud.ufh.hospital.utils;

import cn.medcloud.ufh.hospital.domain.HisReserveServiceTypeDO;
import cn.medcloud.ufh.hospital.domain.resp.HisReserveServiceTypeInternalResp;
import ma.glasnost.orika.impl.DefaultMapperFactory;

public class OrikaBeanCopyTest {

    private static DefaultMapperFactory mapperFactory;
	//通过静态代码块模拟将 MapperFactory 在启动将其注入 Spring 上下文中的效果
    static {
        mapperFactory = new DefaultMapperFactory.Builder().build();
    }
	/* test1方法 和 test2方法分别通过 2中方式拷贝三次对象 */
    public static void main(String[] args) {
        test1();
        //test2();
    }

    /**
     * 生成多个映射类对象 (有问题代码模拟)
     */
    public static void test1() {
        //第一次拷贝
        HisReserveServiceTypeDO hisReserveServiceTypeDO = new HisReserveServiceTypeDO();
        hisReserveServiceTypeDO.setName("测谁数据1");
        mapperFactory.classMap(HisReserveServiceTypeDO.class, HisReserveServiceTypeInternalResp.class)
                .exclude("allowReservePatientTypes").byDefault().register();

        HisReserveServiceTypeInternalResp resp = mapperFactory.getMapperFacade().map(hisReserveServiceTypeDO, HisReserveServiceTypeInternalResp.class);
        //第二次拷贝
        HisReserveServiceTypeDO hisReserveServiceTypeDO2 = new HisReserveServiceTypeDO();
        hisReserveServiceTypeDO2.setName("测谁数据1");
        mapperFactory.classMap(HisReserveServiceTypeDO.class, HisReserveServiceTypeInternalResp.class)
                .exclude("allowReservePatientTypes").byDefault().register();
        HisReserveServiceTypeInternalResp resp2 = mapperFactory.getMapperFacade().map(hisReserveServiceTypeDO2, HisReserveServiceTypeInternalResp.class);


        //第三次拷贝
        HisReserveServiceTypeDO hisReserveServiceTypeDO3 = new HisReserveServiceTypeDO();
        hisReserveServiceTypeDO3.setName("测谁数据1");
        mapperFactory.classMap(HisReserveServiceTypeDO.class, HisReserveServiceTypeInternalResp.class)
                .exclude("allowReservePatientTypes").byDefault().register();
        HisReserveServiceTypeInternalResp resp3 = mapperFactory.getMapperFacade().map(hisReserveServiceTypeDO3, HisReserveServiceTypeInternalResp.class);
    }

    /**
     * 生成一个映射类对象 (优化代码模拟)
     */
    public static void test2() {
        //第一次拷贝
        HisReserveServiceTypeDO hisReserveServiceTypeDO = new HisReserveServiceTypeDO();
        hisReserveServiceTypeDO.setName("测谁数据1");
        HisReserveServiceTypeInternalResp resp = mapperFactory.getMapperFacade().map(hisReserveServiceTypeDO, HisReserveServiceTypeInternalResp.class);

        //第二次拷贝
        HisReserveServiceTypeDO hisReserveServiceTypeDO2 = new HisReserveServiceTypeDO();
        hisReserveServiceTypeDO2.setName("测谁数据1");
        HisReserveServiceTypeInternalResp resp2 = mapperFactory.getMapperFacade().map(hisReserveServiceTypeDO2, HisReserveServiceTypeInternalResp.class);

        //第三次拷贝
        HisReserveServiceTypeDO hisReserveServiceTypeDO3 = new HisReserveServiceTypeDO();
        hisReserveServiceTypeDO3.setName("测谁数据1");
        HisReserveServiceTypeInternalResp resp3 = mapperFactory.getMapperFacade().map(hisReserveServiceTypeDO3, HisReserveServiceTypeInternalResp.class);
    }
}

在执行代码前为该代码配置生成源码的 vm 配置

-Dma.glasnost.orika.GeneratedSourceCode.writeSourceFiles=true
-Dma.glasnost.orika.writeSourceFilesToPath=/Users/tomlee/Documents/orika-class

执行 test1() 方法执行对象 copy 了 三次产生了三个映射类。
在这里插入图片描述
执行 test2() 方法只生成了一个映射类。
[图片]

解决方案

原来代码逻辑


    @Autowired
    private MapperFactory mapperFactory;
    
    /**
     * 转换为内部对象
     *
     * @param serviceTypeDOS
     * @return
     */
    private List<HisReserveServiceTypeInternalResp> toInternalResp(List<HisReserveServiceTypeDO> serviceTypeDOS) {
        if (CollectionUtils.isEmpty(serviceTypeDOS)) {
            return Collections.emptyList();
        }
        mapperFactory.classMap(HisReserveServiceTypeDO.class, HisReserveServiceTypeInternalResp.class)
                .exclude("allowReservePatientTypes").byDefault().register();
        List<HisReserveServiceTypeInternalResp> resps = new ArrayList<>(serviceTypeDOS.size());
        for (HisReserveServiceTypeDO serviceTypeDO : serviceTypeDOS) {
            HisReserveServiceTypeInternalResp resp = mapperFactory.getMapperFacade().map(serviceTypeDO, HisReserveServiceTypeInternalResp.class);
            if (StringUtils.isNotBlank(serviceTypeDO.getAllowReservePatientTypeJson())) {
                resp.setAllowReservePatientTypes(JSON.parseArray(serviceTypeDO.getAllowReservePatientTypeJson(), HisReserveServiceTypeInternalResp.PatientType.class));
            }
            resps.add(resp);
        }
        return resps;
    }

修改后的代码逻辑:

通过 @PostConstruct 方式只执行一次 mapperFactory.getMapperFacade() 获取 MapperFacade 对象,然后直接通过 HisReserveServiceTypeInternalResp resp = mapperFacade.map(serviceTypeDO, HisReserveServiceTypeInternalResp.class); 进行对象拷贝操作。具体代码如下所示:


    @Autowired
    private MapperFactory mapperFactory;
    
    private MapperFacade mapperFacade;
    @PostConstruct
    public void init() {
        mapperFacade = mapperFactory.getMapperFacade();
    }
    
    /**
     * 转换为内部对象
     *
     * @param serviceTypeDOS
     * @return
     */
    private List<HisReserveServiceTypeInternalResp> toInternalResp(List<HisReserveServiceTypeDO> serviceTypeDOS) {
        if (CollectionUtils.isEmpty(serviceTypeDOS)) {
            return Collections.emptyList();
        }
        List<HisReserveServiceTypeInternalResp> resps = new ArrayList<>(serviceTypeDOS.size());
        for (HisReserveServiceTypeDO serviceTypeDO : serviceTypeDOS) {
            //去除掉了原先执行 .register() 的逻辑
            // mapperFactory.classMap(HisReserveServiceTypeDO.class, HisReserveServiceTypeInternalResp.class)
                .exclude("allowReservePatientTypes").byDefault().register();
            HisReserveServiceTypeInternalResp resp = mapperFacade.map(serviceTypeDO, HisReserveServiceTypeInternalResp.class);
            if (StringUtils.isNotBlank(serviceTypeDO.getAllowReservePatientTypeJson())) {
                resp.setAllowReservePatientTypes(JSON.parseArray(serviceTypeDO.getAllowReservePatientTypeJson(), HisReserveServiceTypeInternalResp.PatientType.class));
            }
            resps.add(resp);
        }
        return resps;
    }

同时将原先项目启动 VM 参数

-XX:PermSize=128M -XX:MaxPermSize=256M 

替换为

 -XX:MetaSpaceSize=512M 和 -XX:MaxMetaSpaceSize=512M

修改后线上生产环境监控平台查看基本已经正常,具体如下图所示:

堆内存和非堆内存的使用率下来了
在这里插入图片描述
元空间内存和类加载基本也平稳了
在这里插入图片描述
在这里插入图片描述

问题复盘

核心出问题的代码 toInternalResp 方法是历史老逻辑,本次开发新接口直接沿用老的方法,之前没有问题是因为这个逻辑调用量不大,结果切换到调用量大的逻辑使用问题一下突显出来了,以后复用老逻辑一定要做好老逻辑代码审查。

目前我们公司监控告警最近刚搞起来,目前还不是很完善,内存使用率在很高的情况下没有进行告警,后期要把 JVM 相关的告警配置完善起来。

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1583466.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

kali使用msf+apkhook520+cploar实现安卓手的攻击

学习网络安全的过程中&#xff0c;突发奇想怎么才能控制或者说是攻击手机 边找工作边实验 话不多说启动kali 一、使用msfapktool生成简单的木马程序 首先使用kali自带的msfvenom写上这样一段代码 选择安卓 kali的ip 一个空闲的端口 要输出的文件名 msfvenom -p android/met…

9个最受欢迎的开源自动化测试框架盘点!

自动化测试框架可以帮助测试人员评估多个Web和移动应用程序的功能&#xff0c;安全性&#xff0c;可用性和可访问性。尽管团队可以自己构建复杂的自动化测试框架&#xff0c;但是当他们可以使用现有的开源工具&#xff0c;库和测试框架获得相同甚至更好的结果时&#xff0c;通常…

ubuntu系统逻辑卷Logical Volume扩容根分区

Linux LVM详解 https://blog.csdn.net/qq_35745940/article/details/119054949 https://blog.csdn.net/weixin_41891696/article/details/118805670 https://blog.51cto.com/woyaoxuelinux/1870299 LVM&#xff08;Logical Volume Manager&#xff09;逻辑卷管理&#xff0c…

C++入门语法(命名空间缺省函数函数重载引用内联函数nullptr)

目录 前言 1. 什么是C 2. C关键字 3. 命名空间 3.1 命名空间的定义 3.2 命名空间的使用 4. C输入和输出 5. 缺省函数 5.1 概念 5.2 缺省参数分类 6. 函数重载 6.1 概念 6.2 为何C支持函数重载 7. 引用 7.1 概念 7.2 特性 7.3 常引用 7.4 引用与指针的区别 7…

OSCP靶场--Hetemit

OSCP靶场–Hetemit 考点(python代码注入 systemctrl提权) 1.nmap扫描 ## ┌──(root㉿kali)-[~/Desktop] └─# nmap 192.168.173.117 -sV -sC -Pn --min-rate 2500 -p- Starting Nmap 7.92 ( https://nmap.org ) at 2024-04-10 05:52 EDT Nmap scan report for 192.168.1…

详解多态、虚继承、多重继承内存布局及虚表(C++)

本篇文章深入分析多态、虚继承、多重继承的内存布局及虚函数表以及实现原理。编译器使用VS 2022&#xff0c;直接放结论&#xff0c;代码及内存调试信息在后文。 结论 内存布局 一个没有虚函数的类&#xff0c;它的大小其实就是所有成员变量的大小&#xff0c;此时它就是一个…

13 指针(上)

指针是 C 语言最重要的概念之一&#xff0c;也是最难理解的概念之一。 指针是C语言的精髓&#xff0c;要想掌握C语言就需要深入地了解指针。 指针类型在考研中用得最多的地方&#xff0c;就是和结构体结合起来构造结点(如链表的结点、二叉树的结点等)。 本章专题脉络 1、指针…

redis string底层为什么使用sds, sds好处?redis 的动态字符串优点?

1. redis 的键值对&#xff0c;都是由对象组成的&#xff0c; 其中键总是一个字符串对象&#xff08;string object&#xff09; 而键的value则可以是&#xff1a;“字符串对象”&#xff0c; “列表对象 &#xff08;list object&#xff09;”&#xff0c;“哈希对象 (hash o…

《由浅入深学习SAP财务》:第2章 总账模块 - 2.6 定期处理 - 2.6.3 月末操作:外币评估

2.6.3 月末操作&#xff1a;外币评估 企业的外币业务在记账时一般使用期初的汇率或者即时汇率&#xff0c;但在月末&#xff0c;需要按照月末汇率对外币的余额或者未清项进行重估&#xff08;revaluation&#xff09;。 企业在资产负债表日&#xff0c;应当按照下列规…

nandgame中的Code generation(代码生成)

题目说明&#xff1a; 代码生成为语言的语法规则定义代码生成&#xff0c;以支持加法和减法。 您可以使用在前面级别中定义的堆栈操作&#xff08;如ADD和SUB&#xff09;。代码生成模板通常需要包含规则中其他符号的代码。 这些可以通过方括号中的符号名称插入。例如&#xf…

【初中生讲机器学习】15. EM 算法一万字详解!一起来学!

创建时间&#xff1a;2024-04-08 最后编辑时间&#xff1a;2024-04-10 作者&#xff1a;Geeker_LStar 你好呀~这里是 Geeker_LStar 的人工智能学习专栏&#xff0c;很高兴遇见你~ 我是 Geeker_LStar&#xff0c;一名初三学生&#xff0c;热爱计算机和数学&#xff0c;我们一起加…

西门子PLC(S7-200 SMART)学习笔记1:初识PLC可编程逻辑器件

今日开始我的西门子PLC学习之路&#xff0c;学习的型号以S7-200 SMART为主 主要认识一下PLC是什么、型号怎么看、 通信相关、编程软件、构造及工作原理 目录 西门子官方PLC手册获取&#xff1a; 1、PLC可编程逻辑器件的基本认识&#xff1a; PLC的结构及各部分的作用&#xff…

Kali安装黑屏与进入系统后不显示中文的解决办法

使用镜像版本kali-linux-2024.1-installer-amd64.iso 一.创建虚拟机安装Kali镜像时&#xff0c;安装后要重启时发现左上角有个— 闪动并黑屏&#xff0c;启动不成功 上述办法也很简单&#xff0c;可以试试再windows中的CMD终端输入netsh winsock reset&#xff0c;重启电脑如果…

嵌入式Linux:Linux库函数

目录 1、Linux库函数简介 2、标准C语言库函数 1、Linux库函数简介 Linux 提供了丰富的库函数&#xff0c;涵盖了各种领域&#xff0c;从文件操作到网络编程、图形界面、数学运算等。这些库函数大多数都是标准的 C 库函数&#xff0c;同时也包括一些特定于 Linux 系统的库。 …

【Linux】初识Linux,虚拟机安装Linux系统,配置网卡

前言 VMware软件&#xff1a;首先&#xff0c;确保您已经下载了VMware Workstation软件并安装在电脑上。VMware Workstation是一款功能强大的虚拟化软件&#xff0c;它允许在单一物理机上运行多个操作系统。 Linux镜像文件&#xff1a;需要准备一个Linux操作系统的镜像文件。…

初学python记录:力扣1702. 修改后的最大二进制字符串

题目&#xff1a; 给你一个二进制字符串 binary &#xff0c;它仅有 0 或者 1 组成。你可以使用下面的操作任意次对它进行修改&#xff1a; 操作 1 &#xff1a;如果二进制串包含子字符串 "00" &#xff0c;你可以用 "10" 将其替换。 比方说&#xff0c;…

LWIP一探究竟

1.网卡接收数据的流程 我们网卡接收数据基本上就是开发板上eth接收完数据后产生一个中断,然后释放一个信号量通知网卡接收线程去处理这些接收的数据,然后将这些数据封装成信息,投递到tcpip_mbox邮箱中,LWIP内核线程得到这个消息,就对消息进行解析,根据消息中数据包类型进行处理…

docker使用canal

1. 准备MySql主库 1.1.在服务器新建文件夹 mysql/data&#xff0c;新建文件 mysql/conf.d/my.cnf 其中my.cnf 内容如下 [mysqld] log_timestampsSYSTEM default-time-zone8:00 server-id1 log-binmysql-bin binlog-do-db mall # 要监听的库 binlog_formatROW配置解读&#…

Harmony鸿蒙南向驱动开发-Regulator

Regulator模块用于控制系统中各类设备的电压/电流供应。在嵌入式系统&#xff08;尤其是手机&#xff09;中&#xff0c;控制耗电量很重要&#xff0c;直接影响到电池的续航时间。所以&#xff0c;如果系统中某一个模块暂时不需要使用&#xff0c;就可以通过Regulator关闭其电源…

网络基础三——其他周边问题

3.1ARP原理 ​ ARP不是一个单纯的数据链路层的协议&#xff0c;而是一个介于数据链路层和网络层之间的协议&#xff1b; ​ 以广播的形式(主机号填成全1)构建Mac帧&#xff0c;发送ARP请求包&#xff0c;告诉所有在局域网的主机我的IP地址和Mac帧&#xff0c;与目的IP相同的主…