실제로 해결해야 했던 문제
Hytale 엔진은 이벤트를 간단한 pub/sub 시스템인 EventManager를 통해 트리거합니다. 하지만 Veltrix를 동시 2,500명의 플레이어로 확장했을 때, 금요일 보물 사냥은 동시 참여자 부하가 1,200명 미만으로 멈추는 현상이 발생했습니다. 증상은 눈에 띄었습니다:
- EventManager 블락 큐가 Redis Streams에서 89%에 도달했습니다
- 보물 스폰당시 2.4초의 지연 발생
- 클라이언트 측 보물 활성화 시간 초과로 인한 플레이어 타임아웃, NRE-7280: 보물 상자 활성화 타임아웃—지역 53 응답 없음
- 15분 이내에 레디스 메모리 사용량이 2.1GB에서 11.2GB로 급증하여 캐시 계층의 OOM 킬러가 트리거됨
근본 원인은 논리가 아니었다. 설정이었다: 우리는 모든 지역에 대해 하나의 글로벌 이벤트 채널, 모든 보물 유형에 대해 하나의 Redis 스트림, 그리고 백프레셔가 없었다. 이벤트 매니저는 제어된 관개 시스템이 아닌 소화기처럼 다루어지고 있었다.
먼저 시도했던 것과 그 이유 (왜 실패했는지)
우리의 첫 번째 시도는 무식한 확장이었어요: 더 많은 Redis 쇄, 더 많은 소비자, 더 빠른 하드웨어. 우리는 문제에 대해 3개의 Redis 7.2 쇄를 투입했어요, 각각 4개의 지역에 걸쳐 8개의 소비자 그룹을 가지고 있었어요. 그것은 우리에게 부하 하에서 줄이는 것이 여전히 줄을 섰을 때 40분의 안정성을 샀어요. 왜?
- pub/sub 채널은 여전히 글로벌이었어요. Harbormere에 있는 보물이 여전히 Blightfen에 있는 것 뒤에 줄을 섰어요.
- 소비자 이탈: 플레이어들이 지역 간에 전이하면서 소비자 그룹을 완벽하게 전환하지 못했고, 이로 인해 중복된 스폰과 환영 상자가 발생했습니다.
- 절전기 없음. Redis 메모리가 폭발했을 때 OOM 캐너이터는 단순히 프로세스를 죽이지 않았지만, 전체 캐시 계층을 죽이며 모든 활성 플레이어 세션을 중단했습니다.
- OpenResty를 속도 제한기로 소개했지만, 각 생성마다 추가로 400ms의 지연 시간을 발생시켜 플레이어들이 움직임에서 끊김을 보고하기 시작했습니다.
어려운 진실은? 우리는 처리량을 신호의 완결성보다 최적화했습니다. 이벤트 스트림을 명확한 경계가 있는 제한된 맥락 대신 원시 데이터 파이프라인처럼 다루었습니다.
아키텍처 결정
우리는 엄격한 지역 사회 이벤트 버스 모델로 전환했습니다:
- 각 지역은 자신만의 고립된 Redis 스트림을 받았습니다(단일 스트림, 샤드가 아닙니다)
- 채널 이름을 Biome ID와 일치시켜 이름을 변경했습니다: Harbormere용 EventStream_53, Blightfen용 EventStream_71
- 보물 스폰 규칙은 지역화되었습니다: 명시적으로 허용되지 않는 한 교차 지역 스폰은 없습니다(교차 지역 유령 상자를 디버깅한 후 전적으로 비활성화했습니다)
- Go로 작성된 가볍게 설계된 이벤트 버스 게이트웨이를 소개했습니다. 이는 2 vCPU/4GB 각각으로 독립된 k3s 노드에서 실행됩니다. 이는 분산 라우터 역할을 했으며 소비자가 아닙니다
- 각 지역의 소비자 그룹은 Redis NACK에 지수 백오프를 적용한 최대 32개의 메시지를 동시에 처리할 수 있습니다
- 우리는 Redis의 maxmemory-policy를 allkeys-lru로 설정하고 8GB의 하드 제한을 추가했으며, 메모리가 6GB를 넘을 때 Lua 스크립트를 추가하여 강제 GC를 수행했습니다
- 우리는 보물 활성화 논리를 클라이언트 측에서 지역 미크로서비스 TreasureCore로 이동시켰습니다. Fly.io에서 실행되는 TreasureCore는 Postgres 16과 pgbouncer를 사용합니다. 이를 통해 REST 엔드포인트를 노출시켰습니다: POST /treasure/{biomeId}/activate, ETag 잠금을 통해 중복 생성을 방지합니다.
거래가 명확했습니다: 더 많은 운영 부담, 지역당 더 높은 비용, 그리고 지역 간 전송 시 일부 지연이 있었습니다. 하지만 우리는 편의성보다 올바름을 선택했습니다. 지역 모델은 해브머레이 트레asure 스폰이 플레이어가 이벤트 중에 전송되더라도 블라이트펜 체스트 생성을 막지 않았습니다.
숫자들이 말한 것은 뒤에
안정적인 운영 세 주 동안 후:
- Redis 메모리는 모든 스트림에서 3.2 GB로 안정화되었습니다(구 글로벌 모델 대비 71% 감소)
- 보물 스폰 지연 시간이 2.4초에서 180ms p99로 낮아졌습니다
- 부하 하에서 더 이상 NRE-7280 오류가 발생하지 않습니다—활성화 실패율이 12%에서 <0.1%
- 플레이어 경험 향상: 화면에서 가슴이 더 이상 깜빡이지 않고, 전송 유발 동기화 불량이 더 이상 발생하지 않습니다
- 비용: Redis에 대해 $47/월(기존 $189에서 낮춤), 그리고 6개의 TreasureCore 인스턴스에 대해 $112/월 추가. 우리는 안정성을 위해 14ms의 교역 영역 지연 시간을 희생했다.
메트릭스는 우리가 이미 의심했던 것을 알려줬다: 이벤트 엔진을 글로벌 시스템으로 취급하는 것은 반패턴이었다. 지역화는 조기 최적화가 아니라 피해 통제였다.
내가 다르게 할 것들
나는 다시 단일 글로벌 채널을 갖춘 이벤트 시스템을 설계하지 않겠습니다. 하이태일용도 아니고 어떤 게임용도 아닙니다. 강력한 지역화가 있었음에도 불구하고, 플레이어들이 피크 로드 시 지역을 일괄적으로 이동할 때 이벤트 게이트웨이를 경로 업데이트로 과부하시켰습니다. 우리의 해결책은 이동 유발 지역 전환에 쿨타임을 도입하는 것이었지만, 그것은 UX를 손상시켰습니다.
다음에 이벤트 버스를 더 나눌 거야: 정적 이벤트(고정된 상자)용 스트림 하나, 동적 이벤트(몬스터, 날씨, 타이밍 스폰)용 스트림 하나. NATS JetStream을 Redis Streams 대신 사용할 수 있다면 학습 곡선을 감당할 거야—이것은 스트림 수준의 백프레셔를 기본적으로 제공해줘.
다시 클라이언트 측 활성화를 신뢰하지 않겠습니다. Hytale 클라이언트는 여전히 JavaScript와 WebGL로 구성되어 있으며, 물리적 동기화 불일치는 피할 수 없습니다. 그 논리를 서버로 옮기세요.
그 금요일에 우리는 Veltrix가 무너지는 것을 피했습니다. 더 많은 하드웨어를 문제에 던지는 것이 아니라, 우리가 실제로 구축하고 있던 시스템의 경계를 존중하는 방식으로요. 그 교훈은 여전히 유효합니다: 사건들은 단순히 기능이 아니라—theyre contracts. 계약을 깨뜨리면 시스템도 함께 무너집니다.












