一次元数据空间内存溢出的排查记录 | 京东云技术团队

news2024/11/30 2:29:03

在应用中,我们使用的 SpringData ES的 ElasticsearchRestTemplate来做查询,使用方式不对,导致每次ES查询时都新实例化了一个查询对象,会加载相关类到元数据中。最终长时间运行后元数据出现内存溢出;

问题原因:类加载过多,导致元数据OOM。非类实例多或者大对象问题;

排查方式:

查看JVM运行情况,发现元数据满导致内存溢出;
导出内存快照,通过OQL快速定位肇事者;
排查对应类的使用场景和加载场景(重点序列化反射场景);

起源

06-15 下午正摩肩擦掌的备战着晚上8点。收到预发机器的一个GC次数报警。


【警告】UMP JVM监控
【警告】异步(async采集点:async.jvm.info(别名:jvm监控)15:42:40至15:42:50【xx.xx.xx.xxx(10174422426)(未知分组)】,JVM监控FullGC次数=2次[偏差0%],超过1次FullGC次数>=2次
【时间】2023-06-15 15:42:50
【类型】UMP JVM监控

第一时间诧异了下。该应用主要作用是接MQ消息和定时任务,同时任务和MQ都和线上做了隔离,也没有收到大流量的告警。

先看了下对应JVM监控:

只看上面都怀疑是监控异常(之前用文件采集的时候有遇到过,看CPU确实有波动。但堆基本无涨幅,怀疑非堆。)

问题排查

定位分析

既然怀疑非堆,我们先通过 jstat来看看情况

  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT   
  0.00   0.00   0.89   3.67  97.49  97.96    854   23.720   958  615.300  639.020
  0.00   0.00   0.89   3.67  97.49  97.96    854   23.720   958  615.300  639.020
  0.00   0.00   0.89   3.67  97.49  97.96    854   23.720   958  615.300  639.020
  0.00   0.00   0.89   3.67  97.49  97.96    854   23.720   958  615.300  639.020
  0.00   0.00   0.89   3.67  97.49  97.96    854   23.720   958  615.300  639.020

M列代表了metaspace的使用率,当前已经 97.49% 进一步印证了我们的猜测。

接下来通过 jmap 导出内存快照分析。这里我习惯使用 Visual VM 进行分析。

在这里我们看到有 118588 个类被加载了。正常业务下不会有这么多类。

这里我们走了很多弯路。

首先查看内存对象,根据类的实例数排了个序,试图看看是否是某个或某些类实例过多导致。

这里一般是排查堆异常时使用,可以看大对象和某类的实例数,但我们的问题是类加载过多。非类实例对象多或者大。这里排除。

后续还尝试了直接使用 Visual VM 的聚合按包路径统计,同时排序。收效都甚微。看不出啥异常来。

这里我们使用 OQL 来进行查询统计。

语句如下:

var packageClassSizeMap = {};
// 遍历统计以最后一个逗号做分割
heap.forEachClass(function (it) {
    var packageName = it.name.substring(0, it.name.lastIndexOf('.'));
    if (packageClassSizeMap[packageName] != null) {
        packageClassSizeMap[packageName] = packageClassSizeMap[packageName] + 1;
    } else {
        packageClassSizeMap[packageName] = 1;
    }
});
// 排序 因为Visual VM的查询有数量限制。
var sortPackageClassSizeMap = [];
map(sort(Object.keys(packageClassSizeMap), function (a, b) {
    return packageClassSizeMap[b] - packageClassSizeMap[a]
}), function (it) {
    sortPackageClassSizeMap.push({
        package: it,
        classSize: packageClassSizeMap[it]
    })
});
sortPackageClassSizeMap;

执行效果如下:

可以看到,com.jd.bapp.match.sync.query.es.po 下存在 92172 个类。这个包下,不到20个类。这时我们在回到开始查看类的地方。看看该路径下都是些什么类。

这里附带一提,直接根据路径获取对应的类数量:

var packageClassSizeMap = {};
// 遍历统计以最后一个逗号做分割
heap.forEachClass(function (it) {
    var packageName = it.name.substring(0, it.name.lastIndexOf('.'));
    // 加路径过滤版
    if (packageName.indexOf('com.jd.bapp.match.sync.query.es.po')){
        if (packageClassSizeMap[packageName] != null) {
            packageClassSizeMap[packageName] = packageClassSizeMap[packageName] + 1;
        } else {
            packageClassSizeMap[packageName] = 1;
        }
    }
});

sortPackageClassSizeMap;

查询 com.jd.bapp.match.sync.query.es.po 路径下的classes

我们可以看到:

  • 每个ES的Po对象存在大量类加载,在后面有拼接Instantiator_xxxxx
  • 部分类有实例,部分类无实例。(count为实例数)

从上面得到的信息得出是ES相关查询时出现的。我们本地debug查询跟踪下。

抽丝剥茧

这里列下主要排查流程

在应用中,我们使用的 SpringData ES的 ElasticsearchRestTemplate来做查询,主要使用方法 org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate#search

重点代码如下:

public <T> SearchHits<T> search(Query query, Class<T> clazz, IndexCoordinates index) {
    // 初始化request
    SearchRequest searchRequest = requestFactory.searchRequest(query, clazz, index);
    // 获取值
    SearchResponse response = execute(client -> client.search(searchRequest, RequestOptions.DEFAULT));
  
    SearchDocumentResponseCallback<SearchHits<T>> callback = new ReadSearchDocumentResponseCallback<>(clazz, index);
    // 转换为对应类型
    return callback.doWith(SearchDocumentResponse.from(response));
}

加载

首先看初始化request的逻辑

  • org.springframework.data.elasticsearch.core.RequestFactory#searchRequest

    • 首先是: org.springframework.data.elasticsearch.core.RequestFactory#prepareSearchRequest

      • 这里有段代码是对搜索结果的排序处理: prepareSort(query, sourceBuilder, getPersistentEntity(clazz)); 重点就是这里的 getPersistentEntity(clazz)
        这段代码主要会识别当前类是否已经加载过,没有加载过则加载到内存中:

        @Nullable
        private ElasticsearchPersistentEntity<?> getPersistentEntity(@Nullable Class<?> clazz) {
        	// 从convert上下文中获取判断该类是否已经加载过,如果没有加载过,就会重新解析加载并放入上下文
        	return clazz != null ? elasticsearchConverter.getMappingContext().getPersistentEntity(clazz) : null;
        }
        

具体加载的实现见: 具体实现见:org.springframework.data.mapping.context.AbstractMappingContext#getPersistentEntity(org.springframework.data.util.TypeInformation<?>)

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.mapping.model.MappingContext#getPersistentEntity(org.springframework.data.util.TypeInformation)
	 */
	@Nullable
	@Override
	public E getPersistentEntity(TypeInformation<?> type) {

		Assert.notNull(type, "Type must not be null!");

		try {
			read.lock();
			// 从上下文获取当前类
			Optional<E> entity = persistentEntities.get(type);
			// 存在则返回
			if (entity != null) {
				return entity.orElse(null);
			}
		} finally {
			read.unlock();
		}
		if (!shouldCreatePersistentEntityFor(type)) {
			try {
				write.lock();
				persistentEntities.put(type, NONE);
			} finally {
				write.unlock();
			}
			return null;
		}
		if (strict) {
			throw new MappingException("Unknown persistent entity " + type);
		}
		// 不存在时,添加该类型到上下文
		return addPersistentEntity(type).orElse(null);
	}

使用

上述是加载流程。执行查询后,我们还需要进行一次转换。这里就到了使用的地方:org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate#search中 callback.doWith(SearchDocumentResponse.from(response));

这里这个方法会请求内部的 doWith 方法。实现如下:

@Nullable
public T doWith(@Nullable Document document) {

    if (document == null) {
        return null;
    }
    // 获取到待转换的类实例
    T entity = reader.read(type, document);
    return maybeCallbackAfterConvert(entity, document, index);
}

其中的 reader.read 会先从上下文中获取上述加载到上下文的类信息,然后读取

	@Override
	public <R> R read(Class<R> type, Document source) {
		TypeInformation<R> typeHint = ClassTypeInformation.from((Class<R>) ClassUtils.getUserClass(type));
		typeHint = (TypeInformation<R>) typeMapper.readType(source, typeHint);

		if (conversions.hasCustomReadTarget(Map.class, typeHint.getType())) {
			R converted = conversionService.convert(source, typeHint.getType());
			if (converted == null) {
				// EntityReader.read is defined as non nullable , so we cannot return null
				throw new ConversionException("conversion service to type " + typeHint.getType().getName() + " returned null");
			}
			return converted;
		}

		if (typeHint.isMap() || ClassTypeInformation.OBJECT.equals(typeHint)) {
			return (R) source;
		}
		// 从上下文获取之前加载的类
		ElasticsearchPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(typeHint);
		// 获取该类信息
		return readEntity(entity, source);
	}

读取会走 org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter#readEntity

先是读取该类的初始化器:EntityInstantiator instantiator = instantiators.getInstantiatorFor(targetEntity);

  • 是通过该类实现:org.springframework.data.convert.KotlinClassGeneratingEntityInstantiator#createInstance

    • 然后到:org.springframework.data.mapping.model.ClassGeneratingEntityInstantiator#doCreateEntityInstantiator
	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.convert.ClassGeneratingEntityInstantiator#doCreateEntityInstantiator(org.springframework.data.mapping.PersistentEntity)
	 */
	@Override
	protected EntityInstantiator doCreateEntityInstantiator(PersistentEntity<?, ?> entity) {

		PreferredConstructor<?, ?> constructor = entity.getPersistenceConstructor();

		if (ReflectionUtils.isSupportedKotlinClass(entity.getType()) && constructor != null) {

			PreferredConstructor<?, ?> defaultConstructor = new DefaultingKotlinConstructorResolver(entity)
					.getDefaultConstructor();

			if (defaultConstructor != null) {
				// 获取对象初始化器
				ObjectInstantiator instantiator = createObjectInstantiator(entity, defaultConstructor);

				return new DefaultingKotlinClassInstantiatorAdapter(instantiator, constructor);
			}
		}

		return super.doCreateEntityInstantiator(entity);
	}

这里先请求内部的:createObjectInstantiator

	/**
	 * Creates a dynamically generated {@link ObjectInstantiator} for the given {@link PersistentEntity} and
	 * {@link PreferredConstructor}. There will always be exactly one {@link ObjectInstantiator} instance per
	 * {@link PersistentEntity}.
	 *
	 * @param entity
	 * @param constructor
	 * @return
	 */
	ObjectInstantiator createObjectInstantiator(PersistentEntity<?, ?> entity,
			@Nullable PreferredConstructor<?, ?> constructor) {

		try {
			// 调用生成
			return (ObjectInstantiator) this.generator.generateCustomInstantiatorClass(entity, constructor).newInstance();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

获取对象生成实例:generateCustomInstantiatorClass 这里获取类名称,会追加 _Instantiator_和对应类的 hashCode


		/**
		 * Generate a new class for the given {@link PersistentEntity}.
		 *
		 * @param entity
		 * @param constructor
		 * @return
		 */
		public Class<?> generateCustomInstantiatorClass(PersistentEntity<?, ?> entity,
				@Nullable PreferredConstructor<?, ?> constructor) {
			// 获取类名称
			String className = generateClassName(entity);
			byte[] bytecode = generateBytecode(className, entity, constructor);

			Class<?> type = entity.getType();

			try {
				return ReflectUtils.defineClass(className, bytecode, type.getClassLoader(), type.getProtectionDomain(), type);
			} catch (Exception e) {
				throw new IllegalStateException(e);
			}
		}

		private static final String TAG = "_Instantiator_";

		/**
		 * @param entity
		 * @return
		 */
		private String generateClassName(PersistentEntity<?, ?> entity) {
			// 类名+TAG+hashCode
			return entity.getType().getName() + TAG + Integer.toString(entity.hashCode(), 36);
		}

到此我们元数据中的一堆 拼接了 Instantiator_xxxxx 的类来源就破案了。

真相大白

对应问题产生的问题也很简单。

// 每次search前 都new了个RestTemplate,导致上下文发生变化,每次重新生成加载
new ElasticsearchRestTemplate(cluster);

这里我们是双集群模式,每次请求时会由负载决定使用那一个集群。之前在这里每次都 new了一个待使用集群的实例。

内部的上下文每次初始化后都是空的。

  • 请求查询ES

    • 初始化ES查询

      • 上下文为空
      • 加载类信息(hashCode发生变化)
      • 获取类信息(重计算类名)
      • 重新加载类到元数据

最终长时间运行后元数据空间溢出;

事后结论

1.当时的临时方案是重启应用,元数据区清空,同时临时也可以放大元数据区大小。

2.元数据区的类型卸载或回收,8以后已经不使用了。

3.元数据区的泄漏排查思路:找到加载多的类,然后排查使用情况和可能的加载场景,一般在各种序列化反射场景。

4.快速排查可使用我们的方案。使用OQL来完成。

5.监控可以考虑加载类实例监控和元数据空间使用大小监控和对应报警。可以提前发现和处理。

6.ES查询在启动时对应集群内部初始化一个查询实例。使用那个集群就使用对应的集群查询实例。

附录

VisualVM下载地址:https://visualvm.github.io/

OQL: Object Query Language 可参看在VisualVM中使用OQL分析

获取路径下类加载数量,从高到低排序

var packageClassSizeMap = {};
// 遍历统计以最后一个逗号做分割
heap.forEachClass(function (it) {
    var packageName = it.name.substring(0, it.name.lastIndexOf('.'));
    if (packageClassSizeMap[packageName] != null) {
        packageClassSizeMap[packageName] = packageClassSizeMap[packageName] + 1;
    } else {
        packageClassSizeMap[packageName] = 1;
    }
});
// 排序 因为Visual VM的查询有数量限制。
var sortPackageClassSizeMap = [];
map(sort(Object.keys(packageClassSizeMap), function (a, b) {
    return packageClassSizeMap[b] - packageClassSizeMap[a]
}), function (it) {
    sortPackageClassSizeMap.push({
        package: it,
        classSize: packageClassSizeMap[it]
    })
});
sortPackageClassSizeMap;

获取某个路径下类加载数量

var packageClassSizeMap = {};
// 遍历统计以最后一个逗号做分割
heap.forEachClass(function (it) {
    var packageName = it.name.substring(0, it.name.lastIndexOf('.'));
    // 加路径过滤版
    if (packageName.indexOf('com.jd.bapp.match.sync.query.es.po')){
        if (packageClassSizeMap[packageName] != null) {
            packageClassSizeMap[packageName] = packageClassSizeMap[packageName] + 1;
        } else {
            packageClassSizeMap[packageName] = 1;
        }
    }
});

sortPackageClassSizeMap;

特别鸣谢

感谢黄仕清和Jdos同学提供的技术支持。

作者:京东零售 王建波

来源:京东云开发者社区

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

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

相关文章

vue 动态引入图片地址的方法

我们直接使用 v-bind 的方式导入无法正常导入 <image :src"item.src" alt""/> 是因为 页面显示为htmlcssjs而vue变成我们可以看到的样子是需要打包变成htmlcssjs的&#xff0c; 在打包过程中将item.src的变量 取出变成/image/icon.svg只是 将地址…

集中式自动抄表系统原理与应用

集中式自动抄表系统是一种自动采集电表、水表、气表等计量数据的系统&#xff0c;其原理是通过一定的通信方式&#xff0c;将计量数据从表端传输到数据采集器&#xff0c;再由数据采集器上传至云端或后台处理系统&#xff0c;从而实现对表数据的自动采集、统计和分析。 集中式…

机器学习之随机森林(Random forest)

1 什么是随机森林 随机森林是一种监督式算法&#xff0c;使用由众多决策树组成的一种集成学习方法&#xff0c;输出是对问题最佳答案的共识。随机森林可用于分类或回归&#xff0c;是一种主流的集成学习算法。 1.1 随机森林算法原理 随机森林中有许多的分类树。我们要将一个输…

【Spring】使用注解读取和存储Bean对象

哈喽&#xff0c;哈喽&#xff0c;大家好~ 我是你们的老朋友&#xff1a;保护小周ღ 谈起Java 圈子里的框架&#xff0c;最年长最耀眼的莫过于 Spring 框架啦&#xff0c;本期给大家带来的是&#xff1a; 将对象存储到 Spring 中、Bean 对象的命名规则、从Spring 中获取bean …

解决github无法拉取submodule子模块的问题

引言 当使用git clone --recursive url 拉取一个配置了子模块的仓库后&#xff0c;会卡住。 同时在使用git clone 拉去https的url时&#xff0c;同样可能会出现一直卡在cloning int reposity...本文提供一个简单的脚本来解决该问题。 前置准备 需要配置好git的相关配置&…

今年第十个零日漏洞,苹果发布紧急更新

苹果于7月10日发布了新一轮快速安全响应 (RSR) 更新&#xff0c;以解决在攻击中利用的一个新零日漏洞。 苹果在iOS和macOS的更新公告中引用了一位匿名安全专家对该漏洞&#xff08;CVE-2023-37450&#xff09;的描述&#xff0c;表示“苹果已获悉有关此漏洞可能已被积极利用的…

自动化测试集成指南 -- 本地单元测试

构建本地单元测试 简介&#xff1a; 单元测试(Unit Test) 是针对 程序的最小单元 来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。一个单元可能是单个程序、类、对象、方法等。 如何区分单元测试和集成测试&#xff0c;一般情况下&#xff0c;单元测试应该不…

jacoco merge 合并代码覆盖率(同一个项目代码没有修改)

相关文章&#xff1a; jacoco代码覆盖率_jacoco覆盖率_做测试的喵酱的博客-CSDN博客 一、背景 前提&#xff1a; 同一个项目&#xff0c;代码没有修改的情况下&#xff0c;合并多个代码覆盖率&#xff0c;实现全量代码覆盖率。 java -jar jacococli.jar merge jacoco.exec …

PFC-34、PMO-78、HD3-AMPS比例控制阀放大器

比例驱动放大器 用于HD2-PS、HD3-PS、HD3-AMPS、HD3-PS8、HD5-PS、HD3-PMO、PMO-78、PRO-M24、AMF-RE、PFC-34、PFC-78、PFP3-78电磁比例阀 DIN 连接器安装 微控制器设计 独立调整&#xff08;斜坡上升 - 斜坡下降&#xff09; 3位LED显示屏 显示和调整实际值&#xff08…

转换成mp4格式的方法有哪些?分享两个给大家!

在数字化的世界中&#xff0c;我们经常需要处理各种格式的视频文件。MP4是一种非常常见的视频格式&#xff0c;由于其优秀的兼容性和较小的文件大小&#xff0c;它被广泛用于在线播放、视频编辑和共享。然而&#xff0c;我们可能会遇到一些非MP4格式的视频文件&#xff0c;这就…

YOLO V5 ROS功能包配置及运行(亲测可用、附ROS功能包源码)

一、 依赖项 1. Ubuntu 18.04 安装opencv 4.2.0/4.6.0链接&#xff1a; 查看当前opencv版本 pkg-config --modversion opencv 安装opencv 4.2.0链接&#xff1a; https://note.youdao.com/s/R6ddu2ou 2. 安装PyTorch 官网链接&#xff1a; https://pytorch.org/get-started…

美创科技获2023年杭州市总部企业认定

日前&#xff0c;2023年度杭州市总部企业认定名单新鲜出炉&#xff0c;美创科技被认定“2023年度杭州市总部企业”。 2023年度杭州市总部企业认定名单(排名不分先后) ‍为进一步扶持、培育和引进总部企业&#xff0c;积极打造全国一流总部经济中心&#xff0c;根据《关于推动杭…

vite性能优化提升开发体验之hmr和预编译

一、vite中的预编译 1. 预编译概念介绍 Vite&#xff0c;一个由Vue.js开发者尤雨溪开发的新型前端构建工具&#xff0c;主要利用了现代浏览器支持的ESM&#xff08;ES模块&#xff09;来进行快速开发。Vite在法语中意为“快”&#xff0c;其中最大的亮点就是其开发服务器启动…

gzyj 安全处理

目录 现场操作指导 解决方案 细节验证 4.1.1.1 4.1.1.2 4.1.1.3 4.1.1.4 4.1.1.5 4.1.2.1 4.1.2.2 4.1.2.3 4.1.2.4 4.1.2.5 4.1.3.1 4.1.3.2 4.1.3.3 4.1.3.4 技术支持可以仅看第一节即可。 现场操作指导 &#xff08;1&#xff09; 升级vms (2) 升级 meshview (3) nm…

从推动到拉动:研发效能提升的第一性原理

导语 |随着企业业务的快速发展&#xff0c;产品迭代速度越来越成为企业发展制胜的关键因素。在业务迅速扩张之下&#xff0c;企业研发团队的规模也在不断壮大。如何有效管理研发团队&#xff0c;又该如何提升企业研发效能&#xff0c;让企业在市场竞争中立于不败之地成为了一堂…

基础写作干货分享:微信公众号8个基本写作步骤

公众号怎么写&#xff1f;不知道如何下手&#xff1f;如何写一篇出众的公众号文章&#xff1f;怎么写公众号内容会出爆文&#xff1f;当我们涉及到基础微信公众号写作时不知道怎么写SEO教程自学网教你如何下手&#xff0c;八个基本写作步骤学会立马搞定困难。 确定目标受众&…

提高软件测试质量 需重点关注5个方面

在软件研发管理过程中&#xff0c;我们往往重视开发的进度&#xff0c;而对软件测试关注度不高&#xff0c;有些项目组甚至没有软件测试人员。这样往往容易导致测试工作出现很多问题&#xff0c;如测试目标不明确&#xff0c;不断修改测试方向&#xff1b;测试结果不理想&#…

Android11.0 导航栏添加图标截屏

需求&#xff1a; 导航栏添加截屏和电源键图标&#xff0c;控制截屏和用于设备重启关机功能。设置中添加延时截屏和控制截屏图标显示开关。 1. 导航栏添加图标 1.1 添加布局文件和图标icon 在layout目录下添加nav_power.xml和screenshot.xml文件 frameworks/base/packages…

远程桌面-出现身份验证错误

1. 打开本地组策略编辑器&#xff0c;可以按winr组合键输入gpedit.msc命令打开。 2. 选择“计算机配置”--“管理模板”--“系统”--“凭据分配”&#xff0c;在“凭据分配”设置中选择“加密数据库修正”。 3. 在“加密数据库修正”对话框中选择“已启动”&#xff0c;保护级别…

Redis解决Session共享问题

文章目录 一、集群Session共享问题二、Redis存储验证码和对象三、解决状态登录刷新问题 一、集群Session共享问题 session共享问题&#xff1a;多台Tomcat并不共享session存储空间&#xff0c;当请求切换到不同tomcat服务器时导致数据丢失的问题 tomcat可以进行多台tomcat进行…