分散式系統之快取的微觀應用經驗談(三)【資料分片和叢集篇】
前言
近幾個月一直在忙些瑣事,幾乎年後都沒怎麼閒過。忙忙碌碌中就進入了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:由於個人能力和經驗均有限,自己也在持續學習和實踐,文中若有不妥之處,懇請指正。
個人目前備用地址:
社群1:https://yq.aliyun.com/u/autumnbing
社群2:https://www.cnblogs.com/bsfz/
【預留佔位:分散式系統之快取的微觀應用經驗談(四)【互動場景篇】
End.