Leaderboard
Requirements
Functional
- User should be able to see top 10 players based on score
- Can see specific player's rank as well
- Can see surrounding players rank if search a specific player
- leaderboard can rank player's based on daily, monthly (time filter)
- Can see historic match leaderboard as well
- Leaderboard can be updated in full distributed manner
- Should support thousand of players
- Need realtime
Non functional
- High availability
- Low latency
- Scalability
- Reliability
Estimation
- Users are globally distributed
- read/write ratio - 5:1
- Daily active users for writes - 50 Million
- Player id can be 30 bytes string and score can be 16 bit int consuming 2 bytes.
Traffic
| Desc | value |
|---|---|
| DAU (write) | 50 M |
| QPS(write) | (50M)/(sec in one day) = 587 (600 approx) |
| QPS(read) | 600*5 = 3000 |
| Peak QPS(read) | 4000 (assumption) |
Memory (for fater use)
| Desc | value |
|---|---|
| total player | 50 M |
| Single player record | (30+2) = 32 bytes |
| total storage for all player | 50M*32 = 1.6 GB |
Storage
| Desc | value |
|---|---|
| storage for a day all player | 1.6 GB |
| total storage for all player for 5 years | 1.6 GB * 5 *365 = 2.92 TB |
Bandwidth
Ingress is the network traffic that enters the server (client requests) and Egress is the network traffic that exits the server(server resp)
| Desc | value |
|---|---|
| Ingress | 32 bytes/player _ 50M players/day _ 10^-5 day/sec = 16 KB/sec |
| Egress | 64 bytes/player _ 250 million players/day _ 10^(-5) day/sec = 160 KB/sec |
API design
- for each of scalability we will be using REST. as it allows loose coupling as than RPC.
Update player score
- Request
/players/:player-id/scores
method: POST
authrorization: Bearer <token>
content-length: 100 (size of the request body in bytes)
content-type: application/json
content-encoding: gzip (lossless encoding for reduced data size)
{
player_id:<int>,
score:<int>,
location:geohash
} - Response
200 - success
202 - accepted for async processing
403 - Unauth
400 - invalid payload
Get a single player score
-
Request
/players/:player-id
method: GET
authrorization: Bearer <token>
accept: application/json, text/html
user-agent: chrome -
Response
status code: 200 OK
cache-control: private, no-cache, must-revalidate, max-age=5
content-encoding: gzip
content-type: application/json
{
player_id: <string>,
player_name: <string>,
score: <int>,
rank: <int>,
updated_at: <date_timestamp>
}403 - Unauth
404 - not found
Get top N player
-
Request
/players/top/:count
method: GET
authrorization: Bearer <token>
accept: application/json, text/html
user-agent: chrome -
Response
status code: 200 OK
cache-control: public, no-cache, must-revalidate, max-age=5
content-encoding: gzip
content-type: application/json
{
count: 10(count),
updated_at:<timestamp>,
data: [
{
player_id: <string>,
player_name: <string>,
score: <int>,
rank: <int>
}
]
}403 - Unauth
Get surrounding players score
-
Request
/players/:player-id/:count
method: GET
authrorization: Bearer <token>
accept: application/json, text/html
user-agent: chrome -
Response
status code: 200 OK
cache-control: public, no-cache, must-revalidate, max-age=5
content-encoding: gzip
content-type: application/json
{
count: 10(count),
updated_at:<timestamp>,
data: [
{
player_id: <string>,
player_name: <string>,
score: <int>,
rank: <int>
}
]
}403 - Unauth
Service health
/:service/health
method: HEAD
Response
200 - OK
500 - error
DB
SQL schema
Redis schema
-
here Game and player table is having
1 to manybsc one player can play multiple games -
Friends and player is like follower-followee relationship. (associate entity)
-
Games and leaderboard is
1 to manyas one game can have multiple leaderboards like regional, global, friend_circle. -
Players and leaderboard is also
1 to manyas one player can be part of multiple games so multiple leaderboards. -
SQL database is good if we have to design system for mid level usage and for high usage system Relational DB is not favourable bcs of few reason
- Everytime we have to calculate the rank the full table lookup is needed
- Also if we do indexing the write will become expensive and it will not bring much speed
- Caching will have stale data issue
- the computation of rank in player will require nested queries will not be efficient.
-
For our usecase we need a DB that store data in sorted manner so the fetch of data is very fast. Redis sorted-set is a good option for this.
-
So choice is
- Redis sorted set , but as we know redis sorted set only store score and member identifier so we can use a combination of
Sorted set + Hashto store data. - identifier can contain data like
user:id. - In Hset we can store data like
HSET user:123 name "John Doe" image "john.jpg" location "USA" email "john@example.com"- Image can be stored in object storage Amazon S3
- Redis sorted set , but as we know redis sorted set only store score and member identifier so we can use a combination of
-
How we will achieve other functionalities in case of redis DB
- leaderboard based on game - make a new sorted set
- Similar can be done for based on location and all
- or another thing can be we can store other data in DB (other than redis) and fetch data based on user id
High Level Design
- The client creates a WebSocket connection to the load balancer for real-time communication
- The load balancer delegates the client’s request to the closest data center
- The server queries the cache server to display the leaderboard
- The server queries the relational database on a cache miss and populates the cache server with fetched data
- In cache miss we can use
LRUpolicy.
Design Deep dive
Get top 10 players
ZREVRANGE <key> <start> <stop>
e.g.
ZREVRANGE leaderboard 0 9
After this we can get metadata of the players using the command HMGET from the Hash.
Get rank of a given player
ZREVRANK <key> <member>
returns the rank of member in the sorted set stored at key, with the scores ordered from high to low. The rank is 0-based, which means that the member with the highest score has rank 0.
e.g.
ZREVRANK leaderboard "user:id"
Get surrounding player for a given game
- Fetch rank of given player
- Get surrounding players based on the rank-x to rank+x
ZREVRANK leaderboard "user:id"
res : (integer) 11
ZREVRANGE leaderboard 8 14
FOR MORE DESC visit : Redis Sorted set
How to send score notification update
- DB change can trigger (when score of a player is beaten by another) serverless function call to notify.
- We can also use the Bloom filter to make sure user receives notifcation only once on rating change.
- we can use Pub-sub pattern for this.
PENDING
Global leaderboards
- We can manage multiple sorted sets for distinct gaming scenarios. The global sorted-set can include the agreegated score across all gaming scenarios.
- Can use ZUNIONSTORE command.
Historical leaderboards
- We can use ttl based cache for historical leaderboards and also we can utilize REST instead of socket as global leaderboard can use polling, and big in size.
- Historical we can move to cols storage for cost efficiency
- Flow
- Client checks in CDN for extremely popular leaderboards
- client -> LB -> server -> serverless fn
- serverless function is easy to scale and cost effetive, it checks cache first and if not found in cache then check relational DB, and for image fetch from object storage.
Leaderboards based on daily, weekly, monthly
A new sorted set can be created for different time ranges. At the time end we can calculate from existing and do.
Leaderboards for friend circle
we can create a friend leaderboard
| Feature | Redis Implementation |
|---|---|
| Global Leaderboard | ZADD global_leaderboard <score> <user_id> |
| Store Friends | SADD friends:<user_id> <friend1> <friend2> |
| Create Friend Leaderboard | ZADD leaderboard:<user_id> <score> <friend_id> |
| Fetch Friend Leaderboard | ZRANGE leaderboard:<user_id> 0 -1 WITHSCORES |
| Update Friend Leaderboard | ZADD leaderboard:<user_id> <updated_score> <friend_id> |
High Availability
Enable autoscaling as using serverless functions
Low latency
Can deploy server in multiple region and also using cdn for some usecases. Although sorted set gives logarithmic time complexity.
Scalability
Caching layer is added for read-heavy loads as clients viewing leaderboard. we can also partition the data.
Reliability
We can setup monitoring with help of Prometheus as time series data and Grafana as dashboard. We can also implement circuit breaker for fault tolerance.
Security
- JWT for auth
- rate limiting
- encrypt traffic