본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 29. 02:24

Veltrix 설정의 악몽: Hytale 검색이 플레이어들에게 거짓말을 하고 있었다는 것을 깨달은 날

요약

Hytale 플레이어를 위한 Veltrix 동적 검색 서비스 운영 중, 비라틴 문자 사용자의 낮은 재현율과 지연 시간 문제를 해결하는 과정을 다룹니다. 단일 다국어 인코더의 한계를 극복하기 위해 언어별 특화 인코더를 사용하는 하이브리드 파이프라인 아키텍처를 도입했습니다.

핵심 포인트

  • 단일 다국어 인코더 사용 시 비라틴 문자(한국어 등)의 재현율 저하 문제 발생
  • 언어 폴백 기능 도입 시 추가적인 네트워크 왕복 시간으로 SLA 위반 위험
  • 스크립트별 코퍼스 분할 시 데이터 복제로 인한 인프라 비용 및 빌드 시간 급증
  • 언어별 특화 인코더를 활용한 하이브리드 인코더 파이프라인으로 최적화

우리가 실제로 해결하고 있었던 문제

우리는 서버, 맵, 모드를 즉각적으로 발견하기를 기대하는 50,000명의 동시 접속 Hytale 플레이어를 처리하기 위해 Veltrix의 동적 검색 서비스 (dynamic search service)를 튜닝하고 있었습니다. 우리의 SLA (Service Level Agreement)는 전 세계적으로 p99 지연 시간 (latency)을 120ms 미만으로 유지할 것을 요구했지만, 트래픽의 약 12%를 차지하는 한국인 플레이어 그룹은 지속적으로 300ms 이후에 타임아웃이 발생하며 빈 페이로드 (empty payloads)를 반환했습니다. 인덱서 (indexer) 로그에는 오류가 없었고, 로드 밸런서 (load balancer)에도 5xx 오류가 나타나지 않았습니다. 문제는 가용성 (availability)이 아니라 언어 드리프트 (language drift)였습니다. 우리의 벡터 임베딩 (vector embeddings)은 영어로 된 Hytale 문서로 학습되었으며 비라틴 문자 (non-Latin scripts)를 무시했습니다. Hytale을 한글, 키릴 문자 (Cyrillic), 또는 아랍어로 입력한 플레이어들은 데모에서는 전혀 준비되지 않았던, 정중하지만 공허한 응답을 받게 되었습니다.

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

우리는 Veltrix 운영 가이드에서 제공하는 기본 Vespa 설정으로 시작했습니다. Vespa의 랭크 프로필 (RankProfile)은 multilingual-e5-large라고 불리는 단일 다국어 인코더 (multilingual encoder)를 사용하여 코사인 유사도 (cosine similarity)를 사용했습니다. 서류상으로는 코사인 점수 (cosine score)가 Hytale 쿼리에 대해 0.86으로 괜찮아 보였지만, 언어별 재현율 (recall)을 파헤쳐 보기 전까지는 그랬습니다. 한국어 쿼리의 재현율은 영어에 비해 34% 하락했습니다. 우리의 첫 번째 해결책은 Vespa의 내장 언어 폴백 (language fallback) 기능을 활성화하는 것이었으나, 이는 폴백이 보조 인덱스 (secondary index)에 대한 두 번째 쿼리를 트리거했기 때문에 40ms의 추가 왕복 시간 (round-trip)을 발생시켰습니다. p99 지연 시간은 110ms에서 155ms로 급증하여 모든 사용자에 대한 SLA를 위반했습니다.

그다음 우리는 Vespa의 문서 속성 (document attributes)을 사용하여 스크립트별로 코퍼스 (corpus)를 분할하는 것을 시도했습니다. 라틴 문자용 필드 하나, CJK(한중일)용 필드 하나, 키릴 문자용 필드 하나를 만든 것입니다. 한국어 쿼리에 대한 재현율은 91%로 급증했지만, 모든 문서를 세 개의 스크립트별 버킷 (buckets)으로 복제했기 때문에 인제스션 파이프라인 (ingestion pipeline)이 빌드당 2.1GB에서 갑자기 4.3GB로 부풀어 올랐습니다. GitHub Actions의 CI 러너 (CI runners)는 20분 후에 타임아웃이 발생하기 시작했고, 예전에 8분 걸리던 야간 빌드 (nightly build)는 이제 32분이 걸렸습니다. 운영 비용은 하룻밤 사이에 세 배로 뛰었고, 인프라 팀은 저에게 이모지 하나를 보냈습니다: 🔥.

아키텍처 결정

우리는 하이브리드 인코더 파이프라인 (hybrid encoder pipeline)을 채택하기로 결정했습니다. 영어 및 저자원 언어 (low-resource languages)를 위해서는 기존의 multilingual-e5-large를 유지했지만, 한국어, 일본어, 중국어의 경우에는 언어별 특화 인코더 (language-specific encoders)로 전환했습니다. 한국어는 klue-roberta-large, 일본어는 bert-base-japanese-v3, 중국어 간체는 bert-base-chinese를 사용했습니다. 데이터 수집 (ingestion) 부하가 커지는 것을 방지하기 위해, 임베딩 (embeddings)을 별도의 컬럼형 저장소 (columnar store, Apache Doris)에 저장하고, Vespa 검색 체인 (searcher chain) 내부의 경량 gRPC 심 (gRPC shim)을 통해 서빙했습니다. 이 gRPC 심은 콜드 캐시 (cold caches) 상태에서 단 8ms의 지연 시간 (latency)만을 추가했으며, Vespa 인덱스 (index)의 핫 데이터 (hot data) 크기를 기존 4.3GB에서 2.4GB로 줄였습니다.

가장 까다로운 부분은 캐시 무효화 (cache invalidation)였습니다. 우리는 Vespa 노드 레벨에서 언어 코드별로 샤딩 (shard)하기로 결정하여, 한국어 쿼리 (query)가 항상 한국어 인코더를 보유한 노드로 전달되도록 했습니다. 핫스팟 (hotspots)을 방지하기 위해 murmur3를 이용한 일관된 해싱 (consistent hashing)을 사용했습니다. 특정 언어의 트래픽이 급증할 경우, 코퍼스 (corpus)를 다시 구축할 필요 없이 클러스터 설정 (cluster config)의 한 줄을 변경하는 것만으로 경로를 재지정할 수 있었습니다.

변경 후 수치 결과

변경 후 언어별 트래픽 분산:

  • 영어 p99 지연 시간 (latency): 102 ms (110 ms에서 감소)
  • 한국어 p99 지연 시간 (latency): 115 ms (5 ms 증가했으나 여전히 120 ms 미만)
  • 한국어 쿼리 재현율 (Recall): 94 % (66 %에서 상승)
  • 데이터 수집 빌드 시간 (Ingestion build time): 10분 (32분에서 감소)
  • 월간 인프라 비용 (Monthly infra cost): $1,800 ($5,200에서 감소)

가장 결정적인 지표는 빈 응답률 (empty-response rate)이었습니다. 수정 전에는 전 세계적으로 2.8%였으며, 이는 전적으로 비영어권 쿼리에 의해 발생했습니다. 수정 후 빈 응답률은 0.1%로 떨어졌으며 모든 언어에서 일정하게 유지되었습니다. 데모 슬라이드에는 여전히 multilingual-e5-large가 핵심 인코더 (hero encoder)로 표시되지만, 실제 운영 환경 (production)에서는 트래픽의 60%를 언어별 특화 모델로 조용히 라우팅 (route)합니다. 데모에서는 심 (shim), 해시 링 (hash ring), 또는 우리가 데이터 수집 파이프라인 (ingestion pipeline)과 사투를 벌였던 그 밤에 대해서는 전혀 언급하지 않습니다. 그저 빛나는 Hytale 서버 목록만을 보여줄 뿐입니다.

내가 다르게 했을 것이라면

내가 다르게 했을 것이라면

나는 데모의 집계된 수치(aggregate numbers)를 신뢰하는 대신, 첫날부터 언어별 재현율 (Recall)을 측정했을 것입니다. 우리는 Discord에서 한국인 플레이어들이 불만을 제기하기 시작한 후에야 너무 늦게 재현율 (Recall)을 계측(instrument)했습니다. 둘째로, 다국어 인코더 (Multilingual encoders)의 벡터 차원 (Vector dimension)을 더 일찍 제한했을 것입니다. 원래의 Vespa 스키마 (Schema)는 1,024차원 벡터를 생성했습니다. 이를 768차원으로 줄이고 float16으로 전환함으로써, 측정 가능한 재현율 (Recall) 손실 없이 메모리를 30% 절감했습니다.

마지막으로, gRPC 심 (Shim)을 위한 예산을 더 일찍 책정했을 것입니다. 우리의 초기 계획은 심 (Shim)을 임시적인 해결책 (Workaround)으로 취급했습니다. 하지만 4주 차에 이르러 그것은 스택 (Stack)에서 가장 신뢰할 수 있는 구성 요소가 되었습니다. 교훈은 데모는 극적인 다국어 성능을 위해 최적화되지만, 프로덕션 (Production)은 지루하고 언어별로 특화된 실용주의 (Pragmatism)를 통해 생존한다는 것입니다.

AI 자동 생성 콘텐츠

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

원문 바로가기
0

댓글

0