Docker JVM 메모리 누수, 힙 말고 네이티브를 봐야 하는 이유

by DD
6시간 전
조회수 2

Docker 환경에서 JVM 힙 메모리(Heap Memory)는 정상이나, 전체 서버 메모리 사용량이 지속적으로 증가하는 현상 발생

cgroup 기반 메모리 측정 방식으로 인해 JVM이 관리하지 않는 네이티브 메모리 증가 시 OOM Killer에 의해 프로세스 강제 종료될 위험 존재

Native Memory Tracking(NMT)으로 JVM 관리 네이티브 메모리 추적 시 약 5GB의 미확인 메모리 사용량 발생, 원인 규명 필요

Docker cgroup과 JVM 메모리 측정 기준의 차이

Docker 환경에서는 cgroup의 RSS(Resident Set Size)를 기준으로 컨테이너 메모리 사용량을 측정하지만, JVM의 `-Xmx` 옵션은 Java 힙 영역(Heap Area)만 제한한다.

JVM은 힙 외에도 네이티브 메모리(Native Memory)를 사용하며, 이는 OS의 `malloc` 함수 등을 통해 할당됨

cgroup은 JVM이 직접 관리하지 않는 네이티브 라이브러리, 스레드 스택 등 프로세스 전체의 물리 메모리 점유량을 감시

결과적으로 힙 메모리가 충분해도 네이티브 메모리 누수가 발생하면 Linux OOM Killer에 의해 프로세스가 강제 종료될 수 있음

JVM 네이티브 메모리 구성 요소와 NMT의 한계

JVM은 힙 외에도 Metaspace, Thread Stack, Code Cache, GC 데이터 구조, Direct ByteBuffer 등 다양한 네이티브 메모리를 사용한다.

Thread Stack: 스레드 수 증가 시 네이티브 메모리 사용량이 비례하여 증가하는 주요 원인 중 하나임

NMT(Native Memory Tracking)는 JVM이 직접 할당하고 관리하는 메모리만 추적 가능

NMT의 한계: JVM 위에서 동작하는 네이티브 라이브러리가 `malloc`으로 직접 할당한 메모리는 추적하지 못함

따라서 NMT 결과와 실제 `top` 명령의 RSS 간에 메모리 사용량 불일치가 발생할 수 있음

메모리 누수 원인 규명을 위한 삽질 과정

본문에서는 힙 덤프 분석 등 기존 방식으로는 해결되지 않는 네이티브 메모리 누수 문제 해결을 위해 다양한 시도를 했다고 밝힌다.

로그 비활성화: 로그 파일 기록으로 인한 메모리 누수 가능성 확인 시도 (실패)

Docker 메모리 제한 명시: 컨테이너 메모리 제한 설정을 명시적으로 지정 (실패)

JVM 버전 변경: Amazon Corretto, Temurin, GraalVM 등 다양한 JVM 벤더 및 버전 변경 시도 (실패)

이러한 시도들은 문제의 원인이 특정 JVM 버전이나 설정의 문제가 아님을 시사함

NMT 대비 실제 RSS의 5GB 메모리 차이 분석

NMT(Native Memory Tracking) 결과 약 9GB의 네이티브 메모리가 사용된 것으로 나타났으나, `top` 명령어로 확인한 실제 물리 메모리 사용량(RSS)은 14GB에 달했다.

이는 NMT가 추적하지 못하는 영역에서 약 5GB의 메모리가 추가로 사용되고 있음을 의미함

해당 미확인 메모리 영역은 JVM 외부의 네이티브 라이브러리나 glibc 등 시스템 라이브러리에서 발생했을 가능성이 높음

다음 편에서는 jemalloc, jeprof, async-profiler 등의 도구를 활용하여 이 미스터리한 5GB의 출처를 추적할 예정임

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