컨테이너 환경의 메모리 구조

기존 서버/VM 환경에서는 OS가 물리 메모리를 먼저 차지하고, 나머지를 JVM이 사용했습니다.

[VM/서버] 물리 메모리 → OS 커널 + 시스템 프로세스 → JVM

ECS 컨테이너는 다릅니다. 호스트 OS 커널을 공유하기 때문에, Task에 할당된 메모리는 거의 전부 컨테이너 프로세스(JVM)가 사용합니다.

[ECS Task] Task 메모리 ≈ JVM 전용

“OS 몫을 남겨야 한다"는 상식은 컨테이너 환경에서는 불필요합니다. Fargate든 EC2든 Task 내부의 메모리 설정은 동일합니다.

JVM 메모리 구조

JVM 메모리는 크게 Heap과 Non-Heap으로 나뉩니다.

구분 설명 설정
Heap 애플리케이션 객체가 저장되는 영역 개발자가 -Xmx로 설정
Non-Heap Metaspace, Thread Stack, Code Cache, GC, Direct Buffer 등 JVM이 자동 할당

개발자가 직접 설정하는 건 Heap뿐입니다. Non-Heap은 JVM이 필요에 따라 알아서 할당합니다. 문제는 JVM이 컨테이너 메모리 한도를 신경 쓰지 않고 Non-Heap을 할당한다는 점입니다. Heap을 너무 크게 잡으면 Non-Heap이 남은 공간을 초과하고, ECS가 컨테이너를 OOM Kill(exit code 137) 할 수 있습니다.

Spring Boot의 Non-Heap 사용량

일반적인 Spring Boot 웹 애플리케이션의 Non-Heap 구성입니다.

영역 용도 일반적 사용량
Metaspace 클래스 메타데이터 200~500MB
Thread Stack 스레드당 1MB 기본 (Tomcat 기본 200스레드) 200~500MB
Code Cache JIT 컴파일 결과 100~250MB
GC 오버헤드 가비지 컬렉션 수백MB
Direct Buffer NIO 등 수십~수백MB

Spring 공식 블로그의 Petclinic 실측 기준, Heap -Xmx48M 적용 시 Non-Heap 사용량은 약 223MB였습니다. 다만 이건 가벼운 앱 기준이고, Tomcat 기본 스레드(200개)만으로 200MB를 소비하므로 실무에서는 더 높습니다.

앱 규모 Non-Heap 예상
간단한 앱 (스레드 적음) 150~300MB
일반적인 웹 서비스 300~600MB
무거운 앱 (많은 라이브러리, 스레드 200+) 600MB~1.5GB

메모리 설정 가이드라인

“75% 룰"의 한계

흔히 “Heap은 Task 메모리의 75%“라고 알려져 있습니다. 4~8GB 컨테이너에서는 적절하지만, 메모리가 커질수록 이 비율은 맞지 않습니다. Non-Heap은 메모리 크기에 비례해서 늘어나지 않기 때문입니다.

본질은 비율이 아니라 Non-Heap 여유분(절대값)입니다.

권장 설정

Task 메모리 Heap 여유 (Non-Heap) 비율
2GB 1~1.5GB 0.5~1GB 50~75%
4GB 3GB 1GB 75%
8GB 6~6.5GB 1.5~2GB ~80%
16GB 14.5GB 1.5GB ~90%
32GB 30GB 2GB ~94%

메모리가 작을수록 비율이 낮아지고, 클수록 높아집니다. 75% 룰은 4~8GB 구간에서 절대값 기준과 대략 맞아떨어지기 때문에 흔히 쓰이는 것이고, 대용량에서는 그대로 적용하면 메모리 낭비입니다.

설정 방법

MaxRAMPercentage 사용

Java 10+ (또는 Java 8u191+)에서는 JVM이 컨테이너 메모리 제한을 자동 인식합니다.

-XX:+UseContainerSupport
-XX:MaxRAMPercentage=75.0

간편하지만, 위에서 설명한 대로 메모리 크기별로 적정 비율이 다르므로 대용량에서는 비효율적입니다.

Xmx 직접 지정

-Xmx6g

Task 메모리에 맞춰 직접 계산해서 지정하는 방식입니다. 명확하고 예측 가능합니다.

Non-Heap 제한 (필요 시)

-XX:MaxMetaspaceSize=512m # Metaspace 상한 (기본값은 무제한)
-Xss512k # 스레드당 스택 사이즈 (기본 1MB)
-XX:MaxDirectMemorySize=256m # Direct Buffer 상한

실측으로 검증하기

권장 수치는 어디까지나 가이드라인입니다. 가장 확실한 방법은 실측입니다.

NativeMemoryTracking

JVM 옵션에 추가합니다.

-XX:NativeMemoryTracking=summary

실행 중인 프로세스에서 확인합니다.

jcmd <pid> VM.native_memory summary

실측한 Non-Heap 사용량에 20~30% 버퍼를 더하고, 나머지를 Heap으로 할당하면 됩니다.

CloudWatch Container Insights

ECS 서비스 단위로 메모리 사용량을 모니터링할 수 있습니다. MemoryUtilizedMemoryReserved 지표를 확인하세요.

정리

  • 컨테이너에서는 OS 메모리 고려가 불필요합니다 (Fargate, EC2 모두)
  • Heap 외에 Non-Heap 영역(Metaspace, Thread Stack, GC 등)이 수백MB~1GB 이상 사용합니다
  • “75% 룰"은 4~8GB 기준의 경험칙이고, 본질은 Non-Heap 절대값 확보입니다
  • 실측(NativeMemoryTracking) → 버퍼 추가 → 나머지를 Heap으로 할당하는 것이 가장 정확합니다

참고