Let’s design a Pastebin like web service, where users can store plain text.
Users of the service will enter a piece of text and get a randomly generated URL to access it.
Similar Services: pasted.co, hastebin.com, chopapp.com
Difficulty Level: Easy
Step-1: What is Pastebin?
Pastebin like services enable users to store plain text or images over the network (typically the Internet) and generate unique URLs to access the uploaded data.
Such services are also used to share data over the network quickly, as users would just need to pass the URL to let other users see it.
If you haven’t used pastebin.com before, please try creating a new ‘Paste’ there and spend some time going through different options their service offers. This will help you a lot in understanding this chapter better.
Users can upload maximum 10MB of data; commonly Pastebin like services are used to share source code, configs or logs.
Such texts are not huge, so let’s assume that each paste on average contains 10KB.
At this rate Data Storage per day: 1M * 10KB => 10 GB/day
If we want to store this data for 10 years, we would need the Total storage capacity = 36TB.
With 1M pastes per day we will have 3.6 billion Pastes in 10 years.
We need to generate and store keys to uniquely identify these pastes.
If we use base64 encoding ([A-Z, a-z, 0-9, ., -]) we would need 6 letters strings: 64^6 ~= 68.7 billion unique strings.
If it takes one byte to store one character the total size required to store 3.6B keys would be: 3.6B * 6 => 22 GB
22GB is negligible compared to 36TB. To keep some margin, we will assume a 70% capacity model (meaning we don’t want to use more than 70% of our total storage capacity at any point), which raises our storage needs up to 47TB.
Bandwidth estimates:
For write requests, we expect 12 new pastes/second, the total incoming data per second (ingrees): 12 * 10KB => 120 KB/sec
For read requests, we expect 58 requests/second, the total outgoing data per second (egrees): 58 * 10KB => 0.6 MB/sec
Although total ingress and egress are not big, we should keep these numbers in mind while designing our service.
Memory estimates:
We can cache some of the hot pastes that are frequently accessed.
Following 80-20 rule, meaning 20% of hot pastes generate 80% of traffic, we would like to cache these 20% pastes.
Since we have 5M read requests per day, to cache 20% of these requests, we would need cache storage: 0.2 * 5M * 10KB ~= 10 GB.
Step-5: System APIs
We can have SOAP or REST APIs to expose the functionality of our service.
api_dev_key (string): The API developer key of a registered account. This will be used to, among other things, throttle users based on their allocated quota.
paste_data (string): Textual data of the paste.
custom_url (string): Optional custom URL.
user_name (string): Optional user name to be used to generate URL.
paste_name (string): Optional name of the paste
expire_date (string): Optional expiration date for the paste.
We would need two tables, one for storing information about the Pastes and the other for users’ data.
Step-7: High Level Design
At a high level, we need an application layer that will serve all the read and write requests.
Application layer will talk to a storage layer to store and retrieve data.
We can segregate our storage layer with one database storing metadata related to each paste, users, etc., while the other storing paste contents in some sort of block storage or a database.
This division of data will allow us to scale them individually.
Step-8: Component Design
a) Application Layer
Our application layer will process all incoming and outgoing requests.
The application servers will be talking to the backend data store components to serve the requests.
Upon receiving a write request, our application server will generate a six-letter random string, which would serve as the key of the paste (if the user has not provided a custom key).
The application server will then store the contents of the paste and the generated key in the database.
After the successful insertion, the server can return the key to the user.
Possible Problem:
The insertion fails because of a duplicate key.
Since we are generating a random key, there is a possibility that the newly generated key could match an existing one.
Solution:
In that case, we should regenerate a new key and try again.
We should keep retrying until we don’t see a failure due to the duplicate key.
We should return an error to the user if the custom key they have provided is already present in our database.
Run a standalone KGS that generates random six letters strings beforehand and stores them in a database (let’s call it key-DB).
Whenever we want to store a new paste, we will just take one of the already generated keys and use it.
This approach will make things quite simple and fast since we will not be worrying about duplications or collisions.
KGS will make sure all the keys inserted in key-DB are unique.
KGS can use two tables to store keys, one for keys that are not used yet and one for all the used keys.
As soon as KGS give some keys to any application server, it can move these to the used keys table.
KGS can always keep some keys in memory so that whenever a server needs them, it can quickly provide them.
As soon as KGS loads some keys in memory, it can move them to used keys table, that make sure each server gets unique keys.
If KGS dies before using all the keys loaded in memory, those keys will be wasted but can ignore given a huge number of keys we have.
Isn’t KGS single point of failure ?
Yes, it is. To solve this, we can have a standby replica of KGS, and whenever the primary server dies, it can take over to generate and provide keys.
Can each app server cache some keys from key-DB ?
Yes, this can surely speed things up. Although in this case, if the application server dies before consuming all the keys, we will end up losing those keys.
This could be acceptable since we have 68B unique six letters keys, which are a lot more than we require.
How to handle a paste read request ?
Upon receiving a read paste request, the application service layer contacts the datastore.
The datastore searches for the key, and if it is found, returns the paste’s contents. Otherwise, an error code is returned.
b) Datastore Layer
We can divide our datastore layer into two:
Metadata database: We can use a relational database like MySQL or a Distributed Key-Value store like Dynamo or Cassandra.
Block storage: We can store our contents in a block storage that could be a distributed file storage or an SQL-like database. Whenever we feel like hitting our full capacity on content storage, we can easily increase it by adding more servers.