Redis Cluster
Redis Cluster是Redis官方在Redis 3.0版本正式推出的高可用以及分布式的解決方案。
Redis Cluster由多個Redis實例組成的整體,數據按照槽(slot)存儲分布在多個Redis實例上,通過Gossip協議來進行節點之間通信。
Redis Cluster實現的功能:
? 將數據分片到多個實例(按照slot存儲);
? 集群節點宕掉會自動failover;
? 提供相對平滑擴容(縮容)節點。
Redis Cluster暫未有的:
? 實時同步
? 強一致性
Redis Cluster分片實現
一般分片(Sharding)實現的方式有list、range和hash(或者基于上述的組合方式)等方式。而Redis的實現方式是基于hash的分片方式,具體是虛擬槽分區。
虛擬槽分區
槽(slot):使用分散度良好的hash函數把所有數據映射到一個固定范圍的整數集合中,這個整數集合就是槽。
Redis Cluster槽: Redis Cluster槽的范圍是0 ~ 16383。槽是集群內數據管理和遷移的基本單位。
分片的具體算法
Redis Cluster使用slot ?= CRC16(key) %16384來計算鍵key屬于哪個slot。(Redis先對key使用CRC16算法計算出一個結果,然后再把結果對16384求余,得到結果即跟Redis Cluster的slot對應,也就是對應數據存儲的槽數。)
(注: CRC16算法——循環冗余校驗(Cyclic Redundancy Check/Code),Redis使用的是CRC-16-CCITT標準,即G(x)為:x16+?x12+?x5+ 1。)
Redis Cluster中的每個分片只需要維護自己的槽以及槽所映射的鍵值數據。
Hash標簽
哈希標簽(hash tags),在Redis集群分片中,可以通過哈希標簽來實現指定兩個及以上的Key在同一個slot中。只要Key包含“{…}”這種模式,Redis就會根據第一次出現的’{’和第一次出現的’}’之間的字符串進行哈希計算以獲取相對應的slot數。如上Redis源碼實現。
所以如果要指定某些Key存儲到同一個slot中,只需要在命令Key的之后指定相同的“{…}”命名模式即可。
集群節點和槽
我們現在已經知道,Redis Cluster中的keys被分割為16384個槽(slot),如果一個槽一個節點的話,那Redis Cluster最大的節點數量也就是16384個。官方推薦最大節點數量為1000個左右。
(關于Redis為什么使用16384個槽,原作者有回答:why redis-cluster use 16384 slots?)
1當Redis Cluster中的16384個槽都有節點在處理時,集群處于上線狀態(ok);
如果Redis Cluster中有任何一個槽沒有得到處理(或者某一分片的最后一個節點掛了),那么集群處于下線狀態(fail)。(info cluster中的:cluster_state狀態)。那整個集群就不能對外提供服務。
Redis-3.0.0.rc1加入cluster-require-full-coverage參數,默認關閉,打開集群容忍部分失敗。
但是如果集群超過半數以上master掛掉,無論是否有slave集群進入fail狀態。
節點ID
Redis Cluster每個節點在集群中都有唯一的ID,該ID是由40位的16進制字符組成,具體是節點第一次啟動由linux的/dev/urandom生成。具體信息會保存在node.cnf配置文件中(該文件有Redis Cluster自動維護,可以通過參數cluster-config-file來指定路徑和名稱),如果該文件被刪除,節點ID將會重新生成。(刪除以后所有的cluster和replication信息都沒有了)或者通過Cluster Reset強制請求硬重置。
節點ID用于標識集群中的每個節點,包括指定Replication Master。只要節點ID不改變,哪怕節點的IP和端口發生了改變,Redis Cluster可以自動識別出IP和端口的變化,并將變更的信息通過Gossip協議廣播給其他節點。
ClusterNode
Master 節點維護這一個16384/8字節的位序列,Master節點用bit來標識對于某個槽自己是否擁有。(判斷索引是不是為1即可)
slots屬性是一個二進制位數組(bit arry),這個數組的長度為16384/8 = 2048個字節,共包含16384個二進制。
Redis Cluster對slots數組中的16384個二進制位進行編號:從0為起始索引,16383為終止索引。
根據索引i上的二進制位的值來判斷節點是否負責處理槽i:
?slots數組在索引i上的二進制位的值為1,即表示該節點負責處理槽i;
?slots數組在索引i上的二進制位的值為0,即表示該節點不負責處理槽i;
示例1:(如下節點負責處理slot0-slot7)
即在Redis Cluster中Master節點使用bit(0)和bit(1)來標識對某個槽是否擁有,而Master只要判斷序列第二位的值是不是1即可,時間復雜度為O(1)。
numslots屬性記錄節點負責處理的槽的數量,也就是slots數組中值為1的二進制位的數量。上圖中節點處理的槽數量為8個。
ClusterState
集群中所有槽的分配信息都保存在ClusterState數據結構的slots數組中,程序要檢查槽i是否已經被分配或者找出處理槽i的節點,只需要訪問clusterState.slots[i]的值即可,時間復雜度為O(1)。
slots數組包含16384個項,每個數組項都是一個指向clusterNode結構的指針:
?如果slots[i]指針指向null,那么表示槽i尚未指派給任何節點;
?如果slots[i]指針指向一個clusterNode結構,那么表示槽i已經指派給了clusterNode結構所代表的節點。
示例2:
1. ?slots[0]至slots[4999]的指針都指向端口為6381的節點,即槽0到4999都由節點6381負責處理;
2. ?slots[5000]至slots[9999]的指針都指向端口為6382的節點,即槽5000到9999都由節點6382負責處理;
3.?slots[10000]至slots[16383]的指針都指向端口為6383的節點,即槽10000到16384都由節點6383負責處理。
數組 clusterNode.slots和clusterState.slots:
? clusterNode.slots數組記錄了clusterNode結構所代表的節點的槽指派信息(每個節點負責哪些槽)。
? clusterState.slots數組記錄了集群中所有槽的指派信息。
? 如果需要查看某個節點的槽指派信息,只需要將相應節點的clusterNode.slots數組整個發送出去即可。
? 但是如果需要查看槽i是否被分配或者分配給了哪個節點,就需要遍歷clusterState.nodes字典中所有clusterNode結構,檢查這些結構的slots數組,直到遍歷到負責處理槽i的節點為止,這個過程的時間復雜度為O(N),N是clusterState.nodes字典保存的clusterNode結構的數量。
? 引入clusterState.slots ,將所有槽的指派信息保存在clusterState.slots數組里面,程序要檢查槽i是否已經被指派,或者查看負責處理槽i的節點,只需要訪問clusterState.slots[i]的值即可,這個操作的時間復雜度為O(1)。
? 如果只使用clusterState.slots數組(不引入clusterNode.slots),如果要將節點A的槽指派信息傳播給其他節點時,必須先遍歷整個clusterState.slots數組,記錄節點A負責處理哪些槽,然后再發送給其他節點。比直接發送clusterNode.slots數組要低效的多。
Redis Cluster節點通信
Redis Cluster采用P2P的Gossip協議,Gossip協議的原理就是每個節點與其他節點間不斷通信交換信息,一段時間后節點信息一致,每個節點都知道集群的完整信息。
Redis Cluster通信過程:
(1)集群中的每個節點都會單獨開辟一個TCP通道,用于節點之間彼此通信,通信端口號在基礎端口上加10000;
(2)每個節點在固定周期內通過特定規則選擇幾個節點發送ping消息;
(3)接收到ping消息的節點用pong消息作為響應。
集群中每個節點通過一定規則挑選要通信的節點,每個節點可能知道全部節點,也可能僅知道部分節點,
只要這些節點彼此可以正常通信,最終它們會達到一致的狀態。當節點出故障、新節點加入、主從角色變化、槽信息變更等事件發生時,通過不斷的ping/pong消息通信,經過一段時間后所有的節點都會知道整個集群全部節點的最新狀態,從而達到集群狀態同步的目的。
Gossip消息
Gossip協議的主要職責就是信息交換,信息交換的載體就是節點彼此發送的Gossip消息,常用的Gossip消息可分為:
? meet消息:用于通知新節點加入。消息發送者通知接收者加入到當前集群,meet消息通信正常完成后,接收節點會加入到集群中并進行周期性的ping、pong消息交換;
? ping消息:集群內交換最頻繁的消息,集群內每個節點每秒向多個其他節點發送ping消息,用于檢測節點是否在線和交換彼此狀態信息。ping消息發送封裝了自身節點和部分其他節點的狀態數據。
? pong消息:當接收到ping、meet消息時,作為響應消息回復給發送方確認消息正常通信。pong消息內部封裝了自身狀態數據。節點也可以向集群內廣播自身的pong消息來通知整個集群對自身狀態進行更新。
? fail消息:當節點判定集群內另一個節點下線時,會向集群內廣播一個fail消息,其他節點接收到fail消息之后把對應節點更新為下線狀態。
消息格式:
所有的消息格式劃分為:消息頭和消息體。
消息頭包含發送節點自身狀態數據,接收節點根據消息頭就可以獲取到發送節點的相關數據。
消息格式數據結構
消息頭:包含自身的狀態數據,發送節點關鍵信息,如節點id、槽映等節點標識(主從角色,是否下線)等。
消息格式數據結構
消息體:
定義發送消息的數據。
消息體在Redis內部采用clusterMsgData結構聲明:
通信消息處理流程
當接收到ping、meet消息時,接收節點會解析消息內容并根據自身的識別情況做出相應處理:
接收節點收到ping/meet消息時,執行解析消息頭和
消息體流程:
? 解析消息頭過程:消息頭包含了發送節點的信息,如果發送節點是新節點且消息是meet類型,則加入到本地節點列表;如果是已知節點,則嘗試更新發送節點的狀態,如槽映射關系、主從角色等狀態。
? 解析消息體過程:如果消息體的clusterMsgDataGossip數組包含的節點是新節點,則嘗試發起與新節點的meet握手流程;如果是已知節點,則根據clusterMsgDataGossip中的flags字段判斷該節點是否下線,用于故障轉移。
消息處理完后回復pong消息,內容同樣包含消息頭和消息體,發送節點接收到回復的pong消息后,采用類似的流程解析處理消息并更新與接收節點最后信息時間,完成一次消息通信。
通信規則
Redis集群內節點通信采用固定頻率(定時任務每秒執行10次)。由于內部需要頻繁地進行節點信息交換,而ping/pong消息會攜帶當前節點和部分其他節點的狀態數據,勢必會加重帶寬和計算的負擔。
? 通信節點選擇過多可以讓信息及時交換,但是成本過高;
? 通信節點選擇過少會降低集群內所有節點彼此信息交換頻率,從而影響故障判定、新節點發現等需求的速度。
節點選擇
消息交換的成本主要體現在單位時間選擇發送消息的節點數量和每個消息攜帶的數據量。
(1)選擇發送消息的節點數量
集群內每個節點維護定時任務默認每秒執行10次,每秒會隨機選擇5個節點找出最久沒有通信的節點發送ping消息,用于Gossip信息交換的隨機性。每100毫秒都會掃描本地節點列表,如果發現節點最后一次接受pong消息的時間大于cluster_node_timeout/2,則立刻發送ping消息,防止該節點信息太長時間未更新。根據以上規則得出每個節點每秒需要發送ping消息的數量=1+10*num(node.pong_received> cluster_node_timeout/2),因此cluster_node_timeout參數對消息發送的節點數量影響非常大。當我們的帶寬資源緊張時,可以適當調大此參數。但是如果cluster_node_timeout過大會影響消息交換的頻率從而影響故障轉移、槽信息更新、新節點發現的速度。因此需要根據業務容忍度和資源消耗進行平衡。同時整個集群消息總交換量也跟節點數成正比。
(2)消息數據量
每個ping消息的數據量體現在消息頭和消息體中,其中消息頭主要占用空間的字段是myslots[CLUSTER_SLOTS/8],占用2KB,這塊空間占用相對固定。消息體會攜帶一定數量的其他節點信息用于信息交換。而消息體攜帶數據量跟集群的節點數量相關,集群越大每次消息通信的成本也就更高。
通信開銷
Redis Cluster內節點通信自身開銷:
(1)節點自身信息,主要是自己負責的slots信息:slots[CLUSTER_SLOTS/8],占用2KB;
(2)攜帶總節點1/10的其他節點的狀態信息(1個節點的狀態數據約為104byte)
注:并不是所有的都是攜帶十分之一的節點信息的。
如果total_nodes/10小于3,那就至少攜帶3個節點信息;
如果total_nodes/10大于total_nodes-2,最多攜帶total_nodes-2個節點信息;
Else就total_nodes/10個節點信息。
通信開銷
節點狀態信息:clusterMsgDataGossip,ping、meet、pong采用clusterMsgDataGossip數組作為消息體。
所以每個Gossip消息大小為2KB+total_nodes/10*104b
Redis Cluster帶寬消耗主要為:業務操作(讀寫)消耗+Gossip消息消耗。
我們現在假設節點數為64*2=128,floor(122)=12:
每個Gossip消息的大小約為:2KB+12*104b ≈ 3KB。
根據之前的每個節點每秒需要發送ping消息的數量=1+10*num(node.pong_received> cluster_node_timeout/2)
假設:cluster_node_timeout為15秒時,num=20,即開銷=3KB*(1+10*20)*2*20=25MB/s;
cluster_node_timeout為30秒時,num=5,即開銷=3KB*(1+10*5)*2*20=6MB/s。
可以看出影響Gossip開銷的主要兩點:Cluster Redis的節點數和cluster_node_timeout設置的閾值:
那如果節點越多,Gossip消息就越大,最近接收pong消息時間間隔大于cluster_node_timeout/2秒的節點也會越多,那么帶寬的開銷越大。
所以得出如下結論:
(1)盡量避免大集群,針對大集群就拆分出去;
(2)如果某些場景必須使用大集群,那就可以通過增大cluster_node_timeout來降低帶寬的消耗,但是會影響failover的時效,這個可以根據業務場景和集群具體狀態評估;
(3)docker的分配問題,將大集群打散到小集群的物理機上,可以平衡和更高效的利用資源。