봄 + 휴지 상태:계획 캐시 메모리 사용량 쿼리
최신 버전의 스프링 부트로 애플리케이션을 프로그래밍하고 있습니다.최근에 쓰레기 수거가 안 되는 힙이 자라서 문제가 되었어요.Eclipse MAT를 사용한 힙 분석 결과, 애플리케이션 실행 후 1시간 이내에 힙이 630MB로 증가했으며, Hibernate의 SessionFactoryImpl을 사용한 힙은 전체 힙의 75% 이상을 사용했습니다.
Query Plan Cache 주변에서 가능한 소스를 찾고 있었는데, 제가 찾은 건 이것뿐이었어요. 하지만 효과가 없었어요.속성은 다음과 같이 설정되었습니다.
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 사용 예시로 사용합니다.
질문:쿼리 계획 캐시가 계속 증가하는 이유(정지되지 않고 완전한 힙으로 끝남)와 이를 방지하는 방법은 무엇입니까?비슷한 문제를 겪은 사람이 있나요?
버전:
- 스프링 부트 1.2.5
- 휴지 상태 4.3.10
저도 이 이슈에 부딪혔어요.기본적으로 IN 절과 Hibernate는 이러한 쿼리 계획을 캐시하려고 하는 다양한 수의 값을 갖는 것으로 요약됩니다.
이 주제에 대한 두 가지 훌륭한 블로그 게시물이 있습니다.첫 번째:
.2 및 MySQL을 "Hibernate 4.2" 와 "MySQL" 와 내 .
select t from Thing t where t.id in (?)
휴지 상태에서는 이러한 해석된 HQL 쿼리를 캐시합니다.으로는 휴지 상태
SessionFactoryImpl
있다QueryPlanCache
queryPlanCache
★★★★★★★★★★★★★★★★★」parameterMetadataCache
그러나 인클라우스 파라미터의 수가 많고 다른 경우에는 문제가 있음을 알 수 있습니다.이러한 캐시는 개별 쿼리마다 증가합니다.따라서 6000개의 파라미터를 가진 이 쿼리는 6001과 다릅니다.
기간 내 쿼리가 컬렉션의 매개 변수 수로 확장됩니다.메타데이터는 x10_, x11_ 등의 생성된 이름을 포함하여 쿼리의 각 파라미터에 대한 쿼리 플랜에 포함됩니다.
각각 평균 4000개의 매개변수를 갖는 4000개의 다른 기간 내 매개변수 카운트를 상상해 보십시오.각 파라미터의 쿼리 메타데이터는 가비지 수집이 불가능하기 때문에 메모리에 빠르게 축적되어 히프가 가득 차게 됩니다.
이는 쿼리 파라미터 카운트의 다른 모든 종류가 캐시되거나 JVM의 힙메모리가 부족하여 java.lang이 느려질 때까지 계속됩니다.Out Of Memory Error: Java 힙 공간입니다.
인클래스를 피하는 것은 하나의 옵션이며, 파라미터(또는 적어도 작은 크기)에 고정 수집 크기를 사용한다.
사이즈의 에 대해서는, 해 주세요.
hibernate.query.plan_cache_max_size
로는 " " " 입니다2048
(다양한 매개 변수를 가진 쿼리에 대해 너무 큽니다).
그리고 두 번째(첫 번째부터 참조):
휴지 상태에서는 내부적으로 HQL 문(문자열)을 매핑하는 캐시를 사용하여 계획을 쿼리합니다.캐시는 디폴트로는 2048개의 요소(설정 가능)로 제한된 경계 맵으로 구성됩니다.모든 HQL 쿼리는 이 캐시를 통해 로드됩니다.누락 시 엔트리가 자동으로 캐시에 추가됩니다.따라서 스레싱에 매우 취약합니다.이 시나리오에서는 새로운 엔트리를 재사용하지 않고 캐시에 지속적으로 저장하여 캐시가 성능 향상을 가져올 수 없습니다(캐시 관리 오버헤드도 일부 증가합니다).설상가상으로 이 상황을 우연히 감지하기는 어렵습니다. 캐시에 문제가 있음을 알기 위해서는 캐시를 명시적으로 프로파일링해야 합니다.나중에 어떻게 하면 좋을지 몇 마디 하겠습니다.
따라서 캐시 스레싱은 새로운 쿼리가 고속으로 생성됨에 따라 발생합니다.이는 다양한 문제로 인해 발생할 수 있습니다.가장 일반적인 두 가지는 휴지 상태의 버그입니다.이 버그는 파라미터를 파라미터로 전달하지 않고 JPQL 스테이트먼트에 렌더링하는 원인이 됩니다.또한 "in" - 구를 사용합니다.
휴지 상태에서는 불명확한 버그로 인해 파라미터가 올바르게 처리되지 않고 JPQL 쿼리에 렌더링되는 경우가 있습니다(예: HHH-6280).이러한 결함의 영향을 받는 쿼리가 있고 빠른 속도로 실행되는 경우 생성되는 각 JPQL 쿼리는 거의 고유하기 때문에 쿼리는 쿼리 계획 캐시를 스래시합니다(예를 들어 엔티티의 ID 포함).
두 번째 문제는 "in" 절(예를 들어 회사 ID 필드가 1, 2, 10, 18 중 하나인 모든 개인 엔티티를 나에게 제공)을 사용하여 최대 절전 모드로 쿼리를 처리하는 방식에 있습니다.각 에 대해 를 들어 in-clause는 in-clause의 경우 다음과 같습니다.
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개의 파라미터는 불가능한 값으로 설정됩니다(예: 외부 키에 사용되는 ID의 경우 -1).나는 이것이 추악한 해킹이라는 것에 동의하지만 그 일을 해낼 수 있을 것이다.그 결과 캐시에는 최대 6개의 고유 쿼리만 있으므로 스레싱이 줄어듭니다.그렇다면 문제가 있다는 것을 어떻게 알 수 있을까요?일부 추가 코드를 작성하고 캐시 내의 엔트리 수(예: JMX)로 메트릭을 노출하거나 로깅을 조정하거나 로그를 분석할 수 있습니다.응용 프로그램을 수정하지 않을 경우(또는 변경할 수 없는 경우) 힙을 덤프하고 힙에 대해 다음 OQL 쿼리를 실행할 수 있습니다(예를 들어 mat 사용).
SELECT l.query.toString() FROM INSTANCEOF org.hibernate.engine.query.spi.QueryPlanCache$HQLQueryPlanKey l
모든 현재 힙의 쿼리 계획 캐시에 있는 모든 쿼리를 출력합니다.앞서 언급한 문제 중 하나라도 영향을 받고 있는지 여부를 파악하는 것은 매우 쉽습니다.성능에 미치는 영향은 너무 많은 요소에 의존하기 때문에 말하기 어렵습니다.새로운 HQL 쿼리 플랜을 작성하는데 10~20밀리초의 오버헤드가 발생하는 매우 사소한 쿼리를 본 적이 있습니다.일반적으로 캐시가 어딘가에 있는 경우에는 그럴 만한 이유가 있을 것입니다.실수는 비용이 많이 들기 때문에 가능한 한 실수는 피해야 합니다.마지막으로 데이터베이스도 많은 양의 고유한 SQL 문을 처리해야 합니다. 따라서 데이터베이스에서는 SQL 문을 구문 분석하거나 각 SQL 문에 대해 서로 다른 실행 계획을 작성할 수 있습니다.
IN-queries in in in>>>>>>(> 10000)이치노할 수 요, 제 파라미터의 개수는 항상 .QueryCachePlan
너무 빨리 자라고 있어요
실행 계획 캐시를 지원하는 데이터베이스 시스템의 경우 가능한 IN 절 매개 변수의 수가 줄어들면 캐시에 도달할 가능성이 높아집니다.
다행히 버전 5.2.18 이상의 휴지 상태에서는 in-clause에 파라미터 패딩이 있는 솔루션이 있습니다.
휴지 상태에서는 바인드 파라미터를 2, 4, 8, 16, 32, 64의 거듭제곱으로 확장할 수 있습니다.이와 같이 5, 6, 또는7 bind 파라미터를 가진 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 이후에서는 다음을 사용하여 하이버네이트 설정 속성을 지정하여 리터럴을 JDBC가 준비한 기본 스테이트먼트에 바인드하는 방법을 변경할 수 있습니다.
hibernate.criteria.literal_handling_mode=BIND
Java 매뉴얼에 따르면 이 구성 속성에는 3가지 설정이 있습니다.
- AUTO(디폴트)
- BIND - bind 파라미터를 사용하여 jdbc 스테이트먼트를 캐싱할 가능성을 높입니다.
- INLINE - 매개 변수를 사용하지 않고 값을 인라인합니다(SQL 주입에 주의).
저도 비슷한 문제가 있었습니다.문제는 당신이 쿼리를 만들고 있고 Prepared Statement를 사용하지 않기 때문입니다.이 경우 파라미터가 다른 각 쿼리에 대해 실행계획을 작성하고 캐시합니다.준비된 스테이트먼트를 사용하는 경우는, 사용중의 메모리의 대폭적인 향상을 확인할 수 있습니다.
TL;DR: IN() 쿼리를 ANY()로 바꾸거나 제거합니다.
★★★★
쿼리에 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은 백그라운드에서 생성될 수 있습니다.때로는 예기치 않은 장소에서 생성될 수도 있고, 때로는 최적의 방법이 아닐 수도 있습니다.따라서 실제 쿼리를 표시하기 위해 쿼리 로그를 활성화하는 것이 좋습니다.
★★★★★★★
문제를 해결할 수 있는 (의사) 코드는 다음과 같습니다.
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는 둘 다 장단점이 있고 잘못 사용하면 심각한 성능 문제가 발생할 수 있습니다(아래 참조 예 참조).또한 보안 문제를 방지하기 위해 파라미터 바인딩을 사용해야 합니다.
고고: :
Postgres 퍼포먼스는 Any(ARRAY[])와 ANY(VALUES)의 1행 변경으로 100배 고속화
인덱스는 =any()와 함께 사용되지 않지만 IN과 ANY의 성능이 다릅니다.
플랜 Server에
이게 도움이 됐으면 좋겠다.여러분 같은 사람들을 돕기 위해 그것이 효과가 있었든 없었든 간에 꼭 피드백을 남겨주세요.감사합니다!
이 queryPlanCache에 큰 문제가 있어서 hibernate 캐시 모니터를 실행하여 queryPlanCache의 쿼리를 확인했습니다.QA 환경에서 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;
}
}
또한 힙 사용량이 증가하는 Query Plan Cache도 있었습니다.재작성된 IN-queries와 커스텀 타입을 사용하는 쿼리가 있습니다.휴지 상태 클래스의 커스텀이 확인되었습니다.유형이 equals 및 hashCode를 올바르게 구현하지 않았기 때문에 모든 쿼리 인스턴스에 대해 새 키가 생성되었습니다.이 문제는 Hibernate 5.3으로 해결되었습니다.https://hibernate.atlassian.net/browse/HHH-12463 를 참조해 주세요.userType에 equals/hashCode를 올바르게 구현해야 제대로 작동합니다.
쿼리 계획 캐시가 너무 빠르게 증가하고 gc가 캐시를 수집할 수 없게 되면서 오래된 gen 힙도 함께 증가한다는 이 문제에 직면했습니다.IN 절에 200,000개 이상의 ID를 가진 JPA 쿼리가 원인입니다.쿼리를 최적화하려면 한 테이블에서 ID를 가져오고 다른 테이블에서 ID를 전달하는 대신 join을 사용하여 쿼리를 선택합니다.
언급URL : https://stackoverflow.com/questions/31557076/spring-hibernate-query-plan-cache-memory-usage
'source' 카테고리의 다른 글
jQuery를 사용하지 않고 요소의 오프셋().top 값을 얻으려면 어떻게 해야 합니까? (0) | 2023.03.12 |
---|---|
Spring Boot에서 사용하는 데이터베이스 스키마 변경 (0) | 2023.03.12 |
스프링 보안: OAuth2 클레임을 역할과 매핑하여 리소스 서버 엔드포인트 보호 (0) | 2023.03.12 |
Oracle에서 모든 사용자 테이블/시퀀스 삭제 (0) | 2023.03.12 |
Swift 4의 JSONDecoder를 사용하면 키가 누락된 경우 옵션 속성 대신 기본값을 사용할 수 있습니까? (0) | 2023.03.12 |