Spring Bootの最新バージョンでアプリケーションをプログラミングしています。最近、ヒープの増加に問題が生じました。これはガベージコレクションの対象になりません。 Eclipse MATによるヒープの分析では、アプリケーションの実行から1時間以内に、ヒープが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のクエリマジックによって生成されます。この手法で生成されるクエリは約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の使用例として。
質問:クエリプランキャッシュが増加し続けるのはなぜですか(停止せず、ヒープ全体で終了する)と、これを防ぐ方法は?誰かが同様の問題に遭遇しましたか?
バージョン:
私もこの問題に遭遇しました。基本的には、IN句に可変数の値を設定し、Hibernateがそれらのクエリプランをキャッシュしようとすることになります。
このトピックには2つの素晴らしいブログ投稿があります。 最初の :
次のような節内クエリを含むプロジェクトでHibernate 4.2とMySQLを使用する:
select t from Thing t where t.id in (?)
Hibernateはこれらの解析されたHQLクエリをキャッシュします。具体的には、Hibernate
SessionFactoryImpl
にはQueryPlanCache
とqueryPlanCache
およびparameterMetadataCache
があります。しかし、これは、in-clauseのパラメーターの数が多く、変化する場合に問題になることが判明しました。これらのキャッシュは、クエリごとに増加します。したがって、6000個のパラメーターを持つこのクエリは、6001と同じではありません。
句内クエリは、コレクション内のパラメーターの数に拡張されます。メタデータは、x10_、x11_などの生成された名前を含め、クエリの各パラメーターのクエリプランに含まれています。
句内のパラメーター数の4000の異なるバリエーションを想像してください。これらのそれぞれは、平均4000のパラメーターがあります。各パラメーターのクエリメタデータは、ガベージコレクションできないため、すぐにメモリに追加され、ヒープがいっぱいになります。
これは、クエリパラメータ数のさまざまなバリエーションがすべてキャッシュされるか、JVMがヒープメモリを使い果たし、Java.lang.OutOfMemoryError:Javaヒープスペースをスローし始めるまで続きます。
句の回避はオプションであり、パラメータに固定のコレクションサイズ(または少なくとも小さいサイズ)を使用することもできます。
クエリプランキャッシュの最大サイズの構成については、プロパティ_
hibernate.query.plan_cache_max_size
_を参照してください。デフォルトは_2048
_です(多くのパラメーターを持つクエリには、大きすぎます)。
そして second (最初からも参照されます):
Hibernateは内部的に cache を使用して、HQLステートメントを(文字列として) クエリプラン にマップします。キャッシュは、デフォルトで2048要素(構成可能)に制限された境界マップで構成されます。すべてのHQLクエリは、このキャッシュを通じて読み込まれます。ミスの場合、エントリは自動的にキャッシュに追加されます。これにより、スラッシングの影響を非常に受けやすくなります。つまり、新しいエントリを再利用せずに常にキャッシュに入れて、キャッシュがパフォーマンスを向上させないようにするシナリオです(キャッシュ管理のオーバーヘッドも追加されます)。さらに悪いことに、この状況を偶然に検出することは困難です-そこに問題があることに気づくために、キャッシュを明示的にプロファイルする必要があります。これを後で行う方法について少し説明します。
したがって、キャッシュスラッシングは、新しいクエリが高速で生成された結果です。これは、多くの問題が原因である可能性があります。私が見た最も一般的な2つは、パラメーターとして渡される代わりにJPQLステートメントでパラメーターがレンダリングされる原因となる「休止状態」のバグと「in」節の使用です。
Hibernateのあいまいなバグのため、パラメーターが正しく処理されず、JPQLクエリにレンダリングされる場合があります(例として、チェックアウト HHH-628 )。このような欠陥の影響を受けるクエリがあり、それが高い頻度で実行される場合、生成される各JPQLクエリはほぼ一意であるため(たとえば、エンティティのIDが含まれているため)、クエリプランキャッシュがスラッシュします。
2番目の問題は、Hibernateが "in"句を使用してクエリを処理する方法にあります(たとえば、会社IDフィールドが1、2、10、18のいずれかであるすべての個人エンティティを取得します)。 「in」節のパラメーターの数ごとに、hibernateは異なるクエリを生成します。 1つのパラメーターの場合は
select x from Person x where x.company.id in (:id0_)
、2つのパラメーターの場合はselect x from Person x where x.company.id in (:id0_, :id1_)
などです。クエリプランキャッシュに関する限り、これらのクエリはすべて異なると見なされ、再びキャッシュスラッシングが発生します。おそらく、特定の数のパラメータのみを生成するユーティリティクラスを作成することで、この問題を回避できます。 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
。ヒープのクエリプランキャッシュに現在あるすべてのクエリを出力します。前述の問題のいずれかに影響を受けているかどうかを簡単に特定できるはずです。パフォーマンスへの影響に関しては、あまりにも多くの要因に依存するため、言うのは困難です。新しいHQLクエリプランの作成に10〜20ミリ秒のオーバーヘッドが発生する非常に些細なクエリを見たことがあります。一般に、キャッシュがどこかにある場合は、それには十分な理由があるはずです。ミスはおそらくコストがかかるため、できるだけミスを避けようとする必要があります。最後に重要なことですが、データベースは大量の一意のSQLステートメントも処理する必要があります。データベースがそれらを解析し、それらのそれぞれに対して異なる実行プランを作成する可能性があります。
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つの設定があります
このqueryPlanCacheで大きな問題が発生したため、Hibernateキャッシュモニターを実行して、queryPlanCache内のクエリを確認しました。 QA環境でSpringタスクとして5分ごとに使用しています。キャッシュの問題を解決するために変更する必要がある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;
}
}
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
。
私は同様の問題を抱えていました。問題は、クエリを作成していて、PreparedStatementを使用していないためです。したがって、ここで行われるのは、異なるパラメーターを持つ各クエリに対して実行プランを作成してキャッシュすることです。準備済みステートメントを使用すると、使用中のメモリが大幅に改善されるはずです。
また、ヒープ使用量が増加するQueryPlanCacheもありました。書き換えたINクエリがあり、さらにカスタムタイプを使用するクエリがあります。 HibernateクラスCustomTypeがequalsとhashCodeを適切に実装していないため、すべてのクエリインスタンスに新しいキーが作成されていることがわかりました。これはHibernate 5.3で解決されました。 https://hibernate.atlassian.net/browse/HHH-1246 を参照してください。それでも適切に機能させるには、userTypesにequals/hashCodeを適切に実装する必要があります。
クエリプランキャッシュの成長が速すぎ、gcが収集できなかったために古いgenヒープも一緒に成長してこの問題に直面していました。原因は、IN句で200000を超えるIDを取得するJPAクエリでした。クエリを最適化するために、1つのテーブルからIDをフェッチして他のテーブルの選択クエリに渡す代わりに、結合を使用しました。