系統(tǒng)架構設計之解析:內容分享系統(tǒng)案例深度解析
在數(shù)字時代,內容分享平臺成為人們生活中的重要一環(huán),從分享生活點滴、表達情感,到提供信息和娛樂,這類平臺已經(jīng)深深影響了我們的生活。其中,國外Instagram和國內的LOFTER,作為優(yōu)秀的內容分享平臺,憑借其出色的用戶體驗和強大的功能,吸引了大量的用戶。本文將針對LOFTER的系統(tǒng)架構進行深度解析,為我們提供一種理解和設計大規(guī)模內容分享系統(tǒng)的視角。
項目簡介:LOFTER是由網(wǎng)易公司開發(fā)的一個內容創(chuàng)作和分享平臺,用戶可以創(chuàng)建自己的博客,分享文字、圖片、音樂等內容。該平臺受到創(chuàng)意人群,如攝影愛好者、插畫家等的喜愛,提供自由的創(chuàng)作空間。用戶可以關注其他博主,互相評論和點贊,通過標簽系統(tǒng)找到自己感興趣的內容。LOFTER也會定期舉辦各種創(chuàng)作活動,鼓勵用戶創(chuàng)作和分享。
現(xiàn)在讓我們設計一個類似于LOFTER的內容分享服務,用戶可以上傳照片與其他用戶共享。
類似的產(chǎn)品有:Instagram、Picasa
系統(tǒng)難度等級:中等
1、什么是LOFTER?
LOFTER是一個社交網(wǎng)絡服務,使其用戶能夠上傳并與其他用戶分享他們的照片和視頻。LOFTER用戶可以選擇公開或私密地分享信息。公開分享的任何內容都可以被任何其他用戶看到,而私密分享的內容只能被指定的一組人訪問。LOFTER還使其用戶能夠通過許多其他社交網(wǎng)絡平臺分享,例如微博、Twitter、Flickr和Tumblr。
我們計劃設計一個LOFTER的簡化版本來解決這個設計問題,用戶可以分享照片并關注其他用戶。每個用戶的“新鮮事”將包括用戶關注的所有人的熱門照片。
2、系統(tǒng)的需求和目標
在設計LOFTER時,我們將關注以下一組需求:
功能需求:
- 用戶應能夠上傳/下載/查看照片。
- 用戶可以根據(jù)照片/視頻標題進行搜索。
- 用戶可以關注其他用戶。
- 系統(tǒng)應生成并顯示用戶的“新鮮事”,包括用戶關注的所有人的熱門照片。
非功能性需求:
- 我們的服務需要高度可用。
- 系統(tǒng)生成“新鮮事”的可接受延遲為200毫秒。
- 如果用戶在一段時間內看不到照片,那么為了保持可用性,一定程度的一致性降低是可以接受的。
- 系統(tǒng)應高度可靠;任何上傳的照片或視頻都不應丟失。
不在討論范圍內:給照片添加標簽,根據(jù)標簽搜索照片,評論照片,給照片標記用戶,推薦關注等等。
3、設計考慮
該系統(tǒng)將是讀取密集型的,因此我們將專注于構建一個可以快速檢索照片的系統(tǒng)。
- 實際上,用戶可以上傳他們喜歡的任何數(shù)量的照片;因此,高效的存儲管理在設計此系統(tǒng)時應該是一個關鍵因素。
- 預期查看照片時的延遲非常低。
- 數(shù)據(jù)應該是100%可靠的。如果用戶上傳了一張照片,系統(tǒng)將保證它絕不會丟失。
4、容量估算和約束
讓我們假設我們總共有5億個用戶,每天有100萬活躍用戶。
每天有200萬新照片,每秒23張新照片。
平均照片文件大小=> 200KB
1天的照片所需的總空間
2M * 200KB => 400 GB
10年所需的總空間:
400GB * 365(一年的天數(shù))* 10(年)~= 1425TB
5、高層系統(tǒng)設計
在高層次上,我們需要支持兩種情況,一是上傳照片,另一是查看/搜索照片。我們的服務將需要一些對象存儲服務器來存儲照片,以及一些數(shù)據(jù)庫服務器來存儲關于照片的元數(shù)據(jù)信息。
6、數(shù)據(jù)庫模式
在設計的早期階段定義數(shù)據(jù)庫模式將有助于理解各個組件之間的數(shù)據(jù)流,后期將有助于數(shù)據(jù)分區(qū)。
我們需要存儲關于用戶、他們上傳的照片以及他們關注的人的數(shù)據(jù)。Photo表將存儲與照片相關的所有數(shù)據(jù);我們需要在(PhotoID,CreationDate)上建立索引,因為我們需要先獲取最新的照片。
存儲上述模式的直接方法是使用像MySQL這樣的關系型數(shù)據(jù)庫,因為我們需要聯(lián)接。但是,關系型數(shù)據(jù)庫帶來了他們的挑戰(zhàn),尤其是當我們需要擴展它們的時候。有關詳細信息,請參閱 'SQL vs. NoSQL' 章節(jié)。
我們可以在分布式文件存儲,如HDFS或S3中存儲照片。
我們可以在分布式鍵值存儲中存儲上述模式,以享受NoSQL提供的好處。所有與照片相關的元數(shù)據(jù)都可以放到一個表中,其中 'key' 將是 'PhotoID','value' 將是一個包含 PhotoLocation, UserLocation, CreationTimestamp 等的對象。
一般來說,NoSQL存儲總是維護一定數(shù)量的副本以提供可靠性。此外,在這樣的數(shù)據(jù)存儲中,刪除操作并不會立即執(zhí)行;系統(tǒng)會保留數(shù)據(jù)若干天(支持撤銷刪除)后才會從系統(tǒng)中永久刪除。
7、數(shù)據(jù)大小估計
讓我們估計一下每個表中將要存入多少數(shù)據(jù),以及我們在10年內需要多少總存儲空間。
用戶:假設每個 "int" 和 "dateTime" 是四字節(jié),用戶表中的每一行將是68字節(jié):
UserID(4字節(jié))+ Name(20字節(jié))+ Email(32字節(jié))+ DateOfBirth(4字節(jié))+ CreationDate(4字節(jié))+ LastLogin(4字節(jié))= 68字節(jié)
如果我們有5億用戶,我們將需要32GB的總存儲空間。
5億 * 68 ~= 32GB
Photo:Photo 表中的每一行將是284字節(jié):
PhotoID(4字節(jié))+ UserID(4字節(jié))+ PhotoPath(256字節(jié))+ PhotoLatitude(4字節(jié))+ PhotoLongitude(4字節(jié))+ UserLatitude(4字節(jié))+ UserLongitude(4字節(jié))+ CreationDate(4字節(jié))= 284字節(jié)
如果每天有200萬新照片被上傳,我們將需要0.5GB的存儲空間:
2M * 284字節(jié) ~= 0.5GB 每天
10年我們將需要1.88TB的存儲空間。
UserFollow:UserFollow表中的每一行將占用8字節(jié)。如果我們有5億用戶,每個用戶平均關注500個用戶。我們將需要1.82TB的存儲空間用于UserFollow表:
5億用戶 * 500關注者 * 8字節(jié) ~= 1.82TB
10年內所有表所需的總空間將為3.7TB:
32GB+1.88TB+1.82TB =3.7TB
8、組件設計
照片上傳(或寫入)可能會比較慢,因為它們需要寫入磁盤,而讀取則會更快,尤其是當它們由緩存服務時。
上傳的用戶可能會占用所有可用的連接,因為上傳是一個慢的過程。這意味著如果系統(tǒng)忙于處理所有的 '寫' 請求,那么 '讀' 請求就無法得到服務。在設計我們的系統(tǒng)時,我們應該記住網(wǎng)絡服務器在連接數(shù)量上有一個限制。如果我們假設一個網(wǎng)絡服務器在任何時候最多可以有500個連接,那么它不能同時有超過500個的上傳或讀取。為了處理這個瓶頸,我們可以將讀取和寫入分開成為獨立的服務。我們將有專門的服務器用于讀取,不同的服務器用于寫入,以確保上傳不會拖慢系統(tǒng)。
將照片的讀取和寫入請求分開也將允許我們獨立地對這些操作進行擴展和優(yōu)化。
9、可靠性和冗余性
丟失文件對我們的服務來說是不可接受的。因此,我們將存儲每個文件的多個副本,這樣即使一個存儲服務器出現(xiàn)問題,我們也可以從另一個存儲在不同存儲服務器上的副本中取回照片。
這個原則同樣適用于系統(tǒng)的其他組件。如果我們想要系統(tǒng)具有高可用性,我們需要在系統(tǒng)中運行多個服務的副本,這樣即使有幾個服務出現(xiàn)問題,系統(tǒng)仍然可以保持可用和運行。冗余消除了系統(tǒng)中的單點故障。
如果任何時刻只需要運行一個服務的實例,我們可以運行一個冗余的次要副本,它不服務任何流量,但當主服務有問題時,它可以在故障切換后接管控制。
在系統(tǒng)中創(chuàng)建冗余可以消除單點故障,并在危機時提供備用或備用功能。例如,如果有兩個相同服務的實例在生產(chǎn)環(huán)境中運行,其中一個失敗或降級,系統(tǒng)可以切換到健康的副本。故障切換可以自動發(fā)生,也可以需要手動干預。
10、數(shù)據(jù)分片
我們來討論一下元數(shù)據(jù)分片的不同方案:
A. 基于UserID進行分區(qū)
假設我們根據(jù)'UserID '進行分片,以便我們可以將一個用戶的所有照片保留在同一個分片上。如果一個數(shù)據(jù)庫分片是1TB,我們需要4個分片來存儲3.7TB的數(shù)據(jù)。假設為了更好的性能和可擴展性,我們保留10個分片。
因此,我們可以通過UserID % 10找到分片編號,然后在那里存儲數(shù)據(jù)。為了在我們的系統(tǒng)中唯一標識任何照片,我們可以在每個PhotoID后附加分片編號。
我們如何生成PhotoID呢?每個數(shù)據(jù)庫分片都可以有自己的PhotoID自增序列,而且由于我們將ShardID 附加到每個PhotoID上,因此它在我們的整個系統(tǒng)中都將是唯一的。
這種分區(qū)方案有哪些問題呢?
- 我們如何處理熱門用戶?很多人關注這樣的熱門用戶,很多其他人會看到他們上傳的任何照片。
- 有些用戶相比其他用戶會有很多照片,因此會使存儲分布不均勻。
- 如果我們不能將一個用戶的所有圖片存儲在一個分片上怎么辦?如果我們將用戶的照片分布到多個分片上,會不會導致延遲增加?
- 將用戶的所有照片存儲在一個分片上可能會導致一些問題,比如如果該分片停止工作,用戶的所有數(shù)據(jù)都無法訪問,或者如果它正在承受高負載,延遲會增加等等。
B. 基于照片ID進行分區(qū)
如果我們可以先生成唯一的PhotoID,然后通過“PhotoID % 10”找到分片編號,以上的問題就可以解決了。在這種情況下,我們不需要在PhotoID后附加ShardID,因為PhotoID本身在整個系統(tǒng)中都是唯一的。
我們如何生成PhotoID呢?在這里,我們不能在每個分片中定義一個自增序列來定義PhotoID,因為我們需要先知道PhotoID才能找到它將被存儲的分片。一個解決方案可能是我們專門分配一個數(shù)據(jù)庫實例來生成自增ID。如果我們的PhotoID可以適應64位,我們可以定義一個只包含一個64位ID字段的表。所以每當我們想在我們的系統(tǒng)中添加一張照片時,我們可以在這個表中插入一個新行,并取那個ID作為新照片的PhotoID。
這個生成鍵的數(shù)據(jù)庫不會成為單點故障嗎?是的,它會。一個解決方法可能是定義兩個這樣的數(shù)據(jù)庫,一個生成偶數(shù)ID,另一個生成奇數(shù)ID。對于MySQL,以下腳本可以定義這樣的序列:
KeyGeneratingServer1:
auto-increment-increment = 2
auto-increment-offset = 1
KeyGeneratingServer2:
auto-increment-increment = 2
auto-increment-offset = 2
我們可以在這兩個數(shù)據(jù)庫前面放一個負載均衡器,用來在它們之間輪詢,并處理宕機問題。這兩個服務器可能會失去同步,一個生成的鍵可能比另一個多,但這不會在我們的系統(tǒng)中造成任何問題。我們可以通過為用戶、照片評論或系統(tǒng)中的其他對象定義獨立的ID表來擴展這種設計。
另外,我們可以實施一種類似于我們在“設計像TinyURL這樣的URL縮短服務”中討論的'鍵'生成方案。
我們如何為我們系統(tǒng)的未來增長做計劃?我們可以有大量的邏輯分區(qū)以適應未來的數(shù)據(jù)增長,這樣一開始,多個邏輯分區(qū)就可以駐留在一個物理數(shù)據(jù)庫服務器上。由于每個數(shù)據(jù)庫服務器可以運行多個數(shù)據(jù)庫實例,所以我們可以在任何服務器上為每個邏輯分區(qū)有單獨的數(shù)據(jù)庫。所以,每當我們覺得某個數(shù)據(jù)庫服務器的數(shù)據(jù)量很大時,我們可以將一些邏輯分區(qū)從它遷移到另一個服務器。我們可以維護一個配置文件(或一個單獨的數(shù)據(jù)庫),它可以映射我們的邏輯分區(qū)到數(shù)據(jù)庫服務器;這將使我們能夠輕松地移動分區(qū)。每當我們想移動一個分區(qū)時,我們只需要更新配置文件來宣布改變。
11、排名和新聞動態(tài)生成
為任何給定的用戶創(chuàng)建News-Feed,我們需要獲取用戶關注的人發(fā)布的最新的、最受歡迎的、和相關的照片。
為了簡化,讓我們假設我們需要為用戶的News-Feed獲取最熱門的100張照片。我們的應用服務器首先會獲取用戶關注的人的列表,然后獲取每個用戶最新100張照片的元數(shù)據(jù)信息。在最后一步,服務器會將所有這些照片提交給我們的排名算法,該算法會基于最近性、喜歡程度等因素確定最熱門的100張照片,并返回給用戶。這種方法的一個可能問題是由于我們必須查詢多個表并對結果進行排序/合并/排名,所以延遲可能會更高。為了提高效率,我們可以預先生成News-Feed并將其存儲在一個單獨的表中。
預先生成新聞動態(tài):我們可以有專門的服務器持續(xù)生成用戶的News-Feed,并將其存儲在“UserNewsFeed”表中。所以每當任何用戶需要他們的News-Feed的最新照片時,我們只需要查詢這個表并將結果返回給用戶。
每當這些服務器需要生成用戶的News-Feed時,它們首先會查詢UserNewsFeed表,找出上一次為該用戶生成News-Feed的時間。然后,從那個時間點開始生成新的News-Feed數(shù)據(jù)(按照上述步驟)。
將News-Feed內容發(fā)送給用戶有哪些不同的方法?
- 拉?。嚎蛻舳丝梢远ㄆ诨蛟谛枰獣r手動從服務器拉取News-Feed內容。這種方法可能的問題是
- a) 新數(shù)據(jù)可能不會在客戶端發(fā)出拉取請求之前顯示給用戶
- b) 如果沒有新數(shù)據(jù),大多數(shù)時間拉取請求會返回空響應。
- 推送:服務器可以在新數(shù)據(jù)可用時立即將其推送給用戶。為了有效管理這一點,用戶必須和服務器維持一個長輪詢請求以接收更新。這種方法可能的問題是一個關注了很多人的用戶,或者一個有數(shù)百萬粉絲的名人用戶;在這種情況下,服務器必須頻繁地推送更新。
- 混合:我們可以采取混合方法。我們可以將所有關注人數(shù)較多的用戶移至拉取模型,并只將數(shù)據(jù)推送給關注人數(shù)為幾百(或幾千)的用戶。另一種方法可能是服務器將更新推送給所有用戶,但不超過一定頻率,并讓有大量更新的用戶定期拉取數(shù)據(jù)。
關于News-Feed生成的詳細討論,請參閱“設計Facebook的News-Feed”。
12、分片數(shù)據(jù)的News-Feed創(chuàng)建
創(chuàng)建任何給定用戶的News-Feed的一個最重要的需求是獲取用戶關注的所有人發(fā)布的最新照片。為此,我們需要有一種機制來根據(jù)照片的創(chuàng)建時間進行排序。為了有效地做到這一點,我們可以將照片的創(chuàng)建時間作為PhotoID的一部分。由于我們在PhotoID上有一個主索引,所以找到最新的PhotoID將會非??臁?/p>
我們可以使用紀元時間來實現(xiàn)這一點。假設我們的PhotoID有兩部分;第一部分將表示紀元時間,第二部分將是一個自動遞增的序列。因此,要生成新的PhotoID,我們可以取當前的紀元時間并追加我們的鍵生成數(shù)據(jù)庫的自動遞增ID。我們可以從這個PhotoID中找出碎片號(PhotoID % 10)并在那里存儲照片。
我們的PhotoID的大小可能是多少呢?假設我們的紀元時間從今天開始;我們需要多少位來存儲接下來50年的秒數(shù)?
86400 秒/天 * 365 (days a year) * 50 (years) => 16億秒
我們需要 31 位來存儲這個數(shù)字。由于平均而言,我們預計每秒 23 張新照片,因此我們可以分配 9 個額外位來存儲自動遞增序列。所以每一秒,我們都可以存儲(29≤5122^9\leq51229≤512)新照片。我們?yōu)樾蛄刑柗峙淞?9 位,這超出了我們的要求;我們這樣做是為了獲得完整的字節(jié)數(shù)(如40bits=5bytes)。我們可以每秒重置自動遞增序列。
我們將在“設計 Twitter”中的“數(shù)據(jù)分片”下討論這項技術。
13、緩存和負載均衡
我們的服務將需要一個大規(guī)模的照片傳遞系統(tǒng)來服務全球分布的用戶。我們的服務應該使用大量地理分布的照片緩存服務器和CDN(詳見我的另一篇關于“Caching”的詳細介紹)將其內容推送到用戶附近。
我們可以為元數(shù)據(jù)服務器引入一個緩存,以緩存熱門數(shù)據(jù)庫行。我們可以使用Memcache來緩存數(shù)據(jù),應用服務器在訪問數(shù)據(jù)庫之前可以快速檢查緩存是否有所需的行。最近最少使用(LRU)可以是我們系統(tǒng)的合理緩存淘汰策略。根據(jù)這種策略,我們首先丟棄最近最少查看的行。
我們如何構建一個更智能的緩存?如果我們遵循二八定則,即每日照片的20%閱讀量產(chǎn)生了80%的流量,這意味著某些照片非常受歡迎,大多數(shù)人都會閱讀。這就決定了我們可以嘗試緩存20%的每日照片和元數(shù)據(jù)的閱讀量。