Java Flight Recorder를 활용한 JVM 자원 분석
# Grafana 의 대안적 선택
ISS 2.0 프로젝트를 진행하며 한 가지 문제 상황을 겪은 적이 있습니다.
네트워크가 좋지 않은 특정 호선 내 DomainGateway라는 NATS Message(s)를 처리하는 파드가 지속적으로 OOM이 나는 것이었습니다. 애플리케이션이 구동되는 첫 시점에 쌓여있는 Message(s)를 처리하기 위해 순간적으로 부하가 높아진다고 하나, 해당 부하는 낮아지지 않고 지속적으로 올라가 애플리케이션 구동에 문제를 일으켰습니다.
이는 운영상 크리티컬한 이슈였으나, 당시 자원 모니터링을 위한 Grafana와 같은 툴이 구축되어 있지 않아 방법을 찾던 중 Java Flight Recorder(JFR)와 Java Mission Control을 조합한 모니터링 기법을 발견하게 되었습니다.
JFR은 jstack, Thread Dump와 같은 원시적인 모니터링 기법과 Grafana와 같은 최신 모니터링 기법의 중간 지점에 있는 기술입니다. Grafana가 설치되어 있지 않은 프로젝트 초기 단계에서 자원 분석을 통한 성능 고도화 및 이슈 대응 작업이 필요하다면 JFR과 JMC의 조합이 가장 유효할 것입니다.
# JFR이란?
JFR은 JVM에 내장된 경량 프로파일링 프레임워크입니다.
HotSpot JVM 내부에서 발생하는 다양한 이벤트(GC, 쓰레드, I/O, 락 등)를 오버헤드 최소화(일반적으로 1% 미만)로 지속 기록합니다.
이벤트는 JVM 내부 링 버퍼에 바이너리로 저장되어 디스크 부담이 적고, Safe Point 외부에서도 샘플링이 가능해 정확도가 높습니다.
JDK 11부터는 OpenJDK에 완전히 통합되어 상용 라이선스 없이 사용 가능합니다.


# 기존 모니터링 기법과의 차별성
JFR과 JMC의 조합은 고전적인 모니터링 기법이던 jstack과 Thread Dump와 비교하여 다음과 같은 성능상 이점을 가지고 있습니다.
# JFR 활성화 방법
JFR은 JVM 기동 시에 옵션을 추가하거나, 실행 중인 프로세스에 동적으로 붙여서 기록을 시작할 수 있습니다.

# 뷰(View)
JMC는 목적별로 특화된 다양한 뷰를 제공합니다. 각 뷰는 고유한 화면 구성과 데이터를 가지므로, 상황에 맞는 뷰를 선택해 활용하는 것이 모니터링 효율을 높이는 핵심입니다.
| 뷰 이름 | 위치 | 주요 용도 |
|---|---|---|
| Threads | Java Application > Threads | 쓰레드별 상태 타임라인, 전체 쓰레드 목록 |
| Lock Instances | Java Application > Lock Instances | 락 경합이 발생한 모니터 객체와 대기 시간 |
| Thread Dumps | Java Application > Thread Dumps | JFR 기록 중 자동 수집된 스레드 덤프 |
| Method Profiling | Java Application > Method Profiling | CPU를 많이 사용하는 메서드 Hot Path 분석 |
| Event Browser | Advanced > Event Browser | JFR 이벤트 전체 원시 데이터 조회 |
# Threads 뷰를 활용한 메모리 누수 발견
메모리 사용량이 지속적으로 증가하던 문제 상황에서 JFR의 Threads 뷰를 확인해보니, NATS와의 Connection을 담당하는 NATS Listener Thread가 동시에 여러 개 존재한다는 사실을 확인했습니다.
원래라면 단일로만 존재해야 할 Connection이 왜 중첩되어 생성되는지 확인하기 위해 NATS Subscription 로직을 살펴보던 중, Disconnection 발생 시 기존 Connection을 Release하지 않고 새로운 Connection을 생성하고 있음을 발견하게 되었습니다.
더 문제가 되었던 것은 Connection의 정책이 무한 재연결로 되어 있어, Release되지 않은 Connection이 계속 재연결을 시도해 Garbage Collector가 메모리 회수조차 하지 못한다는 점이었습니다.
결국 네트워크가 좋지 않아 Disconnection이 자주 발생하던 상황에서 Connection 객체는 계속 늘어나 메모리를 잡아먹게 되었고, 각 Connection 쓰레드가 TCP 연결을 시도하며 소켓/파일 디스크립터 고갈 문제까지 발생하게 되었습니다.
이러한 디스크립터 고갈 문제는 네트워크 상황을 지속적으로 악화시켰습니다.
다행히 이러한 사실을 JFR 분석 과정을 통해 발견할 수 있었고, Connection을 Release하는 로직을 추가해 Connection이 폭증하는 상황을 방지할 수 있었습니다.

#마치며
물론 JFR이 모든 상황의 해답은 아닙니다. 서비스가 성숙해질수록 실시간 알림과 장기 트렌드 분석이 필요해지고, 그 시점에는 Micrometer와 Grafana로 모니터링 체계를 고도화하는 것이 맞습니다.
JFR은 그 이전 단계, 혹은 Grafana로 포착하기 어려운 JVM 내부의 깊은 문제를 파고들어야 할 때 꺼내는 도구로 생각하면 좋을 것 같습니다.
jungboke