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

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

Part 1에서 확인한 JVM heap과 Docker RSS의 5GB 격차를 추적하는 디버깅 여정이다. NMT(Native Memory Tracking), jemalloc/jeprof, async-profiler 세 가지 도구를 계층적으로 투입해 범인을 좁혀간다. 최종 원인은 zlib Inflater 클래스로, 제대로 닫히지 않은 스트림이 JAR 파일 압축 해제 과정에서 native malloc 할당의 80% 이상을 차지하고 있었다.

핵심 포인트
  • NMT로 native 메모리 카테고리별 사용량을 확인해 문제 영역을 좁히고, jemalloc/jeprof로 실제 malloc 할당 호출 스택을 추적했다.
  • async-profiler로 Java 코드 경로와 native 할당을 연결해 어떤 Java 코드가 문제의 native 할당을 유발하는지 특정했다.
  • 근본 원인: zlib의 Inflater 클래스. JAR 파일 압축 해제 시 try-with-resources 없이 스트림을 방치하면 native 메모리가 해제되지 않고 누적된다.
  • 수정 후 RSS가 1,328MB에서 516MB로 감소 — native 메모리 누수 해결로 약 60% RSS 절감.
  • glibc arena 설정(MALLOC_ARENA_MAX) 조정 및 jemalloc(dirty_decay_ms 설정)으로 컨테이너 환경에 맞게 메모리 반환 동작을 튜닝할 수 있다.
상세 정리
  • 3단계 조사 방법론: 1단계 NMT(전체 native 메모리 카테고리별 스냅샷) → 2단계 jemalloc/jeprof(malloc 할당 프로파일) → 3단계 async-profiler(Java 호출 스택과 native 할당 상관관계).
  • NMT 활용: -XX:NativeMemoryTracking=detail JVM 옵션으로 활성화. 메모리를 Java Heap, Class, Thread, Code, Compiler, GC, Internal, Other 카테고리로 분류해 어떤 영역이 과하게 사용되는지 식별.
  • jemalloc/jeprof 활용: glibc malloc 대신 jemalloc을 malloc 프로파일러로 사용해 호출 스택별 native 메모리 할당량을 집계. native malloc 할당의 80%+ 가 zlib Inflater에서 발생함을 확인.
  • async-profiler 활용: Java 코드와 native 할당 간 연결고리를 추적. 특정 JAR 로딩 코드 경로에서 Inflater 생성 후 close() 미호출 패턴 발견.
  • zlib Inflater 문제 원인: Java에서 JAR/ZIP 파일을 다룰 때 내부적으로 zlib native 라이브러리 사용. Inflater 객체를 GC에만 맡기면 finalizer 지연으로 native 메모리 해제가 늦어지거나 누적됨.
  • 수정 방법 1: try-with-resources 또는 명시적 close()로 Inflater/InflaterInputStream 자원 즉시 해제.
  • 수정 결과: RSS가 1,328MB → 516MB로 약 60% 감소.
  • 수정 방법 2: MALLOC_ARENA_MAX 환경변수로 glibc의 arena 수를 제한해 멀티스레드 환경에서 메모리 단편화로 인한 과도한 메모리 보유를 줄임.
  • 수정 방법 3: jemalloc을 LD_PRELOAD로 대체 적용 시 dirty_decay_ms 설정으로 해제된 메모리를 OS에 반환하는 속도를 제어해 컨테이너 메모리 사용량 안정화.
왜 읽나Docker에서 JVM native 메모리 누수를 NMT·jemalloc·async-profiler 3종 도구로 실제로 추적하고 해결하는 전 과정을 경험한 팀의 생생한 디버깅 로그로 동일한 문제에 처했을 때 재현 가능한 조사 방법을 확인할 수 있다.
여기어때 (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