리눅스 = 펭귄 = 흉포함
서로 다른 DB의 데이터를 엮어서 무언가를 만들어내야 할 때..
나는 보통 이렇게 썼다.
나만 이렇게 쓰는거야..?
(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.
(대충 번역)
모든 쿼리는 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
ㅋㅋㅋㅋㅋㅋ 아니 게시글이 너무 재밌어요 마무으리