redis從3.0開始支持cluster集群功能,采用無中心節點方式實現,無需proxy代理,客戶端直接與redis集群的每個節點連接,根據同樣的hash算法計算出key對應的slot,然后直接在slot對應的redisj節點上執行命令。redis實現了客戶端對節點的直接訪問,為了去中心化,節點之間通過gossip協議交換互相的狀態,以及探測新加入的節點信息。redis集群支持動態加入節點,動態遷移slot,以及自動故障轉移。
先看一張圖,大致觸摸下redis cluster
redis cluster要求至少需要3個master才能組成一個集群,同時每個master至少需要有一個slave節點。各個節點之間保持TCP通信。當master發生了宕機, Redis Cluster自動會將對應的slave節點提拔為master,來重新對外提供服務
負載均衡
先說下槽,集群中每個redis實例都負責接管一部分槽,總槽數為:16384(2^14),如果有3台master,那么每台負責5461個槽(16384/3)。數據庫中的每個鍵都屬於這 16384 個哈希槽的其中一個, 集群使用公式 CRC16(key) % 16384 來計算鍵 key 屬於哪個槽, 其中 CRC16(key) 語句用於計算鍵 key 的 CRC16 校驗和 。集群中的每個節點負責處理一部分哈希槽。 舉個例子, 一個集群可以有三個節點, 其中:
- 節點 1 負責處理 0 號至 5461 號哈希槽。
- 節點 2 負責處理 5461號至 10922號哈希槽。
- 節點 3 負責處理 10922號至 16383 號哈希槽。
redis節點 | 負責的槽位 |
---|---|
節點1 | 0-5461 |
節點2 | 5461-10922 |
節點3 | 10922-16383 |
這種將哈希槽分布到不同節點的做法使得用戶可以很容易地向集群中添加或者刪除節點。 比如說:
- 如果用戶將新節點 D 添加到集群中, 那么集群只需要將節點 A 、B 、 C 中的某些槽移動到節點 D 就可以了。
- 如果用戶要從集群中移除節點 A , 那么集群只需要將節點 A 中的所有哈希槽移動到節點 B 和節點 C , 然后再移除空白(不包含任何哈希槽)的節點 A 就可以了。
因為將一個哈希槽從一個節點移動到另一個節點不會造成節點阻塞, 所以無論是添加新節點還是移除已存在節點, 又或者改變某個節點包含的哈希槽數量, 都不會造成集群下線。redis集群中,每個節點都會有其余節點ip,負責的槽 等 信息。
集群架構
redis cluster是一個去中心化的集群,每個節點都會跟其他節點保持連接,用來交換彼此的信息。節點組成集群的方式使用cluster meet命令,meet命令可以讓兩個節點相互握手,然后通過gossip協議交換信息。如果一個節點r1在集群中,新節點r4加入的時候與r1節點握手,r1節點會把集群內的其他節點信息通過gossip協議發送給r4,r4會一一與這些節點完成握手,從而加入到集群中。
節點在啟動的時候會生成一個全局的標識符,並持久化到配置文件,在節點與其他節點握手后,這些信息也都持久化下來。節點與其他節點通信,標識符是它唯一的標識,而不是IP、PORT地址。如果一個節點移動位置導致IP、PORT地址發生變更,集群內的其他節點能把該節點的IP、PORT地址糾正過來。
集群數據以數據分布表的方式保存在各個slot上。集群只有在16384個slot都有對應的節點才能正常工作。
slot可以動態的分配、刪除和遷移。每個節點會保存一份數據分布表,節點會將自己的slot信息發送給其他節點,發送的方式使用一個unsigned char的數組,數組長度為16384/8。每個bit標識為0或者1來標識某個slot是否是它負責的。
由於節點間不停的在傳遞數據分布表,所以為了節省帶寬,redis選擇了只傳遞自己的分布數據。但這樣的方式也會帶來管理方面的麻煩,如果一個節點刪除了自己負責的某個slot,這樣該節點傳遞給其他節點數據分布表的slot標識為0,而redis采用了bitmapTestBit方法,只處理slot為1的節點,而並未把每個slot與收到的數據分布表對比,從而產生了節點間數據分布表視圖的不一致。這種問題目前只能通過使用者來避免。
JedisCluster如何尋址集群的
JedisCluster配置只用指定集群中某一個節點的IP,端口信息就可以了。JedisCluster初始化時,會找配置的節點獲取整個集群的信息(cluster nodes命令)。
解析集群信息,得到集群中所有master信息,然后遍歷每台master,通過ip,端口構建jedis實例,然后put到一個全局nodes變量里面(Map類型) , key為ip,端口,值為Jedis實例,nodes值如下:
nodes={172.19.93.120:6380=redis.clients.jedis.JedisPool@74ad1f1f,.....}
在上面遍歷master過程中,還做一件事,遍歷此台master負責的槽索引,然后又put到一個全局map slots里面。值為上面的Jedis實例, slots值如下:
slots={0=redis.clients.jedis.JedisPool@74ad1f1f, 1=redis.clients.jedis.JedisPool@74ad1f1f, 2=redis.clients.jedis.JedisPool@74ad1f1f, .... 5461 = redis.clients.jedis.JedisPool@65aa1f2f, ####另外的master機器 .... 16383=redis.clients.jedis.JedisPool@756d1afd}
有了上面的slots變量,當有值set 時, 會先算出slot = getCRC16(key)&(16383-1),假如是12182 , 然后調用slots.get(12182) 得到jedis實例,然后去操作redis。
如果發現MovedDataException,說明初始化得到的槽位與節點的對應關系有問題,(節點新增或者宕機)就會重置slots。
集群機器之間的通信
集群機器等數據信息通常有兩種方式,一種是集中式,比如springcloud服務集群信息保存在配置中心 。另一種就是redis的方式,gossip。
集中式:好處在於,元數據的更新和讀取,時效性非常好,一旦元數據出現了變更,立即就更新到集中式的存儲中,其他節點讀取的時候立即就可以感知到; 不好在於,所有的元數據的跟新壓力全部集中在一個地方,可能會導致元數據的存儲有壓力。
gossip:好處在於,元數據的更新比較分散,不是集中在一個地方,更新請求會陸陸續續,打到所有節點上去更新,有一定的延時,降低了壓力; 缺點,元數據更新有延時,可能導致集群的一些操作會有一些滯后。
通信的端口就是本身redis監聽端口+10000 ,比如 監聽端口6379,通信端口就是16379 。
Gossip協議的主要職責就是信息交換。信息交換的載體就是節點彼此發送的Gossip消息,常用的Gossip消息可分為:ping
消息、pong
消息、meet
消息、fail
消息等。
- meet消息:用於通知新節點加入。消息發送者通知接收者加入到當前集群,meet消息通信正常完成后,接收節點會加入到集群中並進行周期性的ping、pong消息交換。
- ping消息:集群內交換最頻繁的消息,集群內每個節點每秒向多個其他節點發送ping消息,用於檢測節點是否在線和交換彼此狀態信息。ping消息發送封裝了自身節點和部分其他節點的狀態數據。
- pong消息:當接收到ping、meet消息時,作為響應消息回復給發送方確認消息正常通信。pong消息內部封裝了自身狀態數據。節點也可以向集群內廣播自身的pong消息來通知整個集群對自身狀態進行更新。
- fail消息:當節點判定集群內另一個節點下線時,會向集群內廣播一個fail消息,其他節點接收到fail消息之后把對應節點更新為下線狀態。
舉例當新增一個節點,也就是Meet消息過程
- 節點A會為節點B創建一個clusterNode結構,並將該結構添加到自己的clusterState.nodes字典里面。
- 節點A根據CLUSTER MEET命令給定的IP地址和端口號,向節點B發送一條MEET消息。
- 節點B接收到節點A發送的MEET消息,節點B會為節點A創建一個clusterNode結構,並將該結構添加到自己的clusterState.nodes字典里面。
- 節點B向節點A返回一條PONG消息。
- 節點A將受到節點B返回的PONG消息,通過這條PONG消息節點A可以知道節點B已經成功的接收了自己發送的MEET消息。
- 之后,節點A將向節點B返回一條PING消息。
- 節點B將接收到的節點A返回的PING消息,通過這條PING消息節點B可以知道節點A已經成功的接收到了自己返回的PONG消息,握手完成。
- 之后,節點A會將節點B的信息通過Gossip協議傳播給集群中的其他節點,讓其他節點也與節點B進行握手,最終,經過一段時間后,節點B會被集群中的所有節點認識。
舉例當一個節點故障,怎么判斷下線
集群中的每個節點都會定期向其他節點發送ping命令,如果接受ping消息的節點在指定時間內沒有回復pong,則發送ping的節點就把接受ping的節點標記為主觀下線。
如果集群半數以上的主節點都將主節點A標記為主觀下線,則節點A將被標記為客觀下線(通過節點的廣播)即下線。
數據訪問
客戶端在初始化的時候只需要知道一個節點的地址即可,客戶端會先嘗試向這個節點執行命令,比如“get key”,如果key所在的slot剛好在該節點上,則能夠直接執行成功。如果slot不在該節點,則節點會返回MOVED錯誤,同時把該slot對應的節點告訴客戶端。客戶端可以去該節點執行命令。目前客戶端有兩種做法獲取數據分布表:
- 一種就是客戶端每次根據返回的MOVED信息緩存一個slot對應的節點,但是這種做法在初期會經常造成訪問兩次集群。
- 還有一種做法是在節點返回MOVED信息后,通過cluster nodes命令獲取整個數據分布表,這樣就能每次請求到正確的節點,一旦數據分布表發生變化,請求到錯誤的節點,返回MOVED信息后,重新執行cluster nodes命令更新數據分布表。
在訪問集群的時候,節點可能會返回ASK錯誤。這種錯誤是在key對應的slot正在進行數據遷移時產生的,這時候向slot的原節點訪問,如果key在遷移源節點上,則該次命令能直接執行。如果key不在遷移源節點上,則會返回ASK錯誤,描述信息會附上遷移目的節點的地址。客戶端這時候要先向遷移目的節點發送ASKING命令,然后執行之前的命令。
這些細節一般都會被客戶端sdk封裝起來,使用者完全感受不到訪問的是集群還是單節點。
集群支持hash tags功能,即可以把一類key定位到同一個slot,tag的標識目前不支持配置,只能使用{},redis處理hash tag的邏輯也很簡單,redis只計算從第一次出現{,到第一次出現}的substring的hash值,substring為空,則仍然計算整個key的值,這樣對於foo{}{bar}、{foo}{bar}、foo這些沖突的{},也能取出tag值。使用者需遵循redis的hash tag規范。
我們都知道,redis單機支持mutl-key操作(mget、mset)。redis cluster對mutl-key命令的支持,只能支持多key都在同一個slot上,即使多個slot在同一個節點上也不行。通過hash tag可以很好的做到這一點
public static void main(String...strings) { String[] kvs = {"{k_}1","values1","{k_}2","values2"}; RedisUtils.mset(kvs); List<String> mget = RedisUtils.mget("{k_}1","{k_}2"); System.out.println(mget); }
同理,對於事務的支持只能在也一個slot上完成;其次,redis cluster只使用db0。
故障轉移
為了使得集群在一部分節點下線或者無法與集群的大多數(majority)節點進行通訊的情況下, 仍然可以正常運作, Redis 集群對節點使用了主從復制功能: 集群中的每個節點都有 1 個至 N 個復制品(replica), 其中一個復制品為主節點(master), 而其余的 N-1 個復制品為從節點(slave)。
集群間節點支持主從關系,復制的邏輯基本復用了單機版的實現。不過還是有些地方需要注意。
- 首先集群間節點建立主從關系不再使用原有的SLAVEOF命令和SLAVEOF配置,而是通過cluster replicate命令,這保證了主從節點需要先完成握手,才能建立主從關系。
- 集群是不能組成鏈式主從關系的,也就是說從節點不能有自己的從節點。不過對於集群外的沒開啟集群功能的節點,redis並不干預這些節點去復制集群內的節點,但是在集群故障轉移時,這些集群外的節點,集群不會處理。
- 集群內節點想要復制另一個節點,需要保證本節點不再負責任何slot,不然redis也是不允許的。
- 集群內的從節點在與其他節點通信的時候,傳遞的消息中數據分布表和epoch是master的值。
集群主節點出現故障,發生故障轉移,其他主節點會把故障主節點的從節點自動提為主節點,原來的主節點恢復后,自動成為新主節點的從節點。當一個從節點發現自己正在復制的主節點進入了已下線狀態時,從節點將開始對下線主節點進行故障轉移,以下是故障轉移執行的步驟:
- 從節點會執行SLAVEOF no one命令,成為新的主節點;
- 新的主節點會撤銷所有對已下線主節點的槽指派,並將這些槽全部指派給自己;
- 新的主節點向集群廣播一條PONG消息,這條PONG消息可以讓集群中的其他節點立即知道這個節點已經由從節點變成了主節點,並且這個主節點已經接管了原本由已下線節點負責處理的槽。
- 新的主節點開始接收和自己負責處理的槽有關的命令請求,故障轉移完成。
這里先說明,把一個master和它的全部slave描述為一個group,故障轉移是以group為單位的,集群故障轉移的方式跟sentinel的實現很類似。某個master節點一段時間沒收到心跳響應,則集群內的master會把該節點標記為pfail,類似sentinel的sdown。集群間的節點會交換相互的認識,超過一半master認為該異常master宕機,則這些master把異常master標記為fail,類似sentinel的odown。fail消息會被master廣播出來。group的slave收到fail消息后開始競選成為master。競選的方式跟sentinel選主的方式類似,都是使用了raft協議,slave會從其他的master拉取選票,票數最多的slave被選為新的master,新master會馬上給集群內的其他節點發送pong消息,告知自己角色的提升。其他slave接着開始復制新master。等舊master上線后,發現新master的epoch高於自己,通過gossip消息交互,把自己變成了slave。大致就是這么個流程。自動故障轉移的方式跟sentinel很像。
redis還支持手動的故障轉移,即通過在slave上執行cluster failover
命令,可以讓slave提升為master。failover命令支持傳入FORCE和TAKEOVER參數。
- 不傳入額外參數:如果主節點異常,則不能進行failover,主節點正常的情況下需要先比較從節點和主節點的偏移量,此時會讓主節點停止客戶端請求,直到超時或者故障轉移完成。主從偏移量相同后開始手動故障轉移流程。
- FORCE:使用FORCE參數與sentinel的手動故障轉移流程基本類似,強制開始一次故障轉移。
- TAKEOVER:這種手動故障轉移的方式比較暴力,slave直接提升自己的epoch為最大的epoch。並把自己變成master。這樣在消息交互過程中,舊master能發現自己的epoch小於該slave,同時兩者負責的slot一致,它會把自己降級為slave。
網絡分區說明
redis的集群模式下,客戶端需要和全部的節點保持連接,這樣可能出現網絡分區問題,客戶端和一些節點在一個網絡分區,另一部分節點在另一個網絡分區。在分區期間,客戶端仍然能執行命令,直到集群經過cluster-node-timeout發現分區情況,節點探測到有slot無法提供服務,才開始禁止客戶端執行命令。
這時候會出現一種現象,假設客戶端和一個master在小分區,其他節點在大分區。超時后,其他節點共同投票把group內的一個slave提為master,等分區恢復。舊的master會成為新master的slave。這樣在cluster-node-timeout期間對舊master的寫入數據都會丟失。
這個問題可以通過設置cluster-node-timeout來減少不一致。如果對一致性要求高的應用還可以通過min-slaves-to-write配置來提高寫入的要求