分散式系統設計:服務模式之分片服務
來自專欄 進擊的雲計算
在上一篇中,我們看到了複製無狀態服務的可靠性,冗餘性和擴展性的價值。 本文介紹分片服務。 通過我們在前一章中介紹的複製服務,每個副本都是完全同構的,並且能夠滿足每個請求。 與使用分片服務的複製服務相比,每個副本或分片僅能夠提供所有請求的子集。 負載平衡節點或root負責檢查每個請求並將每個請求分發到適當的分片或碎片進行處理。 圖1表示了複製和分片服務之間的對比。
複製服務通常用於構建無狀態服務,而分片服務通常用於構建有狀態服務。 分解數據的主要原因是因為狀態的大小太大而無法由單個機器提供服務。 分片使你能夠根據需要提供的狀態的大小來擴展服務。
分片緩存
為了完整地說明分片系統的設計,本節深入介紹了分片緩存系統的設計。 分片緩存是位於用戶請求和實際前端實現之間的緩存。 圖2顯示了該系統的架構圖。
前面我們的介紹中,我們討論了如何使用大使模式來分發數據到分片服務。 本節討論如何構建該服務。 在設計分片緩存時,需要考慮許多設計方面的問題:
?為什麼需要分片緩存?
?緩存在架構中的角色
?複製和分片緩存
?分片函數
為什麼需要分片緩存?
正如介紹中提到的那樣,分解任何服務的主要原因是增加存儲在服務中的數據的大小。要了解並實現緩存系統,可以想像以下系統:每個緩存都有10 GB的RAM可用於存儲結果,並且可以每秒處理100個請求(RPS)。假設我們的服務總共有200 GB可能的結果可以返回,並且預計有1,000 RPS。顯然,我們需要10個緩存副本以滿足1,000 RPS(每個副本每秒10個副本×100個請求)。如前一章所述,部署此服務的最簡單方法是作為複製服務。但以這種方式部署,分散式緩存最多只能保存我們所服務的總數據集的5%(10 GB / 200 GB)。這是因為每個高速緩存副本都是獨立的,因此每個高速緩存副本大致在高速緩存中存儲完全相同的數據。這對冗餘很有用,但對於最大化內存利用來說非常糟糕。相反,如果我們部署一個10路分片緩存,我們仍然可以提供適當數量的RPS(10×100仍然是1,000),但由於每個緩存都提供一組完全獨特的數據,因此我們可以存儲50個總數據集的百分比(10×10 GB / 200 GB)。高速緩存存儲增加了10倍,這意味著高速緩存的內存利用率更高,因為每個密鑰只存在於單個高速緩存中。
緩存在系統性能中的作用
在前面,我們討論了如何使用緩存來優化終端用戶的性能和延遲,但是沒有涉及的一件事是緩存對應用程序性能,可靠性和穩定性的關鍵作用。
簡而言之,你需要考慮的問題是:如果緩存失敗,那麼的用戶和服務會受到什麼影響?
當我們討論複製緩存時,這個問題不太相關,因為緩存本身是水平可伸縮的,特定副本的失敗只會導致暫時失敗。同樣,緩存可以水平縮放以響應增加的負載而不影響用戶。
當你考慮分片緩存時,就會發生變化。因為特定的用戶或請求總是映射到同一個分片,所以如果該分片失敗,則該用戶或請求將始終未命中緩存,直到分片恢復。考慮到緩存作為瞬態數據的性質,這種未命中本質上不成問題,你的系統必須知道如何重新計算數據。但是,這種重新計算本質上比直接使用緩存要慢,因此它對用戶的訪問具有性能影響。
你的緩存的性能是按其命中率來定義的。命中率是你的緩存包含用戶請求數據的時間百分比。最後,命中率決定了分散式系統的整體容量,並影響系統的整體容量和性能。想像一下,你擁有可處理1,000 RPS的請求服務層,在1,000 RPS後,系統開始向用戶返回HTTP 500錯誤。如果你在此請求服務層前放置一個命中率為50%的緩存,則添加此緩存可將您的最大RPS從1,000 RPS提高到2,000 RPS。為了理解為什麼這是真的,你可以看到2000個入站請求中有1,000個(50%)可以被緩存服務,而剩下的1,000個請求將由服務層提供服務。在這種情況下,緩存對於您的服務非常重要,因為如果緩存失敗,那麼服務層將會過載,所有用戶請求中的一半將失敗。鑒於此,對你的服務進行評估可能最多為1,500 RPS,而不是完整的2,000 RPS。如果你這樣做,那麼你可以維持一半的緩存副本失敗,並保持服務的穩定。
但是,系統的性能不僅僅是根據它可以處理的請求的數量來定義的。系統的最終用戶性能也是根據請求延遲來定義的。來自緩存的結果通常比從頭開始計算結果快得多。因此,緩存可以提高請求的速度以及處理請求的總數。為了弄清楚為什麼這是真的,假設你的系統可以在100毫秒內提供來自用戶的請求。您添加一個25%命中率的緩存,可以在10毫秒內返回結果。因此,系統中請求的平均延遲時間現在為77.5毫秒。與每秒最大請求數不同,高速緩存只是簡化了您的請求速度,因此,如果緩存失敗或正在升級,請求速度會變慢,這樣就不用擔心了。但是,在某些情況下,性能影響會導致太多的用戶請求堆積在請求隊列中,並最終超時。總是建議您載入測試系統,無論是否使用高速緩存,以了解高速緩存對系統整體性能的影響。
最後,這不僅僅是你需要考慮的失敗。如果你需要升級或重新部署分片緩存,則不能僅部署新副本並假定其將承擔負載。部署新版本的分片緩存通常會導致暫時失去一些容量。另一個更高級的選項是複製你的碎片。
複製的分片緩存
有時你的系統如此依賴於緩存的延遲或負載,如果發生故障或正在進行部署新版本,丟失整個緩存碎片是不可接受的。或者,你可能在特定高速緩存分片上承受如此多的負載,你需要對其進行擴展以處理負載。由於這些原因,你可以選擇部署分片的複製服務。分片複製服務將前一章中描述的複製服務模式與前面部分中描述的分片模式相結合。簡而言之,不是讓一台伺服器實現緩存中的每個分片,而是使用複製服務來實現每個緩存分片。
這種設計顯然比實施和部署更複雜,但與簡單的分片服務相比,它具有幾個優勢。最重要的是,通過用複製服務替換單個伺服器,每個緩存分片都具有故障恢復能力,並且在故障期間始終存在。你可以依賴緩存提供的性能改進,而不是將系統設計為容忍緩存碎片故障導致的性能下降。假設你願意過度配置分片容量,這意味著你在高峰流量期間執行高速緩存是安全的,而不是等待服務的安靜期。
此外,由於每個複製緩存分片都是獨立的複製服務,因此可以縮放每個緩存分片以響應其負載;在本章最後討論這種「熱分片」。
分片函數實驗
到目前為止,我們已經討論過簡單分片和複製分片緩存的設計和部署,但我們沒有花太多時間考慮流量如何分配到不同的分片。考慮一個分片服務,其中有10個獨立的分片。給定一些特定的用戶請求Req,你如何確定哪個分片S在0到9的範圍內用於請求?這個映射是分片函數的責任。分片函數與散列函數非常相似,你可能在學習散列表數據結構時遇到過散列函數。實際上,基於桶的散列表可以被視為分片服務的一個例子。鑒於Req和Shard,分割函數的作用是將它們聯繫在一起,具體為:
通常,使用哈希函數和模(%)運算符來定義分片函數。散列函數是將任意對象轉換為整數散列的函數。散列函數對於我們的分片有兩個重要特徵:
- 確定性:輸出對於唯一輸入應始終相同。
- 均勻度:輸出空間的輸出分布應該是相等的。
對於我們的分包服務,確定性和均勻性是最重要的特徵。確定性很重要,因為它確保了一個特定的請求R總是進入服務中的同一個分片。均勻性很重要,因為它確保負載均勻分布在不同的碎片之間。
對我們來說幸運的是,現代編程語言包括各種高質量的散列函數。但是,這些散列函數的輸出通常明顯大於分片服務中的分片數量。因此,我們使用模運算符(%)將散列函數減少到適當的範圍。通過10個分片返回到我們的分片服務,我們可以看到我們可以將分片函數定義為:
Shard = hash(Req)%10
如果散列函數的輸出在威懾方面具有適當的屬性,這些屬性將由模運算符保存。
選擇一個鍵
考慮到這個分片函數,只需使用內置於編程語言中的哈希函數,對整個對象進行哈希處理,並在一天內調用它可能會很誘人。但是,這樣做的結果不會是一個非常好的分片功能。
要理解這一點,請考慮一個簡單的HTTP請求,其中包含三件事情:
- 請求的時間
- 來自客戶端的源IP地址
- HTTP請求路徑(例如/some/page.html)
如果我們使用簡單的基於對象的散列函數shard(request),那麼{12:00, 1.2.3.4, /some/file.html} 具有與{12:01,5.6.7.8, /some/file.html}。分片函數的輸出是不同的,因為客戶端的IP地址和請求時間在兩個請求之間是不同的。但是,當然,在大多數情況下,客戶端的IP地址和請求時間不會影響對HTTP請求的響應。因此,而不是散列整個請求對象,更好的分片功能將是碎片(request.path)。當我們使用request.path作為分片鍵時,我們將兩個請求映射到同一個分片,並且因此對一個請求的響應可以從緩存中提供以服務另一個請求。
當然,有時客戶端IP對於從前端返回的響應很重要。例如,可以使用客戶端IP來查找用戶所在的地理區域,並且可以將不同的內容(例如,不同的語言)返回到不同的IP地址。在這種情況下,前一個分片函數shard(request.path)實際上會導致錯誤,因為來自法文IP地址的緩存請求可能會從英文緩存中的結果頁面提供。在這種情況下,緩存功能過於籠統,因為它將沒有相同響應的請求分組在一起。
考慮到這個問題,那麼將分片函數定義為shard(request.ip,request.path)會很誘人,但是這個分片函數也有問題。它會導致兩個不同的法國IP地址映射到不同的分片,從而導致分片效率低下。這個分片功能太特殊,因為它無法將相同的請求分組在一起。對於這種情況,更好的分片功能是:
shard(country(request.ip),request.path)
首先從IP地址確定國家,然後將該國家用作分片功能的關鍵部分。因此來自法國的多個請求將被路由到一個碎片,而來自美國的請求將被路由到一個不同的碎片。
為你的分片功能確定合適的密鑰對於設計你的分片系統至關重要。確定正確的分片密鑰需要了解您期望看到的請求。
一致的哈希函數
為新服務設置初始分片比較簡單:設置合適的分片和根分片來執行分片,然後您就可以參加比賽了。但是,當您需要更改分片服務中的分片數時會發生什麼情況?這種「重新分片」往往是一個複雜的過程。
要理解為什麼這是真的,請考慮先前檢查的分片緩存。確切地說,將緩存從10個擴展到11個副本與使用容器協調器直接相關,但考慮將擴展功能從哈希(Req)%10更改為哈希(Req)%11的效果。當您部署這個新的縮放功能,大量的請求將被映射到與之前映射到的不同的分片。在分片緩存中,這將顯著提高您的遺漏率,直到緩存重新填充新的分片函數已映射到該緩存分片的新請求的響應。在最糟糕的情況下,為分片緩存推出新的分片功能將相當於完全緩存失敗。
為了解決這些問題,許多分片函數使用一致的散列函數。一致的哈希函數是特殊的哈希函數,當被調整為#分片時,保證只重映射#keys /#分片。例如,如果我們對分片緩存使用一致的散列函數,則從10分片移動到11分片只會導致重新映射<10%(K / 11)個密鑰。這比丟失整個分片服務好得多。
分片複製服務
到目前為止,本章中的大部分示例都描述了緩存服務的分片。但是,當然,緩存並不是可以從分片中受益的唯一服務。在考慮任何類型的服務時,如果數據量多於單個機器上的數據量,Sharding就很有用。與前面的例子不同,key和sharding函數不是HTTP請求的一部分,而是用戶的一些上下文。
例如,考慮實施大型多人遊戲。這樣的遊戲世界可能太大而不適合單個機器。但是,在這個虛擬世界中彼此遠離的玩家不太可能互動。因此,遊戲世界可以在許多不同的機器上分割。分片功能從玩家的位置被鎖定,以便特定位置的所有玩家都落在同一組伺服器上。
熱分片系統
理想情況下,分片緩存中的負載將完全平衡,但在很多情況下,這不是真實的,並且出現「熱碎片」,因為有機負載模式會將更多流量驅動到某個特定碎片。
作為一個例子,考慮用戶照片的分片緩存;當一張特定的照片發生病毒並突然收到不成比例的流量時,包含該照片的緩存分片將變為「熱」。發生這種情況時,使用複製的分片緩存,可以縮放緩存分片以響應增加的負載。事實上,如果你為每個緩存碎片設置自動縮放,則隨著服務的有機流量轉移,你可以動態增大和縮小每個複製碎片。圖3給出了這個過程的例子。最初,分片服務接收到所有三個分片的相等流量。然後流量發生變化,使得碎片A的接收量是碎片B和碎片C的四倍。熱碎片系統將碎片B移動到與碎片C相同的機器,並將碎片A複製到第二台機器。現在,流量再次在副本之間平均分配。
推薦閱讀:
※用zookeeper來構建的一種一致性副本協議
※Elasticell和Jepsen測試
※集群資源調度系統設計架構總結
※OceanBase知乎官方號開通了!
※morning paper: strong consistent quorum read in