본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 27. 11:55

우리의 보물찾기 엔진이 동시 접속자 2,000명에서 충돌한 이유와 해결 방법

요약

동시 접속자 2,000명 상황에서 발생한 게임 설정 계층의 분산 시스템 충돌 사례를 다룹니다. YAML/JSON DSL 기반의 설정 푸시 방식과 Kubernetes ConfigMap, gRPC 사이드카 패턴의 한계를 분석하고 해결 과정을 설명합니다.

핵심 포인트

  • 분산 시스템에서 멱등성(Idempotency) 부재로 인한 중복 이벤트 발생 문제
  • Kubernetes ConfigMap 기반 핫 리로드 시 발생하는 재시작 폭풍(Restart Storms) 현상
  • gRPC 사이드카 패턴 도입 시 캐시 무효화 및 롤백 전략의 중요성
  • DSL 대신 Rust 코드를 생성하는 선언적 규칙 컴파일러로의 아키텍처 전환

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

진짜 문제는 보물찾기 로직이 아니었습니다. 바로 Veltrix 설정 계층(configuration layer)이었는데, 이는 엔지니어가 아닌 사람들도 코드 수정 없이 게임 동작을 조정할 수 있도록 설계된 얇은 YAML/JSON DSL (Domain Specific Language)이었습니다. 하지만 실제로 이것은 분산 시스템(distributed systems)의 악몽이 되어버렸습니다. 스폰율(spawn rates), 전리품 테이블(loot tables), 또는 이벤트 타이머를 변경할 때마다 운영 콘솔(operator console)은 전체 규칙 세트(ruleset)를 다시 컴파일하여 500개의 엣지 서버(edge servers)로 푸시했습니다. 설정 계층과 서비스 사이의 네트워크는 UDP 기반이었고, 우리에게는 멱등성(idempotency)이 없었습니다. 동시 접속자 2,000명 상황에서는 패킷 하나만 유실되어도 47개의 중복 이벤트 실행으로 이어지는 연쇄 반응이 일어났고, 이는 인벤토리 복사, 텔레포트 버그, 그리고 Discord에서의 분노 섞인 스레드로 이어졌습니다.

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

우리는 Kubernetes에서 Git 기반의 ConfigMap을 사용하는 전형적인 핫 리로드(hot-reload) 전략으로 시작했습니다. CI 파이프라인(CI pipeline)이 PR을 병합하고, Docker 이미지를 빌드하며, 자동으로 포드(pods)를 재시작하는 방식이었습니다. 스테이징(staging) 환경에서는 잘 작동했지만, 프로덕션(production) 환경에서는 재시작 폭풍(restart storms)이 발생하여 오토스케일러(autoscaler)가 30초 이내에 최대 포드 수에 도달하게 만들었습니다. 또한, 동적 설정 로더(dynamic config loader)가 원자적 쓰기(atomic writes)를 가정했기 때문에 새로운 포드들은 즉시 충돌했습니다. 절반 정도의 확률로 YAML 파서(parser)는 124번 라인에서 MalformedSequence 에러를 던졌고, 운영 콘솔은 우아하게 롤백(rollback)하는 대신 '규칙 세트 조정 실패(Failed to reconcile ruleset)'라는 빨간색 배너를 표시했습니다.

그다음 우리는 gRPC 설정 서버 (config server)를 활용한 사이드카 패턴 (sidecar pattern)을 시도했습니다. 핵심 아이디어는 게임 서비스 (game services)를 규칙 엔진 (rules engine)으로부터 분리하는 것이었습니다. 서버는 지속적인 연결 (persistent connection)을 통해 델타 (deltas)를 스트리밍하고, 각 서비스는 변경 사항을 로컬에서 적용합니다. 첫 일주일은 유망해 보였습니다. 플레이어 5,000명 기준 지연 시간 (latency)이 150ms로 떨어졌기 때문입니다. 하지만 3일째 되는 날, 예정된 데이터베이스 유지보수 중에 gRPC keepalive 타이머가 작동했고, 연결이 재설정되었으며, 캐시 무효화 (cache invalidation) 로직이 실행되었습니다. 알려진 정상 상태 (known-good state)로 롤백할 수 있는 능력이 없었기에, 모든 게임 인스턴스가 지난 15분 동안의 스폰 이벤트 (spawn events)를 다시 재생했고, 플레이어들이 동시에 열 수 있는 중복된 전리품 상자 (loot boxes)를 생성했습니다. 동일한 동굴에서 20마리의 똑같은 드래곤이 스폰되는 화면 녹화 영상과 함께 고객 지원 티켓이 쌓여갔습니다.

아키텍처 결정 (The Architecture Decision)

우리는 운영자 대상의 DSL (Domain Specific Language)을 완전히 포기했습니다. 대신, 빌드 타임 (build time)에 Rust 코드를 생성하는 선언적 규칙 컴파일러 (declarative rules compiler)를 구축했습니다. 규칙은 제한된 HCL 방언 (dialect)으로 작성되고, 프리 커밋 훅 (pre-commit hook)에 의해 검증되며, 각 게임 인스턴스 옆의 사이드카에서 실행되는 정적 바이너리 (static binary)로 컴파일됩니다. 사이드카는 클러스터 전체 상태를 위해 가십 기반 합의 계층 (gossip-based consensus layer, etcd)과 통신하지만, 각 인스턴스는 자신만의 컴파일된 규칙 세트 (ruleset)만 소비합니다. 핵심적인 트레이드오프 (tradeoff)는 개발 속도 (developer velocity)와 운영 안정성 (operational safety) 사이의 균형이었습니다. 규칙 세트가 컴파일되면 불변 (immutable) 상태가 됩니다. 스폰율 (spawn rates)을 변경해야 하는 경우, Helm 차트 (Helm chart)의 버전을 올리고 블루-그린 배포 (blue-green deployment)를 트리거하며, 새 버전이 안정화될 때까지 이전 버전이 활성 상태로 유지됩니다. 이제 롤백 경로는 차트 버전을 되돌리는 단일 kubectl patch 명령어로 단순화되었습니다.

또한 설정 전파 (config propagation)를 위해 UDP를 TCP 기반의 가십 프로토콜 (gossip protocol)로 교체했습니다. 가십 계층은 벡터 시계 (vector clocks)를 사용하여 중복 이벤트를 감지하고 거부하며, 각 서버는 현재 상태의 머클 트리 (Merkle tree)를 유지하여 복구 중 즉각적인 차이점 (diffs)을 확인할 수 있게 합니다. 지연 시간 비용은 델타당 약 40ms가 발생하지만, 결정론적 재생 (deterministic replay)과 데이터 손실 제로 (zero data loss)를 얻었습니다.

이후 수치가 말해준 것 (What The Numbers Said After)

컴파일된 규칙 세트 (compiled rulesets) 및 가십 기반 전파 (gossip-based propagation) 방식으로 마이그레이션한 후, 시스템은 95백분위수 지연 시간 (95th-percentile latency) 420ms로 12,000명의 동시 접속 플레이어를 처리했습니다. 운영 콘솔은 더 이상 잘못된 YAML 입력으로 인해 충돌하지 않으며, 롤백 (rollback) 시간은 5분에서 30초로 단축되었습니다. 중복 이벤트 (duplicate events) 발생률은 18%에서 0.02%로 떨어졌습니다. 가장 놀라운 점은 성능이었습니다. Rust로 컴파일된 규칙 세트는 동적 파서 (dynamic parser)와 리플렉션 (reflection) 레이어가 제거됨에 따라 CPU 사용량을 38% 감소시켰습니다.

내가 다르게 했을 것들 (What I Would Do Differently)

나는 다시는 운영자에게 YAML을 주지 않을 것입니다. 다음에 런타임 유연성 (runtime flexibility)이 필요하다면, Rust 대신 WebAssembly (Wasm)로 컴파일되는 작은 DSL (Domain Specific Language)을 사용할 것입니다. Wasm 모듈은 원자적 (atomically)으로 업데이트되고 샌드박스 (sandboxed) 처리될 수 있어, 불변성 (immutability)의 안전성과 핫 리로드 (hot reload)의 유연성을 동시에 제공할 수 있습니다. 또한, 가십 레이어 (gossip layer)에 OpenTelemetry 트레이스 (traces)를 적용하여 프로덕션 (production) 환경에 도달하기 전에 불일치 (divergence)를 포착할 수 있도록 하겠습니다. 그리고 단순한 하트비트 (heartbeats) 이상의 용도로 UDP가 수용 가능하다는 척하는 것도 그만둘 것입니다.

AI 자동 생성 콘텐츠

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

원문 바로가기
0

댓글

0