redis(17)--集群
目錄
節點
啟動節點
集群數據結構
CLUSTER MEET 命令的實現:
槽指派
記錄節點的槽指派信息
傳播節點的槽指派信息
記錄集群所有槽的指派信息
CLUSTER ADDSLOTS 命令的實現
在集群中執行命令
計算鍵屬于哪個槽
?判斷槽是否由當前節點
MOVED 錯誤
節點數據庫的實現
重新分片
重新分片的原理
ASK 錯誤
CLUSTER SETSLOT IMPORTING 命令的實現
CLUSTER SETSLOT MIGRTING 命令的實現
ASK 錯誤
ASKING 命令
ASK錯誤和MOVED錯誤的區別
復制與故障轉移
?設置從節點
故障檢測
故障轉移
選舉新的主節點
消息
消息頭
MEET 、PING 、PONG 消息的實現
FAIL 消息的實現
PUBLISH 消息的實現
Redis集群是分布式數據庫方案,通過分片(sharding)來進行數據共享,并提供復制和故障轉移功能。
節點
一個Redis 集群通常由多個節點(Node)組成,在剛開始的時候,每個節點都是獨立的,只處于只包含自己的集群中,當要組成一個真正可工作的集群時,就需要將這些獨立的節點連接起來,構建成一個包含多個節點的集群。
連接各節點,可以使用:
CLUSTER MEET <ip> <port>
?讓節點與ip和port所指定的節點進行握手,握手成功,節點就會將ip和port指定的節點添加到當前的集群中。
啟動節點
Redis服務器在啟動時根據cluster-enabled配置選項是否為yes來決定是否開啟服務器的群集模式。
節點繼續使用redisServer來保存服務器狀態,redisClient保存客戶端狀態。在集群模式下使用到的數據,保存在cluserNode,clusterLink,clusterState結構。
集群數據結構
//一個節點的當前狀態 struct clusterNode{ // 創建節點的時間 mstime_t ctime; // 節點的名字,由40個16進制字符組成 char name[REDIS_CLUSTER_NAMELEN]; // 節點標識 int flags; // 節點當前的配置紀元,用于實現故障轉移 uint64_t configEpoch; // 節點的ip地址 char ip[REDIS_IP_STR_LEN]; // 節點的端口號 int port; // 保存連接節點所需的有關信息 clusterLink *link; //…… };clusterNode保存了節點自己的以及其他節點的狀態。
typedef struct clusterLink {// 連接的創建時間mstime_t ctime;// TCP 套接字描述符int fd;// 輸出緩沖區,保存著等待發送給其他節點的消息(message)。sds sndbuf;// 輸入緩沖區,保存著從其他節點接收到的消息。sds rcvbuf;// 與這個連接相關聯的節點,如果沒有的話就為 NULLstruct clusterNode *node;} clusterLink;clusterLink保存連接點需要的信息。
redisClient和clusterLink比較
redisClient和clusterLink都有自己的套接字描述符,輸入,輸出緩沖區,區別在于:redisClient用于連接客戶端,clusterLink用于連接節點。
?
ypedef struct clusterState {// 指向當前節點的指針clusterNode *myself;// 集群當前的配置紀元,用于實現故障轉移uint64_t currentEpoch;// 集群當前的狀態:是在線還是下線int state;// 集群中至少處理著一個槽的節點的數量int size;// 集群節點名單(包括 myself 節點)// 字典的鍵為節點的名字,字典的值為節點對應的 clusterNode 結構dict *nodes;// ... } clusterState;記錄當前節點所處的集群目前所處狀態,包含多少節點,集群當前配置紀元。
CLUSTER MEET 命令的實現:
向節點A發送 CLUSTER MEET 命令,能讓接收命令的節點A將另一個節點B添加到節點A當前所處的集群里。
收到命令的節點A 和節點B進行握手,以此來確認彼此的存在,并為將來的進一步通信打好基礎:
槽指派
當數據庫中的 16384 個槽都有節點在處理時,集群處于上線狀態,否則,處于下線狀態。
通過向節點發送 CLUSTER ADDSLOTS 命令,可以執行槽指派:
CLUSTER ADDSLOTS 0 1 2 3 4 ... 5000
記錄節點的槽指派信息
// 一個節點的當前狀態 struct clusterNode{//……// 記錄處理那些槽// 二進制位數組,長度為 2048 (2^11)個字節,包含 16384 個二進制位// 如果slots數組在索引i上的二進制位的值為1,那么表示節點負責處理槽i;否則表示節點不負責處理槽iunsigned char slots[16384/8];//記錄自己負責處理的槽的數量int numslots;//…… };傳播節點的槽指派信息
一個節點除了會將自己負責處理的槽記錄在clusterNode結構的slots屬性和numslots屬性之外,它還會將自己的slots數組通過消息發送給集群中其他的節點,以此來告知其他節點自己目前負責處理哪些槽。
當節點A通過消息從節點B那里接收到節點B的slots數組時,節點A會在自己的clusterState.nodes字典中查找接節點B對應的clusterNode結構,并對結構中的slots數組進行保存或者更新。
記錄集群所有槽的指派信息
// 當前節點視角下集群目前所處的狀態,集群中所有16384個槽的指派信息 typedef struct clusterState{// ……// slots[i]指針如果指向NULL,說明槽i尚未被指派給任何節點;// slots[i]指針如果指向一個clusterNode 結構,// 說明槽i已經被指派給了這個clusterNode結構所代表的節點;clusterNode *slots[16384];// …… };clusterState.slots可以高效解決判斷槽i是否已指派或指派的節點等信息,復雜度為O(1),比clusterState.nodes效率高(要遍歷)。
CLUSTER ADDSLOTS 命令的實現
這個命令接受一個或多個槽作為參數,并將所有輸入的槽指派給接收該命令的節點負責:
CLUSTER ADDSLOTS <slot> [slot ...]
命令實現的偽代碼:
def CLUSTER_ADDSLOTS(*all_input_slots):# 遍歷所有輸出槽,檢查他們是否都是未指派槽for i in all_input_slots;# 如果有任意一個槽已經被指派給了某個節點,那么向客戶端返回錯誤,并終止命令執行if clusterState.slots[i] != NULLreply_error()return;# 如果所有輸入槽都是未指派槽 # 如果通過檢查,再一次遍歷所有槽,將這些槽指派給當前節點for i in all_input_slots;# 設置clusterState.slots數組,# 將slots[i]的指針指向代表當前節點的clusterNode結構clusterState.slots[i] = clusterState.myself# 訪問當前節點的clusterNode結構的slots數組,將數組在索引i上的二進制位設置位1setSlotBit(clusterState.myself.slots,i)# 發送消息告知集群中的其他節點,自己目前正在負責處理那些槽在集群中執行命令
計算鍵屬于哪個槽
節點用一下算法計算給定鍵 key 屬于哪個槽:
def slot_number(key):return CRC16(key) & 16383其中 CRC16(key) 語句計算鍵 key 的CRC-16校驗和,& 16383 語句用于計算出一個介于0至16383之間的整數作為鍵 key 的槽號。
查看key屬于哪個槽
CLUSTER KEYSLOT? <key>
?判斷槽是否由當前節點
當節點計算出鍵所屬槽 i 之后,節點會檢查自己在 clusterState.slots 數組中的項 i ,判斷鍵所處的槽是否由自己負責:
- 如果 clusterState.slots[i] 等于 clusterState.myself ,那么說明槽 i 由當前節點負責,節點可以執行客戶端發送的命令;
- 否則,槽 i 不由當前節點負責,節點會根據 clusterState.slots[i] 所指向的 clusterNode 結構所記錄的節點IP和端口號,向客戶端返回 MOVED 錯誤并指引客戶端轉向正在處理槽i的節點。
?
MOVED 錯誤
當節點發現鍵所在的槽并非由自己負責處理的時候,就會向客戶端返回一個 MOVED 錯誤,指引客戶端轉向正在負責槽的節點。
格式如下:
MOVED <slot> <ip>:<port>
客戶端接收到 MOVED 命令之后,根據其提供的IP和端口,轉向負責處理槽 slot 的節點,并向節點重新發送之前想要執行的命令。
單機模式,redis-cli不識別MOVED命令,只會打印錯誤,而不進行轉向,集群模式可以識別并自動轉向。
節點數據庫的實現
節點只能使用 0 號數據庫,單機服務器沒有限制,但兩者都能保存鍵值對及鍵值對過期時間,且實現都是一樣的。
除了將鍵值對保存在數據庫里面之外,節點還會用 clusterState 結構中的 slots_to_keys 跳躍表來保存槽和鍵之間的關系:
typedef struct clusterState{// ...zskiolist *slots_to_keys;// ... } clusterState;這個跳躍表每個節點的分值( score )都是一個槽號,節點的成員( member )都是一個數據庫鍵:
- 當節點往數據庫添加鍵值對時,節點會將這個鍵以及鍵的槽號關聯到 slots_to_keys 跳躍表中
- 當節點刪除某個鍵值對時,節點就會在這個跳躍表中解除被刪除鍵和槽號之間的關聯
通過這個跳躍表中記錄各個數據庫鍵對應的槽,節點可以很方便對某個或某些槽的所有數據庫鍵進行批量操作。例如:
CLUSTER GETKEYSINSLOT <slot> <count>
由于僅保存了鍵的指針,因此占用空間不大。
重新分片
Redis集群的重新分片操作可以將任意數量已經指派給某個節點(源節點)的槽改為指派給另一個節點(目標節點),并且相關槽所屬的鍵值對也會從源節點移動到目標節點。 重新分片可以在線進行,在這過程中,集群不用下線,且源節點和目標節點都可以繼續處理命令。
重新分片的原理
由 redis-trib 負責執行,redis-trib 通過向源節點和目標節點發送命令來進行重新分片。
redis-trib對集群的單個槽 slot 進行重新分片的步驟如下:
如果重新分片涉及多個槽,那么 redis-trib 將對每個給定的槽分別執行上面給出的步驟。
ASK 錯誤
在重新分片期間,源節點向目標節點遷移一個槽的過程中,可能會出現這樣一種情況:屬于被遷移槽的一部分鍵值對保存在源節點里面,而另一部分鍵值對保存在目標節點中。
當客戶端向源節點發送關于鍵key的命令,源節點先在自己的數據庫里查找這個鍵,如果找到就直接返回執行客戶端命令,如果沒找到,這個鍵可能已經被遷移到了目標節點,源節點向客戶端返回一個 ASK 錯誤,指引客戶端轉向正在導入槽的目標節點,并再次發送之前要執行的命令。
CLUSTER SETSLOT IMPORTING 命令的實現
格式:
CLUSTER SETSLOT <slot> IMPORTING <source_id>
clusterState 結構的 importing_slots_from 數組記錄了當前節點正在從其他節點導入的槽:
typedef struct clusterState{// ……// 如果importing_slots_from[i]的值不為NULL,而是指向一個clusterNode結構,表示當前節點正在從// clusterNode所代表的節點導入槽iclusterNode *importing_slots_from[16384];// …… }clusterState;CLUSTER SETSLOT MIGRTING 命令的實現
格式:
CLUSTER SETSLOT <i> MTGRATING <target_id>
clusterState 結構的 migrating_slots_to 數組記錄了當前節點正在遷移至其他節點的槽:
typedef struct clusterState{// ……// 如果migrating_slots_to[i]的值不為NULL,而是指向一個clusterNode結構,表示當前節點正在將// 槽i遷移至clusterNode所代表的節點clusterNode *migrating_slots_to[16384];// …… }clusterState;ASK 錯誤
接到ASK 錯誤的客戶端會根據錯誤提供的IP地址和端口號(返回:ASK 1680 127.0.0.1:7003 ),轉向至正在導入槽的目標節點(通過migrating_slots_to 屬性),然后向目標節點發送一個 ASKING 命令, 之后再重新發送原本想要執行的命令。
ASKING 命令
ASKING命令要做的就是打開發送該命令的客戶端的 REDIS_ASKING 標識。如果該客戶端的 REDIS_ASKING 標識未打開,直接發送請求,由于槽的遷移過程還未完成,請求的鍵還屬于源節點,此時直接請求目標節點,目標節點會返回一個MOVED錯誤。 但是,如果節點的 clusterState.importing_slots_from[i] 顯示節點正在導入槽 i ,并且發送命令的客戶端帶有 REDIS_ASKING 標識,那么節點將破例執行這個關于槽 i 的命令一次。
注意:客戶端的 REDIS_ASKING 標識是一個一次性標識,當節點執行了一個帶有 REDIS_ASKING 標識的客戶單發送的命令之后,客戶端的這個表示就會被移除。
命令的偽代碼:
def ASKING():# 打開標識client.flags |= REDIS_ASKING# 向客戶端返回OK回復reply("OK")ASK錯誤和MOVED錯誤的區別
-
MOVED錯誤代表槽的負責全已經從一個結點轉移到了另一個節點
-
ASK錯誤只是兩個節點在遷移槽的過程中使用的一種臨時措施
復制與故障轉移
Redis集群中的節點分為主節點(master)和從節點(slave),其中主節點用于處理槽,從節點用于復制某個主節點,并在主節點下線時代理主節點繼續處理命令請求。
?設置從節點
向一個節點發送命令:CLUSTER REPLICATE <node_id>
這個命令可以讓接收命令的節點成為 node_id 所指定的從節點,并開始對主節點進行復制:
故障檢測
集群中的每個節點都會定期地向集群中的其他節點發送 PING 消息,以此來檢測對方是否在線,如果接受 PING 消息的節點沒有在規定時間內返回 PONG ,那么發送 PING 的節點就會將接受 PING 消息的節點標記為疑似下線(probable fail ,PFAIL)。
當一個主節點A通過消息得知主節點B認為主節點C進入疑似下線狀態,主節點A會在自己的 clusterState.nodes 字典中找到主節點C所對應的 clusterNode 結構,并將主節點B的下線報告添加到這個結構的 fail_reports 鏈表里面
struct clusterNode{// ...// 一個鏈表,記錄了所有其他節點對該節點的下線 報告list *fail_reports;// ... }每個節點的下線報告由一個 clusterNodeFailReport 結構表示:
struct clusterNodeFailReport{// 報告目標節點已經下線的節點struct clusterNode *node;// 最后一次從node節點收到下線報告的時間// 程序使用這個時間戳來檢查下線報告是否過期// (與電氣概念時間相差太久的下線報告會被刪除)mstime_t time; }如果一個集群里,半數以上負責處理槽的主節點都將某個主節點X報告為疑似下線,那么這個主節點X將被標記為已下線(FAIL),將主節點X標記為已下線的節點會向集群廣播一條關于主節點X的FAIL消息,所有收到這條FAIL消息的節點都會立即將主節點X標記為已下線。
故障轉移
當一個從節點發現自己正在復制的主節點進入了已下線狀態,從節點將開始對下線主節點進行故障轉移:
選舉新的主節點
?
消息
集群中的各個節點通過發送和接收消息來進行通信的,包括發送者和接收者2個角色。
- MEET 消息:當發送者接到客戶端發送的 CLUSTER MEET 命令時,發送者會向接收者發送 MEET 消息,請求接收者加入發送者當前所處的集群中。
- PING 消息:集群里的每個節點默認每隔一秒鐘就會從已知節點列表中隨機選出五個節點,然后對這五個節點中最長時間沒有發送過 PING 消息的節點發送 PING 消息,以此來檢測選中的節點是否在線
- PONG 消息:當接收者收到發送者發來的 MEET 消息或者 PING 消息時,為了向發送者確認這條MEET消息或者PING消息已到達,接收者會向發送者返回一條 PONG 消息
- FAIL 消息:當一個主節點A判斷另一個主節點B已經進入FAIL 狀態時,節點A會向集群廣播一條關于B的FAIL 消息,所有收到這條消息的節點都會立即將節點B標記為已下線。
- PUBLISH 消息:當節點接收到一個 PUBLISH 命令時,節點會執行這個命令,并向集群廣播一條PUBLISH 消息,所有接收到這條 PUBLISH 消息的節點都會執行相同的 PUBLISH 消息。
消息頭
節點發送的所有消息都由一個消息頭包裹,消息頭除了包含消息正文之外,還記錄了消息發送者自身的一些消息,因為這些消息也會被消息接收者用到,所有消息頭本身也是消息的一部分
每個消息頭都由一個 cluster.h/clushterMsg 結構表示:
?
clusterMsg.data 屬性指向聯合 clusterh/clusterMsgData,這個聯合就是消息正文:
union clusterMsgData{// MEET、PING、PONG消息的正文struct{// 每條MEET、PING、PONG消息都包括兩個clusterMsgDataGossip結構clusterMsgDataGossip gossip[1];}ping;// FAIL消息的正文struct{clusterMsgDataFail about;}fail;// PUBLISH消息的正文struct{clusterMsgDataPublish msg;}publish;// 其他消息的正文…… };clusterMsg 結構的currentEpoch、sender、myslots 等屬性記錄了發送者自身的節點信息。接受者會根據這些信息,在自己的 clusterState.nodes 字典里找到發送者對應的 clusterNode 結構,并對結構進行更新。
MEET 、PING 、PONG 消息的實現
節點通過消息頭的 type 判斷是三種消息中的哪一種。每次發送 MEET、PING、PONG 消息時,發送者都會從自己的已知節點中隨機選出兩個節點(可以是主節點或者從節點),并將這兩個被選中節點的信息分別保存到兩個clusterMsgDataGossip中。接收者收到消息,會取出這兩個 clusterMsgDataGossip結構 ,并根據其中的信息對自己的 clusterState.nodes 進行更新。
- 如果被選中節點不存在與接收者的已知節點列表,那么表明接收者第一次接觸到被選中節點,接收者將根據結構中的IP地址和端口號等信息,與被選中節點進行握手
- 如果在列表中,那么說明接收者之前已經與被選中節點進行過接觸,接收者將根據 clusterMsgDataFossip結構記錄的信息,對被選中節點所對應的 clusterNode 結構進行更新。
clusterMsgDataGossip(記錄了被選中節點的名字,發送者和被選中節點最后一次發送和接收 PING 消息和 PONG 消息的時間戳,被選中節點的IP地址和端口號,以及被選中節點的標識值)
custerMsgDataGossip 結構:
?
FAIL 消息的實現
在集群的節點數量比較大的情況下,單純用 Gossip 協議來傳播節點的已下線信息會給節點的信息更新帶來一定延遲,,而FAIL消息對實時性要求比較高。
FAIL 消息的正文由 cluster.h/clusterMsgDataFail 結構表示,這個節點只包含一個 nodename 屬性,改屬性記錄了已下線節點的名字:
?
PUBLISH 消息的實現
當客戶端先集群中的某個節點發送命令
PUBLISH <channel> <message>
的時候,接收到 PUBLISH 命令的節點不僅會向 channel 頻道發送消息 message ,它還會想集群廣播一條 PUBLISH 消息,所接收這條消息的節點都會向 channel 頻道發送 message 消息。
PUBLISH 消息的正文由 cluster.h/clusterMsgDataPublish 結構表示:
?
?
總結
以上是生活随笔為你收集整理的redis(17)--集群的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spring Aware接口
- 下一篇: Redis设计与实现笔记