•
이번 장에서는 온라인 모바일 게임의 리더보드, 순위표를 설계해 본다.
1단계: 문제 이해 및 설계 범위 확정
기능 요구사항
•
순위표에 상위 10명의 플레이어를 표시한다.
•
특정 사용자의 순위를 표시한다.
•
어떤 사용자보다 4순위 위와 아래에 있는 사용자를 표시한다.
비기능 요구사항
•
점수 업데이트는 실시간으로 순위표에 반영한다.
•
일반적인 확장성, 가용성 및 안정성 요구사항
개략적 규모 추정
•
게임 사용자가 24시간 동안 고르게 분포한다고 가정하면 DAU가 500만 기준 초당 평균 50명의 사용자가 게임을 플레이한다.
•
그러나 대부분의 게임 플레이는 특정 시간에 집중해서 발생하므로 이는 잘못된 가정이다. 따라서 최대 부하는 평균의 5배 정도인 250으로 가정한다.
•
사용자 점수 획득 QPS의 경우, 한 사용자가 평균 10번의 게임을 플레이한다고 가정하였으므로 평균은 50 * 10, 최대 부하는 250 * 10이다.
•
상위 10명의 순위표 가져오기 QPS의 경우, 각 사용자가 하루에 한 번 게임을 열고 상위 10명 순위표는 사용자가 처음 게임을 열 때만 표시한다고 가정하면 QPS는 약 50이다.
2단계: 개략적 설계안 제시 및 동의 구하기
API 설계
•
POST /v1/scores
◦
사용자가 게임에서 승리하면 순위표를 갱신한다.
•
GET /v1/scores
◦
순위표에서 상위 10명의 플레이어를 가져온다.
•
GET /v1/scores/{:userId}
◦
특정 사용자의 순위를 가져온다.
개략적 설계안
•
게임 서비스와 순위표 서비스가 존재하며 프로세스는 다음과 같다.
1.
사용자가 게임에서 승리하면 클라이언트는 게임 서비스에 요청을 보낸다.
2.
게임 서비스는 해당 승리가 정당하고 유효한 것인지 확인한 다음 순위표 서비스에 점수 갱신 요청을 보낸다.
3.
순위표 서비스는 순위표 저장소에 기록된 해당 사용자의 점수를 갱신한다.
4.
해당 사용자의 클라이언트는 순위표 서비스에 직접 요청하여 다음과 같은 데이터를 가져온다.
a.
상위 10명의 순위표
b.
해당 사용자 순위
•
이 설계안에서 더 생각해볼점은 다음과 같다.
◦
클라이언트가 순위표 서비스와 직접 통신해야하는가?
▪
중간자 공격이 발생할 수 있으므로 점수는 서버가 설정하게끔 해야한다.
◦
게임 서비스와 순위표 서버 사이에 메시지 큐가 필요한가?
▪
이는 게임 점수가 어떻게 사용되는지에 따라 달라지는데, 해당 데이터가 다른 곳에도 사용되는 등 확장성을 지원해야하는 경우, 큐를 사용하는 것이 합리적일 수 있다.
데이터 모델
•
관계형 데이터베이스
◦
규모 확장성이 중요하지 않은 경우
◦
규모가 크지 않은 경우
•
레디스
◦
안정적인 성능이 필요한 경우
•
저장소 요구사항
◦
최소한 사용자 ID와 점수는 저장해야하므로 ID가 24자 문자열이고 점수가 16비트라고 하는 경우 순위표 한 항목당 26바이트가 필요하다.
◦
26바이트 * 2500만(MAU)의 경우, 하루에 약 650MB의 저장공간이 필요하다.
◦
최대 QPS는 대략 2500/초인데, 이는 단일 레디스 서버로도 충분히 감당할 수 있다.
3단계: 상세 설계
클라우드 서비스 사용 여부
•
AWS 람다 등 서버리스 접근 방법을 사용하면 DAU 성장세에 맞춰 자동적으로 확장해나가므로 좋다.
레디스 규모 확장
•
데이터 샤딩 방안
◦
고정 파티션
▪
순위표에 등장하는 점수의 범위에 따라 파티션을 나누는 방안이다.
▪
이 파티션이 정상적으로 동작하려면 순위표 전반에 점수가 고르게 분포되어야 하며, 그렇지 않은 경우, 샤드에 할당되는 점수 범위를 조정하여 비교적 고른 분포가 되도록 연산이 필요하다.
◦
해시 파티션
▪
레디스 클러스터를 사용하는 것으로, 사용자들의 점수가 특정 대역에 과도하게 모여있는 경우 효과적이다.
▪
이는 각각의 키가 해시 슬롯에 속하도록 하는 샤딩 기법을 사용한다.
•
레디스 노드 크기 조정
◦
노드 크기를 설정하기 위해, 성능 벤치마킹 도구인 redis-benchmark를 활용해볼 수 있다.
▪
여러 클라이언트가 동시에 여러 질의를 실행하는 것을 시뮬레이션하여 주어진 하드웨어로 초당 얼마나 많은 요청을 처리할 수 있는지 측정한다.
대안: NoSQL
•
쓰기 연산에 최적화되어있고, 항목을 점수에 따라 효율적으로 정렬 가능한 훌륭한 대안이다.
•
그러나 규모 확장이 여러운데, 레코드가 많아지면 상위 점수를 찾기 위해 전체 테이블을 뒤져야하므로 사용자가 많아질수록 성능이 떨어진다.
4단계: 마무리
더 빠른 조회 및 동점자 순위 판정 방안
•
레디스 해쉬를 사용해 문자열 필드와 값 사이의 대응관계를 저장해둘 수 있다.
1.
순위표에 표시할 사용자 ID와 사용자 객체 사이의 대응관계를 저장하여 데이터베이스에 질의하지 않아도 빠르게 사용자 정보를 확인할 수 있도록 한다.
2.
두 사용자의 점수가 같은 경우, 누가 먼저 점수를 받았는지에 따라 순위를 매길 수 있다.
•
사용자 ID와 해당 사용자가 마지막으로 승리한 경기의 타임스탬프 사이의 대응 관계를 저장해 두는 것이다.
•
이를 기반으로 동점자가 발생하는 경우, 더 오래된 사용자의 순위가 높다고 하면 된다.
시스템 장애 복구
•
MySQL 데이터베이스에 타임스탬프와 함께, 기록을 저장해두어 대규모 장애가 발생했을때, 해당 데이터를 기반으로 데이터를 복구하도록할 수 있다.