一次元數據空間記憶體溢出的排查記錄

来源:https://www.cnblogs.com/jingdongkeji/archive/2023/07/11/17543204.html
-Advertisement-
Play Games

在應用中,我們使用的 SpringData ES的 ElasticsearchRestTemplate來做查詢,使用方式不對,導致每次ES查詢時都新實例化了一個查詢對象,會載入相關類到元數據中。最終長時間運行後元數據出現記憶體溢出; ...


在應用中,我們使用的 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同學提供的技術支持。

作者:京東零售 王建波

來源:京東雲開發者社區


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • # shell腳本-監控多台伺服器磁碟利用率 ## 介紹 > 第一步:實現免密登錄伺服器 ,為後續腳本免密登錄做好準備。 > > 第二步:把要監控伺服器的ip地址 root用戶 埠port 寫入host.info文件中以便後續腳本從這個文件讀取伺服器信息。 > > 第三步:寫shell腳本,先從h ...
  • # 一、Python學習兩大道具 ## 1. dir()工具 - 作用:支持打開package,看到裡面的工具函數 - 示例: (1) 輸出torch庫包含的函數 ```python dir(torch) ``` (2) 輸出torch.AVG函數中的參數 ```python dir(torch.A ...
  • # 不小心刪除服務[null]後,git bash出現錯誤,如何解決? # 錯誤描述:打開 git bash、msys2都會出現錯誤「bash: /dev/null: No such device or address」 # 問題定位: 1.使用搜索引擎搜索「bash: /dev/null: No ...
  • ![](https://img2023.cnblogs.com/blog/3076680/202307/3076680-20230711143234011-1452662689.png) # 1. 兩個日期之間相差的月份和年份 ## 1.1. DB2 ## 1.2. MySQL ## 1.3. sq ...
  • 博客推行版本更新,成果積累制度,已經寫過的博客還會再次更新,不斷地琢磨,高質量高數量都是要追求的,工匠精神是學習必不可少的精神。因此,大家有何建議歡迎在評論區踴躍發言,你們的支持是我最大的動力,你們敢投,我就敢肝 ...
  • ## 問題 在執行數據插入時,postgresql 提示*more than one owned sequence found*錯誤。這個和之前文章中寫的[序列編號錯亂](https://www.cnblogs.com/podolski/p/17349217.html)不同,是由數據表的一個列生成了 ...
  • 摘要:本文從客戶視角的三個疑問出發,一起瞭解華為雲GaussDB資料庫的遷移解決方案具有哪些核心技術,如何做到讓客戶遷移過程安心、放心、省心。 遷移是資料庫選型過程中客戶最為關心的話題之一,經過大量的溝通調研,我們總結客戶在資料庫遷移方面的主要期望:遷移不影響業務運行(安心)、遷移不能丟數據(放心) ...
  • 向量資料庫Faiss是Facebook AI研究院開發的一種高效的相似性搜索和聚類的庫。它能夠快速處理大規模數據,並且支持在高維空間中進行相似性搜索。本文將介紹如何搭建Faiss環境並提供一個簡單的使用示例。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...