Spring Boot 메모리 설정 기준: Heap·RSS·컨테이너 limit 함께 보기

반응형

Spring Boot 애플리케이션의 메모리는 -Xmx 하나로 설명되지 않는다. Java heap 밖에도 Metaspace, Code Cache, 스레드 stack, direct buffer와 JVM 네이티브 메모리가 있다. 컨테이너에서는 heap 상한과 프로세스 전체 메모리 사이에 실제 non-heap 사용량과 안전 여유를 남기는 것이 출발점이다.

Spring 전용 메모리 영역은 없다

Spring Bean도 JVM heap에 생성되는 Java 객체다. Spring Boot가 별도의 메모리 영역을 만드는 것은 아니다. 다만 프레임워크가 로드하는 class와 Bean, embedded server, connection pool, cache와 요청 객체가 여러 JVM 메모리 영역의 사용량에 영향을 준다.

영역 주로 들어가는 것 확인할 문제
Java Heap Bean, 요청·응답 객체, 컬렉션, cache GC 빈도, live set, 보존 누수
Metaspace 로드된 class metadata class 수, 동적 class loading
Code Cache JIT가 compile한 code compiled code와 상한
Thread Stack 호출 frame과 지역 정보 thread 수, -Xss
Direct·Native NIO buffer, JVM 구조, native library RSS와 heap의 차이

-Xmx1g는 Java heap의 최대 크기이지 프로세스 RSS의 최대 크기가 아니다. 컨테이너 memory limit도 1GiB인데 -Xmx1g를 주면 heap 밖의 영역이 사용할 자리가 부족해질 수 있다.

메모리는 시작·요청·유지 단계에서 다르게 늘어난다

애플리케이션 시작 시 Spring은 설정과 class를 읽고 ApplicationContext를 구성하며 Bean을 만든다. class metadata와 초기 객체가 늘고, database connection이나 cache warming을 시작 과정에 넣었다면 그 비용도 나타난다.

요청을 처리할 때는 요청·응답 객체, 역직렬화된 데이터와 비즈니스 객체가 heap에 생성된다. JPA를 사용한다면 한 persistence context가 관리하는 entity 수와 query 결과 크기, batch 범위가 live set을 키울 수 있다.

cache, connection pool과 thread pool은 자원을 재사용하기 위해 메모리를 보유한다. 요청마다 새 OS thread를 하나씩 만드는 구조로 단정하면 안 된다. 일반적인 embedded web server는 worker thread pool을 사용하고, 설정과 workload에 따라 thread 수와 stack 총량이 달라진다. pool이 있다는 사실도 자동으로 메모리가 최적화됐다는 뜻은 아니다. 상한과 queue, 만료 정책이 부하에 맞아야 한다.

Heap과 컨테이너 limit 사이에 여유를 둔다

고정 비율을 먼저 외우기보다 목표 부하에서 다음 값을 측정한다.

  1. 충분히 예열된 뒤의 heap live set
  2. 목표 traffic에서 heap peak와 GC pause
  3. thread 수와 stack 크기
  4. Metaspace, Code Cache와 direct buffer 사용량
  5. 프로세스 RSS와 cgroup memory peak
  6. 일시적인 burst와 장애 분석을 위한 안전 여유

Kubernetes의 memory request는 scheduler 배치 판단에 쓰이고, limit는 cgroup에서 반응적으로 강제된다. memory limit를 넘으면 kernel OOM 처리가 container process를 종료할 수 있다. 따라서 heap 상한은 container limit보다 작아야 하지만, 얼마를 남길지는 애플리케이션별 non-heap 측정으로 결정한다.

apiVersion: v1
kind: Pod
metadata:
  name: spring-sample
spec:
  containers:
    - name: app
      image: example.invalid/spring-app:1.0
      resources:
        requests:
          memory: "768Mi"
        limits:
          memory: "1Gi"

이 숫자는 구조를 보여 주기 위한 예시다. limit 1Gi → Xmx 768Mi처럼 모든 앱에 적용되는 공식은 아니다. thread, class, direct memory와 native library 사용량이 다르기 때문이다.

JDK는 Linux container의 CPU와 memory constraint를 감지해 ergonomics에 반영할 수 있고 -XX:MaxRAMPercentage 같은 option도 제공한다. 그래도 실제 시작 option은 deployment manifest, image entrypoint와 environment를 합쳐 확인해야 한다. 자동 계산에만 기대지 말고 최종 heap 상한과 RSS peak를 관찰한다.

실행 중인 JVM 설정부터 확인한다

추정하기 전에 실제 command line과 flag, heap 정보를 확인한다.

jcmd PID VM.command_line
jcmd PID VM.flags
jcmd PID GC.heap_info

heap 객체 분포가 필요하면 class histogram을 볼 수 있다.

jcmd PID GC.class_histogram

GC.class_histogram과 heap dump는 대상 프로세스에 부담을 줄 수 있다. 운영에서는 저장 공간과 정지 영향을 검토하고, 가능하면 같은 조건을 재현한 환경에서 먼저 실행한다.

RSS와 Heap의 차이는 NMT로 좁힌다

HotSpot Native Memory Tracking(NMT)은 JVM 내부 native memory를 범주별로 보여 준다. 기본값은 꺼져 있으므로 프로세스 시작 시 활성화해야 한다.

java -XX:NativeMemoryTracking=summary -jar app.jar

실행 중 baseline과 차이를 비교할 수 있다.

jcmd PID VM.native_memory baseline
jcmd PID VM.native_memory summary.diff scale=MB

NMT는 JVM 내부 할당을 추적하지만 모든 third-party native allocation을 보여 주지는 않는다. 추적 overhead도 있으므로 적용 범위는 환경에서 확인한다. NMT 합계와 RSS가 크게 다르면 memory-mapped file, native library, allocator 동작과 container 지표를 함께 본다.

GC는 메모리 누수를 예방하지 않는다

GC는 도달할 수 없는 객체를 회수한다. 애플리케이션의 map, cache, listener나 ThreadLocal이 객체를 계속 참조하면 GC 관점에서는 살아 있는 객체다. GC가 동작한다고 해서 보존 누수가 사라지지 않는다.

heap 사용량이 GC 뒤에도 계속 높아지고 live set이 증가한다면 histogram, heap dump와 allocation profile로 무엇이 객체를 붙잡고 있는지 확인한다. RSS만 늘고 heap은 안정적이라면 thread stack, direct buffer, native library와 mapped 영역으로 범위를 옮긴다.

Lazy initialization은 비용을 뒤로 미룬다

spring.main.lazy-initialization=true를 사용하면 필요한 시점에 Bean을 만들 수 있어 시작 시간이 줄어들 수 있다. 그러나 총 메모리 요구량이 사라지는 것은 아니다. 첫 요청으로 초기화 비용이 이동하고, 잘못된 Bean 설정도 시작 시점이 아니라 사용 시점에 드러날 수 있다.

Spring Boot 공식 문서도 lazy initialization을 켤 때 결국 모든 Bean을 수용할 heap이 있는지 확인해야 한다고 안내한다. 시작 속도 문제와 steady-state 메모리 문제를 같은 처방으로 다루지 않는다.

진단은 OOM의 주체부터 구분한다

  1. container가 OOMKilled됐는지 JVM이 OutOfMemoryError를 냈는지 구분한다.
  2. RSS, heap 사용량, GC log와 cgroup event를 같은 시간축으로 본다.
  3. heap live set이 증가하면 histogram·heap dump·allocation profile을 검토한다.
  4. RSS만 증가하면 thread, direct buffer와 NMT 범주를 확인한다.
  5. traffic이 멈춘 뒤에도 사용량이 회복되는지 비교한다.
  6. 설정 변경 뒤에는 같은 부하와 관찰 시간으로 다시 측정한다.

전체 분석 흐름은 실제 메모리 문제 분석 순서, 시스템 지표는 Linux free·page cache·slab, request와 limit의 의미는 Kubernetes resources와 QoS에서 이어서 볼 수 있다. CPU와 RAM의 기본 관계는 CPU와 메모리 차이에 정리했다.

핵심 정리

Spring Boot 메모리 설정은 -Xms-Xmx를 크게 주는 일이 아니다. heap과 non-heap, 프로세스 RSS, cgroup memory limit를 분리하고 목표 부하에서 peak와 회복 여부를 측정해야 한다. 특히 -Xmx와 container limit를 같은 값으로 두지 않고 JVM 밖 메모리가 사용할 여유를 남긴다.

참고 자료

반응형
KEEP READING
카테고리 전체 보기 →

댓글