[JVM 최적화 + 금융공학] GC 튜닝과 알고리즘 주문 실행의 지연 제어
G1GC Mixed GC로 인한 200ms 지연 스파이크가 주문 타임아웃을 유발한 장애를 분석하고, G1GC·ZGC·Shenandoah의 중단 시간 보장 방식을 비교합니다.
G1GC 기본 설정으로 운영하던 주문 매칭 엔진에서 1~2분 간격으로 200ms 이상의 지연 스파이크가 반복됐습니다. GC 로그를 분석하니 Old Generation 점유율이 75%를 넘을 때마다 Mixed GC가 발생하며 STW(Stop-the-World)가 일어나고 있었습니다. 이 장애를 계기로 G1GC·ZGC·Shenandoah 세 컬렉터의 중단 시간 보장 방식과 저지연 금융 시스템에서의 트레이드오프를 체계적으로 정리하게 됐습니다.
배경
JVM 기반 트레이딩 시스템에서 GC STW는 예측 불가능한 지연의 가장 흔한 원인입니다. 애플리케이션 스레드가 모두 정지된 채 GC가 실행되는 이 구간이 주문 처리 흐름에 끼어들면, 브로커로부터 받은 주문 수락 응답을 정해진 시간 내에 처리하지 못하는 타임아웃이 발생합니다.
실제 장애 시나리오는 다음과 같았습니다. 알고리즘이 시장 진입 신호를 감지하고 주문을 제출했는데, 그 순간 GC STW가 시작됩니다. 브로커는 주문을 접수하고 ExecutionReport를 150ms 후에 전송했지만, 클라이언트 JVM은 200ms 동안 STW 상태여서 응답 처리가 지연됐습니다. 클라이언트 측 타임아웃 로직이 이를 실패로 판단하고 중복 주문을 재제출하는 상황으로 이어졌습니다.
HFT 환경이 아니더라도, 알고리즘 트레이딩에서 p99 지연이 SLA를 초과하면 체결 실패와 슬리피지가 누적됩니다. GC 튜닝은 JVM 기반 트레이딩 시스템의 필수 과제입니다.
GC 컬렉터 비교
G1GC — Region 기반 부분 수집
G1GC(Garbage-First GC)는 JDK 9부터 기본 컬렉터로 채택됐습니다. 힙을 동일한 크기의 Region으로 분할하고, 가비지 밀도가 높은 Region을 우선적으로 수집하는 방식입니다.
G1GC의 핵심 문제는 Mixed GC입니다. Old Generation 점유율이 InitiatingHeapOccupancyPercent(기본값 45%)를 초과하면 Concurrent Mark 사이클이 시작되고, 이후 Young GC와 Old Region 수집을 함께 수행하는 Mixed GC가 발생합니다. Mixed GC의 STW 시간은 수집 대상 Region 수와 객체 참조 복잡도에 비례하며, 수십 ms에서 수백 ms에 이를 수 있습니다.
ZGC — Colored Pointer 기반 동시 압축
ZGC는 JDK 15에서 정식 도입됐으며, 힙 크기와 무관하게 STW 시간을 1ms 미만으로 제한하는 것을 설계 목표로 합니다. 이를 위해 포인터에 색상 메타데이터를 인코딩하는 Colored Pointer 기법을 사용합니다. 객체 이동과 참조 갱신 작업을 애플리케이션 스레드와 동시에 수행하므로 중단 시간이 극도로 짧습니다.
단점은 처리량(Throughput)입니다. 동시 GC 작업을 위해 추가 CPU를 지속적으로 사용하므로, 피크 처리량이 G1GC 대비 10~15% 낮아질 수 있습니다.
Shenandoah — Brooks Pointer 기반 동시 압축
Shenandoah는 Red Hat이 개발하고 JDK 12에 도입한 컬렉터입니다. ZGC와 유사하게 동시 압축을 지원하지만, Brooks Pointer라는 간접 참조 헤더를 객체에 추가하는 방식을 채택합니다. JDK 8 백포트가 가능해 레거시 시스템 마이그레이션에 유리합니다.
JVM 설정 예시
G1GC와 ZGC의 JVM 플래그 차이와 저지연 트레이딩에 권장되는 옵션을 정리합니다.
# G1GC 저지연 튜닝 예시
java -server \
-Xms8g -Xmx8g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=20 \
-XX:InitiatingHeapOccupancyPercent=35 \
-XX:G1HeapRegionSize=16m \
-XX:G1ReservePercent=20 \
-XX:+ParallelRefProcEnabled \
-XX:+UnlockExperimentalVMOptions \
-XX:+AlwaysPreTouch \
-Xlog:gc*:file=/var/log/trading/gc.log:time,uptime,level,tags:filecount=5,filesize=50m \
-jar trading-engine.jar
# ZGC 저지연 튜닝 예시 (JDK 15+)
java -server \
-Xms8g -Xmx8g \
-XX:+UseZGC \
-XX:SoftMaxHeapSize=6g \
-XX:ZCollectionInterval=60 \
-XX:ZUncommitDelay=300 \
-XX:+AlwaysPreTouch \
-XX:+DisableExplicitGC \
-Xlog:gc*:file=/var/log/trading/gc-zgc.log:time,uptime,level,tags:filecount=5,filesize=50m \
-jar trading-engine.jar
GC 로그에서 장애 원인을 확인하는 패턴은 다음과 같습니다.
// GC 로그 분석 유틸리티 예시
// 실제 로그 패턴: [2026-06-29T09:31:15.234+0000][gc,pause] GC(42) Pause Mixed (G1 Evacuation Pause) 187.3ms
import java.io.BufferedReader;
import java.io.FileReader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class GcLogAnalyzer {
// Mixed GC 중단 시간 추출
private static final Pattern MIXED_GC_PATTERN =
Pattern.compile("Pause Mixed.*?(\\d+\\.\\d+)ms");
public static void analyzeMixedGcPauses(String logFile) throws Exception {
try (BufferedReader reader = new BufferedReader(new FileReader(logFile))) {
String line;
int count = 0;
double totalPause = 0;
double maxPause = 0;
while ((line = reader.readLine()) != null) {
Matcher m = MIXED_GC_PATTERN.matcher(line);
if (m.find()) {
double pause = Double.parseDouble(m.group(1));
count++;
totalPause += pause;
maxPause = Math.max(maxPause, pause);
if (pause > 50) { // 50ms 초과 스파이크 경고
System.out.printf("경고: Mixed GC STW %.1fms — %s%n",
pause, line.substring(0, 50));
}
}
}
System.out.printf("Mixed GC 횟수: %d, 평균: %.1fms, 최대: %.1fms%n",
count, totalPause / count, maxPause);
}
}
}
금융공학 관점
주문 실행 SLA와 GC 중단 예산
알고리즘 트레이딩 시스템의 일반적인 지연 SLA는 다음과 같이 정의됩니다.
- p50 (중앙값): 2ms 이하
- p99: 10ms 이하
- p99.9: 50ms 이하
- 최악(Max): 200ms 이하
G1GC 기본 설정에서 Mixed GC STW가 200ms를 초과하면 p99.9와 Max SLA 모두 위반됩니다. ZGC는 최대 STW가 1ms 미만이므로 모든 SLA 구간을 GC 관점에서 통과합니다.
워밍업 전략
JVM은 시작 직후 JIT 컴파일이 완료되지 않아 해석 모드로 코드가 실행됩니다. 이 구간에는 GC 압박도 높고 지연도 예측 불가능합니다. 저지연 트레이딩 시스템에서는 다음 워밍업 전략이 필요합니다.
첫째, 시장 개장 전 주문 시뮬레이션으로 핫 경로(Hot Path)를 JIT 컴파일 상태로 만들어 둡니다. 둘째, -XX:+AlwaysPreTouch 플래그로 힙 메모리를 JVM 시작 시 미리 OS에서 할당받아 런타임 페이지 폴트를 제거합니다. 셋째, 힙 크기를 -Xms와 -Xmx를 동일하게 설정해 힙 확장으로 인한 GC 트리거를 방지합니다.
트레이드오프
| 기준 | G1GC | ZGC | Shenandoah |
|---|---|---|---|
| 최대 STW 시간 | 50 ~ 300 ms | < 1 ms | < 10 ms |
| 평균 처리량 | 높음 | 중간 (10~15% 낮음) | 중간 |
| 메모리 오버헤드 | 낮음 | 중간 (포인터 메타) | 중간 (Brooks Pointer) |
| 최소 JDK 버전 | JDK 7 | JDK 15 (정식) | JDK 12 |
| JDK 8 백포트 | 기본 포함 | 불가 | 가능 (Red Hat 배포) |
| 튜닝 복잡도 | 중간 | 낮음 | 낮음 |
| 적합한 힙 크기 | 4 ~ 32 GB | 8 GB ~ 수 TB | 8 GB ~ 수백 GB |
| 주요 사용 사례 | 범용 | 저지연 필수 환경 | 레거시 JDK 저지연 |
결론
저지연 트레이딩 시스템에서 GC 컬렉터를 선택할 때는 STW 보장이 최우선 기준입니다. JDK 17 이상 환경이라면 ZGC가 1ms 미만의 STW를 제공하므로 주문 실행 SLA 충족에 가장 유리합니다. JDK 8이나 11 환경을 유지해야 한다면 G1GC를 적극 튜닝하거나 Shenandoah 백포트를 검토해야 합니다.
G1GC를 계속 사용한다면 InitiatingHeapOccupancyPercent를 35% 이하로 낮춰 Mixed GC 트리거 시점을 앞당기고, Region 크기를 늘려 수집 단위를 줄이는 방향으로 튜닝합니다. MaxGCPauseMillis 목표를 20ms로 설정해 G1GC가 스스로 수집 범위를 조절하도록 유도하는 것도 효과적입니다.
어떤 컬렉터를 선택하든, GC 로그를 항상 활성화하고 p99.9 중단 시간을 모니터링하는 것이 운영의 기본입니다. 장애는 대부분 “기본 설정으로도 괜찮겠지”라는 가정에서 시작됩니다.