JPA hibernate Query Plan Cache로 인한 OutOfMemory 해결

recordsbeat·2021년 3월 31일
4
post-thumbnail

리눅스 = 펭귄 = 흉포함

Spring Batch를 사용했다.

서로 다른 DB의 데이터를 엮어서 무언가를 만들어내야 할 때..
나는 보통 이렇게 썼다.

  1. chunk-oriented step 사용 간 ItemReader에서 db1의 데이터 select 결과를 ItemWriter로 송신.
  2. ItemReader에서 넘어온 items를 다시 db1의 다른 테이블에서 select
    (성능 문제로 join을 사용하지 않고 select-in을 사용하여 소스코드로 객체를 매핑 )
  3. 위의 결과를 db2 조회 조건으로 사용하여 select
  4. 최종적으로 db에 write

나만 이렇게 쓰는거야..?

(ItemReader에서 단 건이 아닌 list로 반환 시키도록 설정하여 processor에서 처리도 해보았다가. 이게 무슨 뻘 짓 인가 싶어서 그냥 맘편하게 ItemWriter에서 재조회 하기 이르렀다.)

실패했다.

유명하고도 유명한 OutOfMemory Error
(실제 운영환경에서 발생한 에러로 spring batch meta 테이블의 내용으로 대체)

무수한 실패 내역이 !!

대용량이라서 메모리 이슈가 발생하는 줄 알았다.
그래서 batch insert update 등 별에 별 짓을 다해봤다..(물론 이도 조금은 도움이 됐을듯)

hibernate batch insert update 트랜잭션 설정
Spring Data에서 Batch Insert 최적화
..여담으로 나는 그냥 hibernate 설정 값을 주는 걸로 무마(?) 했다.

현상을 살펴봤을 때
meta table의 step_execution 테이블 commit 수를 보면서 모니터링을 해보았을 때
초반에는 준수한 속도로 가다가 일정 commit 을 넘어설 때 즘부터 속도가 0에 수렴. 그 이후 OOM 에러 발생.

pinpoint 같은 apm 툴로 진작에 메모리 버퍼를 확인했으면 좋았을 것을 당시에는 거기까지 생각이 미치지 못했다..

결론부터 말씀드리자면..

JPA는 사용자 편의를 제공하는 Hibernate의 인터페이스 계층
즉, Hibernate는 JPA의 구현체

출처 - https://suhwan.dev/2019/02/24/jpa-vs-hibernate-vs-spring-data-jpa/

문제는 이 hibernate 내부에서 QueryPlan Cache 라는 기능이 존재했다.

Query Plan Cache
Every JPQL query or Criteria query is parsed into an Abstract Syntax Tree (AST) prior to execution so that Hibernate can generate the SQL statement. Since query compilation takes time, Hibernate provides a QueryPlanCache for better performance.

For native queries, Hibernate extracts information about the named parameters and query return type and stores it in the ParameterMetadata.

For every execution, Hibernate first checks the plan cache, and only if there's no plan available, it generates a new plan and stores the execution plan in the cache for future reference.

Hibernate Query Plan Cache

(대충 번역)
모든 쿼리는 AST(추상 구문 트리)로 변환되어 hibernate가 이를 통해 SQL을 생성한다.
이 과정에서 더 나은 성능을 위해 QueryPlanCache 라는 기능을 제공하는데, 이는 파라미터의 정보를 추출하여 ParameterMetadata 에 저장한다.
모든 쿼리 실행 간 hibernate는 plan cache를 체크하고 맞는 plan이 없을 경우 새로운 plan을 생성한다.


출처 - https://vladmihalcea.com/improve-statement-caching-efficiency-in-clause-parameter-padding/

근데 이 QueryPlanCache와 ParameterMetadata의 디폴트 용량이 어떤가 ?

어플리케이션 메모리 사이즈가 1024mb인데
hibernate - query plan cache만 해도 default가 2048이다.

배보다 배꼽이 더 큰 상황

원인을 살펴봅시다

hibernate 가 각 쿼리마다 query plan cache를 사용하는 것은 알겠다.
근데 왜 이 query plan cache가 말썽이었는가?

맨 처음에 말했던 로직상 간과한 부분이 있었다.

그렇다.

hibernate는 각 쿼리를 실행하기 전 QueryPlanCache를 체크하고 가용한plan이 없는 경우 새로운 plan cache를 생성한다고 했다.

위 그림을 살펴보면 New selected List<item>의 size가 일정하지 않아
select-in 구문의 파라미터의 개수가 들쭉날쭉하게 되었다.
따라서 고정되지 않은 select-in 에 사용되는 List의 크기마다 QueryPlanCache가 생성되었고, 이것이 메모리에 쌓여 OutOfMemory를 터트린 것.

해결방안

간단하다.

nhn의 정지범 개발자님께서 명쾌한 해결책을 올려주셨다.

https://meetup.toast.com/posts/211
1) in_clause_parameter_padding
2) padding programmatically
3) execution plan cache 사이즈 조정

위에서 1번과 3번을 택하여 설정하였다.
1번 (in_clause_parameter_padding)은
select - in 에 들어갈 파라미터의 개수를 2의 제곱으로 고정 시키는 것.
이렇게 하면 n개의


    protected Map<String, Object> jpaProperties() {
        Map<String, Object> props = new HashMap<>();
        props.put("hibernate.session_factory.interceptor", interceptor);

        // execution query plan optimization
        props.put("hibernate.query.in_clause_parameter_padding", true);
        props.put("hibernate.query.plan_cache_max_size", 256);
        props.put("hibernate.query.plan_parameter_metadata_max_size", 16
        
        return props;
    }

plan_cache_max_size와 plan_parameter_metadata_max_size를 줄인 기준은

...By default, the maximum number of entries in the plan cache is 2048. An HQLQueryPlan object occupies approximately 3MB. ~3 * 2048 = ~6GB, and I the heap size is limited to 4GB
Finally! That must be the cause!
The solution is simple: decreasing the query plan cache size by setting the following properties:

spring.jpa.properties.hibernate.query.plan_cache_max_size: controls the maximum number of entries in the plan cache (defaults to 2048)

spring.jpa.properties.hibernate.query.plan_parameter_metadata_max_size: manages the number of ParameterMetadata instances in the cache (defaults to 128)

I set them to 1024 and 64 respectively.

https://medium.com/quick-code/what-a-recurring-outofmemory-error-taught-me-11f2061063a1

윗 글에서 4GB 일 경우
plan_cache_max_size = 1024
plan_parameter_metadata_max_size 64
로 세팅하였길래 나도 비율에 맞추어 세팅하였다.

그리고 위 해결방법으로 OutOfMemory 에러는 주것다고 한다.

마무으리

hibernate에서 파라미터의 크기가 고정되지 않은 select-in 절을 사용하려면 매우 조심해야 한다.

사용환경
Spring Batch
JPA - QueryDsl
QuerydslItemReader - 이동욱님
(https://github.com/jojoldu/spring-batch-querydsl)
JVM memory - max 1024

참고링크

https://vladmihalcea.com/improve-statement-caching-efficiency-in-clause-parameter-padding/

https://medium.com/quick-code/what-a-recurring-outofmemory-error-taught-me-11f2061063a1

https://meetup.toast.com/posts/211

https://www.baeldung.com/hibernate-query-plan-cache

profile
Beyond the same routine

3개의 댓글

comment-user-thumbnail
2021년 6월 8일

ㅋㅋㅋㅋㅋㅋ 아니 게시글이 너무 재밌어요 마무으리

1개의 답글
comment-user-thumbnail
1일 전

ㅋㅋㅋㅋㅋ 좋았습니다~ 저도 덕분에 해! 결!ㅎㅎ

답글 달기