前言
系統設計實踐篇的文章將會根據《系統設計面試的萬金油》為前置模板,講解數十個常見系統的設計思路。
前置閱讀:
設計目標
讓我們設計一個類似於Pastebin的網站,使用者可以在其中儲存純文字。該服務的使用者將輸入一段文字,並獲得一個隨機生成的URL來訪問它。
一. 什麼是Pastebin?
Pastebin是一個文字儲存的網站,使用者可以在網站上儲存(貼上)純文字 ,例如程式碼片段,生成一個網址,開啟該網址就可以看到對應的文字。可以選擇文字的型別(程式碼所屬的程式語言)、文字儲存的時間(1天、7天、30天、閱後即焚等等)、文字分享者的暱稱等資訊。因為第一個文字分享網站叫 http://pastebin.com,所以文字儲存網站也常被稱為Pastebin。
二. 系統的需求與目標
Pastebin服務應滿足以下要求:
功能性需求
- 使用者應該能夠上傳或貼上他們的文字資料,並獲得訪問它的唯一URL。
- 使用者只能上傳文字。
- 資料和連結地址將在特定時間間隔後自動過期; 使用者可以指定過期時間。
- 使用者可以為他們的文字內容選擇一個自定義的別名。
非功能性需求
- 系統應該是高度可靠的,任何上傳的資料都不應該丟失。
- 系統應該是高度可用的。這是必須的,因為如果我們的服務關閉,使用者將無法訪問他們的貼上內容。
- 使用者應該能夠以最小的延遲實時訪問他們的貼上。
- 貼上連結地址不應該是可猜測的(不可預測的)。
擴充套件需求
- 分析,例如,貼上地址被訪問多少次
- 我們的服務也應該可以通過REST API被其他服務訪問。
三. 系統相似性
Pastebin與上一篇《系統設計實踐(01) - 短鏈服務》有很多相似性的地方,所以我建議在開始閱讀前再去讀一讀短鏈服務那篇文章,此外還有一些額外的設計注意事項。
使用者一次可以貼上的文字數量的限制是什麼?
我們可以限制使用者的貼上不超過10MB,以防止濫用服務。
我們應該對自定義url施加大小限制嗎?
由於我們的服務支援自定義URL,使用者可以自定義他們喜歡URL,但提供自定義URL不是強制性的。然而,對自定義URL施加大小限制是合理的(通常也是可取的),這樣我們就有了一致的URL資料庫。
四. 容量估算與約束
與短鏈服務類似,我們的服務讀請求會更多,與建立新的貼上相比,將有更多的讀取請求。我們可以假設讀和寫的比例是5:1。
流量估計
我們假設系統每天有100萬新貼上生成, 這樣我們每天就有500萬次讀取。
每秒新貼上
1M / (24 hours * 3600 seconds) ~= 12 pastes/sec
貼上每秒讀取:
5M / (24 hours * 3600 seconds) ~= 58 reads/sec
儲存估計
使用者最多可以上傳10MB的資料; 通常,Pastebin之類的服務用於共享原始碼、配置或日誌。這樣的文字並不大,所以我們假設每個貼上平均包含10KB。
按照這個速度,我們每天將儲存10GB的資料。
1M * 10KB => 10 GB/day
如果我們想將這些資料儲存10年,我們需要36TB的總儲存容量。
每天有 100 萬個貼上,我們將在 10 年內擁有 36 億個貼上。 我們需要生成並儲存金鑰以唯一標識這些貼上。 如果我們使用 base64 編碼([A-Z, a-z, 0-9, ., -]),我們將需要六個字母字串:
64^6 ~= 68.7 billion unique strings
如果儲存一個字元需要一個位元組,那麼儲存3.6B鍵所需的總大小將是
3.6B * 6 => 22 GB
與36TB相比,22GB可以忽略不計。為了保持一定的餘量,我們將採用70%容量模型(即任何時候都不希望使用超過70%的總儲存容量),從而將儲存容量增加到51.4TB。
頻寬估計
對於寫請求,我們預計每秒12個新貼上,每秒會有120KB的輸入。
12 * 10KB => 120 KB/s
至於讀取請求,我們預計每秒有 58 個請求。 因此,總資料出口(傳送給使用者)將為 0.6 MB/s。
58 * 10KB => 0.6 MB/s
雖然總入口和出口不是很大,但我們在設計服務時應該記住這些數字
記憶體估計
我們可以快取一些經常訪問的熱貼上。遵循80-20規則,即20%的熱點貼上會產生80%的流量,我們希望快取這20%的貼上,因為我們每天有5M的讀請求,要快取這些請求的20%,我們需要
0.2 * 5M * 10KB ~= 10 GB
五. 系統API設計
我們可以使用 SOAP 或 REST API 來公開我們服務的功能。 以下可能是用於建立/檢索/刪除貼上的 API 的定義:
addPaste(api_dev_key, paste_data, custom_url=None, user_name=None, paste_name=None, expire_date=None)
引數
- api_dev_key (string): 註冊帳戶的API開發者金鑰.
- paste_data (string): 貼上的文字內容.
- custom_url (string): 可選的使用者指定url.
- user_name (string): 可選的使用者嗎,用於生成URL.
- paste_name (string): 可選的貼上名稱.
- expire_date (string): 可選的過期時間.
返回
成功將返回可以訪問貼上的URL,否則將返回錯誤程式碼。
getPaste(api_dev_key, api_paste_key)
其中api貼上鍵是一個字串,表示要檢索的貼上鍵。這個API將返回貼上的文字資料。
deletePaste(api_dev_key, api_paste_key)
成功刪除返回true,否則返回false。
六. 資料庫設計
關於我們正在儲存的資料的性質的一些觀察
- 我們需要儲存數十億條記錄。
- 我們儲存的每個後設資料物件都很小(小於100位元組)
- 我們儲存的每個貼上物件可以是中等大小(可以是幾MB)。
- 記錄之間沒有關係,除非我們想要儲存哪個使用者建立了什麼貼上。
- 我們的服務讀請求很多
資料庫選型
我們需要兩張表,一個用於儲存關於paste的資訊,另一個用於儲存使用者資料。
Paste | User |
---|---|
[PK] URL Hash: varchar(16) | [PK] UserID: int |
ContentKey: varchar(512) | Name: varchar(20) |
CreationDate: datetime | Email: varchar(20) |
ExpirationDate: datatime | CreationDate: datetime |
LastLoginDate: datetime |
在這裡,URl Hash是TinyURL的URL等價物,ContentKey是儲存貼上內容的物件鍵。
七. 高階設計
在更高的層次上,我們需要一個應用程式層來服務於所有的讀寫請求。應用層將與儲存層通訊以儲存和檢索資料。我們可以隔離儲存層,一個資料庫儲存與每個貼上、使用者等相關的後設資料,而另一個資料庫將貼上內容儲存在某些物件儲存中(如Amazon S3)。這種資料劃分也將允許我們對它們進行單獨的縮放。
八. 元件設計
應用層
我們的應用層將處理所有傳入和傳出的請求。應用伺服器將與後端資料儲存元件通訊來處理請求。
如何處理寫請求?
在接收到寫請求時,我們的應用伺服器將生成一個6個字母的隨機字串,它將作為貼上的金鑰(如果使用者沒有提供自定義金鑰)。然後,應用程式伺服器將在資料庫中儲存貼上的內容和生成的鍵。成功插入後,伺服器可以將金鑰返回給使用者。這裡的一個可能問題是,由於金鑰重複,插入失敗。因為我們生成了一個隨機金鑰,所以新生成的金鑰有可能與現有金鑰匹配。在這種情況下,我們應該重新生成一個新的金鑰並再試一次,直到沒有發現因為重複金鑰。如果使用者提供的自定義鍵已經存在於資料庫中,則應該向使用者返回一個錯誤。
上述問題的另一個解決方案是執行一個獨立的金鑰生成服務(KGS),它事先生成隨機的6個字母字串,並將它們儲存在一個資料庫中(我們稱之為Key-db)。每當我們想要儲存一個新的貼上時,我們只需要一個已經生成的鍵並使用它。這種方法將使事情變得非常簡單和快速,因為我們不需要擔心重複或碰撞。KGS將確保插入到key-DB中的所有鍵是唯一的。KGS可以使用兩個表來儲存鍵,一個用於尚未使用的鍵,另一個用於所有已使用的鍵。一旦KGS嚮應用伺服器提供了一些鍵,它就可以將這些鍵移動到所使用的鍵表中。KGS可以在記憶體中儲存一些金鑰,以便每當伺服器需要它們時,它可以快速提供它們。一旦KGS在記憶體中載入了一些鍵,它就可以將它們移動到已使用的鍵表中,這樣我們就可以確保每個伺服器獲得唯一的鍵。如果KGS在使用記憶體中載入的所有鍵之前當機,這些鍵會被浪費,不過可以忽略,因為KGS中6個字母可生成的字串足夠多。
KGS不是單點故障嗎?
是的。為了解決這個問題,我們可以有一個KGS的備用副本,每當主伺服器死亡時,它可以接管生成並提供金鑰。
每個應用伺服器是否可以從key-DB中快取一些key?
是的,這肯定能加快響應速度。儘管在這種情況下,如果應用伺服器在使用所有金鑰之前就掛掉了,我們最終會丟失這些金鑰。這是可以接受的,因為我們有68B唯一的6個字母的鑰匙,這比我們需要的多得多。
它如何處理貼上讀請求?
在接收到讀貼上請求後,應用程式服務層請求資料儲存。資料儲存搜尋金鑰,如果找到,返回貼上的內容。否則,返回錯誤程式碼。
資料層
我們可以講資料儲存劃為兩層。
- 後設資料資料庫:我們可以使用關聯式資料庫如MySQL或分散式鍵值儲存如Dynamo或Cassandra。
- 物件儲存:可以像Amazon S3一樣將內容儲存在物件儲存中。當我們想要在內容儲存上達到最大容量時,我們可以通過新增更多伺服器來輕鬆增加容量。