问题背景
小奎公司的运维同时今天反映核心业务一个服务目前 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 相关的告警配置完善起来。