8. GC 메트릭 모니터링: JVM 힙과 G1GC STW 추적

지금까지 우리는 인프라(Prometheus)와 시각화(Grafana)를 배웠습니다. 이번 장에서는 시선을 한 단계 위로 옮겨, 우리가 만든 Java 애플리케이션 그 자체를 모니터링합니다. 아무리 Cassandra, Kafka, Redis가 튼튼해도, 결국 이것들을 호출하는 Java 프로세스가 무너지면 모든 게 멈춥니다. 트래픽 폭증 시 JVM 힙이 포화되고 GC가 오래 걸려 수백 ms의 응답 지연(STW)이 발생하는 현상을 Prometheus 메트릭으로 사전에 감지하고 대응하는 방법을 배웁니다.

1. JVM 메모리 구조: 무엇을 모니터링해야 하는가

GC를 이해하려면 먼저 JVM이 메모리를 어떻게 나눠 쓰는지 알아야 합니다.

JVM Heap 구조 (G1GC 기준)

Young Generation
Eden
Survivor 0
Survivor 1
새로 생성된 객체
Minor GC 대상
Old Generation
Tenured Objects
오래 살아남은 객체
Major/Mixed GC 대상
+
Metaspace
Class Metadata
클래스 정보 (힙 외부)
무한 증가 가능

GC 단계별 STW 발생 패턴

GC 종류 대상 영역 STW 시간 발생 빈도 위험 수준
Minor GC Young Generation 수 ms ~ 50ms 초당 수 회 가능 낮음 (짧고 빠름)
Mixed GC Young + Old Gen 일부 50ms ~ 200ms 간헐적 중간 (P99 영향)
Full GC 전체 힙 수백 ms ~ 수 초 드물게 (비정상) 높음 (서비스 장애)

G1GC가 Full GC를 피하는 원리

G1GC(Garbage First GC)는 힙을 여러 개의 동일 크기 Region으로 나눕니다. GC 사이클마다 "가장 가비지가 많은(Garbage First) Region"을 우선 수집하여 Old Gen이 꽉 차기 전에 점진적으로 비워냅니다. 덕분에 Full GC 없이 힙을 관리할 수 있지만, 할당 속도가 GC 속도를 초과하면 결국 Full GC가 발생합니다. 이 상태를 Prometheus로 조기에 감지하는 것이 이번 장의 핵심입니다.

2. JVM 핵심 메트릭과 PromQL

4장에서 설정한 Spring Boot Actuator + Micrometer가 아래 메트릭들을 자동으로 Prometheus에 노출합니다. 이 메트릭들로 "힙이 얼마나 찼는가"와 "GC가 얼마나 오래 걸리는가"를 실시간으로 추적합니다.

핵심 메트릭 목록

메트릭명 의미 경보 기준
jvm_memory_used_bytes 현재 사용 중인 메모리 (area=heap/nonheap) Heap 85% 초과 시 경보
jvm_memory_max_bytes 설정된 최대 메모리 사용률 계산에 사용
jvm_gc_pause_seconds_sum GC pause 누적 시간 (cause별 분류) 평균 pause > 500ms 경보
jvm_gc_pause_seconds_count GC pause 발생 횟수 빈도 급증 시 경보
jvm_gc_memory_promoted_bytes_total Young → Old Gen 승격 바이트 수 승격률 급증 = OOM 전조
jvm_gc_memory_allocated_bytes_total Eden 영역 신규 할당 바이트 수 할당률 비정상 급증 감지

실전 PromQL 쿼리

# 1. Heap 사용률 (%) — 가장 먼저 확인하는 지표
sum(jvm_memory_used_bytes{area="heap"})
  / sum(jvm_memory_max_bytes{area="heap"}) * 100

# 2. 평균 GC pause 시간 (초) — P99 레이턴시 스파이크의 원인
rate(jvm_gc_pause_seconds_sum[5m])
  / rate(jvm_gc_pause_seconds_count[5m])

# 3. GC pause 빈도 (초당 횟수) — 급증하면 힙 압박 신호
rate(jvm_gc_pause_seconds_count[5m])

# 4. Old Gen 승격률 (bytes/sec) — 이 값이 급증하면 Full GC 전조
rate(jvm_gc_memory_promoted_bytes_total[5m])

# 5. 객체 할당률 (bytes/sec) — 트래픽 폭증 시 Eden 포화 속도 측정
rate(jvm_gc_memory_allocated_bytes_total[5m])

3. Grafana 패널 구성

앞 장에서 배운 Grafana 대시보드에 아래 패널들을 추가해 JVM 전용 뷰를 만듭니다.

JVM 모니터링 대시보드 패널 구성

Heap 사용률 게이지

타입: Gauge
임계값: 70% 노란색, 85% 빨간색
쿼리: sum(used) / sum(max) * 100

GC Pause 시간 그래프

타입: Time series
cause 레이블로 Minor/Mixed/Full 구분
쿼리: rate(sum)/rate(count)

메모리 영역별 사용량

타입: Time series
id 레이블로 Eden/Survivor/Old 분리
쿼리: jvm_memory_used_bytes by (id)

Old Gen 승격률

타입: Time series
Full GC 전조 신호
쿼리: rate(promoted[5m])

4. Alertmanager 경보 규칙 설정

6장에서 배운 Alertmanager에 JVM 경보를 추가합니다. 아래 두 개가 핵심입니다.

groups:
  - name: jvm_alerts
    rules:
      # 경보 1: Heap 85% 초과 — OOM 전 최후 경고선
      - alert: HighHeapUsage
        expr: |
          sum(jvm_memory_used_bytes{area="heap"})
          / sum(jvm_memory_max_bytes{area="heap"}) * 100 > 85
        for: 2m
        labels:
          severity: warning
        annotations:
          summary: "Heap 사용률 {{ $value | printf \"%.1f\" }}% 초과"
          description: "2분 이상 85%를 넘었습니다. OOM 발생 전에 조치하세요."

      # 경보 2: GC pause 평균 500ms 초과 — P99 레이턴시 직접 영향
      - alert: LongGCPause
        expr: |
          rate(jvm_gc_pause_seconds_sum[5m])
          / rate(jvm_gc_pause_seconds_count[5m]) > 0.5
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "평균 GC pause {{ $value | printf \"%.2f\" }}초"
          description: "GC pause가 500ms를 넘어 P99 레이턴시에 직접 영향을 줍니다."

      # 경보 3: Full GC 발생 감지 (cause="G1 Compaction Pause")
      - alert: FullGCDetected
        expr: |
          increase(jvm_gc_pause_seconds_count{cause=~"G1 Compaction.*|G1 Humongous.*"}[5m]) > 0
        labels:
          severity: critical
        annotations:
          summary: "Full GC 발생 감지"
          description: "Full GC는 수백 ms ~ 수 초 STW를 유발합니다. 즉시 힙 덤프 분석이 필요합니다."

5. OOM 발생 시: 힙 덤프 분석 실습

Prometheus 경보가 울렸는데도 조치를 못 했거나, 이미 OOM이 발생했을 때의 대응 절차입니다.

Step 1. 힙 덤프 자동 생성 설정

애플리케이션 기동 옵션에 아래 JVM 플래그를 추가해두면 OOM 발생 시 자동으로 힙 덤프 파일이 생성됩니다.

# JVM 시작 옵션에 추가
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/app/heapdump.hprof

Step 2. 힙 덤프 분석 (Eclipse MAT)

생성된 .hprof 파일을 Eclipse Memory Analyzer Tool(MAT)로 열어 아래 순서로 분석합니다.

  1. Leak Suspects Report 실행 — MAT가 자동으로 메모리 누수 가능성이 있는 객체를 찾아줍니다. "Problem Suspect 1"에 표시된 클래스가 가장 의심스러운 대상입니다.
  2. Dominator Tree 확인 — 힙을 가장 많이 점유하는 객체 계층을 내림차순으로 보여줍니다. 상위 5개 객체가 힙의 80% 이상을 차지한다면 누수를 강하게 의심해야 합니다.
  3. Retained Heap 기준 정렬 — 어떤 객체가 GC를 막고 있는지(참조를 붙들고 있는지) 확인합니다. List, Map, Cache 구현체가 비정상적으로 크다면 해제되지 않은 캐시나 세션을 의심합니다.

트래픽 폭증 시 힙 포화 시나리오

선착순 이벤트 오픈 → OOM 직전 징후 패턴

  1. 할당률 급증rate(jvm_gc_memory_allocated_bytes_total[1m]) 가 평소의 5~10배
  2. Minor GC 빈도 급증 — Eden이 빠르게 채워지고 GC가 초당 수 회 발생
  3. Old Gen 승격률 상승 — Minor GC가 객체를 다 처리하지 못해 Old Gen으로 넘어가기 시작
  4. Heap 사용률 85% 돌파 경보 → 여기서 트래픽 차단 또는 인스턴스 스케일 아웃
  5. Mixed/Full GC 발생 — STW로 수백 ms P99 레이턴시 스파이크 → 서비스 지연 감지
  6. OOM Killed — 조치 없으면 프로세스 종료

핵심: 4단계에서 경보를 받고 대응하면 5~6단계를 막을 수 있습니다. Prometheus 경보가 존재하는 이유가 바로 이겁니다.

6. GC 튜닝 기본 전략

증상 원인 조치
Minor GC 초당 수십 회 Eden 영역이 너무 작음 -Xmx로 힙 전체 증설 또는 Young Gen 비율 조정
Mixed GC가 자주 오래 걸림 Old Gen 승격률 높음 → 단명해야 할 객체가 Tenured됨 대용량 임시 객체 생성 로직 리뷰, 캐시 TTL 점검
Full GC 발생 (특히 반복) 메모리 누수 또는 힙 절대 부족 힙 덤프 분석으로 누수 객체 식별 및 수정
Metaspace OOM 동적 클래스 생성 과다 (리플렉션, Proxy) -XX:MaxMetaspaceSize 설정 + 동적 생성 코드 리뷰

면접관의 시선 💡

Q: "트래픽 폭증 시 Java 애플리케이션에서 발생할 수 있는 장애와 모니터링 방법을 설명해주세요."

A: "가장 흔한 패턴은 힙 포화로 인한 GC 압박입니다. 트래픽이 몰리면 객체 할당률이 급증하고 Eden이 빠르게 채워집니다. Minor GC가 객체를 충분히 처리하지 못하면 Old Gen으로 승격률이 높아지고, 결국 Mixed/Full GC가 발생해 STW로 수백 ms 레이턴시 스파이크가 생깁니다. 저는 Prometheus에서 jvm_gc_pause_secondsjvm_memory_used_bytes{area="heap"}를 추적하며, Heap 사용률 85% 초과 또는 평균 GC pause 500ms 초과 시 Alertmanager로 경보를 설정합니다. OOM이 실제로 발생했을 때는 -XX:+HeapDumpOnOutOfMemoryError로 자동 수집된 힙 덤프를 Eclipse MAT으로 분석해 Dominator Tree에서 누수 객체를 찾습니다."


Q: "G1GC를 선택하는 이유와 Full GC를 방지하는 방법은?"

A: "G1GC는 힙을 Region 단위로 나눠 가비지가 많은 Region을 우선 수집합니다. 덕분에 Old Gen 전체를 멈추는 Full GC 없이 힙을 점진적으로 비워낼 수 있어 STW 시간을 예측 가능한 범위로 제한할 수 있습니다. Full GC 방지를 위해서는 첫째로 -XX:InitiatingHeapOccupancyPercent(기본 45%)를 낮춰 Mixed GC를 더 일찍 시작하게 하고, 둘째로 Prometheus로 Old Gen 승격률을 모니터링하다가 급증 추세가 보이면 트래픽을 제한하거나 인스턴스를 스케일 아웃합니다."