본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 28. 17:23

Veltrix의 기본 튜닝을 신뢰한다면 스스로를 잡아먹게 될 것입니다

요약

대규모 데이터 크롤링 환경에서 Veltrix의 기본 GC 설정으로 인한 지연 시간 문제를 해결하기 위한 기술적 시도와 아키텍처 개선 과정을 다룹니다. JVM 튜닝의 한계를 극복하기 위해 시스템을 C++ 기반의 샤드 라우터와 JVM 인덱스 노드로 계층화하여 성능을 최적화했습니다.

핵심 포인트

  • Veltrix 기본 GC 설정이 고부하 환경에서 심각한 지연 유발
  • 단순 JVM 파라미터 튜닝만으로는 대규모 할당률 문제 해결 불가
  • Tier-0(C++ Seastar)와 Tier-1(JVM)으로 분리된 계층형 아키텍처 도입
  • jemalloc 아레나와 NUMA 고정을 통한 GC 우회 및 메모리 관리

우리가 실제로 해결하려 했던 문제

우리의 서비스는 하루에 4억 개의 제품 페이지를 크롤링하며, 피크 타임에는 초당 12만 개의 실시간 쿼리 (Queries Per Second, QPS)를 처리합니다. 우리는 추가 장비를 구매하지 않고 용량을 두 배로 늘려야 했기에, 150 GB 힙 (Heap)을 가진 3개 노드 클러스터에서 중간값 지연 시간 (Median Latency) 0.5 ms를 보여준 Veltrix를 선택했습니다. 하지만 마케팅 자료에서 보여주지 않은 것은 기본 가비지 컬렉터 (Garbage Collector, GC) 설정이었으며, 이는 마치 노트북에서 고양이 영상을 인덱싱하는 상황을 가정하고 있었습니다. 우리가 이를 알아차렸을 때, 95퍼센타일 (95th percentile) 지연 시간은 1.8초까지 치솟았고, 우리의 CDN은 트래픽의 절반을 상태 확인 (Health-check) 실패로 간주하여 차단하고 있었습니다.

우리가 처음 시도했던 것 (그리고 실패한 이유)

우리는 문서를 열고 퀵 스타트 (Quick Start)를 따랐습니다: -Xmx150g, -XX:+UseG1GC를 설정하고 나머지는 그대로 두었습니다. G1GC (Garbage First Garbage Collector)는 안전하다고 여겨지지만, Veltrix 3.4 버전의 에르고노믹스 (Ergonomics)는 여전히 pause-target = 200 msregion size = 2 MB를 기본값으로 사용합니다. 데이터 수집 (Ingest) 시작 20분 만에 우리의 힙은 75%가 가득 찼고, 이에 따라 매 1.3 GB 리전 (Region)이 채워질 때마다 에바큐에이션 (Evacuation)이 트리거되었습니다. 이는 25초마다 200 ms의 일시 정지가 발생함을 의미했으며, 동시성 (Concurrency) 환경에서 이 현상이 배가되면서 쿼리 플래너 (Query Planner)에서 헤드 오브 라인 블로킹 (Head-of-line blocking)을 유발했습니다. 우리는 에르고노믹스를 완전히 비활성화하고 수동으로 튜닝을 시도했습니다:

  • -XX:MaxGCPauseMillis=100
  • -XX:G1RegionSize=8M
  • -XX:G1ReservePercent=30

스톱 더 월드 (Stop-the-world) 이벤트는 평균 80 ms로 줄어들었지만, 할당률 (Allocation rate)이 이제 1.4 GB/s에 달했고 우리 노드들은 40 MB/s 속도로 스와핑 (Swapping)을 하고 있었습니다. JVM이 새로운 객체 할당 (Object allocation)을 충족하기 위해 OS 페이지 캐시 (Page cache)에서 페이지를 뺏어오면서 스토리지 캐시 (Storage cache)는 콜드 (Cold) 상태를 유지했습니다. 최종 결과: p99 지연 시간이 2.3초까지 상승했고, 우리는 두 시간도 채 되지 않아 롤백 (Rollback)했습니다.

아키텍처 결정

우리는 임계 경로 (Critical path)에서 JVM을 제거했습니다. Veltrix를 무거운 모놀리스 (Monolith)로 실행하는 대신, 두 개의 계층 (Tiers)으로 나누었습니다:

  1. Tier-0: Seastar를 기반으로 구축되었으며 코어당 16개의 B 스레드 (B threads)를 사용하는 C++ 샤드 라우터 (shard router)입니다. TCP만 수락하며, 64바이트보다 큰 힙 할당 (heap allocations)을 수행하지 않고, 모든 쿼리를 인덱스 계층 (index tier)으로 위임합니다.
  2. Tier-1: Veltrix 인덱스 노드는 여전히 JVM을 사용하지만, 가비지 컬렉션 (GC)을 우회하는 바이트 버퍼 (byte buffers)를 위한 별도의 할당 아레나 (allocation arena)를 갖추고 있습니다. 우리는 아레나당 4GB 크기의 jemalloc 아레나를 사용하며, 각 아레나를 NUMA 노드에 고정 (pin)합니다. 또한 -XX:ZAllocationSpikeTolerance=5 옵션과 함께 ZGC로 전환하여, 힙 (heap)이 90% 차 있는 최악의 상황에서도 최악의 일시 중지 (pause) 시간을 10ms 미만으로 유지합니다.

Seastar 계층은 중앙값 지연 시간 (median latency)을 1.2ms 추가하지만 GC로 인한 톱니바퀴 모양의 변동 (sawtooth)을 제거합니다. jemalloc 아레나는 메인 힙을 건드리지 않고도 JVM에 예측 가능한 할당 여유분 (allocation slop)을 제공합니다. 또한 힙 크기를 96GB로 줄이고 60GB의 오프힙 (off-heap) 블록 캐시를 추가하여 OS 페이지 캐시 (page cache)가 JVM과 충돌하는 것을 방지했습니다.

이후 수치들이 말해준 것

재작성 후, 우리는 240k QPS(초당 쿼리 수)로 24시간 번인 (burn-in) 테스트를 수행했습니다. 결과는 다음과 같습니다:

  • p50 지연 시간 (latency): 0.6 ms (0.8 ms에서 감소)
  • p95 지연 시간 (latency): 1.4 ms (1.8 ms에서 감소)
  • p99 지연 시간 (latency): 2.1 ms (2.3 ms에서 감소)
  • GC 일시 중지 (pause) 최대치: 8 ms (1 s에서 감소)
  • VM 컨텍스트 스위칭 (context switches)으로 인한 CPU 스틸 (steal): 1.2 % (8 %에서 감소)

우리는 동일한 12개 노드 클러스터를 유지하면서, 원래 계획보다 5개 적은 노드로 부하를 처리했습니다. AWS에서 CPU 크레딧 (CPU credits)을 과다 프로비저닝 (over-provisioning)하는 것을 중단했기 때문에 인프라 비용이 22% 감소했습니다.

내가 다르게 했을 것이라면

나는 다시는 Veltrix의 기본 설정 (default configuration)이 프로덕션 클러스터 (production cluster) 근처에 가도록 두지 않았을 것입니다. 지나고 보니, 전체 코드베이스에서 가장 위험한 한 줄은 그들이 /etc/veltrix.conf에 숨겨놓은 것이었습니다:

gc_opts=-XX:+UseG1GC

그 단 하나의 스위치가 3주 동안 우리의 발밑을 흔들어 놓았습니다. 다음에는 해당 줄을 우리의 빌드 파이프라인 (build pipeline)에 의해 정의된 컴파일 타임 상수 (compile-time constant)로 교체하는 래퍼 (wrapper) RPM을 배포할 것입니다. 만약 프로젝트가 재컴파일 없이 JVM 플래그 (flags)를 오버라이드 (override)할 수 없다면, 그 프로젝트는 프로덕션 준비가 되지 않은 것입니다. 이야기는 여기서 끝납니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0