【问题标题】:Spring + Hibernate: Query Plan Cache Memory usageSpring + Hibernate:查询计划缓存内存使用情况
【发布时间】:2015-10-11 23:18:15
【问题描述】:

我正在使用最新版本的 Spring Boot 编写应用程序。我最近遇到了堆增长的问题,即不能被垃圾收集。使用 Eclipse MAT 对堆的分析表明,在运行应用程序的一小时内,堆增长到 630MB,而 Hibernate 的 SessionFactoryImpl 使用了整个堆的 75% 以上。

Is 正在围绕查询计划缓存寻找可能的来源,但我发现的唯一内容是 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 魔术生成的,使用像 in this documentation 这样的存储库接口。使用这种技术生成了大约 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 用法的示例。

问题是:为什么查询计划缓存不断增长(它不会停止,它会以满堆结束)以及如何防止这种情况?有没有人遇到过类似的问题?

版本:

  • Spring Boot 1.2.5
  • 休眠 4.3.10

【问题讨论】:

  • 贴一些代码和配置。您是否配置了链接到的帖子中提到的属性?将它们添加到application.properties 时,请确保在它们前面加上spring.pa.properties,否则它们将不会被应用。另外请添加您正在使用的 Hibernate 版本。
  • 用版本和例子更新了文本
  • 您是在自己的应用程序类还是在另一个@Configuration 类中配置东西?如果有请添加。
  • 不,只有一个连接池(hikaricp),但我想这与此无关?其他一切都来自@EnableAutoConfiguration
  • 尝试添加新属性 hibernate.query.plan_cache_max_sizehibernate.query.plan_parameter_metadata_max_size 其他属性已被弃用一段时间。

标签: spring hibernate postgresql spring-boot


【解决方案1】:

TL;DR:尝试将 IN() 查询替换为 ANY() 或消除它们

说明:
如果查询包含 IN(...),则为 IN(...) 中的每个值创建一个计划,因为 查询 每次都不同。 因此,如果您有 IN('a','b','c') 和 IN ('a','b','c','d','e') - 它们是两个不同的查询字符串/计划缓存。这个answer 详细介绍了它。
在 ANY(...) 的情况下,可以传递单个(数组)参数,因此查询字符串将保持不变,并且准备好的语句计划将被缓存一次(示例如下)。

原因:
此行可能会导致问题:

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

在后台,它为“urls”集合中的每个值生成不同的 IN() 查询。

警告:
您可能有 IN() 查询而不写它,甚至不知道它。
诸如 Hibernate 之类的 ORM 可能会在后台生成它们——有时在意想不到的地方,有时以非最佳方式。 因此,请考虑启用查询日志以查看您的实际查询。

修复:
这是一个可以解决问题的(伪)代码:

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 都有利有弊,如果使用不当会带来严重的性能问题(参见下面参考中的示例)。还要确保使用parameter binding 以避免安全问题。

参考:
100x faster Postgres performance by changing 1 line - Any(ARRAY[]) 与 ANY(VALUES()) 的性能
Index not used with =any() but used with in - IN 和 ANY 的不同性能
Understanding SQL Server query plan cache

希望这会有所帮助。确保留下反馈是否有效 - 为了帮助像你这样的人。谢谢!

【讨论】:

    【解决方案2】:

    我对 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

    有关详细信息,请参阅this articleatlassian

    【讨论】:

      【解决方案3】:

      我们遇到了这个问题,查询计划缓存增长过快,并且旧 gen 堆也随之增长,因为 gc 无法收集它。罪魁祸首是 JPA 查询在 IN 子句中占用了超过 200000 个 id。为了优化查询,我们使用了连接,而不是从一个表中获取 id 并将其传递给其他表选择查询。

      【讨论】:

        【解决方案4】:

        我遇到了类似的问题,问题是因为您正在创建查询而不是使用 PreparedStatement。所以这里发生的是对于每个具有不同参数的查询,它会创建一个执行计划并缓存它。 如果您使用准备好的语句,那么您应该会看到正在使用的内存有很大的改进。

        【讨论】:

          【解决方案5】:

          我们还有一个堆使用量不断增长的 QueryPlanCache。我们有我们重写的 IN 查询,此外我们还有使用自定义类型的查询。事实证明,Hibernate 类 CustomType 没有正确实现 equals 和 hashCode 从而为每个查询实例创建一个新键。现在在 Hibernate 5.3 中解决了这个问题。 见https://hibernate.atlassian.net/browse/HHH-12463。 您仍然需要在 userTypes 中正确实现 equals/hashCode 以使其正常工作。

          【讨论】:

            【解决方案6】:

            我对这个 queryPlanCache 有一个大问题,所以我做了一个 Hibernate 缓存监视器来查看 queryPlanCache 中的查询。 我每 5 分钟在 QA 环境中使用一个 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;
            }
            }
            

            【讨论】:

              【解决方案7】:

              从 Hibernate 5.2.12 开始,您可以指定一个 hibernate 配置属性来更改如何将文字绑定到底层 JDBC 准备语句,方法是使用以下内容:

              hibernate.criteria.literal_handling_mode=BIND
              

              根据 Java 文档,此配置属性有 3 个设置

              1. 自动(默认)
              2. BIND - 增加使用绑定参数缓存 jdbc 语句的可能性。
              3. INLINE - 内联值而不是使用参数(小心 SQL 注入)。

              【讨论】:

                【解决方案8】:

                我在使用带有 Spring Data (Hibernate) 的 Spring Boot 1.5.7 时遇到了完全相同的问题,以下配置解决了该问题(内存泄漏):

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

                【讨论】:

                • 这里你可能会有性能损失。如果您修复了计划缓存大小,但仍然不修复填充缓存的实际查询 - 您的所有缓存都可能被该错误查询填满,没有空间缓存其他查询。因此,缓存可能大部分时间都在忙于那个错误的查询,并且其他查询的性能可能会下降,因为它们没有被正确缓存或过早地从缓存中清除。
                【解决方案9】:

                我也遇到过这个问题。它基本上归结为在您的 IN 子句中具有可变数量的值,并且 Hibernate 试图缓存这些查询计划。

                关于这个主题有两篇很棒的博客文章。 The first:

                在具有子句查询的项目中使用 Hibernate 4.2 和 MySQL 如:select t from Thing t where t.id in (?)

                Hibernate 缓存这些已解析的 HQL 查询。特别是休眠 SessionFactoryImplQueryPlanCachequeryPlanCacheparameterMetadataCache。但这被证明是一个问题,当 in-clause 的参数数量很大并且变化很大。

                这些缓存会随着每个不同的查询而增长。所以这个查询有 6000 参数和6001不一样。

                in-clause 查询扩展为 收藏。元数据包含在每个参数的查询计划中 在查询中,包括生成的名称,如 x10_、x11_ 等。

                想象 4000 种不同的子句参数数量变化 计数,其中每一个平均有 4000 个参数。查询 每个参数的元数据在内存中迅速增加,填满 堆,因为它不能被垃圾回收。

                这会一直持续到查询参数出现所有不同的变化 count 被缓存或 JVM 耗尽堆内存并开始抛出 java.lang.OutOfMemoryError: Java 堆空间。

                避免使用子句是一种选择,也可以使用固定集合 参数的大小(或至少较小的大小)。

                有关配置查询计划缓存最大大小,请参阅属性 hibernate.query.plan_cache_max_size,默认为2048(也很容易 对于具有许多参数的查询来说很大)。

                还有second(也引用自第一个):

                Hibernate 在内部使用映射 HQL 语句的 cache(如 字符串)到query plans。缓存由有限的有界地图组成 默认为 2048 个元素(可配置)。已加载所有 HQL 查询 通过这个缓存。如有遗漏,自动进入 添加到缓存中。这使得它非常容易受到冲击 - a 我们不断将新条目放入缓存中的场景 曾经重用它们,从而防止缓存带来任何 性能提升(它甚至增加了一些缓存管理开销)。到 更糟糕的是,很难偶然发现这种情况 - 你 必须显式分析缓存才能注意到您有 那里有问题。我将就如何做到这一点说几句话 稍后。

                因此,缓存抖动是由生成的新查询导致的 高利率。这可能是由许多问题引起的。两个最 我见过的常见问题是 - 休眠中导致参数的错误 在 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_) 1 个参数, select x from Person x where x.company.id in (:id0_, :id1_) 2 参数等。所有这些查询都被认为是不同的,因为 就查询计划缓存而言,再次导致缓存 颠簸。您可能可以通过编写一个来解决这个问题 实用程序类只产生一定数量的参数 - 例如1、 10, 100, 200, 500, 1000。例如,如果您传递 22 个参数,则 将返回包含 22 个参数的 100 个元素的列表 它和其余 78 个参数设置为不可能的值(例如 -1 用于外键的 ID)。我同意这是一个丑陋的黑客,但是 可以完成工作。结果,您最多只有 6 个 缓存中的唯一查询,从而减少抖动。

                那么你如何发现你有问题呢?你可以写一些 附加代码并公开带有条目数的指标 缓存例如通过 JMX,调整日志记录并分析日志等。如果你这样做 不想(或不能)修改应用程序,你可以转储 堆并针对它运行此 OQL 查询(例如,使用 mat):SELECT l.query.toString() FROM INSTANCEOF org.hibernate.engine.query.spi.QueryPlanCache$HQLQueryPlanKey l。它 将输出当前位于任何查询计划缓存中的所有查询 你的堆。应该很容易发现您是否受到影响 由上述任何问题。

                就性能影响而言,很难说,因为这取决于 在太多的因素上。我看到一个非常简单的查询导致 10-20 毫秒 创建新 HQL 查询计划所花费的开销。一般来说,如果 某处有缓存,必须有充分的理由-a 错过可能很昂贵,所以你应该尽量避免错过 尽可能。最后但并非最不重要的一点是,您的数据库必须处理 也有大量独特的 SQL 语句 - 导致它解析它们 并且可能为它们中的每一个创建不同的执行计划。

                【讨论】:

                • 非常感谢!我们也面临同样的问题,并做了大量工作来优化我们的代码。但是,只有在启动 tomcat 时我们为 java 启用了 heapDumpOnOutOfMemoryErrors 选项后,才发现原因。堆转储显示的问题与您上面描述的完全相同。
                • 遇到了完全相同的问题。花了一个星期找出原因。最后堆转储给出了图片。之后,搜索“JPA 查询缓存”并结束。
                • 嗨。我找到了你的答案,我在 Wildfly 10.1 部署的应用程序中看到了这个问题。 Wildfly 16.0.0 中的相同应用程序(使用休眠 5.3.9)并使用推荐的属性集生成“清除”查询缓存。奇怪的是,既然默认值是 2048,那么这个 oql 怎么会在我们的 cae 3,8K 缓存查询中产生呢?这怎么可能?
                • 不知道,我不是 Hibernate 专家。在 StackOverflow 上提出您自己的问题,或者向 Hibernate 用户/开发人员提问。
                • 只要您使用的是 Hibernate 5.2.17 或更高版本,请参阅下面 Alex 的回答,以更轻松地使用 hibernate.query.in_clause_parameter_padding=true 提供此功能。
                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2018-06-24
                • 2011-03-05
                • 1970-01-01
                • 2013-09-23
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多