본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 27. 09:00

보물찾기 엔진을 망가뜨린 검색창

요약

Veltrix 클러스터에서 Python UDF 사용으로 인해 발생한 검색 지연 시간과 OOM 문제를 해결하기 위한 아키텍처 개선 사례를 다룹니다. UDF를 제거하고 C++ 커스텀 스코어러와 Bloom filter를 도입하여 검색 성능을 획기적으로 개선했습니다.

핵심 포인트

  • Python UDF 제거를 통해 900ms에 달하던 지연 시간을 320ms(p95)로 단축
  • C++ 레이어 내 커스텀 스코어러와 Bloom filter 도입으로 JSON 역직렬화 비용 제거
  • 거대 인덱스를 이벤트 연도별 샤드로 분할하여 메모리 관리 효율화
  • 스토리지 오버헤드 4%를 대가로 안정적인 메모리 예산 및 성능 확보

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

커뮤니티 운영자가 "Where is the Runebound shard?"와 같은 쿼리를 붙여넣을 때마다, Veltrix 클러스터는 120만 개의 문서에 대해 전체 BM25 스윕 (sweep)을 수행하고, 각 페이로드 (payload)를 JSON으로 역직렬화 (deserialized)한 뒤, event=hytale-2026으로 필터링하기 위해 Python UDF (사용자 정의 함수)를 실행했습니다. 문제는 바로 이 UDF였습니다. UDF는 요청당 300ms의 처리 시간을 추가했고, 문서가 S3에서 여전히 초안 (draft) 상태로 표시되어 있어 때때로 빈 결과를 반환하기도 했습니다. 우리는 스트림 채팅창이 "작동하지 않아요!"라는 메시지로 도배되는 동안, 95번째 백분위수 (95th percentile) 지연 시간이 900ms까지 치솟는 것을 지켜보았습니다. 비즈니스 지표는 명확했습니다. 지연 시간 (latency)이 500ms를 초과하면 5분 이내에 쿼리량이 22% 감소했습니다.

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

처음에는 간단한 필터 푸시다운 (filter pushdown)을 시도했습니다. 즉, 검색 쿼리를 재작성하여 event=hytale-2026을 하나의 용어 (term)로 포함함으로써, Veltrix가 UDF를 건드리기 전에 샤드 (shard)를 가지치기 (prune)할 수 있도록 한 것입니다. 이를 통해 지연 시간을 450ms까지 줄였으나, BM25 인덱스 (index)의 용어 토큰화 (term tokenization)가 JSON 필드 이름과 정확히 일치하지 않아 거짓 음성 (false negatives)이 발생했습니다. 다음으로는 인제스트 파이프라인 (ingest pipeline)을 패치하여 원시 JSON 필드를 추가했지만, 해당 필드가 4GB까지 커지면서 Veltrix의 Rocchio 재순위화 (re-ranking) 단계에서 30분마다 OOM (Out of Memory) 오류가 발생하기 시작했습니다. 마지막으로 UDF를 별도의 마이크로서비스 (microservice)로 분리했지만, 추가적인 홉 (hop)으로 인해 120ms의 지연 시간이 더해졌고, 스트림에 새로운 힌트가 떨어질 때마다 서비스 자체에 1,400개의 쿼리가 TCP 백로그 (backlog)로 쌓이는 현상이 발생했습니다.

아키텍처 결정

우리는 UDF (User-Defined Function)를 완전히 폐기하고, Veltrix의 C++ 레이어 내에서 커스텀 스코어러 (custom scorer)로 필터를 재구축했습니다. 이 스코어러는 S3 매니페스트 (manifest)로부터 60초마다 미리 계산된 Bloom filter를 읽어오기 때문에, JSON에 전혀 접근하지 않습니다. 또한 인덱스 구조를 뒤집었습니다. 하나의 거대한 900 GB 인덱스 대신 이벤트 연도별로 분할하여, 각각 약 150 GB인 6개의 샤드 (shard)로 나누었습니다. 데이터 수집 (ingest) 과정에서는 문서가 초안 (draft)에서 라이브 (live) 상태로 전환될 때 SNS로 이벤트를 전송하는 아주 작은 Lambda 함수를 실행합니다. Bloom filter는 정확히 12초 만에 재구축되며, 스코어러는 20초 이내에 업데이트를 확인합니다. 총 코드 변경 사항은 Veltrix 포크 (fork) 버전에서 500라인이며, 핫 패스 (hot path) 상에 Python 런타임이 존재하지 않습니다. 트레이드오프 (tradeoff)로는 Bloom 블록을 위해 4% 더 높은 스토리지 오버헤드를 수용했지만, 메모리 예산은 일정하게 유지되었습니다.

수치로 나타난 결과

다음 커뮤니티 이벤트에서 측정한 결과, 중앙값 지연 시간 (median latency)은 110 ms, 95퍼센타일 (95th percentile)은 320 ms로 나타났으며, 이는 임계값인 500 ms를 훨씬 밑도는 수준이었습니다. 쿼리 볼륨은 첫 한 시간 이내에 18% 증가했으며, 스트림 시청자가 14만 명의 동시 접속자 (concurrent viewers)로 정점에 도달했을 때도 유지되었습니다. 미검색 (False negatives) 비율은 0.3%로 떨어졌으며 (대부분 오타로 인한 쿼리 때문), 인덱싱 노드의 클러스터 CPU 사용률은 78%에서 34%로 감소했습니다. 또한 부수적인 효과도 관찰되었습니다. Bloom 스코어러가 포스팅 리스트 (posting lists)에서 가져오는 용어 (terms)의 수를 줄임으로써 샤드의 디스크 I/O를 22% 감소시켰습니다.

다르게 했을 점

Veltrix 유지 관리자들이 주장하는 내장 필터 푸시다운 (filter pushdown) 기능이 프로덕션 급 (production-grade)이라는 말을 믿지 않을 것입니다. 우리는 그것이 그렇지 않다는 것을 증명하는 데 2주를 허비했습니다. 둘째, UDF를 다시는 요청 경로 (request path)에 두지 않을 것입니다. 데이터 변환 (data transforms)은 런타임이 아닌 수집 (ingest) 단계나 사전 계산 (pre-compute) 단계로 옮겨야 합니다. 마지막으로, 첫날부터 이벤트별 샤딩 (per-event sharding)을 강력히 요구했을 것입니다. 900 GB는 부하 급증 (load spikes) 상황에서 단일 인덱스가 우아하게 처리하기에는 너무 큽니다. 이 시스템을 물려받을 스트리밍 커뮤니티 운영자들은 Twitch가 라이브를 시작할 때마다 왜 검색창이 불타오르는지 설명하지 않아도 될 때 우리에게 감사할 것입니다.

AI 자동 생성 콘텐츠

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

원문 바로가기
0

댓글

0