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

问题背景

小奎公司的运维同时今天反映核心业务一个服务目前 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() 方法只生成了一个映射类。
[图片]

解决方案

原来代码逻辑

@Autowiredprivate 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); 进行对象拷贝操作。具体代码如下所示:

@Autowiredprivate MapperFactory mapperFactory;private MapperFacade mapperFacade;@PostConstructpublic 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.mzph.cn/news/805980.shtml

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

相关文章

Go第三方框架--ants协程池框架

1. 背景介绍 1.1 goroutine ants是站在巨人的肩膀上开发出来的&#xff0c;这个巨人是goroutine&#xff0c;这是连小学生都知道的事儿&#xff0c;那么为什么不继续使用goroutine(以下简称go协程)呢。这是个思考题&#xff0c;希望讲完本文大家可以有个答案。 go协程只涉及用…

【STL】队列(queue)

队列 queue没有迭代器 Queue所有元素的进出都必须符合”先进先出”的条件&#xff0c;只有queue的顶端元素&#xff0c;才有机会被外界取用。Queue不提供遍历功能&#xff0c;也不提供迭代器。 头文件&#xff1a; #include <queue>queue<T> queT;//queue采用模…

Docker部署minio集群

1.基本定义 由于是非常轻量级的软件&#xff0c;所以架构上也没有这么复杂&#xff0c;他使用操作系统的文件系统作为存储介质&#xff0c;我们在向任意节点写数据的时候&#xff0c;minio会自动同步数据到另外的节点&#xff0c;而机制叫做erasure code&#xff08;纠删码&am…

Java基础知识总结(48)

&#xff08;1&#xff09;super关键字 1. super代表父类对象 2. 在构造器中访问父类的构造器&#xff08;创建子类对象时会先创建父类对象&#xff09; 3. super访问父类的实例变量 4. 访问父类的实例方法 如&#xff1a;/*** 父类* author Ray**/public class Animal {String…

linux内核驱动-在内核代码里添加设备结点

linux中&#xff0c;一切皆文件 我们在用户层用一些系统函数&#xff08;如&#xff1a;fopen等等&#xff09;时&#xff0c;会进入内核&#xff0c;内核会在字符注册了的设备号链表中查找。如果找到就运行我们写的设备文件的&#xff08;驱动&#xff09;函数 我们在前面已经…

【滑动窗口】无重复字符的最长字串

给定一个字符串 s &#xff0c;请你找出其中不含有重复字符的 最长子串的长度 示例 1: 输入: s "abcabcbb"输出: 3 解释: 因为无重复字符的最长子串是 "abc"&#xff0c;所以其长度为 3。 示例 2: 输入: s "bbbbb"输出: 1解释: 因为无重复字…

day54 买卖股票的最佳时机含冷冻期 买卖股票的最佳时机含手续费

题目1&#xff1a;309 买卖股票的最佳时机含冷冻期 题目链接&#xff1a;309 买卖股票的最佳时机含冷冻期 题意 整数数组prices中prices[i]表示第i天的股票价格&#xff0c;可以进行多次交易&#xff0c;但是在卖出股票后&#xff0c;无法在第二天买入股票&#xff08;冷冻期…

设计模式(017)行为型之责任链模式

责任链模式&#xff0c;它允许你将请求沿着处理者链传递&#xff0c;直到有一个处理者能够处理该请求为止。在责任链模式中&#xff0c;有三个核心角色&#xff1a;请求者&#xff08;Client&#xff09;、处理者&#xff08;Handler&#xff09;、以及具体处理者&#xff08;C…

RuoYi-Vue若依框架-vue前端给对象添加字段

处理两个字段的时候有需求都要显示在下拉框的同一行&#xff0c;这里有两种解决方案&#xff0c;一是后端在实体类添加一个对象&#xff0c;加注解数据库忽略处理&#xff0c;在接口处拼接并传给前端&#xff0c;二是在前端获取的数据数组内为每个对象都添加一个字段&#xff0…

cannal的使用

搭建MySQL 安装canal 1.新建文件夹logs, 新建文件canal.properties instance.properties docker.compose.yml instance.properties ################################################# ## mysql serverId , v1.0.26 will autoGen # canal.instance.mysql.slaveId0# enable g…

06 Php学习:字符串

PHP 中的字符串变量 在 PHP 中&#xff0c;字符串是一种常见的数据类型&#xff0c;用于存储文本数据。字符串变量可以包含字母、数字、符号等字符&#xff0c;并且可以进行各种操作和处理。以下是关于 PHP 中字符串变量的一些重要信息&#xff1a; 定义字符串变量&#xff1…

【SpringBoot3】Bean管理

1.Bean扫描 1.1传统Spring 标签&#xff1a;<context:component-scan base-package"com. example "/>注解&#xff1a;ComponentScan(basePackages "com.example") 1.2SpringBoot SpringBoot默认扫描启动类所在的包及其子包 2.Bean注册 如果要注…

Linux从入门到精通 --- 1.初始Linux

文章目录 第一章&#xff1a;1.1 Linux的诞生1.2 Linux系统内核1.3 Linux系统发行版 第一章&#xff1a; 1.1 Linux的诞生 1991年由林纳斯 托瓦兹创立并发展至今称为服务器操作系统领域的核心系统。 1.2 Linux系统内核 Linux内核提供了系统的主要功能&#xff0c;甚至是开源…

最大公约数算法详解

一、引言 最大公约数&#xff08;Greatest Common Divisor&#xff0c;GCD&#xff09;是数学中一个非常重要的概念。在计算机科学中&#xff0c;求解最大公约数不仅是数学问题的实际应用&#xff0c;也是算法设计的基本技能之一。本文将详细介绍几种常见的求解最大公约数的算…

【JS】判断双层数组中每一项是否都不为空

需求 判断双层数组中每一项是否都不为空&#xff0c;都不为空返回true&#xff0c;否则返回false 数组格式&#xff1a; arr1 [{list: [12, 25, 7, 18, 55]},{list: [36, 9]} ] arr2 [{list: [12, , 7, , 55]},{list: [36, 9]} ]实现 export function checkEmpty(arr) {le…

坚持十天做完Python入门编程100题第三天

坚持十天做完Python入门编程100题第三天 第15题 依次输出列表的元素和下标第16题 编写加速度函数第17题 编写函数计算球的体积第18题 函数默认值参数第19题 全局变量第20题 局部变量第21题 字符串格式化第22题 字符串格式化&#xff08;二&#xff09;第23题 字符串分隔 第15题…

SQL注入sqli_labs靶场第五、六题

第五题 根据报错信息&#xff0c;判断为单引号注入 没有发现回显点 方法&#xff1a;布尔盲注&#xff08;太耗时&#xff0c;不推荐使用&#xff09; 1&#xff09;猜解数据库名字&#xff1a;&#xff08;所有ASCII码值范围&#xff1a;0~127&#xff09; ?id1 and length…

TDengine too many open files

too many open files 是比较常见的报错&#xff0c;尤其使用TDengine 3.0 集群时&#xff0c;大概率会遇到。这个报错很简单&#xff0c;但要想顺利解决&#xff0c;却涉及到很多知识点。 目录 知识点&#xff1a;fs.nr_open知识点&#xff1a;file-max & fs.file-nr知识点…

stl set 的 insert()

set 跟 map 容器很像&#xff0c;但它俩又有很大的不同&#xff0c;它们的底层实现都是树&#xff0c;元素都是有序的&#xff0c;但map是可以修改元素的&#xff0c;而 set 就不行了&#xff0c;set 里的元素只有删除的情况&#xff0c;没有修改的情况&#xff1b;所以发现没有…

客户关系处理成功秘籍

目录 绪论: 第一章:客户关系管理概述 1.1 客户关系管理简介 1.2 客户关系管理的重要性 1.3 CRM的目标和原则 1.4 本章小结 第二章:建立客户数据库 2.1 客户数据库的重要性 2.2 建立客户数据库的步骤 2.3 数据库管理系统(DBMS) 2.4 数据分析工具 2.5 数据库安全…