在應用中,我們使用的 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同學提供的技術支持,
作者:京東零售 王建波
來源:京東云開發者社區
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/557079.html
標籤:其他
上一篇:Windows下SqlServer2008通過ODBC連接到DM資料庫安裝部署
下一篇:返回列表