分區,又稱為分片,是解決大數據存儲的常見解決方案,大數據存儲量超過了單節點的存儲上限,因此需要進行分區操作將數據分散存儲在不同節點上,通常每個單個分區可以理解成一個小型的數據庫,盡管數據庫能同時支持多個分區操作;分區引入多分區概念,可以同時對外服務提高性能。
常常和分區一並提及的概念是復制,分區通常與復制結合使⽤,使得每個分區的副本存儲在多個節點上。 這意味着,即使每條記錄屬於⼀個分區,它仍然可以存儲在多個不同的節點上以獲得容錯能⼒。分區在許多技術或框架中都有體現,例如MQ中topic下的分區消息實現,如kafka中的partion、rocketmq中的queue等;例如SQL/NoSQL中分區數據儲存實現,如ElascticSearch中的Shards分片、MySQL中的分表等。
關於分區,本文主要討論下鍵值分區的幾種方式、分區再平衡策略和請求路由處理機制等,最后以ES(ElascticSearch)的查詢請求處理為例,分析分區下查詢的請求處理流程。話不多說,Let's Go~
鍵值分區的幾種方式
如果有大量數據需要分散存儲,應該如何進行分區呢?分區的目前就是將數據均衡的分散在各節點,這樣同時也能分散對數據的處理請求,如果分區不均衡,那么會造成某些分區有大量的數據或查詢請求,這就是常說的傾斜。數據傾斜會造成高負載節點形成熱點,避免熱點可以使用隨機路由方式將數據散列到各分區中。對數據進行分區操作,不能僅僅是隨機數據存儲,因為存儲之后肯定還是要進行查詢的,所以要按照固定鍵值來進行散列分區操作,方便后續查詢請求的路由。常見的鍵值分區方式有按照范圍分區、按照鍵的散列分區:
按照范圍分區
按照范圍分區就是每個分區存儲指定一段連續的數據,比如按照時間戳來存儲數據,最簡單常見的日志按照時間分割為不同的文件;按照編號id來存儲數據,如圖書館書籍陳列,編號連續數據存放在同一個書架上。按照范圍分區有時候會造成分區數據不均衡,比如按照時間戳,可能某段時間內數據比較少而某些時間段數據較多而造成分區不均衡。
鍵值散列分區
由於按照范圍分區容易造成數據負載不均衡問題,所以一般應用場景下(非順序類型數據)為了避免偏斜和熱點的⻛險,會使⽤散列函數來確定給定鍵的分區。一個好的散列函數會盡量隨機分區,許多語言內都內置了散列函數,但是有些可能不太適合分區場景,比如Java的 Object.hashCode()和Ruby的 Object#hash,其同⼀個鍵可能在不同的進程中有不同的哈希值。
有了合適的散列函數,有時候想要讓一定散列范圍內的數據分布在同一分區,此時可使用一致性哈希,一致性哈希可減小因為分區變動造成會已有數據分區映射的影響。
熱點問題
哈希分區可幫助減少熱點,但是無法避免,極端情況下可能存在所有請求都打到同一分區中。熱點分區問題解決思路是:一種是給熱點分區再分區操作,比如針對熱點數據的key再路由分散多個分區中;還有一種是熱點數據增加冗余(也就是復制),增加熱點數據的復制節點,一同對外提供服務。
分區再平衡
隨着時間的推移,分區數據會有以下變化:
- 查詢吞吐量增加,所以您想要添加更多的CPU來處理負載。
- 數據集⼤⼩增加,所以您想添加更多的磁盤和RAM來存儲它。
- 機器出現故障,其他機器需要接管故障機器的責任。
所有這些更改都需要數據和請求從⼀個節點移動到另⼀個節點。 將負載從集群中的⼀個節點向另⼀個節點移動的過程稱為再平衡(reblancing),再平衡過程一般要求如下:再平衡之后數據盡量均衡、在平衡時分區要正常地外提供服務、節點之間只移動必要數據以加快再平衡進度。(一般來說直接使用取余方式散列的分區再平衡時大都需要將所有數據重新取余再分區,成本較大。)
固定數目的分區
為了避免分區的擴容再平衡操作,可以創建⽐節點更多的分區,並為每個節點分配多個分區。例如,運⾏在10個節點的集群上的數據庫可能會從⼀開始就被拆分為1000個分區,因此⼤約有100個分區被分配給每個節點。比如ES就是用了這種再平衡方式,ES中的shards分片在運行時是無法更改的,因此生產環境一般會建議針對分區數設定留一定的余量,方便后續擴容操作。這樣的話,分區的數量不會變化,知識分區數據會在節點間移動而已,鍵所指定的分區也不會改變。唯⼀改變的是分區所在的節點。這種變更並不是即時的,在⽹絡上傳輸⼤量的數據需要⼀些時間,所以在傳輸過程中,原有分區仍然會接受讀寫操作。如下圖所示:
動態分區
對於使用鍵范圍場景來說,具有固定邊界的固定數量的分區將⾮常不便:如果出現邊界錯誤,則可能會導致⼀個分區中的所有數據或者其他分區中的所有數據為空。⼿動重新配置分區邊界將⾮常繁瑣。因此,按鍵范圍進行分區的數據庫(如HBase和RethinkDB)會動態創建分區。當分區增⻓
到超過配置的⼤⼩時(在HBase上,默認值是10GB),會被分成兩個分區,每個分區約占⼀半的數據。與之相反,如果⼤量數據被刪除並且分區縮⼩到某個閾值以下,則可以將其與相鄰分區合並,類似B樹的過程類似。
動態分區的⼀個優點是分區數量適應總數據量。如果只有少量的數據,少量的分區就⾜夠了,所以開銷很⼩;如果有⼤量的數據,每個分區的⼤⼩被限制在⼀個可配置的最⼤值,當超過閾值時觸發分區操作。
再平衡操作觸發時,到底應該由人為觸發還是由程序自動觸發呢?程序自動觸發,一般是檢測節點負載過高或者(通過網絡心跳發現)某個節點掛了,自動再平衡可能因為某些外界環境的影響就執行了,可能達不到我們的預期,因此,一個合理的方案是,程序自動發現應該執行再平衡時,可以報警通知到運維人員,由人工介入來處理后續的再平衡執行。
請求路由處理
當處理請求時,如何確定哪個節點執行呢?隨着分區再平衡,分區對節點的分配也發生變化,為了回答這個問題,需要有⼈知曉這些變化:如果我想讀或寫鍵“foo”,需要連接哪個節點IP地址和端⼝號?這個問題本質上就是服務發現,它不僅僅體現在數據庫,任何網絡通信場景都有這個問題,特別是如果它的⽬標是⾼可⽤性(在多台機器上運⾏冗余配置),都需要服務發現。概括來說,請求路由處理,有以下幾種處理方案:
- 允許客戶聯系任何節點(例如,通過循環策略的負載均衡(Round-Robin Load Balancer))。如果該節點恰巧擁有請求的分區,則它可以直接處理該請求;否則,它將請求轉發到適當的節點,接收回復並傳遞給客戶端。
- ⾸先將所有來⾃客戶端的請求發送到路由層,它決定了應該處理請求的節點,並相應地轉發。此路由層本身不處理任何請求;它僅負責分區的負載均衡。
- 要求客戶端知道分區和節點的分配。在這種情況下,客戶端可以直接連接到適當的節點,⽽不需要任何中介代理。
以上所有情況的關鍵問題是,做出路由決策的組件(可能是節點之一、客戶端或者路由代理)如何知道分區-節點之間的映射關系。映射關系可以使固定寫死在代碼中,也可以是配置在配置中心中。許多分布式數據系統都依賴於⼀個獨⽴的協調服務,⽐如ZooKeeper來跟蹤集群元數據。 每個節點在ZooKeeper中注冊⾃⼰,ZooKeeper維護分區到節點的可靠映射。 其他參與者(如路由層或分區感知客戶端)可以在ZooKeeper中訂閱此信息。 只要分區分配發⽣的改變,或者集群中添加或刪除了⼀個節點,ZooKeeper就會通知路由層使路由信息保持最新狀態。
執行查詢
請求處理查詢可分為兩種場景,單節點查詢和集群查詢,前者一般是針對一類數據的查詢並且該類數據存儲在同一個節點上,后者是同時發給多個節點,最后再做聚合操作。集群查詢也稱為並行查詢,通常⽤於分析的⼤規模並⾏處理(MPP, Massively parallel processing) 關系型數據庫產品在
其⽀持的查詢類型⽅⾯要復雜得多。⼀個典型的數據倉庫查詢包含多個連接,過濾,分組和聚合操作。
ES的查詢處理流程
ES使用開源的Lucene作為存儲引擎,它賦予ES高性能的數據檢索能力,但Lucene僅僅是一個單機索引庫。ES基於Lucene進行分布式封裝,以支持集群管理、分布式查詢、聚合分析等功能。
從使用的直觀感受看,ES查詢分為2個階段,query和fetch階段。在query階段會從所有的shard上讀取相關document的docId及相關的排序字段值,並最終在coordinating節點上收集所有的結果數進入一個全局的排序列表后,然后獲取根據from+size指定page頁的數據,獲取這些docId后再構建一個multi-get請求發送相關的shard上從_source里面獲取需要加載的數據,最終再返回給client端。
query階段:
fetch階段:
所有的搜索系統一般都是兩階段查詢,第一階段查詢到匹配的DocID,第二階段再查詢DocID對應的完整文檔,這種在Elasticsearch中稱為query_then_fetch,還有一種是一階段查詢的時候就返回完整Doc,在Elasticsearch中稱作query_and_fetch,一般第二種適用於只需要查詢一個Shard的請求。由上圖可知,ES允許客戶聯系任何節點,如果該節點恰巧擁有請求的分區,則它可以直接處理該請求;否則,它將請求轉發到適當的節點,接收回復然后聚合並傳遞最終的聚合結果給客戶端。
小結
大數據量場景在單台機器上存儲和處理不再可⾏,則分區⼗分必要。分區的⽬標是在多台機器上均勻分布數據和查詢負載,避免出現熱點(負載不成⽐例的節點)。這需要選擇適合於您的數據的分區⽅案,並在將節點添加到集群或從集群刪除時進⾏再分區。
常見的鍵值分區方式有按照范圍分區、按照鍵的散列分區兩種。請求的處理機制一般有客戶端處理、代理處理、服務節點處理3種方式,不管哪種方式,都需要其知道分區-節點之間的映射關系,一般映射關系是保存在配置中心上,比如zookeeper。
推薦閱讀