지금까지 우리는 인프라(Prometheus)와 시각화(Grafana)를 배웠습니다. 이번 장에서는 시선을 한 단계 위로 옮겨, 우리가 만든 Java 애플리케이션 그 자체를 모니터링합니다. 아무리 Cassandra, Kafka, Redis가 튼튼해도, 결국 이것들을 호출하는 Java 프로세스가 무너지면 모든 게 멈춥니다. 트래픽 폭증 시 JVM 힙이 포화되고 GC가 오래 걸려 수백 ms의 응답 지연(STW)이 발생하는 현상을 Prometheus 메트릭으로 사전에 감지하고 대응하는 방법을 배웁니다.
GC를 이해하려면 먼저 JVM이 메모리를 어떻게 나눠 쓰는지 알아야 합니다.
| GC 종류 | 대상 영역 | STW 시간 | 발생 빈도 | 위험 수준 |
|---|---|---|---|---|
| Minor GC | Young Generation | 수 ms ~ 50ms | 초당 수 회 가능 | 낮음 (짧고 빠름) |
| Mixed GC | Young + Old Gen 일부 | 50ms ~ 200ms | 간헐적 | 중간 (P99 영향) |
| Full GC | 전체 힙 | 수백 ms ~ 수 초 | 드물게 (비정상) | 높음 (서비스 장애) |
G1GC(Garbage First GC)는 힙을 여러 개의 동일 크기 Region으로 나눕니다. GC 사이클마다 "가장 가비지가 많은(Garbage First) Region"을 우선 수집하여 Old Gen이 꽉 차기 전에 점진적으로 비워냅니다. 덕분에 Full GC 없이 힙을 관리할 수 있지만, 할당 속도가 GC 속도를 초과하면 결국 Full GC가 발생합니다. 이 상태를 Prometheus로 조기에 감지하는 것이 이번 장의 핵심입니다.
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 영역 신규 할당 바이트 수 | 할당률 비정상 급증 감지 |
# 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])
앞 장에서 배운 Grafana 대시보드에 아래 패널들을 추가해 JVM 전용 뷰를 만듭니다.
타입: Gauge
임계값: 70% 노란색, 85% 빨간색
쿼리: sum(used) / sum(max) * 100
타입: Time series
cause 레이블로 Minor/Mixed/Full 구분
쿼리: rate(sum)/rate(count)
타입: Time series
id 레이블로 Eden/Survivor/Old 분리
쿼리: jvm_memory_used_bytes by (id)
타입: Time series
Full GC 전조 신호
쿼리: rate(promoted[5m])
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를 유발합니다. 즉시 힙 덤프 분석이 필요합니다."
Prometheus 경보가 울렸는데도 조치를 못 했거나, 이미 OOM이 발생했을 때의 대응 절차입니다.
애플리케이션 기동 옵션에 아래 JVM 플래그를 추가해두면 OOM 발생 시 자동으로 힙 덤프 파일이 생성됩니다.
# JVM 시작 옵션에 추가
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/app/heapdump.hprof
생성된 .hprof 파일을 Eclipse Memory Analyzer Tool(MAT)로 열어 아래 순서로 분석합니다.
List, Map, Cache 구현체가 비정상적으로 크다면 해제되지 않은 캐시나 세션을 의심합니다.
rate(jvm_gc_memory_allocated_bytes_total[1m]) 가 평소의 5~10배핵심: 4단계에서 경보를 받고 대응하면 5~6단계를 막을 수 있습니다. Prometheus 경보가 존재하는 이유가 바로 이겁니다.
| 증상 | 원인 | 조치 |
|---|---|---|
| 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_seconds와 jvm_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 승격률을 모니터링하다가 급증 추세가 보이면 트래픽을 제한하거나 인스턴스를 스케일 아웃합니다."