再也不怕面试官问 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协程只涉及用…

Docker部署minio集群

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

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

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

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注册 如果要注…

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知识点…

Linux多进程通信(4)——消息队列从入门到实战!

Linux多进程通信总结——进程间通信看这一篇足够啦&#xff01; 1.基本介绍 1&#xff09;消息队列的本质其实是一个内核提供的链表&#xff0c;内核基于这个链表&#xff0c;实现了一个数据结构&#xff0c;向消息队列中写数据&#xff0c;实际上是向这个数据结构中插入一个…

怎样系统地学习自动化测试?

&#x1f345; 视频学习&#xff1a;文末有免费的配套视频可观看 &#x1f345; 关注公众号&#xff1a;互联网杂货铺&#xff0c;回复1 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 平时的测试工作其实细分一下&#xff0c;大概有三个领域…

C++ 线程库(thread)与锁(mutex)

一.线程库(thread) 1.1 线程类的简单介绍 thread类文档介绍 在C11之前&#xff0c;涉及到多线程问题&#xff0c;都是和平台相关的&#xff0c;比如windows和linux下各有自己的接口&#xff0c;这使得代码的可移植性比较差。C11中最重要的特性就是对线程进行支持了&#xff…

操作系统的基础知识:操作系统的特征:并发,共享,虚拟,异步

操作系统的特性&#xff1a; 1.并发 并发:指两个或多个事件在同一时间间隔内发生。这些事件宏观上是同时发生的&#xff0c;但微观上是交替注意&#xff1a;并行:指两个或多个事件在同一时刻同时发生。 操作系统的并发性指计算机系统中“同时”运行着多个程序&#xff0c;这…

graphicLayer.startDraw({开启连续绘制isContinued之后,无法获取连续标绘的坐标数据

摘要&#xff1a;graphicLayer.startDraw({开启连续绘制isContinued之后&#xff0c;无法获取连续标绘的坐标数据的解决方案 问题前景&#xff1a; graphicLayer.startDraw({开启连续绘制isContinued之后&#xff0c;.then()方法只走一次&#xff0c;无法获取连续标绘的所有坐…

【Linux】shell 脚本基础使用

在终端中输入命令可以完成一些常用的操作&#xff0c;但是我们都是一条一条输入命令&#xff0c;比较麻烦&#xff0c;为了解决这个问题&#xff0c;就会涉及到 shell 脚本&#xff0c;它可以将很多条命令放到一个文件里面&#xff0c;然后直接运行这个文件即可。 shell 脚本类…

【3GPP】【核心网】核心网/蜂窝网络重点知识面试题二(超详细)

1. 欢迎大家订阅和关注&#xff0c;3GPP通信协议精讲&#xff08;2G/3G/4G/5G/IMS&#xff09;知识点&#xff0c;专栏会持续更新中.....敬请期待&#xff01; 目录 1. 对于主要的LTE核心网接口&#xff0c;给出运行在该接口上数据的协议栈&#xff0c;并给出协议特征 2. 通常…

Centos7使用docker安装Jenkins(含pipeline脚本语句)

一、下载Jenkins docker pull jenkins/jenkins:lts 二、启动Jenkins docker run \-u root \--rm \-d \-p 8081:8080 \-p 50000:50000 \-v /root/docker/jenkins/var/jenkins_home:/var/jenkins_home \-v /var/run/docker.sock:/var/run/docker.sock \-v /usr/bin/docker:/usr…

番外篇 | YOLOv8改进之引入YOLOv9的ADown模块 | 替换YOLOv8卷积

前言:Hello大家好,我是小哥谈。YOLOv9是一种目标检测算法,而ADown模块是YOLOv9中的一个重要组成部分。ADown模块主要用于特征提取和下采样操作,以便在后续的检测任务中更好地捕捉目标的特征。具体来说,ADown模块是YOLOv9中的一个卷积块,由一系列卷积层和池化层组成。它的…

spring boot —— Spring-Cloud-Zuul(网关服务getway),kafka笔记

一、 引入zuul依赖&#xff1a; org.springframework.cloud spring-cloud-starter-zuul 二、创建应用主类。使用EnableZuulProxy注解开启zuul的API网关服务功能&#xff1a; EnableZuulProxy SpringCloudApplication public class Application { public static void mai…

FPN(Feature Pyramid Network)详解

文章涉及个人理解部分&#xff0c;可能有不准确的地方&#xff0c;敬请指正 0. 概述 FPN&#xff0c;全名Feature Pyramid Networks&#xff0c;中文称为特征金字塔网络。它是2017年cvpr上提出的一种网络&#xff0c;主要解决的是目标检测中的多尺度问题。FPN通过简单的网络连…