Q: How many URLs are generated per second? 200
Storage: 200 * 60 * 60 * 24 * 365 * 5 * (300 Byte) = 9.5 TB
Q: How many URLs are redirected per second?
20,000
Query rate: 20,000 URLs / s
Bandwidth:
shorten: 200 * 200Byte * 8 bits = 320 Kbps
redirect: 20000 * 200Byte * 8bits =32 Mbps
Cache Memory: cache 20% daily
20000 * 60 * 60 * 24 * (200 Byte) * 20% = 69 GB
Servers needed at peak load
20000 * 5000 / 64000 RPS = 1562 servers
Response: short_url
Response: original_url
Store
MongoDB
User
userId (20 byte) Primary Key
user_name (20 bytes)
createTime (10 Byte)
every row is 50 byte
url
short_url (50 bytes) Primary Key
user_id (20 bytes) Secondary index
original_url (200 bytes)
creation_time (10 bytes) Secondary index
expiry_time (10 bytes) SecondaryKey
every row is 300 byte
flowchart TD C[client] --shortenURL(), redirectURL()--> API[API Gateway] API --shortenURL()--> SHORT[Shortening Service] C --redirectURL()-->CDN[CDN] API-->Map[Mapping Service] SHORT --> MQ[Message Queue] QW[Queue Worker] --> MQ QW --> Cache[Cache] QW --> DB[Database] Map --> Cache Map --> DB
shortenURL() API:
Client sends the request. API Gateway forwards it to Shortening Service. It creates the short URL -> long URL mapping and store it in the database.
redirectURL() API:
Client sends the request. If the mapping is found in a regional, nearby CDN, it is returned from the CDN. Otherwise, the request reaches API Gateway and the Mapping Service. It checks if the mapping exists in Redis Cache. If it does, the mapping is returned. If not, it reads the Database.
sequenceDiagram
participant Client
participant APIGateway
participant AuthService
participant ShortenURLServer
participant Cache
participant Database
participant URLGenerator
Client->>APIGateway: POST {long_url}
APIGateway->>AuthService: Check Auth
AuthService-->>APIGateway: Auth Success
APIGateway->>ShortenURLServer: Forward Request
ShortenURLServer->>Cache: Check Cache for {long_url}
Cache-->>ShortenURLServer: Miss (No Result)
ShortenURLServer->>Database: Check Database for {long_url}
alt Long URL exists
Database-->>ShortenURLServer: Found {short_url}
ShortenURLServer-->>Client: Return {short_url}
else Long URL does not exist
Database-->>ShortenURLServer: Not Found
ShortenURLServer->>URLGenerator: Generate new {short_url}
URLGenerator-->>ShortenURLServer: New {short_url}
ShortenURLServer->>Database: Store {long_url, short_url}
ShortenURLServer->>Cache: Optionally Cache {long_url, short_url}
ShortenURLServer-->>Client: Return new {short_url}
end
sequenceDiagram participant Client participant APIGateway participant AuthService participant RedirectURLServer participant Cache participant Database participant URLGenerator Client->>APIGateway: GET {short_url} APIGateway->>AuthService: Check Auth (OAuth) AuthService-->>APIGateway: Auth Success APIGateway->>RedirectURLServer: Forward Request RedirectURLServer->>Cache: Check Cache for {short_url} alt Short URL in Cache and Valid Cache-->>RedirectURLServer: Found and Valid RedirectURLServer-->>Client: Return 301 Redirect to Original URL else Short URL not in Cache or Expired/Deleted Cache-->>RedirectURLServer: Not Found RedirectURLServer->>Database: Check Database for {short_url} alt Short URL in Database Database-->>RedirectURLServer: Found RedirectURLServer-->>Client: Return 301 Redirect to Original URL RedirectURLServer->>Cache: Optionally Cache {short_url} else Short URL not in Database Database-->>RedirectURLServer: Not Found RedirectURLServer->>URLGenerator: Generate new {short_url} URLGenerator-->>RedirectURLServer: New {short_url} RedirectURLServer->>Database: Store {long_url, short_url} RedirectURLServer->>Cache: Optionally Cache new {short_url} RedirectURLServer-->>Client: Return New {short_url} end end
[Mid-level deep dive topic]
Performance and scalability of redirectURL() API is extremely important for this system. As such, we employ two levels of caching. Requests will naturally have locality of access, so caching will be effective.
At the closest location from the clients, we will have CDN storing short -> long mappings for the most frequently requested URLs. For example, if a celebrity posts a short URL link in their Social Network post, this mapping should be in CDN. CDN can be hosted at Internet Exchange Points (IXPs), making the response time from client quite short. It has limited storage space, so it should store a small set of the most frequently accessed mappings. High volume of requests are handled by CDN, without even reaching the API Gateway. It is quite beneficial from scalability & fault tolerance perspective.
In the data center, we will employ a caching node, e.g., Redis. As we can install multiple Redis nodes with 100s of GBs of memory, it can store larger set of mappings. It is still faster than accessing the database, so this would provide performance and scalability gain.
Both CDN and Redis Cache can employ Least Recently Used eviction algorithm to ensure currently popular mappings stay in cache.
[Mid-level deep dive topic]
Database and Cache should be partitioned for improved scalability.
Short URL is a good choice for a partitioning key because:
Other partitioning keys (long URL, user ID) would have disadvantages about these points.
Database: MongoDB, because the system is read heavily and the data is unstructured. atomatically when write.
Message queue like Kafka for the invalidate cache, because the async protocol and eventual consistency.
[Junior-level deep dive topic]
There are two ways to create a short URL:
There is a tradeoff:
Pro of Hash approach is that you don't have to generate random numbers. Con is that the created hashes might collide. In particular, since our random string (8 characters) will be shorter than what the hash algorithms generate (20 bytes or larger), the risk of collision would increase.
Pro of random generation is the possibility of collision is lower. If a newly created random string collides with an already existing one, we can simply generate one more random string. Con is that it would require computational power to generate random numbers. However, since Linux and other OSes support fast random number generation with /dev/urandom, we assume the cost is manageable.
We will pick random generation in this exercise.
we can add read limiter to limit the number of requests for the short-term reduce the query
and add cache for all levels, local, DNS, webserver, apiserver, database to avoid the database hit and improve the latency.
For the long term, we can add more server, use the database sharding and database geologically distribute to improve the database query efficiency.
supporting custom URL