컨테이너 메모리를 2Gi 줬는데 자바 힙이 512M까지만 늘어납니다
지난번에 내부 ALB 504로 질문 올렸던 그 서비스입니다. 이번엔 다른 증상인데, EKS에 올린 스프링부트 앱(Spring Boot 3.2, Temurin JDK 17, 리눅스 컨테이너)에서 특정 배치가 돌 때 java.lang.OutOfMemoryError: Java heap space가 납니다.
이상한 건 파드 메모리 limit은 2Gi로 넉넉히 줬고, 터지는 순간에도 파드 실측 사용량(container_memory_working_set_bytes)은 700MB 안팎밖에 안 됩니다. 즉 컨테이너 한도는 아직 한참 남았는데 힙만 먼저 터집니다. Dockerfile에서 -Xmx를 따로 안 주고 JAVA_OPTS도 비워둔 상태로 java -jar app.jar로 띄웁니다.
resources:
limits:
memory: 2Gi
requests:
memory: 1Gi컨테이너 안에서 실제 힙 상한을 찍어보니 512MB 근처로 잡혀 있습니다.
$ jcmd 1 VM.flags | tr ' ' '\n' | grep -i -E 'MaxHeapSize|MaxRAMPercentage|UseContainerSupport'
-XX:MaxHeapSize=536870912
-XX:MaxRAMPercentage=25.000000
-XX:+UseContainerSupport
$ cat /sys/fs/cgroup/memory.max
2147483648cgroup 한도는 2Gi로 제대로 읽히는데 왜 힙은 그 1/4인 512M에서 멈추는지 이해가 안 됩니다. limit을 4Gi로 올려봐도 힙 상한이 비례해서 1G쯤으로만 바뀔 뿐 여전히 전체를 못 씁니다. 컨테이너 메모리는 남아도는데 힙만 작게 잡히는 이 동작이 정상인지, 맞다면 어디서 결정되는 값인지 궁금합니다.
결론부터 말하면 버그가 아니라 JVM의 컨테이너 기본 동작입니다. 힙 상한은 컨테이너 메모리의 25%로 잡히는 게 기본값이고, 2Gi의 25%가 정확히 512MB라서 지금 보시는 536870912(=512MB)가 나온 겁니다. limit을 4Gi로 올리면 25%인 1G로 바뀌는 것도 같은 공식이 적용된 결과라 정상입니다.
동작을 정리하면 이렇습니다.
- UseContainerSupport(JDK 10부터 도입, 8u191 백포트, 기본 on)가 cgroup의 메모리 한도를 읽습니다. 그래서
memory.max의 2Gi를 제대로 인식하고 있는 거고, 이 값이 힙 계산의 기준(총 가용 메모리)이 됩니다. - 그 기준값에 MaxRAMPercentage를 곱해 최대 힙을 정합니다. 이 값의 기본이 25.0 입니다. 즉 MaxHeapSize = 컨테이너 메모리 x 25% 입니다.
그래서 실측 사용량(working set 700MB)이 컨테이너 한도 2Gi에 한참 못 미쳐도, 힙 자체의 천장이 512MB로 박혀 있으니 배치가 그 512MB를 다 쓰면 컨테이너가 OOMKilled 되기 전에 java.lang.OutOfMemoryError: Java heap space부터 납니다. 지금 증상과 정확히 일치합니다.
해결은 힙 상한을 명시해주면 됩니다. 두 가지 방식 중 하나를 쓰시면 됩니다.
1) 비율로 주기(파드 메모리를 바꿔도 비례해서 따라오므로 K8s에서 권장):
env:
- name: JAVA_OPTS
value: "-XX:MaxRAMPercentage=75.0"
# 기동: java $JAVA_OPTS -jar app.jar2) 절대값으로 고정:
java -Xmx1500m -jar app.jar2Gi 한도 기준으로 75%면 약 1.5G가 힙에 할당됩니다. 비율은 100%로 두면 안 됩니다. 메타스페이스, 스레드 스택, 코드 캐시, GC 구조, 다이렉트 버퍼 같은 비(非)힙 영역이 컨테이너 메모리를 추가로 먹기 때문에, 보통 70~80% 정도를 힙에 주고 나머지를 비힙과 여유분으로 남깁니다. 배치 특성상 대용량 객체를 많이 들고 있으면 애초에 limit 자체를 더 키우는 것도 같이 검토하시는 게 좋습니다.
참고로 MinRAMPercentage(기본 50%)는 이름과 달리 "작은" 컨테이너(대략 256MB 미만)에서만 최대 힙 계산에 쓰이는 값이라, 지금 2Gi 환경에서는 MaxRAMPercentage만 보시면 됩니다.
적용 후 컨테이너 안에서 다시 확인하시면 상한이 올라간 걸 바로 볼 수 있습니다.
$ jcmd 1 VM.flags | tr ' ' '\n' | grep -i -E 'MaxHeapSize|MaxRAMPercentage'
$ java -XX:MaxRAMPercentage=75.0 -XX:+PrintFlagsFinal -version | grep -i maxheapsize정리하면, 컨테이너 메모리가 남는데 힙만 작게 잡히는 건 MaxRAMPercentage 25% 기본값 때문이고, MaxRAMPercentage나 -Xmx로 힙 상한을 명시해주면 해결됩니다. 아래 문서에서 UseContainerSupport와 RAMPercentage 계산 방식을 확인하실 수 있습니다.
🔗 https://docs.oracle.com/en/java/javase/17/docs/specs/man/java.html
🔗 https://developers.redhat.com/articles/2022/04/19/java-17-whats-new-openjdks-container-awareness
로그인 후 답글을 남길 수 있습니다.