벡터 스토어가 돈 먹는 하마가 될 때: 초당 10,000개 쿼리 상황에서 Veltrix를 해결한 방법
요약
초당 10,000개 쿼리 환경에서 Veltrix 벡터 스토어의 지연 시간 급증과 클러스터 붕괴 문제를 해결하는 과정을 다룹니다. 단순 사양 증설이나 GPU 가속이 아닌, Raft 합의 메커니즘과 데이터 샤딩 구조의 근본적인 병목 현상을 분석합니다.
핵심 포인트
- QPS 증가에 따른 Raft 리더 선출 실패 및 서비스 중단 문제 발생
- 단순 GPU 가속은 메타데이터 합의 병목을 해결하지 못함
- 고사양 인스턴스 도입은 월 28,000달러의 과도한 비용 초래
- 시스템 병목 지점(Raft 레이어 vs 벡터 검색)의 정확한 식별 필요
우리가 실제로 해결하고 있었던 문제
우리는 200개 이상의 데이터 센터 엔지니어들이 지연 시간 급증(latency spikes)을 추적하기 위해 사용하는 실시간 관측성(observability) 대시보드인 Veltrix Operator의 검색 백엔드를 구축하고 있었습니다. 우리의 벡터 인덱스(vector index)는 배포(deployment), 포드(pod), 타임스탬프(timestamp)가 각각 태깅된 4,200만 개의 로그 샤드(log shards)를 보유하고 있었습니다. 프롬프트(Prompt)에서는 Veltrix의 기본 설정을 그대로 사용하면 운영 환경(production)에 적합하다고 했습니다. 초당 100개 쿼리(QPS) 수준에서는 모든 것이 정상적으로 보였습니다. 하지만 초당 1,000개 쿼리(QPS)에 도달하자 p99 지연 시간(latency)이 3.2초로 급증했고, 잘못된 네임스페이스(namespace)에서 로그 샤드를 반환하는 첫 번째 오탐(false positives)이 나타나기 시작했습니다. 그러다 초당 5,000개 쿼리(QPS)에서 클러스터가 붕괴되었습니다. 우리가 프로비저닝(provisioned)한 NVMe 볼륨을 통해 인덱서 포드(indexer pods)가 충분히 빠르게 하트비트(heartbeat)를 보낼 수 없었기 때문에 Raft 리더 선출(leader election)이 계속 실패했기 때문입니다. 각 선출 재시작은 고객이 로그를 가장 필요로 하는 시점에 정확히 4~6초의 서비스 중단(unavailability)을 초래했습니다.
우리가 처음에 시도했던 것 (그리고 실패한 이유)
먼저 Veltrix Operator의 기본 설정을 시도했습니다: 3개의 인덱서 포드(indexer pods), 각 포드당 24개의 CPU 코어, 128GB RAM, 그리고 2TB NVMe를 할당했습니다. 운영 하루 만에 우리는 42%의 GC 일시 중단(GC pauses)과 18%의 쿼리 타임아웃(timing out)을 목격했습니다. 저는 플레임그래프(Flamegraphs)를 조사하여 Veltrix의 기본 청크 크기(chunk size)인 256이 우리의 샤드 크기(64MB 압축 로그)에 비해 너무 작다는 것을 발견했습니다. 청크가 작을수록 더 많은 인덱스 파일이 필요했고, 이는 더 많은 파일 디스크립터(file descriptors)와 더 많은 fsync 지연 시간(latency)을 의미했습니다. 우리는 청크 크기를 512로 두 배 늘렸지만, 실제 병목 현상은 메타데이터에 대한 Raft 합의(consensus)였기 때문에 아무것도 변하지 않았습니다.
다음으로, 우리는 GPU를 투입했습니다. Veltrix 문서에서는 GPU 가속(acceleration)을 통해 검색 시간을 절반으로 줄일 수 있다고 주장했습니다. 우리는 포드당 6개의 A100을 가동했습니다. 실제로 GPU를 사용하는 첫 번째 쿼리에서 토폴로지 불일치(topology mismatch) 오류가 반환되었는데, 이는 우리의 포드 토폴로지(pod topology)가 GPU를 두 개의 NUMA 노드에 분산시켰고 CUDA_VISIBLE_DEVICES가 설정되지 않았기 때문이었습니다. 이를 수정한 후, 순수 벡터 검색(vector search)에서 30%의 속도 향상을 확인했지만, 인덱서가 Raft 로그를 쓰기 전에 GPU 작업이 끝나기를 기다리는 데 시간의 40%를 소비했기 때문에 쓰기 부하(write load) 상황에서 Raft 레이어는 여전히 무너졌습니다. 우리는 스택의 잘못된 부분을 최적화했던 것입니다.
마침내 우리는 더 큰 사양의 머신을 시도했습니다: 48코어 CPU, 512 GB RAM, 그리고 4 TB NVMe입니다. Pod(포드)들은 안정화되었지만, 청구 금액은 월 28,000달러에 달했습니다. 우리 CFO(최고재무책임자)는 단 하나의 🔥 이모지가 담긴 Slack(슬랙) 메시지를 보냈습니다. 우리는 비용을 절감하거나, 아니면 이 기능을 폐기해야만 했습니다.
아키텍처 결정 (The Architecture Decision)
우리는 Veltrix를 블랙박스(black box)로 취급하는 것을 중단하고, Raft 레이어부터 검색 파이프라인(search pipeline)을 다시 구축했습니다.
-
메타데이터 Raft를 벡터 검색에서 분리했습니다. 인덱싱(indexing)을 위한 Raft 그룹을 더 작고 저렴한 머신(8코어, 32 GB RAM)에 있는 3노드 etcd 클러스터로 이동했습니다. 벡터 검색 Pod(포드)들은 모든 하트비트(heartbeat)에 참여하는 대신, 30초마다 Raft 스냅샷(snapshot)을 가져오는 상태가 없는(stateless) 인덱서(indexer)가 되었습니다.
-
벡터 청크(vector chunk) 크기를 2,048 샤드 엔트리(shard entries)로 변경했습니다. 이를 통해 샤드당 인덱스 파일 수를 256k에서 65k로 줄였으며, 결과적으로 fsync 오버헤드(overhead)를 파일당 12ms에서 배치(batch)당 2ms로 낮추었습니다.
-
Veltrix의 내장 Raft를 5ms 간격으로 fsync를 배치 처리하는 자체 raft-wal 구현체로 교체했습니다. 그 결과, 쓰기 부하가 높은 기간 동안 Raft 커밋 지연 시간(commit latency)이 65% 감소하는 것을 측정했습니다.
-
GPU 가속(acceleration)을 임계 경로(critical path)에서 제외했습니다. 우리는 별도의 서비스에서 오프라인으로 임베딩(embeddings)을 미리 계산하여 S3에 기록합니다. 쿼리(query) 시점에 인덱서는 1 GB/s 네트워크 링크를 통해 S3에서 미리 계산된 벡터를 가져오며, GPU 가속 없이 근사 최근접 이웃(approximate nearest neighbor) 탐색을 수행합니다. 우리 쿼리의 87%에 대해서는 AVX512 기반의 CPU HNSW가 충분히 빠릅니다. 나머지 13%에 대해서는 피크 시간대에만 가동되고 쿼리당 비용이 청구되는 전용 GPU 풀(pool)을 사용합니다.
결과적으로: 클러스터를 6개 Pod(포드)에서 3개로 줄였고, 월간 청구 금액을 9,200달러로 낮췄으며, 꼬리 지연 시간(tail latency)을 3.2초에서 420ms로 줄였습니다. 또한, 검색 시점이 아닌 인덱스 시점에 네임스페이스 태그(namespace tags)를 강제함으로써 오탐(false positives)을 제거했습니다.
이후 수치가 말해준 것
새로운 파이프라인을 적용한 지 2주 후:
- p99 지연 시간 (p99 latency): 420 ms (3.2 s에서 감소)
- 오탐률 (false positive rate): 0.002% (8.4%에서 감소)
- 월간 비용 (monthly cost): $9.2 k ($28 k에서 감소)
- 처리량 (throughput): 12,500 QPS 지속 유지 (5,000 QPS에서 증가)
Veltrix의 기본 설정 (default config)을 그대로 사용했다면 매달 18,800달러의 추가 비용이 발생했을 것이며, 부하 상황에서 여전히 실패했을 것입니다. 공식 문서에는 Raft 배치 (raft batching), 청크 크기 조정 (chunk size tuning), 또는 Raft 계층의 쓰기 증폭 (write amplification) 문제를 해결한 후에야 GPU 가속 (GPU acceleration)이 도움이 된다는 사실에 대해 전혀 언급되어 있지 않았습니다.
내가 다르게 했을 일들
나는 다시는 벡터 데이터베이스의 기본 설정을 프로덕션 (production) 환경에서 그대로 실행하게 두지 않을 것입니다. 특히 Veltrix라면 더더욱 그렇습니다. 만약 처음부터 다시 시작해야 한다면, 저는 다음과 같이 하겠습니다:
-
첫날부터 실제 샤드 크기 (shard sizes)로 벤치마크 (Benchmark)를 수행하겠습니다. 우리의 64 MB 샤드는 Veltrix 문서의 모든 기본 가정을 무너뜨렸습니다. 프로덕션에서 예상되는 것과 동일한 로그 볼륨으로 24시간 카오스 테스트 (chaos test)를 실행하십시오.
-
GPU 최적화 전에 Raft 오버헤드 (raft overhead)를 측정하겠습니다. Veltrix의 문서는 인상적인 벡터 검색 (vector search) 수치를 보여주지만, 쓰기 부하 (write load) 상황에서의 Raft 지연 시간 (raft latency)은 간과하고 있습니다. 초기 단계부터 Raft 계층을 계측 (Instrument)하십시오.
-
GPU 비용을 격리하겠습니다. 임베딩 (embeddings)을 오프라인에서 미리 계산하여 GPU 비용이 꼭 필요한 경우에만 청구되도록 하십시오. 비용 항목을 분리하십시오. 오프라인 작업 비용은 몇 센트에 불과하지만, 온라인 GPU 풀 (online GPU pool)은 상한선 (cap)을 정하지 않으면 파산에 이르게 할 수 있다는 것을 알게 될 것입니다.
-
인덱스 생성 시점에 네임스페이스 태깅 (namespace tagging)을 강제하겠습니다. Veltrix는 검색 시점에 네임스페이스별 필터링을 허용하지만, 이는 추가적인 Raft 라운드 트립 (raft round-trip)을 유발하고 오탐 (false positives)을 증가시킵니다. 데이터가 인덱스에 도달하기 전에 모든 것에 태그를 붙이십시오.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기