pile·
백엔드·여기어때 (GC컴퍼니)여기어때 (GC컴퍼니)·

JVM heap은 멀쩡한데 왜 메모리가 터질까? — Docker 네이티브 메모리 삽질기 Part 1

JVM의 -Xmx 설정으로 heap을 8GB로 제한했음에도 Docker 컨테이너가 OOM으로 강제 종료되는 상황이 발생했다. 실제 프로세스 메모리(RSS)가 14GB에 달하는데 heap은 멀쩡했다는 게 핵심 수수께끼다. JVM 메모리가 heap과 native 메모리 두 구성요소로 이뤄져 있으며, cgroup은 전체 RSS를 기준으로 OOM을 판단한다는 사실이 이 미스터리의 열쇠다.

핵심 포인트
  • JVM 메모리는 GC가 관리하는 heap과 malloc으로 할당되는 native 메모리 두 영역으로 구성된다.
  • -Xmx8G는 heap만 제한하며, native 메모리 사용량은 무관하게 증가할 수 있다.
  • Docker cgroup은 JVM heap이 아닌 전체 프로세스 RSS를 기준으로 메모리 제한을 적용한다.
  • cgroup이 보는 RSS가 설정 한도를 초과하는 순간 Linux OOM Killer가 컨테이너를 강제 종료한다.
  • heap 모니터링 도구만으로는 이 문제를 발견할 수 없으며, native 메모리를 별도로 추적해야 한다.
상세 정리
  • 현상: -Xmx8G로 heap을 8GB 제한했음에도 Docker 컨테이너가 OOM으로 사망.
  • 측정 격차: JVM이 보고하는 heap 사용량은 정상 범위인데 실제 프로세스 RSS는 14GB — 약 5GB의 설명되지 않는 메모리 소비 존재.
  • OOM 발생 메커니즘: cgroup이 RSS 기준으로 메모리 한도를 감시. RSS가 설정 한도를 넘으면 Linux OOM Killer가 컨테이너 프로세스를 즉시 종료. "cgroup이 보는 RSS가 한도를 넘자 리눅스 OOM Killer가 컨테이너를 그냥 죽인 겁니다."
  • JVM 메모리 구성: heap(Eden/Survivor/Old/Metaspace 포함, -Xmx로 제한), native 메모리(JIT 컴파일, 클래스 로딩, JNI, 스레드 스택, 직접 버퍼, OS 라이브러리 등 malloc 사용).
  • 모니터링 맹점: JVM GC 모니터링 도구, Heap Dump, VisualVM 등은 heap 영역만 추적. Native 메모리는 표준 JVM 모니터링에서 보이지 않음.
  • Docker 환경 특수성: 베어메탈 환경에서는 OS가 스왑 등으로 메모리 압박을 흡수하지만 Docker cgroup은 즉각적인 하드 한도를 적용해 여유 없이 종료.
  • 문제의 심각성: heap만 관리한다고 생각했는데 native 메모리가 예측 불가능하게 성장해 컨테이너가 반복 재시작되는 운영 장애로 이어짐.
  • 다음 단계(Part 2 예고): NMT(Native Memory Tracking), jemalloc/jeprof, async-profiler 등 전문 도구로 native 메모리 5GB의 실제 원인을 추적하는 디버깅 과정.
왜 읽나Docker 컨테이너에서 JVM 애플리케이션이 원인 불명 OOM으로 종료될 때 heap 너머 native 메모리 영역을 의심해야 하는 이유와 문제 구조를 처음부터 이해할 수 있다.
여기어때 (GC컴퍼니)
여기어때 (GC컴퍼니) 블로그
원문은 여기서 이어서 읽을 수 있어요
원문 읽기
읽음 (0)

이 글과 비슷한

  1. 백엔드·여기어때 (GC컴퍼니)여기어때 (GC컴퍼니)·

    트랜잭션 스크립트에서 숙소 메타 + 가격 계산 모듈로 — 전시 아키텍처 개선기 (2/3)

    여기어때 전시개발팀이 숙소 상세(PDP) API를 해부한 결과, 코드상으로는 DB 호출 3번처럼 보이던 요청이 실제로는 MongoDB $lookup 체인으로 컬렉션을 19회 접근하는 구조였다. 이 트랜잭션 스크립트 방식의 핵심 문제는 "aggregation이 I/O를 가린다"는 점으로, 독립적인 쿼리 10개가 단일 파이프라인에 직렬화되어 병렬화 기회를 잃고, 가격 때문에 거의 안 바뀌는 이미지까지 매 요청마다 읽어야 하는 읽기 증폭이 발생했다. V3에서는 "조회 시점 조립"을 "쓰기 시점 사전 조립"으로 전환하고, 화면별로 복제되던 가격 계산 로직을 goodsprice 단일 모듈로 수렴했다. 4개 API(PLP/PDP/RDP/ILP)의 반복 마이그레이션은 Claude Code skill로 절차를 고정하고 쉐도잉 + 동일성 검증으로 안전망을 마련하는 방식으로 진행됐다.

    #architecture#migration#caching+2