Skip to Content
Backend대용량 데이터 수집기를 위한 JVM GC 튜닝 실전 (Tomcat & Collector)
💻 Backend2018년 10월 10일

대용량 데이터 수집기를 위한 JVM GC 튜닝 실전 (Tomcat & Collector)

#jvm#java#tuning#gc#performance#backend

사내 빅데이터 모니터링 플랫폼 엔진을 최적화할 때 가장 많은 시간을 쏟았던 영역이 바로 Java Virtual Machine(JVM)의 가비지 컬렉터(Garbage Collection) 튜닝이었습니다.

플랫폼 제품군은 크게 두 개의 핵심 Java 프로세스로 나뉘어 구동되었습니다.

  1. Collector (수집기): 수백 대의 하둡/리눅스 에이전트로부터 초당 수만 건의 성능 지표(JSON)를 받아 파싱하고 버퍼링한 뒤 HBase와 DB로 쏟아붓는 백엔드 데몬.
  2. Web Server (WAS): 관리자에게 실시간 차트와 대시보드를 렌더링하는 Spring/Tomcat 기반의 프론트 API 서버.

두 프로세스는 객체의 생존 기간(Object Lifetime)이 완전히 달랐기 때문에, 하나의 일원화된 JVM 옵션을 쓸 수 없었고 역할을 철저히 분리하여 setenv.sh를 튜닝해야만 했습니다.


1. JVM Heap 구조와 GC 기본 원리

튜닝에 앞서 JVM이 메모리를 어떻게 나누어 쓰는지 이해하는 것이 핵심입니다.

plaintext
┌─────────────────────────────────────────────────────────┐ │ Heap │ │ ┌────────────────────────────┐ ┌────────────────────┐ │ │ │ Young Generation │ │ Old Generation │ │ │ │ ┌───────┐ ┌───┐ ┌───┐ │ │ │ │ │ │ │ Eden │ │ S0│ │ S1│ │ │ (Tenured Space) │ │ │ │ └───────┘ └───┘ └───┘ │ │ │ │ │ └────────────────────────────┘ └────────────────────┘ │ └─────────────────────────────────────────────────────────┘ ↑ Minor GC가 발생하는 곳 ↑ Full GC가 발생하는 곳
  • Eden: 새로운 객체가 최초로 할당되는 곳. 빠르게 꽉 찬다.
  • Survivor (S0, S1): Minor GC 후 살아남은 객체가 이동하는 중간 공간. Age 카운터가 누적된다.
  • Old Generation: Survivor를 여러 번 통과한 장수(長壽) 객체들의 안식처. Full GC가 발생하면 STW(Stop-The-World)로 전체 애플리케이션이 멈춘다.

GC 알고리즘 선택

알고리즘특징적합한 상황
Serial GC단일 스레드, 단순소규모 앱, 테스트
Parallel GC (UseParallelOldGC)멀티 스레드 처리량(Throughput) 최적화배치성 데이터 처리 서버
CMS GCSTW 시간 최소화, CPU 를 더 사용응답속도가 중요한 웹 서버 (Java 9 이후 Deprecated)
G1 GC대용량 Heap에서 균형 잡힌 성능Java 9+ 기본값, 4GB+ 환경
ZGC / Shenandoah수십 GB Heap에서도 STW < 1msJava 15+, 초저지연 요구

당시 Java 8 환경이었기에 Parallel GC 를 채택했습니다. Collector는 처리량(Throughput) 우선, Web 서버는 응답 지연(Latency) 최소화가 목표였습니다.


2. 상태 진단 및 측정 도구

튜닝은 감으로 하는 것이 아니라 지표로 증명해야 합니다.

shell
# 1. 실시간 GC 통계 모니터링 (1초 간격으로 힙 현황 출력) $ jstat -gcutil <PID> 1000 # 출력 예시 # S0 S1 E O M CCS YGC YGCT FGC FGCT GCT # 0.00 47.02 83.14 32.01 95.72 92.11 152 6.343 3 1.512 7.855 # ↑ ↑ ↑ ↑ # S0% S1% Eden% Old% → Eden이 83% 찬 상태, 곧 Minor GC 발생 예정
shell
# 2. GC 로그를 파일로 남기는 JVM 플래그 (setenv.sh에 추가) -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:/var/log/app/gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20m
shell
# 3. Heap Dump — OOM 발생 시 자동 덤프 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/app/heap-dump.hprof

분석 도구:

  • VisualVM + Visual GC 플러그인: 힙 메모리 영역별 추이를 실시간 그래프로 시각화
  • GCEasy (gceasytool.com): GC 로그 파일 업로드 시 처리량, 중단 시간, 메모리 효율 자동 분석

[튜닝 전 최악의 상태] Collector 로그를 분석해 보니 Minor GC가 1~2초에 한 번씩 극한으로 발생하고 있었습니다. 수집된 JSON 객체들이 파싱 직후 버려져야 하는데, Heap 비율이 엉망이라 Eden이 즉시 꽉 차버리고 CPU 스파이크가 반복되는 상황이었습니다.


3. 프로세스별 JVM 튜닝 설정 (setenv.sh)

객체의 생존 기간(Age) 특성에 따라 Young/Old Generation 비율(NewRatio), Survivor 크기(SurvivorRatio)를 다르게 가져갔습니다.

Web 서버 튜닝 (1GB Heap)

웹 서버는 API 요청을 받고 응답하면 객체가 금방 소멸하지만, 사용자 권한 정보나 클러스터 토폴로지 같은 메타데이터는 캐싱되어 Old 영역에 오래 머물러야 합니다. Old 영역을 넉넉하게 가져가는 일반적인 비율(NewRatio=3, Old:Young = 3:1)을 채택했습니다.

shell
CATALINA_OPTS="-server \ -XX:+UseParallelOldGC \ -Xms1g -Xmx1g \ -XX:NewRatio=3 \ -XX:SurvivorRatio=6 \ -XX:+PrintGCDetails \ -XX:+PrintGCTimeStamps \ -Xloggc:/var/log/app/web-gc.log"
옵션의미
-Xms / -Xmx동일하게 설정런타임 Heap 리사이징 오버헤드 제거
NewRatio=3Old:Young = 3:1Old에 750MB, Young에 250MB
SurvivorRatio=6Eden:S0:S1 = 6:1:1Eden에 약 187MB

Collector 튜닝 (2GB Heap)

컬렉터는 1분만 지나도 버려질 단기 메트릭 객체(Short-lived Objects)들이 폭우처럼 쏟아지는 환경입니다. 객체들이 Old 영역으로 프로모션(Promotion)되면 나중에 무거운 Full GC를 유발하므로, Young 영역(Eden)을 비정상적으로 크게 잡아서 Minor GC 선에서 모두 소멸시키는 전략을 썼습니다.

shell
# Collector Daemon setenv.sh CATALINA_OPTS="-server \ -XX:+UseParallelOldGC \ -Xms2g -Xmx2g \ -XX:NewSize=1536m \ -XX:MaxNewSize=1536m \ -XX:SurvivorRatio=20 \ -XX:+PrintGCDetails \ -XX:+PrintGCTimeStamps \ -Xloggc:/var/log/app/collector-gc.log"
옵션의미
NewSize / MaxNewSize1536m전체 2GB 중 1.5GB를 Young에 몰아넣음
SurvivorRatio=20Eden:S0:S1 = 20:1:1Survivor를 최소화해 Eden을 최대한 확보

SurvivorRatio를 높이면 Survivor 공간이 작아지는 대신 Eden이 커집니다. 단기 객체가 Survivor를 거치지 않고 Eden에서 바로 회수되므로 Old 영역으로의 프로모션이 억제됩니다.


4. 튜닝 결과

튜닝 후 일주일간 jstat 모니터링과 GCEasy 분석을 진행한 결과입니다.

지표튜닝 전튜닝 후
Collector Minor GC 주기1~2초10~15초
Collector GC 처리량(Throughput)~94%99.84%
Web Minor GC 평균 시간~80ms21ms
Web GC 처리량~98%99.98%
Full GC 발생 횟수 (24h)5~8회0~1회

VisualVM으로 확인했을 때 Young 영역의 톱니바퀴 그래프가, 이전의 미친 듯한 진동에서 벗어나 규칙적이고 부드러운 산등성이 모양으로 평온하게 유지되는 것을 보며 JVM 튜닝의 매력을 실감할 수 있었습니다.


5. Java 11+ 환경에서의 권고사항

위 설정은 Java 8 + Parallel GC 기준입니다. 최신 Java 환경에서는 아래를 고려하세요.

shell
# Java 11+ G1GC 권장 기본 설정 JAVA_OPTS="-server \ -Xms2g -Xmx2g \ -XX:+UseG1GC \ -XX:MaxGCPauseMillis=200 \ -XX:G1HeapRegionSize=16m \ -XX:+HeapDumpOnOutOfMemoryError"
  • G1GC는 Heap을 고정 크기 Region으로 나눠 동적으로 Young/Old를 할당하므로 NewRatio를 직접 조정할 필요가 없습니다.
  • MaxGCPauseMillis로 목표 STW 시간을 설정하면 G1GC가 알아서 Region 크기를 조절합니다.
  • Java 15+ 초저지연 환경에서는 ZGC (-XX:+UseZGC)를 검토하세요.
Last updated on