Design Real-time Game Leaderboard
- 상위 플레이어의 순위 실시간 제공
- 특정 사용자의 일정 범위의 순위 제공
- DAU 500만 명, MAU 2,500만 명
- 하루 평균 10 게임 플레이
- 순위 반영은 가능한 실시간에 가깝게 처리
위 요구 사항을 만족하기 위해 다음과 같은 개략적인 규모를 추정할 수 있다.
- 초당 평균 유저: 500만 명 / 24 / 3600 = 57.87 (24시간 고르게 분포되어 있다고 가정)
- 진행 게임: 57.87 * 10 = 578.7 (하루 평균 10게임 플레이한다고 가정)
- 사용자 점수 획득 QPS: 578.7 * 5 = 2,893.5 (피크 시간에 5배의 트래픽이 발생한다고 가정)
- 순위 조회 QPS: 57.87 (최초 접속 시 조회한다고 가정)
API 단위 기능 정의
Section titled “API 단위 기능 정의”POST /scores: 게임 플레이 후 점수를 기록하는 APIGET /scores: 상위 플레이어의 순위를 조회하는 APIGET /scores/{user_id}: 특정 사용자의 순위를 조회하는 API
개략적 설계안
Section titled “개략적 설계안”게임 서비스와 순위표 서비스로 나누어 아래와 같은 흐름으로 설계할 수 있다.
- 사용자가 게임을 플레이하고 점수를 획득 후 게임 서비스에 요청
- 게임 서비스는 점수가 유효한지 확인 후 순위표 서비스에 점수 기록 요청
- 순위표 서비스는 점수를 기록하고, 해당 사용자의 순위를 갱신
- 순위 갱신 시 Redis의 Sorted Set을 사용하여 점수를 내림차순으로 정렬
- 사용자는 지속적으로 순위표 서비스에 플레이어의 순위를 조회
게임 서비스와 순위표 서비스 간 통신에는 상황에 따라 REST API 또는 메시지 큐를 사용할 수 있다.
저장소 요구사항 - Redis Sorted Set 사용
Section titled “저장소 요구사항 - Redis Sorted Set 사용”순위 정보를 Redis의 Sorted Set을 사용하여 저장한다고 가정하고, 다음과 같은 요구 사항이 있다고 가정하자.
- 사용자 ID - 점수 형태로 저장
- 월간 활성 사용자 2,500만 명 모두 순위에 포함
- ID 24 character 문자열 / 점수 16비트 정수 = 26바이트
- 26바이트 * 2,500만 명 = 약 650MB
650MB는 오버헤드와 정렬 집합 해시를 고려해 두 배를 사용한다고 가정하더라도, 한 대의 Redis 인스턴스에 충분히 저장할 수 있으며, 2,900 QPS도 한 대의 Redis에서 처리할 수 있는 범위이다.
Redis Sorted Set 동작 방식
Section titled “Redis Sorted Set 동작 방식”Redis의 Sorted Set은 해시 테이블과 스킵 리스트 두 가지 자료구조를 사용하여 구현된다.
- 해시 테이블: 사용자의 점수 저장
- 스킵 리스트: 특정 점수를 딴 사용자들의 목록을 저장
이 중 스킵리스트는 빠른 검색을 가능하게 하는 자료 구조로, 정렬된 연결 리스트에 다단계 색인을 두어 검색 속도를 높인다.
1 -------------- 15 ---------------- 60| | |1 ----- 8 ------ 15 ------ 36 ------ 60| | | | |1 - 4 - 8 - 10 - 15 - 26 - 36 - 45 - 60 - 68이진 검색 트리처럼 중간 지점에 더 빨리 도달할 수 있도록 n차의 색인을 추가하여, 검색 속도를 O(log n)으로 줄일 수 있다.
Redis Sorted Set 명령어
Section titled “Redis Sorted Set 명령어”Redis Sorted Set에서 사용하게 되는 명령어는, 다음과 같이 활용할 수 있다.
ZADD: 기존에 없던 사용자를 집합에 추가하거나, 기존 사용자 점수를 갱신ZINCRBY: 기존 사용자 점수 증감ZRANGE/ZREVRANGE: 특정 범위에 있는 사용자 조회ZRANK/ZREVRANK: 특정 사용자의 순위 조회
클라우드 서비스 사용
Section titled “클라우드 서비스 사용”자체 서비스를 구축하는 경우, Redis와 MySQL를 직접 사용하여 구현할 수 있지만, 클라우드 서비스를 사용한다고 가정하면 API Gateway와 AWS 람다 기술을 사용해볼 수 있다.
- API Gateway를 호출하고, 해당 게이트웨이는 적절한 AWS 람다 함수를 호출
- 람다 함수는 스토리지 계층의 명령을 호출하여 얻은 결과를 API Gateway에 반환
- API Gateway는 람다 함수의 응답을 클라이언트에 반환
이렇게 하면, 서버 인스턴스를 만들지 않아도 API를 제공할 수 있으며, 서버 관리의 부담을 줄일 수 있다.
레디스 규모 확장
Section titled “레디스 규모 확장”기존 규모보다 더 큰 규모로 확장하기 위해선 샤딩이 필요한데, 고정 파티션과 해시 파티션 두 가지 방법이 있다.
- 고정 파티션: 점수 범위에 따라 파티션을 나누는 방법
- 0
100점은 파티션 A, 101200점은 파티션 B 등 - 해당 사용자가 어느 파티션에 속하는지 알아야 함(사용자ID-점수를 저장하는 2차 캐시를 통해 성능을 높일 수 있음)
- 점수가 고르게 분포되어 있어야 기대하는 성능을 낼 수 있음
- 0
- 해시 파티션: 레디스 클러스터가 자동으로 샤딩
- 각각의 키가 특정한 해시 슬롯에 속하도록 하는 샤딩 기법 사용
- 16,384개의 해시 슬롯이 있으며,
CRC16(key) % 16384로 해시 슬롯을 계산 - 노드 별로 해시 슬롯 범위를 할당(0 - 5500은 노드 A, 5501 - 11000은 노드 B 등)
- 해당 슬롯에 속하는 레디스 노드로 요청을 전달
- 16,384개의 해시 슬롯이 있으며,
- 점수 분포가 고르지 않아도 성능을 유지할 수 있음
- 상위 N명의 사용자 조회 시, 모든 노드에 요청을 보내고 응답을 합쳐야 하므로 성능이 저하될 수 있음
- 각각의 키가 특정한 해시 슬롯에 속하도록 하는 샤딩 기법 사용
서비스 특성상 상위 N명의 사용자 조회가 빈번하게 발생하기 때문에, 고정 파티션을 사용하는 것이 더 적합할 수 있다.
추가 고려사항
Section titled “추가 고려사항”더 빠른 조회 및 동점자 순위 판정
Section titled “더 빠른 조회 및 동점자 순위 판정”레디스 해시를 사용하면 문자열 필드와 값 상의 대응관계를 저장하여 활용할 수 있다.
- 순위표에 표시할 사용자 ID와 사용자 객체 사이의 대응 관계를 저장
- 데이터베이스에 질의 하지 않고, 빠르게 사용자 정보를 조회할 수 있음
- 두 사용자 점수가 같은 경우 누가 먼저 점수를 획득했는지 판별하기 위해, 사용자 ID와 점수 획득 시간을 함께 저장
- 동점자 순위 판별을 위해 시간 정보를 추가하여, 동일 점수의 사용자 중 먼저 점수를 획득한 사용자가 상위 순위를 차지하도록 함
시스템 장애 복구
Section titled “시스템 장애 복구”시스템 장애가 발생했을 때, 다음과 같은 복구 방법을 고려할 수 있다.
- RDB 스냅샷 / AOF(Append Only File) 로그를 사용하여 Redis 데이터를 복구
- MySQL에 점수 증감 이력을 저장하여, ZADD / ZINCRBY 명령어를 사용해 점수를 복구