簡體   English   中英

Spring + Hibernate:查詢計划緩存內存使用情況

[英]Spring + Hibernate: Query Plan Cache Memory usage

我正在使用最新版本的 Spring Boot 編寫應用程序。 我最近遇到了堆增長的問題,無法進行垃圾收集。 使用 Eclipse MAT 對堆的分析表明,在運行應用程序的一小時內,堆增長到 630MB,並且 Hibernate 的 SessionFactoryImpl 使用了整個堆的 75% 以上。

在此處輸入圖片說明

正在尋找有關查詢計划緩存的可能來源,但我發現的唯一一件事是this ,但這並沒有發揮作用。 屬性設置如下:

spring.jpa.properties.hibernate.query.plan_cache_max_soft_references=1024
spring.jpa.properties.hibernate.query.plan_cache_max_strong_references=64

數據庫查詢全部由 Spring 的 Query 魔術生成,使用本文檔中的存儲庫接口。 使用這種技術生成了大約 20 個不同的查詢。 不使用其他本機 SQL 或 HQL。 樣本:

@Transactional
public interface TrendingTopicRepository extends JpaRepository<TrendingTopic, Integer> {
    List<TrendingTopic> findByNameAndSource(String name, String source);
    List<TrendingTopic> findByDateBetween(Date dateStart, Date dateEnd);
    Long countByDateBetweenAndName(Date dateStart, Date dateEnd, String name);
}

或者

List<SomeObject> findByNameAndUrlIn(String name, Collection<String> urls);

作為 IN 用法的示例。

問題是:為什么查詢計划緩存不斷增長(它不會停止,它以完整的堆結束)以及如何防止這種情況? 有沒有人遇到過類似的問題?

版本:

  • 彈簧靴 1.2.5
  • 休眠 4.3.10

我也碰到過這個問題。 它基本上歸結為在您的 IN 子句中具有可變數量的值,並且 Hibernate 嘗試緩存這些查詢計划。

關於這個主題有兩篇很棒的博客文章。 第一個

在項目中使用 Hibernate 4.2 和 MySQL 並帶有 in-clause 查詢,例如: select t from Thing t where t.id in (?)

Hibernate 緩存這些解析的 HQL 查詢。 特別是 Hibernate SessionFactoryImplQueryPlanCachequeryPlanCacheparameterMetadataCache 但是當子句的參數數量很大並且變化時,這被證明是一個問題。

這些緩存會隨着每個不同的查詢而增長。 所以這個有 6000 個參數的查詢和 6001 是不一樣的。

子句查詢擴展到集合中的參數數量。 元數據包含在查詢中每個參數的查詢計划中,包括生成的名稱,如 x10_、x11_ 等。

想象一下子句參數計數的數量有 4000 種不同的變化,每一種都有平均 4000 個參數。 每個參數的查詢元數據在內存中快速累加,填滿堆,因為它不能被垃圾收集。

這一直持續到查詢參數計數中的所有不同變化都被緩存或 JVM 耗盡堆內存並開始拋出 java.lang.OutOfMemoryError: Java heap space。

避免使用 in-clauses 是一種選擇,以及為參數使用固定的集合大小(或至少較小的大小)。

要配置查詢計划緩存最大大小,請參閱屬性hibernate.query.plan_cache_max_size ,默認為2048 (對於具有許多參數的查詢來說很容易太大)。

第二個(也從第一個引用):

Hibernate 內部使用緩存將 HQL 語句(作為字符串)映射到查詢計划 緩存由默認限制為 2048 個元素(可配置)的有界地圖組成。 所有 HQL 查詢都通過此緩存加載。 在未命中的情況下,條目會自動添加到緩存中。 這使得它非常容易受到顛簸 - 在這種情況下,我們不斷地將新條目放入緩存中而從未重用它們,從而阻止緩存帶來任何性能提升(它甚至增加了一些緩存管理開銷)。 更糟糕的是,很難偶然檢測到這種情況 - 您必須明確地分析緩存才能注意到那里有問題。 稍后我將就如何做到這一點說幾句話。

因此緩存抖動是由高速生成的新查詢造成的。 這可能是由多種問題引起的。 我見過的兩個最常見的是 - 休眠中的錯誤,導致參數在 JPQL 語句中呈現而不是作為參數傳遞,以及使用“in” - 子句。

由於 hibernate 中的一些模糊錯誤,有些情況下參數沒有正確處理並呈現到 JPQL 查詢中(例如查看HHH-6280 )。 如果您有一個受此類缺陷影響的查詢並且它以高速率執行,它會破壞您的查詢計划緩存,因為生成的每個 JPQL 查詢幾乎都是唯一的(例如,包含您的實體的 ID)。

第二個問題在於 hibernate 處理帶有“in”子句的查詢的方式(例如,給我公司 ID 字段為 1、2、10、18 之一的所有個人實體)。 對於“in”子句中每個不同數量的參數,hibernate 將生成不同的查詢 - 例如, select x from Person x where x.company.id in (:id0_) for 1 個參數中select x from Person x where x.company.id in (:id0_, :id1_)用於 2 個參數等。 就查詢計划緩存而言,所有這些查詢都被認為是不同的,再次導致緩存抖動。 您可能可以通過編寫一個實用程序類來僅生成特定數量的參數(例如 1、10、100、200、500、1000)來解決此問題。例如,如果您傳遞 22 個參數,它將返回一個包含 100 個參數的列表其中包含 22 個參數且其余 78 個參數設置為不可能值的元素(例如 -1 表示用於外鍵的 ID)。 我同意這是一個丑陋的黑客,但可以完成工作。 因此,您的緩存中最多只能有 6 個唯一查詢,從而減少抖動。

那么你怎么知道你有這個問題呢? 您可以編寫一些額外的代碼並使用緩存中的條目數量公開指標,例如通過 JMX、調整日志記錄和分析日志等。如果您不想(或不能)修改應用程序,您可以轉儲堆並針對它運行此 OQL 查詢(例如使用mat ): SELECT l.query.toString() FROM INSTANCEOF org.hibernate.engine.query.spi.QueryPlanCache$HQLQueryPlanKey l 它將輸出當前位於堆上任何查詢計划緩存中的所有查詢。 應該很容易發現您是否受到上述任何問題的影響。

至於性能影響,很難說,因為它取決於太多因素。 我見過一個非常簡單的查詢,導致在創建新的 HQL 查詢計划時花費了 10-20 毫秒的開銷。 一般而言,如果某處有緩存,則必須有充分的理由 - 未命中可能代價高昂,因此您應該盡量避免未命中。 最后但並非最不重要的一點是,您的數據庫也必須處理大量獨特的 SQL 語句 - 導致它解析它們並可能為每個語句創建不同的執行計划。

我對 IN 查詢中的許多(> 10000)個參數有同樣的問題。 我的參數數量總是不同的,我無法預測這一點,我的QueryCachePlan增長得太快了。

對於支持執行計划緩存的數據庫系統,如果可能的 IN 子句參數數量減少,則更有可能命中緩存。

幸運的是,5.3.0 及更高版本的 Hibernate 有一個在 IN 子句中填充參數的解決方案。

Hibernate 可以將綁定參數擴展為 2 的冪:4、8、16、32、64。這樣,具有 5、6 或 7 個綁定參數的 IN 子句將使用 8 IN 子句,因此重用其執行計划.

如果要激活此功能,則需要將此屬性設置為 true hibernate.query.in_clause_parameter_padding=true

有關更多信息,請參閱本文atlassian

我在使用 Spring Boot 1.5.7 和 Spring Data (Hibernate) 時遇到了完全相同的問題,以下配置解決了這個問題(內存泄漏):

spring:
  jpa:
    properties:
      hibernate:
        query:
          plan_cache_max_size: 64
          plan_parameter_metadata_max_size: 32

從 Hibernate 5.2.12 開始,您可以指定一個 hibernate 配置屬性,以通過使用以下內容來更改文字如何綁定到底層 JDBC 准備好的語句:

hibernate.criteria.literal_handling_mode=BIND

從 Java 文檔中,此配置屬性有 3 個設置

  1. 自動(默認)
  2. BIND - 使用綁定參數增加 jdbc 語句緩存的可能性。
  3. INLINE - 內聯值而不是使用參數(注意 SQL 注入)。

我有一個類似的問題,問題是因為您正在創建查詢而不是使用 PreparedStatement。 所以這里發生的是對於每個具有不同參數的查詢,它創建一個執行計划並緩存它。 如果您使用准備好的語句,那么您應該會看到所用內存的重大改進。

我對這個 queryPlanCache 有一個大問題,所以我做了一個 Hibernate 緩存監視器來查看 queryPlanCache 中的查詢。 我在 QA 環境中每 5 分鍾使用一次作為 Spring 任務。 我發現我必須更改哪些 IN 查詢才能解決我的緩存問題。 一個細節是:我使用的是 Hibernate 4.2.18,我不知道對其他版本是否有用。

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.hibernate.ejb.HibernateEntityManagerFactory;
import org.hibernate.internal.SessionFactoryImpl;
import org.hibernate.internal.util.collections.BoundedConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.dao.GenericDAO;

public class CacheMonitor {

private final Logger logger  = LoggerFactory.getLogger(getClass());

@PersistenceContext(unitName = "MyPU")
private void setEntityManager(EntityManager entityManager) {
    HibernateEntityManagerFactory hemf = (HibernateEntityManagerFactory) entityManager.getEntityManagerFactory();
    sessionFactory = (SessionFactoryImpl) hemf.getSessionFactory();
    fillQueryMaps();
}

private SessionFactoryImpl sessionFactory;
private BoundedConcurrentHashMap queryPlanCache;
private BoundedConcurrentHashMap parameterMetadataCache;

/*
 * I tried to use a MAP and use compare compareToIgnoreCase.
 * But remember this is causing memory leak. Doing this
 * you will explode the memory faster that it already was.
 */

public void log() {
    if (!logger.isDebugEnabled()) {
        return;
    }

    if (queryPlanCache != null) {
        long cacheSize = queryPlanCache.size();
        logger.debug(String.format("QueryPlanCache size is :%s ", Long.toString(cacheSize)));

        for (Object key : queryPlanCache.keySet()) {
            int filterKeysSize = 0;
            // QueryPlanCache.HQLQueryPlanKey (Inner Class)
            Object queryValue = getValueByField(key, "query", false);
            if (queryValue == null) {
                // NativeSQLQuerySpecification
                queryValue = getValueByField(key, "queryString");
                filterKeysSize = ((Set) getValueByField(key, "querySpaces")).size();
                if (queryValue != null) {
                    writeLog(queryValue, filterKeysSize, false);
                }
            } else {
                filterKeysSize = ((Set) getValueByField(key, "filterKeys")).size();
                writeLog(queryValue, filterKeysSize, true);
            }
        }
    }

    if (parameterMetadataCache != null) {
        long cacheSize = parameterMetadataCache.size();
        logger.debug(String.format("ParameterMetadataCache size is :%s ", Long.toString(cacheSize)));
        for (Object key : parameterMetadataCache.keySet()) {
            logger.debug("Query:{}", key);
        }
    }
}

private void writeLog(Object query, Integer size, boolean b) {
    if (query == null || query.toString().trim().isEmpty()) {
        return;
    }
    StringBuilder builder = new StringBuilder();
    builder.append(b == true ? "JPQL " : "NATIVE ");
    builder.append("filterKeysSize").append(":").append(size);
    builder.append("\n").append(query).append("\n");
    logger.debug(builder.toString());
}

private void fillQueryMaps() {
    Field queryPlanCacheSessionField = null;
    Field queryPlanCacheField = null;
    Field parameterMetadataCacheField = null;
    try {
        queryPlanCacheSessionField = searchField(sessionFactory.getClass(), "queryPlanCache");
        queryPlanCacheSessionField.setAccessible(true);
        queryPlanCacheField = searchField(queryPlanCacheSessionField.get(sessionFactory).getClass(), "queryPlanCache");
        queryPlanCacheField.setAccessible(true);
        parameterMetadataCacheField = searchField(queryPlanCacheSessionField.get(sessionFactory).getClass(), "parameterMetadataCache");
        parameterMetadataCacheField.setAccessible(true);
        queryPlanCache = (BoundedConcurrentHashMap) queryPlanCacheField.get(queryPlanCacheSessionField.get(sessionFactory));
        parameterMetadataCache = (BoundedConcurrentHashMap) parameterMetadataCacheField.get(queryPlanCacheSessionField.get(sessionFactory));
    } catch (Exception e) {
        logger.error("Failed fillQueryMaps", e);
    } finally {
        queryPlanCacheSessionField.setAccessible(false);
        queryPlanCacheField.setAccessible(false);
        parameterMetadataCacheField.setAccessible(false);
    }
}

private <T> T getValueByField(Object toBeSearched, String fieldName) {
    return getValueByField(toBeSearched, fieldName, true);
}

@SuppressWarnings("unchecked")
private <T> T getValueByField(Object toBeSearched, String fieldName, boolean logErro) {
    Boolean accessible = null;
    Field f = null;
    try {
        f = searchField(toBeSearched.getClass(), fieldName, logErro);
        accessible = f.isAccessible();
        f.setAccessible(true);
    return (T) f.get(toBeSearched);
    } catch (Exception e) {
        if (logErro) {
            logger.error("Field: {} error trying to get for: {}", fieldName, toBeSearched.getClass().getName());
        }
        return null;
    } finally {
        if (accessible != null) {
            f.setAccessible(accessible);
        }
    }
}

private Field searchField(Class<?> type, String fieldName) {
    return searchField(type, fieldName, true);
}

private Field searchField(Class<?> type, String fieldName, boolean log) {

    List<Field> fields = new ArrayList<Field>();
    for (Class<?> c = type; c != null; c = c.getSuperclass()) {
        fields.addAll(Arrays.asList(c.getDeclaredFields()));
        for (Field f : c.getDeclaredFields()) {

            if (fieldName.equals(f.getName())) {
                return f;
            }
        }
    }
    if (log) {
        logger.warn("Field: {} not found for type: {}", fieldName, type.getName());
    }
    return null;
}
}

我們還有一個 QueryPlanCache,堆使用量不斷增加。 我們有重寫的 IN 查詢,另外我們有使用自定義類型的查詢。 原來 Hibernate 類 CustomType 沒有正確實現 equals 和 hashCode,從而為每個查詢實例創建一個新鍵。 現在在 Hibernate 5.3 中解決了這個問題。 請參閱https://hibernate.atlassian.net/browse/HHH-12463 您仍然需要在您的 userTypes 中正確實現 equals/hashCode 以使其正常工作。

我們遇到過查詢計划緩存增長過快的問題,並且舊的 gen 堆也隨之增長,因為 gc 無法收集它。罪魁禍首是 JPA 查詢在 IN 子句中使用了超過 200000 個 id。 為了優化查詢,我們使用了連接而不是從一個表中獲取 id 並將其傳遞到其他表選擇查詢中。

TL;DR:嘗試用 ANY() 替換 IN() 查詢或消除它們

解釋:
如果查詢包含 IN(...),則為 IN(...) 中的每個值創建一個計划,因為每次查詢都不同。 因此,如果您有 IN('a','b','c') 和 IN ('a','b','c','d','e') - 那是兩個不同的查詢字符串/計划緩存。 這個答案告訴了更多關於它的信息。
在 ANY(...) 的情況下,可以傳遞單個(數組)參數,因此查詢字符串將保持不變,並且准備好的語句計划將被緩存一次(下面給出的示例)。

原因:
此行可能會導致問題:

List<SomeObject> findByNameAndUrlIn(String name, Collection<String> urls);

在幕后,它為“urls”集合中的每個值生成不同的 IN() 查詢。

警告:
您可能會在不編寫甚至不知道它的情況下進行 IN() 查詢。
ORM 之類的 Hibernate 可能會在后台生成它們——有時在意想不到的地方,有時以非最佳的方式。 因此,請考慮啟用查詢日志以查看您的實際查詢。

使固定:
這是一個可以解決問題的(偽)代碼:

query = "SELECT * FROM trending_topic t WHERE t.name=? AND t.url=?"
PreparedStatement preparedStatement = connection.prepareStatement(queryTemplate);
currentPreparedStatement.setString(1, name); // safely replace first query parameter with name
currentPreparedStatement.setArray(2, connection.createArrayOf("text", urls.toArray())); // replace 2nd parameter with array of texts, like "=ANY(ARRAY['aaa','bbb'])"

但:
不要將任何解決方案作為現成的答案。 確保在投入生產之前測試實際/大數據的最終性能 - 無論您選擇哪個答案。 為什么? 因為 IN 和 ANY 都有利有弊,如果使用不當,它們會帶來嚴重的性能問題(參見下面參考資料中的示例)。 還要確保使用參數綁定來避免安全問題。

參考:
通過更改 1 行將 Postgres 性能提高 100 倍- Any(ARRAY[]) 與 ANY(VALUES()) 的性能
索引不與 =any() 一起使用,但與 in 一起使用- IN 和 ANY 的不同性能
了解 SQL Server 查詢計划緩存

希望這可以幫助。 無論是否有效,請務必留下反饋 - 以幫助像您這樣的人。 謝謝!

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM