如何設計一個支持千萬用戶的內容分享系統?
設計 Pastebin
讓我們設計一個類似 Pastebin 的網絡服務,用戶可以在其中存儲純文本。用戶會輸入一段文本並獲得一個隨機生成的 URL 以訪問它。
類似的服務:pastebin.com, pasted.co, chopapp.com
難度級別:中等
1. 什麼是 Pastebin?
Pastebin 之類的服務使用戶可以在網絡上(通常是 Internet)存儲純文本或圖像,並生成唯一的 URL 來訪問上傳的數據。此類服務也用於快速在網絡上共享數據,用戶只需要傳遞 URL,其他用戶就可以看到它。
如果你以前沒有使用過 pastebin.com,請試着在那裏創建一個新的'Paste',並花一些時間研究他們服務提供的不同選項。這將幫助你更好地理解這個章節。
2. 系統的要求和目標
我們的 Pastebin 服務應滿足以下要求:
功能要求:
-
- 用戶應能夠上傳或 “粘貼” 他們的數據並獲得一個唯一的 URL 來訪問它。
-
- 用戶只能上傳文本。
-
- 數據和鏈接將在特定的時間段後自動過期;用戶也應能夠指定過期時間。
-
- 用戶應可以選擇爲他們的粘貼選擇一個自定義別名。
非功能性要求:
-
- 系統應高度可靠,上傳的任何數據都不應丟失。
-
- 系統應高度可用。這是必要的,因爲如果我們的服務中斷,用戶將無法訪問他們的粘貼內容。
-
- 用戶應能夠實時訪問他們的粘貼內容,延遲最小。
-
- 粘貼鏈接不應能被猜測(不可預測)。
擴展需求:
-
- 分析,例如,一個粘貼被訪問了多少次?
-
- 我們的服務還應通過 REST API 供其他服務訪問。
3. 一些設計考慮
Pastebin 與 URL 縮短服務有一些共同的要求,但我們應保持一些額外的設計考慮。
用戶一次可以粘貼多少文本應有個限制?我們可以限制用戶不要上傳大於 10MB 的粘貼內容以防止濫用服務。
我們應該對自定義 URL 的大小設限嗎?因爲我們的服務支持自定義 URL,用戶可以選擇他們喜歡的任何 URL,但提供自定義 URL 並非強制。然而,限制自定義 URL 的大小是合理的(通常也是希望的),這樣我們就有了一致的 URL 數據庫。
4. 容量估計和約束
我們的服務將是讀取密集型的;與新建粘貼的請求相比,將有更多的讀取請求。我們可以假設讀和寫之間的比例爲 5:1。
流量估計:Pastebin 服務預計不會有像 Twitter 或 Facebook 那樣的流量,我們這裏假設每天有一百萬新粘貼添加到我們的系統。這使我們每天有五百萬次的讀取。
每秒新粘貼數量:1M / (24 小時 * 3600 秒) ~= 12 粘貼 / 秒
每秒粘貼讀取數量:5M / (24 小時 * 3600 秒) ~= 58 讀取 / 秒
存儲估計:用戶可以上傳最大 10MB 的數據;通常,Pastebin 之類的服務用於分享源代碼,配置或日誌。這樣的文本不會很大,所以我們假設每個粘貼平均包含 10KB。
按照這個速率,我們每天將存儲 10GB 的數據。1M * 10KB => 10 GB / 天 bash
如果我們想將這些數據存儲十年,我們將需要總共 36TB 的存儲容量。
每天有 1M 粘貼,十年後我們將有 36 億粘貼。我們需要生成和存儲鍵來唯一標識這些粘貼。如果我們使用 base64 編碼([A-Z,a-z,0-9,.,-]),我們將需要六個字母的字符串:64^6 ~= 687 億個唯一字符串
如果需要一個字節來存儲一個字符,存儲 36 億鍵所需的總大小將是:3.6B * 6 => 22 GB
22GB 與 36TB 相比微不足道。爲了保留一些餘量,我們將假定一個 70% 的容量模型(意味着我們在任何時點都不想使用超過總存儲容量的 70%),這將使我們的存儲需求增加到 51.4TB。
帶寬估計:對於寫請求,我們預計每秒有 12 個新粘貼,導致每秒進入 120KB。12 * 10KB => 120 KB/s bash
對於讀取請求,我們預計每秒有 58 個請求。因此,發送給用戶的總數據出口將是 0.6 MB/s。58 * 10KB => 0.6 MB/s
儘管總的入口和出口不大,但我們在設計我們的服務時應該記住這些數字。
內存估計:我們可以緩存一些經常訪問的熱門粘貼。按照 80-20 規則,也就是 20% 的熱門粘貼產生 80% 的流量,我們希望緩存這 20% 的粘貼
因爲我們每天有 5M 的讀取請求,要緩存這些請求的 20%,我們需要:0.2 * 5M * 10KB ~= 10 GB markdown
5. 系統設計和算法
在我們的服務中,用戶將上傳他們的粘貼,並獲取一個唯一的 URL 來訪問它。當用戶用這個 URL 訪問我們的服務時,他們將能看到粘貼的內容。
我們將使用一個 hash 表來存儲數據,其中 key 是我們的 URL,value 是粘貼內容。爲了讓這個設計工作,我們需要一個用來生成 key 的 hash 函數。然後,我們可以將粘貼的內容和過期時間(如果有的話)存儲在數據庫中。
Hash 函數
在我們的場景中,我們需要一個能爲每個粘貼生成一個唯一 key 的 hash 函數。但是,這個函數必須滿足一下要求:
-
• 均勻分佈:我們需要保證所有的 key 都能均勻分佈在 hash 表中,以便於我們平均分攤讀寫負載。
-
• 快速計算:因爲我們的服務可能會同時處理大量請求,我們需要一個能快速計算 key 的 hash 函數。
-
• 不可預測:爲了安全考慮,我們不能讓外部用戶預測出未來可能的 key。
在實踐中,我們通常會使用一些已經存在的、經過大量測試的 hash 函數,例如 MD5 或 SHA-256。這些函數都是經過設計的,能滿足我們上面提到的要求。
數據庫設計
爲了滿足我們的需求,我們需要一個支持快速 key-value 查詢的數據庫。NoSQL 數據庫是一個不錯的選擇,因爲它們通常在這種場景下表現得更好。我們可以使用諸如 Redis 或 Cassandra 這樣的 NoSQL 數據庫。
對於每個粘貼,我們需要保存以下信息:
-
• PasteId(Key):一個唯一的標識符,用於獲取粘貼內容。
-
• Content:用戶上傳的粘貼內容。
-
• ExpirationDate:粘貼過期的日期和時間。
緩存
我們可以在數據庫和客戶端之間引入一個緩存層,以加速頻繁請求的響應時間。對於這種用途,我們可以使用像 Memcached 或 Redis 這樣的內存數據存儲系統。當用戶請求一個粘貼時,我們首先查看是否在緩存中。如果在,我們直接返回緩存的數據;否則,我們從數據庫中取出數據,放入緩存,並返回給用戶。
我們還需要實現一種策略來決定何時從緩存中刪除條目。一種常用的策略是 LRU(最近最少使用),它將最近最少使用的條目從緩存中刪除。
6. 數據一致性
我們的系統可能會在多個服務器上運行,因此我們需要考慮數據一致性問題。如果一個用戶在服務器 A 上創建了一個新的粘貼,然後立即在服務器 B 上請求這個粘貼,服務器 B 需要有這個粘貼的最新數據。
一種解決這個問題的方法是使用強一致性模型。在這種模型中,一旦一個寫操作完成,所有的讀操作都能看到這個寫操作的結果。然而,這種模型可能會降低系統的性能,因爲它需要在所有的服務器上同步數據。
另一種方法是使用最終一致性模型。在這種模型中,系統不保證在所有服務器上立即同步數據。然而,如果沒有新的更新,最終所有的副本都會達到一致。這種模型允許更高的性能,但可能導致短時間內的數據不一致。
根據我們的系統需求,我們可能會選擇最終一致性模型,因爲我們的系統不需要立即看到所有的更新。當用戶創建一個新的粘貼時,我們可以告訴他們可能需要一些時間才能在所有服務器上看到他們的粘貼。
7. 可擴展性
我們的設計應該能夠處理增加的負載。爲了實現這一點,我們可以使用以下技術:
-
• 分片:我們可以通過將數據分佈在多個數據庫服務器上來增加系統的容量。我們可以使用一種稱爲一致性哈希的技術來實現這一點。
-
• 負載均衡:我們可以在我們的服務器和數據庫之間使用負載均衡器來分發請求。這可以幫助我們均勻地分配負載,並在服務器出現問題時提供冗餘。
以上就是關於如何設計一個類似 Pastebin 的網絡服務的簡單概述。在實際設計和實現過程中,可能還需要考慮更多的細節和特殊情況。
8. 組件設計
a. 應用層
我們的應用層將處理所有的入站和出站請求。應用服務器將與後端數據存儲組件交流以滿足請求。
如何處理寫請求?在收到寫請求後,我們的應用服務器將生成一個六位隨機字符串,這將作爲粘貼的鍵(如果用戶沒有提供自定義鍵)。然後,應用服務器將把粘貼的內容和生成的鍵存儲在數據庫中。成功插入後,服務器可以將鍵返回給用戶。這裏可能存在的一個問題是因爲重複的鍵導致插入失敗。由於我們生成的是隨機鍵,新生成的鍵可能與現有的鍵相匹配。在這種情況下,我們應該重新生成一個新的鍵並嘗試再次插入。我們應該一直嘗試,直到我們看不到由於重複鍵導致的失敗。如果用戶提供的自定義鍵已經存在於我們的數據庫中,我們應該向用戶返回一個錯誤。
上述問題的另一個解決方案可能是運行一個獨立的鍵生成服務(KGS),它提前生成隨機的六位字母字符串,並將它們存儲在數據庫中(我們稱之爲 key-DB)。每當我們想要存儲一個新的粘貼,我們只需要取一個已經生成的鍵並使用它。這種方式會使事情變得相當簡單和快速,因爲我們不必擔心重複或衝突。KGS 會確保所有插入到 key-DB 的鍵都是唯一的。KGS 可以使用兩個表來存儲鍵,一個用於尚未使用的鍵,另一個用於所有已使用的鍵。只要 KGS 將一些鍵提供給應用服務器,就可以將這些鍵移動到已使用的鍵表中。KGS 可以始終在內存中保留一些鍵,以便無論何時服務器需要它們,都能快速提供。只要 KGS 將一些鍵加載到內存中,就可以將它們移動到已使用的鍵表中,這樣我們就可以確保每個服務器得到的鍵都是唯一的。如果 KGS 在使用所有加載到內存中的鍵之前死掉,我們將浪費這些鍵。鑑於我們有大量的鍵,我們可以忽略這些鍵。
KGS 是不是一個單點故障? 是的,確實是。爲了解決這個問題,我們可以有一個備用的 KGS,只要主服務器死機,它就可以接管生成和提供鍵的工作。
每個應用服務器能從 key-DB 緩存一些鍵嗎?是的,這肯定可以加快速度。儘管在這種情況下,如果應用服務器在消耗所有鍵之前死機,我們將最終丟失這些鍵。這可能是可以接受的,因爲我們有 68B 個唯一的六字母鍵,這比我們需要的要多得多。
它是如何處理粘貼讀取請求的?在接收到讀取粘貼請求後,應用服務層會聯繫數據存儲。數據存儲搜索該鍵,如果找到,就返回粘貼的內容。否則,返回一個錯誤代碼。
b. 數據存儲層
我們可以將我們的數據存儲層分爲兩部分:
-
- 元數據數據庫:我們可以使用關係數據庫,如 MySQL,或者分佈式鍵值存儲,如 Dynamo 或 Cassandra。
-
- 對象存儲:我們可以在對象存儲中存儲我們的內容,如 Amazon 的 S3。只要我們感覺即將達到內容存儲的全容量,我們就可以通過添加更多的服務器來輕易增加它。
9. 清理或數據庫清理
請參見設計 URL 縮短服務。
10. 數據分區和複製
請參見設計 URL 縮短服務。
11. 緩存和負載均衡器
請參見設計 URL 縮短服務。
12. 安全和權限
請參見設計 URL 縮短服務。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/3GbTEKqt_179iXjJTIl6tg