준호씨의 블로그

Gradle test task에서 OOM이 발생한다면? 해결책과 삽질기록 본문

개발이야기

Gradle test task에서 OOM이 발생한다면? 해결책과 삽질기록

준호씨 2023. 1. 7. 00:39
반응형

어느 날 PR요청을 날리고 Jenkins에서 gradle test가 완료되길 기다리고 있었는데요. 이상하게도 끝날 생각을 안 합니다. Jenkins에 들어가서 로그를 보니 OOM (OutOfMemory) 오류가 발생하고 있었습니다. (참고로 로컬 개발환경인 맥북에서는 잘 되었습니다.)

 

Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Xmemcached-Reactor-66"
...

하나만 있는 것은 아니고 시간이 지나면 하나씩 계속 나타났습니다.

차라리 그냥 끝나버리면 좋겠다 싶었지만 희망 고문 하듯 계속 끝나지 않았습니다. 밤에 돌려놓고 아침에 일어나 보니 8시간 넘게 계속 돌고 있더군요.

 

Jenkins 출력에서는 보기가 어려운데 커맨드 라인으로 직접 테스트를 돌려 보면 특정 테스트 클래스 파일들이 종료되지 않은 상태로 남아 있었습니다. 그리고 그 테스트들은 모두 @SpringBootTest 애노테이션이 붙어 있는 테스트 들이었습니다.

 

결론 먼저

바쁘신 분들을 위해 결론부터 적어보면 gradle 설정에서 test task의 maxHeapSize 설정을 높여주면 됩니다. GradleWorker에서 사용하는 메모리 사용량을 늘려주는 설정입니다.

 

build.gradle.kts처럼 kotlin방식을 사용하고 있어서 다음과 같이 설정하였습니다.

...
    tasks.withType<Test> {
        maxHeapSize = "1024m"
...

 

만약 그냥 build.gradle 파일을 사용한다면

...
    test {
        maxHeapSize = "1024m"
...

처럼 설정하면 됩니다.

 

사실 멀티모듈 프로젝트라서 다음과 같은 구조였습니다.

subprojects {
...
    tasks.withType<Test> {
        maxHeapSize = "1024m"
...

 

참고로 maxHeapSize설정을 따로 하지 않는 경우 기본 값은 512MB입니다. Gradle 5 미만 버전에서는 물리 메모리의 1/4이었다고 합니다.

All workers, including compilers and test executors, now start with 512MB of heap. The previous default was 1/4th of physical memory. Large projects may have to increase this setting on the relevant tasks, e.g. JavaCompile or Test.

참고: https://docs.gradle.org/current/userguide/upgrading_version_4.html#rel5.0:default_memory_settings

 

Upgrading your build from Gradle 4.x to 5.0

This chapter provides the information you need to migrate your older Gradle 4.x builds to Gradle 5.0. In most cases, you will need to apply the changes from all versions that come after the one you’re upgrading from. For example, if you’re upgrading fr

docs.gradle.org

 

 

삽질 1. gradle.properties 파일의 org.gradle.jvmargs=-Xmx... 설정 변경

gradle OOM 관련 글들을 검색해보면 대부분 gradle.properties파일에서 org.gradle.jvmargs=-Xmx 설정을 해보라는 글이 많았습니다. 참고로 앞서 공유한 글에 나오지만 Gradle daemon의 기본 메모리 사용량은 512MB이고 큰 프로젝트에서는 늘려주라고 합니다.

The Gradle daemon now starts with 512MB of heap instead of 1GB. Large projects may have to increase this setting using the org.gradle.jvmargs property.

참고: https://docs.gradle.org/current/userguide/upgrading_version_4.html#rel5.0:default_memory_settings

 

그런데 이미 프로젝트에는 org.gradle.jvmargs 설정이 -Xmx4g로 넉넉하게 잡혀 있었습니다.

ps명령어로 GradleDaemon의 상태를 보면 org.gradle.jvmargs설정들이 적용되어 있는 것을 확인할 수 있습니다.

junho85          51868   0.3  1.7 416184304 568992   ??  S    11:31AM   0:52.11 /Users/junho85/.sdkman/candidates/java/11.0.17-tem/bin/java -XX:+HeapDumpOnOutOfMemoryError --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.prefs/java.util.prefs=ALL-UNNAMED -Xmx4g -Dfile.encoding=UTF-8 -Duser.country=KR -Duser.language=en -Duser.variant -cp /Users/junho85/.gradle/wrapper/dists/gradle-6.8.3-bin/7ykxq50lst7lb7wx1nijpicxn/gradle-6.8.3/lib/gradle-launcher-6.8.3.jar org.gradle.launcher.daemon.bootstrap.GradleDaemon 6.8.3

Jenkins 서버의 메모리는 넉넉했기에 6g, 8g, 10g, 12g로 올려 보았습니다. 하지만 결과는 실패였습니다. 돌이켜 보면 gradle test task는 GradleWorker가 돌리고 있었는데 엄한 GradleDaemon의 설정만 바꾸고 있었던 것입니다.

 

삽질 2. Gradle과 JDK 버전업

현재 사용하는 Gradle이나 JDK에서 메모리 누수 이슈가 있을 수 있지 않을까? 버전업 하면서 메모리 관리를 좀 더 잘하게 되지 않았을까?라는 생각으로 버전업을 시도해 보았습니다.

 

Jenkinsfile 설정은 다음과 같은 구조로 되어 있었는데요.

pipeline {
    agent {
        docker {
            image 'gradle:7.4.2-jdk11'
            customWorkspace "/var/jenkins_home/workspace/${JOB_NAME}_${BUILD_NUMBER}"
        }
    }
...

gradle image를 7.5.1, 7.6.0 등의 버전으로 올려 보았습니다.

하지만 막연한 기대와는 달리 문제는 해결되지 않았습니다.

 

 

삽질 3. spring.test.context.cache.maxSize=1

해당 이슈로 삽질하고 있을 때 동료들이 참고해보라고 알려준 글들 중 다음글이 있었습니다.

https://stackoverflow.com/questions/54117965/spring-integration-test-consumes-a-lot-of-memory-uses-a-high-number-of-duplicat

 

Spring integration test consumes a lot of memory, uses a high number of duplicate threads in GradleWorkerMain

I have a somewhat complicated Spring Boot app, with a large number of tests. When running the tests, it seems to be accumulating a lot of threads, one of which there is multiple instances of and is

stackoverflow.com

spring.properties 파일을 만들고 spring.text.context.cache.maxSize=1 설정을 추가해 주는 방법입니다. 참고로 maxSize설정의 기본값은 32입니다.

spring 문서에 다음과 같은 설명이 있었습니다.

The size of the context cache is bounded with a default maximum size of 32. Whenever the maximum size is reached, a least recently used (LRU) eviction policy is used to evict and close stale contexts. You can configure the maximum size from the command line or a build script by setting a JVM system property named spring.test.context.cache.maxSize. As an alternative, you can set the same property via the SpringProperties mechanism.

maxSize를 줄여서 사용하지 않는 자원을 반환하여 여유 메모리를 늘릴 수 있겠다 싶었습니다.

하지만 결국 이 설정을 적용했지만 성공하지 못했습니다. (신기한 건 이 설정을 적용하고 로컬환경에서 OutOfMemory이슈가 재현되었습니다.) (참고로 공유받은 글의 Bonus내용에 maxHeapSize 설정 내용이 있었었네요;;)

 

기타 삽질

jenkins 재시작을 했었지만 해결되지 않았습니다.

gradle이 docker로 실행되다 보니 docker에서 메모리 설정을 해보았지만 해결되지 않았습니다. 설정을 하면 오히려 메모리 사용량을 제한하는 설정 같았습니다.

JMX 모니터링이나 힙덤프를 떠서 확인해 볼까도 생각했었지만 그전에 해결되어서 다행이었습니다. 사실 이걸 확인한다고 한 들 당장 뭔가 해결할 수는 없었을 거 같습니다.

 

해결. test task maxHeapSize 조정

이번 이슈 말고 로컬환경에서 ktlint 관련 task에서 OOM이 발생했던 적이 있었는데요. 당시 다른 동료가 알려준 글에서 maxHeapSize조정 이야기가 있었습니다. 하지만 당시에는 로컬환경에 GradleDaemon프로세스가 많이 떠 있어서 메모리가 부족했던 상황이라 GradleDaemon 프로세스들을 죽여서 메모리를 확보해서 해결했었기에 test task maxHeapSize를 조정해보지는 못했습니다.

 

참고로 GradleDaemon 프로세스들을 죽였던 명령어는 다음과 같습니다.

$ pkill -f '.*GradleDaemon'

이번에 삽질을 하다가 문득 그때 글이 생각나서 다시 읽어 보았고 해결할 수 있었습니다.

 

gradle test task의 maxHeapSize를 조정하고 task를 돌렸을 때 GradleWorkerMain의 Xmx가 1024m으로 바뀐 것을 볼 수 있었습니다.

junho85          65613  96.4  4.3 413018864 1441520   ??  R     1:15PM   1:28.65 /Users/junho85/.sdkman/candidates/java/11.0.17-tem/bin/java -Djunit.jupiter.execution.parallel.config.strategy=dynamic -Djunit.jupiter.execution.parallel.enabled=true -Djunit.jupiter.execution.parallel.mode.classes.default=concurrent -Djunit.jupiter.execution.parallel.mode.default=same_thread -Dorg.gradle.native=false @/private/var/folders/_s/lvsk41rn0y1gybby303zq94w0000gn/T/gradle-worker-classpath300053084464251738txt -Xmx1024m -Dfile.encoding=UTF-8 -Duser.country=KR -Duser.language=en -Duser.variant -ea worker.org.gradle.process.internal.worker.GradleWorkerMain 'Gradle Test Executor 3'

 

반응형
Comments