如何設計一個Pastebin.com?
1.用例
我們將問題的範疇限定在如下用例
- 用戶 輸入一段文本,然後得到一個隨機生成的鏈接
- 過期設置
- 默認的設置是不會過期的
- 可以選擇設置一個過期的時間
- 過期設置
- 用戶 輸入一個 paste 的 url 後,可以看到它存儲的內容
- 用戶 是匿名的
- Service 跟蹤頁面分析
- 一個月的訪問統計
- Service 刪除過期的 pastes
- Service 需要高可用
超出範疇的用例
- 用戶 可以註冊一個賬戶
- 用戶 通過驗證郵箱
- 用戶 可以用註冊的賬戶登錄
- 用戶 可以編輯文檔
- 用戶 可以設置可見性
- 用戶 可以設置短鏈接
2.約束和狀態假設
- 流量分佈不均
- 跟隨一個短鏈接應該很快
- 粘貼只是文本
- 頁面瀏覽分析不需要是實時的
- 1000萬用戶
- 每月 1000 萬次粘貼寫入
- 每月 1 億次粘貼讀取
- 10:1 讀寫比
大致計算一下
- 每個粘貼的大小
- 每個粘貼 1 KB 內容
shortlink
- 7 個字節expiration_length_in_minutes
- 4字節created_at
- 5 個字節paste_path
- 255 字節- 總計 = ~1.27 KB
- 每月 12.7 GB 的新粘貼內容
- 每次粘貼 1.27 KB * 每月 1000 萬次粘貼
- 3 年內約 450 GB 的新粘貼內容
- 3 年 3.6 億個短鏈接
- 假設大多數是新粘貼而不是對現有粘貼的更新
- 平均每秒 4 次粘貼寫入
- 平均每秒 40 個讀取請求
方便的轉換指南:
- 每月 250 萬秒
- 每秒 1 個請求 = 每月 250 萬個請求
- 每秒 40 個請求 = 每月 1 億個請求
- 每秒 400 個請求 = 每月 10 億個請求
3.系統設計
1.基礎框架
這是系統最基礎的框架,後續所有優化都基於此進行
主要分爲三層
- 第一層:客戶端+webserver層,負責提供web服務並接收轉發來自客戶端的請求
- 第二層:Write API + Read API +Analytics層:負責專門處理上一層的讀寫請求
- 第三層:SQL+對象儲存層:負責儲存業務數據和儲存文件對象
2.用例
用例1:用戶輸入一段文本並獲得一個隨機生成的鏈接
可以將關係型數據庫當做大型的Hash表,將生成的URL映射到對象存儲上的文件路徑,同樣,我們也可以將關係型數據庫改爲No SQL,下面討論關係型數據庫的使用方法
- 客戶端向作爲反向代理運行的 Web服務器 發送創建粘貼請求
-
Web 服務器將請求轉發到Write API服務器
-
Write API服務器執行以下操作:
- 生成一個唯一的 url
- 通過查看SQL 數據庫中的重複項來檢查 url 是否唯一
- 如果 url 不唯一,則生成另一個 url
- 如果我們支持自定義 url,我們可以使用用戶提供的(也檢查重複)
- 保存到SQL 數據庫
pastes
表 - 將粘貼數據保存到對象存儲
- 返回url
- 生成一個唯一的 url
上述pastes
表可以具有以下結構:
shortlink char(7) NOT NULL
expiration_length_in_minutes int NOT NULL
created_at datetime NOT NULL
paste_path varchar(255) NOT NULL
PRIMARY KEY(shortlink)
將主鍵設置爲基於shortlink
列會創建一個索引
,數據庫使用該索引來強制唯一性。我們將創建一個額外的索引created_at
來加快查找速度(記錄時間而不是掃描整個表)並將數據保存在內存中。
從內存中順序讀取 1 MB 大約需要 250 微秒,而從 SSD 讀取需要 4 倍,從磁盤讀取需要 80 倍以上。
要生成唯一的 url,我們可以:
-
第一步:取用戶 ip_address + timestamp 的MD5 hash
- MD5 是一種廣泛使用的散列函數,可產生 128 位散列值
- MD5 是均勻分佈的
- 或者,我們也可以採用隨機生成數據的 MD5 哈希
-
第二步:Base 62 來編碼 MD5 哈希
-
Base 62 編碼
[a-zA-Z0-9]
適用於 url,無需轉義特殊字符 -
原始輸入只有一個哈希結果,Base 62 是確定性的(不涉及隨機性)
-
Base 64 是另一種流行的編碼,但由於額外的
+
和/
字符而爲 url 提供了問題 -
以下Base 62 僞代碼在 O(k) 時間內運行,其中 k 是位數 = 7
def base_encode(num, base=62): digits = [] while num > 0 remainder = modulo(num, base) digits.push(remainder) num = divide(num, base) digits = digits.reverse
-
-
第三步:取輸出的前 7 個字符,這會產生 62^7 個可能的值,應該足以處理我們在 3 年內 3.6 億個短鏈接的約束
url = base_encode ( md5 ( ip_address + timestamp ))[: URL_LENGTH ]
- 第四步:使用公共的REST API返回url,對於內部響應,我們可以採用grpc
REST API
curl -X POST --data '{ "expiration_length_in_minutes": "60", \
"paste_contents": "Hello World!" }' https://pastebin.com/api/v1/paste
響應
{
"shortlink": "foobar"
}
用例2:用戶輸入粘貼的 url 並查看內容
- 客戶端向Web 服務器發送獲取粘貼請求
- Web 服務器將請求轉發到讀取 API服務器
- 讀取 API 服務器執行以下操作 :
- 檢查 SQL 數據庫中生成的 url
- 如果 url 在SQL 數據庫中,則從對象存儲中獲取粘貼內容
- 否則,爲用戶返回錯誤消息
- 檢查 SQL 數據庫中生成的 url
$ curl https://pastebin.com/api/v1/paste?shortlink=foobar
{
"paste_contents": "Hello World"
"created_at": "YYYY-MM-DD HH:MM:SS"
"expiration_length_in_minutes": "60"
}
用例3:服務跟蹤頁面分析
由於不需要實時分析,我們可以簡單地MapReduce Web服務器日誌來生成命中計數。
只需要分析web服務的日誌即可
class HitCounts(MRJob):
def extract_url(self, line):
"""從日誌行中提取生成的url"""
...
def extract_year_month(self, line):
"""返回時間戳的年份和月份部分"""
...
def mapper(self, _, line):
"""解析每個日誌行,提取和轉換相關行
發出以下形式的鍵值對
(2016-01, url0), 1
(2016-01, url0), 1
(2016-01, url1), 1
"""
url = self.extract_url(line)
period = self.extract_year_month(line)
yield (period, url), 1
def reducer(self, key, values):
"""每個鍵的總和值.
(2016-01, url0), 2
(2016-01, url1), 1
"""
yield key, sum(values)
用例4:服務刪除過期的粘貼
要刪除過期的paste,我們可以掃描SQL 數據庫中所有過期時間戳早於當前時間戳的條目。然後將從表中刪除所有過期條目(或標記爲過期)。
4.系統架構優化
優化要點如下:
- DNS:使用DNS優化域名到IP的查找
- CDN:使用CDN內容分發,利用全局負載技術將用戶的訪問指向距離最近的正常工作的緩存服務器上,由緩存服務器直接響應用戶請求,可以減少打到源服務集羣的流量
- 添加一個Memory Cache,交給Read API 服務器使用,加速read類型的響應
- 添加一個 SQL Analytics數據庫,專門用於Analytics分析服務
- SQL Write採用主從模式,SQL Read採用多副本分散壓力
Analytics SQL可以採用數據倉庫的解決方案,例如 Amazon Redshift 或 Google BigQuery。
像 Amazon S3 這樣的對象存儲可以輕鬆處理每月 12.7 GB 新內容的限制。
爲了解決每秒40 個*平均讀取請求(峯值更高),流行內容的流量應該由內存緩存而不是數據庫來處理。內存緩存對於處理不均勻分佈的流量和流量峯值也很有用。SQL 只讀副本應該能夠處理緩存未命中,只要副本不會因複製寫入而陷入困境。
對於單個SQL Write Master-Slave,每秒4次平均粘貼寫入(峯值更高)應該是可行的。否則,我們將需要使用額外的 SQL 擴展模式:
- 分片
- sql調優
我們還應該考慮將一些數據移動到NoSQL 數據庫。