[原創]分佈式系統之緩存的微觀應用經驗談(三)【數據分片和集羣篇】

分佈式系統之緩存的微觀應用經驗談(三)【數據分片和集羣篇】
前言 
 
  近幾個月一直在忙些瑣事,幾乎年後都沒怎麼閒過。忙忙碌碌中就進入了2018年的秋天了,不得不感嘆時間總是如白駒過隙,也不知道收穫了什麼和失去了什麼。最近稍微休息,買了兩本與技術無關的書,其一是 Yann Martel 寫的《The High Mountains of Portugal》(葡萄牙的高山),發現閱讀此書是需要一些耐心的,對人生暗喻很深,也有足夠的留白,有興趣的朋友可以細品下。好了,下面迴歸正題,嘗試寫寫工作中緩存技術相關的一些實戰經驗和思考。
正文
 
  在分佈式Web程序設計中,解決高併發以及內部解耦的關鍵技術離不開緩存和隊列,而緩存角色類似計算機硬件中CPU的各級緩存。如今的業務規模稍大的互聯網項目,即使在最初beta版的開發上,都會進行預留設計。但是在諸多應用場景裏,也帶來了某些高成本的技術問題,需要細緻權衡。本系列主要圍繞分佈式系統中服務端緩存相關技術,也會結合朋友間的探討提及自己的思考細節。文中若有不妥之處,懇請指正。
爲了方便獨立成文,原諒在內容排版上的一點點個人強迫症。
第三篇這裏嘗試談談緩存的數據分片(Sharding)以及集羣(Cluster)相關方案(具體應用依然以Redis 舉例)
另見:分佈式系統之緩存的微觀應用經驗談(二) 【主從和主備高可用篇】( https://www.cnblogs.com/bsfz/
一、先分析緩存數據的分片(Sharding
(注:由於目前個人工作中大多數情況應用的是Redis 3.x,以下若有特性關聯,均是以此作爲參照說明。)
緩存在很多時候同 RDBMS類似,解決數據的分佈式存儲的基礎理念就是把整個數據集按照一定的規則(切分算法)映射到多個節點(node)中,每個 node負責處理整體數據的一個子集。給緩存作 Sharding 設計,圍繞基礎數據的存儲、通信、數據複製和整合查詢等,很多時候比較類似 RDBMS中的水平分區(Horizontal Partitioning),事實上很多點在底層原理上是保持一致的。在緩存的分區策略中,最常見的是基於哈希的各種算法。
1.1 基於 Round Robin
實現思路是以緩存條目的標識進行哈希取餘,如對緩存中的 Key 進行 Hash計算,然後將結果 R 與 node 個數 N 進行取餘,即 R%N 用來指定數據歸屬到的 node索引。
個人認爲在數據量比較固定並有一定規律的場景下,則可以考慮基於這種方式的設計。在落地實踐前需要注意,這種方案看起來簡潔高效,但卻無法良好的解決 node的彈性伸縮問題,比如數量 N發生變化時均需要重新覆蓋計算,存儲的數據幾乎是重新遷移甚至重置,對數據的支撐本身會有些勉強和侷限。另外,早期使用手動進行預分區,後來增加了一些相應的路由策略可進行翻倍擴容等,都可考慮作爲實踐場景中的某些細小優化和輔助。
1.2 基於 Consistent Hashing
實現思路是將 node 進行串聯組合形成一個 Hash環,每個 node 均被分配一個 token作爲 sign,sign取值範圍對應 Hash結果區間,即通過Hash計算時,每個 node都會在環上擁有一個獨一無二的位置,此時將緩存中的Key做基本Hash計算,根據計算結果放置在就近的 node 上,且 node 的 sign 大於等於該計算結果。
這種策略的優勢體現在,當數據更迭較大,動態調整node(增加/刪除)隻影響整個 Hash環中個別相鄰的 node,而對其他 node則無需作任何操作。也就是說,當數據發生大量變動時,可以有效將影響控制在局部區間內,避免了不必要的過多數據遷移,這在緩存的條目非常多、部署的 node也較多的時候,可以有效形成一個真正意義上的分佈式均衡。但在架構落地之前,這種方案除了必要的對 node 調整時間的額外控制,還需要權衡下緩存數據的體量與 node 的數量進行比較後的密集度,當這個比值過大時,數據影響也自然過大,這個就是不符合設計初衷的,不僅沒有得到應有的優化,反而增加了一定的技術成本。
1.3 基於 Range Partitioning
實現思路實際是一種增加類似中間件的包裝思想,首先將所有的緩存數據統一劃分到多個自定義區間(Range),然後將這些 Range逐一綁定到關聯服務的各個 node中,每個 Range將在數據變化時進行相應調整以達到均衡負載。
這其實並非是一個完全新穎的策略,但針對大數據的劃分和交互做了更多的考慮。以 Redis的 Sharding算法類比,截止目前的集羣方案(Redis Cluster,以3.x舉例)中,其策略同樣包含了 Range這一元素概念。Redis採用虛擬槽(slot)來標記,數據合計 16384個 slot。緩存數據根據 key進行 Hash歸類到各個 node綁定的 slot。當動態伸縮 node時,針對 slot做相應的分配,即間接對數據作遷移。
這種上層的包裝,雖然極大地方便了集羣的關注點和線性擴展,在目前落地方案裏 Redis也已經盡力擴展完善,但依然還是處於半自動狀態。集羣是分佈的 N個 node,如需要伸展爲 N+2個node,那麼可以手動或者結合其他輔助框架給每個 node進行劃分和調整,一般均衡數約爲 16384 /(N+2) slot。同樣的,Cluster 的Sharding 在實際落地之前,也需要注意到其不適合的場景,並根據實際數據體量和 QPS瓶頸來合理擴展node,同時處理事務型的應用、統計查詢等,對比單機自然是效率較低,這時候可能需要權衡規避過多的擴展,越多從來都不代表越穩定或者說性能越高。
二、談談緩存的集羣實踐與相關細節
2.1 提下集羣的流程

我在之前一篇文章裏,主要圍繞主從和高可用進行了一些討論(主從和主備高可用篇:https://www.cnblogs.com/bsfz/p/9769503.html),要提出的是 Master-Slave 結構上來說同樣算是一種集羣的表現形式,而在 Redis Cluster 方案裏,則更側重分佈式數據分片集羣,性能上能規避一些冗餘數據的內存浪費以及木桶效應,並同時具備類似 Sentinel機制的 HA和 Failover等特性(但要注意並非完全替換)。
這裏同樣涉及到運維、架構 、開發等相關,但個人依然側重於針對架構和開發來做一些討論。 當然,涉及架構中對網絡I/O、CPU的負載,以及某些場景下的磁盤I/O的代價等問題的權衡,其實大體都是相通的,部分可參見之前文章中的具體闡述。
簡單說在集羣模式 Redis Cluster 下, node 的角色分爲 Master node 和 Slave node,明面上不存在第三角色。 Master node 將被分配一定範圍的 slots 作爲數據 Sharding 的承載,而 Slave node 則主要負責數據的複製(相關交互細節可參照上一篇) 並進行出現故障時半自動完成故障轉移(HA的實現)。node之間的通信依賴一個相對完善的去中心化的高容錯協議Gossip, 當擴展node、node不可達、node升級、 slots修改等時, 內部需要經過一段較短時間的反覆ping/pong消息通信, 並最終達到集羣狀態同步一致。
以 Redis 3.x 舉例,假定一共 10 個 node,Master / Slave = 5 / 5,執行基礎握手指令 " cluster meet [ip port] "後,就能很快在 cluster nodes裏看到相應會話日誌信息,在這個基礎上,再給每個node 添加指定 Range 的 lots( addslots指令),就基本完成了 Redis Cluster的基礎構建。(當然,如果是側重運維,一般你可以手動自定義配置,也可以使用 redis-trib.rb來輔助操作,這裏不討論)。
2.2 Redis Cluster的部分限制
Redis node的拓撲結構設計,目前只能採用單層拓撲,即不可直接進行樹狀延伸擴展node,注意這裏是不同於 Redis Mater-Slave 基本模式的,然後記得在上一篇也提到了本人迄今爲止也並未有機會在項目中使用,也是作爲備用。

對於原有 DB空間的劃分基本等同取消,這個有在第一篇設計細節話題中提到過,並且 Redis Cluster 模式下只能默認使用第一個DB, 即索引爲0 標識的庫。
假如存在大數據表 “Table”,例如 hash、list 等,是不可以直接採用更細粒度的操作來 Sharding 的,即使強制分散到不同 node 中,也會造成 slot 的覆蓋錯誤。
緩存數據的批量操作無法充分支持,如 mset / mget 並不能直接操作到所有對應 key中,除非是具有相同 slot。 這是由於 Sharding機制原理決定的,舉一反三,若是存在事務操作,也存在相關的限制(另外稍微注意,截止目前,不同node 本身也是無法事務關聯的)。
2.3 Redis Cluster的相關細節考慮
對於 Redis node 的數量 N 理論上需要保持偶數臺,一般不少於6個才能保證組成一個閉環的高可用的集羣,這也意味着理想狀態至少需要6臺服務器來承載,但這裏個人在架構設計中,往往場景不是很敏感,那麼將設計爲 N/2臺服務器分佈,目的是兼顧成本以及折中照顧到主從之間的 HA機制(HA相關可以參照上一篇裏提到的部分延伸,這裏儘量避免重複性討論)。 這裏要稍微注意的是,對於機器的配對,儘量保證不要在同一臺機器上配置過多的Master,否則會嚴重影響選舉,甚至failover被直接拒絕,無法重建
對於 node 之間的消息交互,每次發送的數據均包含 slot數據和集羣基礎狀態,node越多分發的數據也幾近是倍增,這個在上面 Sharding 算法裏也表明,那麼一方面可以針對 node數量進行控制,另一個則是設置合理的消息發送頻率,比如在主要配置 cluster-node-timeout上,適當由默認15秒遞增 5/10 秒。但是過度調大 cluster_node_timeout相關設置一定會影響到消息交換的實時性,所以我認爲這裏可以嘗試微調,在大多數本身比較均勻分佈數據的場景下適當放寬,這樣不會對node檢測和選舉產生較大影響,同時也間接節約了一定網絡IO。
對於數據增長粒度較大的場景,優先控制集羣的 node數量,否則同樣避免不了一個比較大頭的用戶指令消耗 和 Gossip維護消息開銷(集羣內所有 node的ping/pong消息),官方早前就建議控制集羣規模不是沒有道理的。個人認爲真到了需要的場景,必須主動作減法,縮減node數,取而代之使用小的集羣來分散業務,而且這也有利於更精確的控制風險和針對性優化。
對於集羣的伸縮,如項目中應用較多操作的一般是擴容場景,增加新的node 建議跟集羣內的已有 node 配置保持一致,並且在完成 cluster meet 後,需要合理控制劃分出的 slot 數,一般沒有特殊要求,應該都是均勻化。額外要稍微注意的是,新的 node 務必保證是一個乾淨的node,否則會造成不必要的拓撲錯誤(這種是可能會導致數據分佈複製嚴重錯亂的),當然新增 node這裏也可以藉助 redis-trib.rb 或者其他第三方包裝的方案來輔助操作。
對於不在當前 node 的鍵指令查詢,默認是隻回覆重定向轉移響應(redirect / moved)給到調用的客戶端(這裏特指應用程序端),並不負責轉發。這是跟單機是完全不同的,所以即使是使用相關的第三方驅動庫(比如JAVA的Jedis、和.Net的 StackExchange.Redis)完成程序端的封閉式控制,也仍舊需要權衡數據的熱點分散是否足夠集中在各自的node中等細節。當然,假如是 hashset等結構,由於Cluster本身的Sharding機制涉及到不可分散負載,倒是無需過多編碼實現,也不用擔心性能在這裏的損耗。
結語
 
  本篇先寫到這裏,下一篇會繼續圍繞相關主題嘗試擴展闡述。
  PS:由於個人能力和經驗均有限,自己也在持續學習和實踐,文中若有不妥之處,懇請指正。
 
個人目前備用地址:

【預留佔位:分佈式系統之緩存的微觀應用經驗談(四)【交互場景篇】
End.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章