一個 Redis 集群通常由多個節(jié)點(node)組成, 在剛開始的時候, 每個節(jié)點都是相互獨立的, 它們都處于一個只包含自己的集群當中, 要組建一個真正可工作的集群, 我們必須將各個獨立的節(jié)點連接起來, 構(gòu)成一個包含多個節(jié)點的集群。
連接各個節(jié)點的工作可以使用 CLUSTER MEET 命令來完成, 該命令的格式如下:
CLUSTER MEET <ip> <port>
向一個節(jié)點 node
發(fā)送 CLUSTER MEET 命令, 可以讓 node
節(jié)點與 ip
和 port
所指定的節(jié)點進行握手(handshake), 當握手成功時, node
節(jié)點就會將 ip
和 port
所指定的節(jié)點添加到 node
節(jié)點當前所在的集群中。
舉個例子, 假設(shè)現(xiàn)在有三個獨立的節(jié)點 127.0.0.1:7000
、 127.0.0.1:7001
、 127.0.0.1:7002
(下文省略 IP 地址,直接使用端口號來區(qū)分各個節(jié)點), 我們首先使用客戶端連上節(jié)點 7000 , 通過發(fā)送 CLUSTER NODE 命令可以看到, 集群目前只包含 7000 自己一個節(jié)點:
$ redis-cli -c -p 7000
127.0.0.1:7000> CLUSTER NODES
51549e625cfda318ad27423a31e7476fe3cd2939 :0 myself,master - 0 0 0 connected
通過向節(jié)點 7000 發(fā)送以下命令, 我們可以將節(jié)點 7001 添加到節(jié)點 7000 所在的集群里面:
127.0.0.1:7000> CLUSTER MEET 127.0.0.1 7001
OK
127.0.0.1:7000> CLUSTER NODES
68eef66df23420a5862208ef5b1a7005b806f2ff 127.0.0.1:7001 master - 0 1388204746210 0 connected
51549e625cfda318ad27423a31e7476fe3cd2939 :0 myself,master - 0 0 0 connected
繼續(xù)向節(jié)點 7000 發(fā)送以下命令, 我們可以將節(jié)點 7002 也添加到節(jié)點 7000 和節(jié)點 7001 所在的集群里面:
127.0.0.1:7000> CLUSTER MEET 127.0.0.1 7002
OK
127.0.0.1:7000> CLUSTER NODES
68eef66df23420a5862208ef5b1a7005b806f2ff 127.0.0.1:7001 master - 0 1388204848376 0 connected
9dfb4c4e016e627d9769e4c9bb0d4fa208e65c26 127.0.0.1:7002 master - 0 1388204847977 0 connected
51549e625cfda318ad27423a31e7476fe3cd2939 :0 myself,master - 0 0 0 connected
現(xiàn)在, 這個集群里面包含了 7000 、 7001 和 7002 三個節(jié)點, 圖 IMAGE_CONNECT_NODES_1 至 IMAGE_CONNECT_NODES_5 展示了這三個節(jié)點進行握手的整個過程。
本節(jié)接下來的內(nèi)容將介紹啟動節(jié)點的方法, 和集群有關(guān)的數(shù)據(jù)結(jié)構(gòu), 以及 CLUSTER MEET 命令的實現(xiàn)原理。
一個節(jié)點就是一個運行在集群模式下的 Redis 服務(wù)器, Redis 服務(wù)器在啟動時會根據(jù) cluster-enabled
配置選項的是否為 yes
來決定是否開啟服務(wù)器的集群模式, 如圖 IMAGE_NODE_OR_SERVER 所示。
節(jié)點(運行在集群模式下的 Redis 服務(wù)器)會繼續(xù)使用所有在單機模式中使用的服務(wù)器組件, 比如說:
serverCron
函數(shù), 而 serverCron
函數(shù)又會調(diào)用集群模式特有的 clusterCron
函數(shù): clusterCron
函數(shù)負責執(zhí)行在集群模式下需要執(zhí)行的常規(guī)操作, 比如向集群中的其他節(jié)點發(fā)送 Gossip 消息, 檢查節(jié)點是否斷線; 又或者檢查是否需要對下線節(jié)點進行自動故障轉(zhuǎn)移, 等等。諸如此類。
除此之外, 節(jié)點會繼續(xù)使用 redisServer
結(jié)構(gòu)來保存服務(wù)器的狀態(tài), 使用 redisClient
結(jié)構(gòu)來保存客戶端的狀態(tài), 至于那些只有在集群模式下才會用到的數(shù)據(jù), 節(jié)點將它們保存到了 cluster.h/clusterNode
結(jié)構(gòu), cluster.h/clusterLink
結(jié)構(gòu), 以及 cluster.h/clusterState
結(jié)構(gòu)里面, 接下來的一節(jié)將對這三種數(shù)據(jù)結(jié)構(gòu)進行介紹。
clusterNode
結(jié)構(gòu)保存了一個節(jié)點的當前狀態(tài), 比如節(jié)點的創(chuàng)建時間, 節(jié)點的名字, 節(jié)點當前的配置紀元, 節(jié)點的 IP 和地址, 等等。
每個節(jié)點都會使用一個 clusterNode
結(jié)構(gòu)來記錄自己的狀態(tài), 并為集群中的所有其他節(jié)點(包括主節(jié)點和從節(jié)點)都創(chuàng)建一個相應(yīng)的clusterNode
結(jié)構(gòu), 以此來記錄其他節(jié)點的狀態(tài):
struct clusterNode {
// 創(chuàng)建節(jié)點的時間
mstime_t ctime;
// 節(jié)點的名字,由 40 個十六進制字符組成
// 例如 68eef66df23420a5862208ef5b1a7005b806f2ff
char name[REDIS_CLUSTER_NAMELEN];
// 節(jié)點標識
// 使用各種不同的標識值記錄節(jié)點的角色(比如主節(jié)點或者從節(jié)點),
// 以及節(jié)點目前所處的狀態(tài)(比如在線或者下線)。
int flags;
// 節(jié)點當前的配置紀元,用于實現(xiàn)故障轉(zhuǎn)移
uint64_t configEpoch;
// 節(jié)點的 IP 地址
char ip[REDIS_IP_STR_LEN];
// 節(jié)點的端口號
int port;
// 保存連接節(jié)點所需的有關(guān)信息
clusterLink *link;
// ...
};
clusterNode
結(jié)構(gòu)的 link
屬性是一個 clusterLink
結(jié)構(gòu), 該結(jié)構(gòu)保存了連接節(jié)點所需的有關(guān)信息, 比如套接字描述符, 輸入緩沖區(qū)和輸出緩沖區(qū):
typedef struct clusterLink {
// 連接的創(chuàng)建時間
mstime_t ctime;
// TCP 套接字描述符
int fd;
// 輸出緩沖區(qū),保存著等待發(fā)送給其他節(jié)點的消息(message)。
sds sndbuf;
// 輸入緩沖區(qū),保存著從其他節(jié)點接收到的消息。
sds rcvbuf;
// 與這個連接相關(guān)聯(lián)的節(jié)點,如果沒有的話就為 NULL
struct clusterNode *node;
} clusterLink;
redisClient
結(jié)構(gòu)和 clusterLink
結(jié)構(gòu)的相同和不同之處
redisClient
結(jié)構(gòu)和 clusterLink
結(jié)構(gòu)都有自己的套接字描述符和輸入、輸出緩沖區(qū), 這兩個結(jié)構(gòu)的區(qū)別在于, redisClient
結(jié)構(gòu)中的套接字和緩沖區(qū)是用于連接客戶端的, 而 clusterLink
結(jié)構(gòu)中的套接字和緩沖區(qū)則是用于連接節(jié)點的。
最后, 每個節(jié)點都保存著一個 clusterState
結(jié)構(gòu), 這個結(jié)構(gòu)記錄了在當前節(jié)點的視角下, 集群目前所處的狀態(tài) —— 比如集群是在線還是下線, 集群包含多少個節(jié)點, 集群當前的配置紀元, 諸如此類:
typedef struct clusterState {
// 指向當前節(jié)點的指針
clusterNode *myself;
// 集群當前的配置紀元,用于實現(xiàn)故障轉(zhuǎn)移
uint64_t currentEpoch;
// 集群當前的狀態(tài):是在線還是下線
int state;
// 集群中至少處理著一個槽的節(jié)點的數(shù)量
int size;
// 集群節(jié)點名單(包括 myself 節(jié)點)
// 字典的鍵為節(jié)點的名字,字典的值為節(jié)點對應(yīng)的 clusterNode 結(jié)構(gòu)
dict *nodes;
// ...
} clusterState;
以前面介紹的 7000 、 7001 、 7002 三個節(jié)點為例, 圖 IMAGE_CLUSTER_STATE_OF_7000 展示了節(jié)點 7000 創(chuàng)建的 clusterState
結(jié)構(gòu), 這個結(jié)構(gòu)從節(jié)點 7000 的角度記錄了集群、以及集群包含的三個節(jié)點的當前狀態(tài) (為了空間考慮,圖中省略了 clusterNode
結(jié)構(gòu)的一部分屬性):
currentEpoch
屬性的值為 0
, 表示集群當前的配置紀元為 0
。size
屬性的值為 0
, 表示集群目前沒有任何節(jié)點在處理槽: 因此結(jié)構(gòu)的 state
屬性的值為 REDIS_CLUSTER_FAIL
—— 這表示集群目前處于下線狀態(tài)。nodes
字典記錄了集群目前包含的三個節(jié)點, 這三個節(jié)點分別由三個 clusterNode
結(jié)構(gòu)表示: 其中 myself
指針指向代表節(jié)點 7000 的 clusterNode
結(jié)構(gòu), 而字典中的另外兩個指針則分別指向代表節(jié)點 7001 和代表節(jié)點 7002 的 clusterNode
結(jié)構(gòu), 這兩個節(jié)點是節(jié)點 7000 已知的在集群中的其他節(jié)點。clusterNode
結(jié)構(gòu)的 flags
屬性都是 REDIS_NODE_MASTER
,說明三個節(jié)點都是主節(jié)點。節(jié)點 7001 和節(jié)點 7002 也會創(chuàng)建類似的 clusterState
結(jié)構(gòu):
clusterState
結(jié)構(gòu)中, myself
指針將指向代表節(jié)點 7001 的 clusterNode
結(jié)構(gòu), 而節(jié)點 7000 和節(jié)點 7002 則是集群中的其他節(jié)點。clusterState
結(jié)構(gòu)中, myself
指針將指向代表節(jié)點 7002 的 clusterNode
結(jié)構(gòu), 而節(jié)點 7000 和節(jié)點 7001 則是集群中的其他節(jié)點。通過向節(jié)點 A 發(fā)送 CLUSTER MEET 命令, 客戶端可以讓接收命令的節(jié)點 A 將另一個節(jié)點 B 添加到節(jié)點 A 當前所在的集群里面:
CLUSTER MEET <ip> <port>
收到命令的節(jié)點 A 將與節(jié)點 B 進行握手(handshake), 以此來確認彼此的存在, 并為將來的進一步通信打好基礎(chǔ):
clusterNode
結(jié)構(gòu), 并將該結(jié)構(gòu)添加到自己的 clusterState.nodes
字典里面。MEET
消息(message)。MEET
消息, 節(jié)點 B 會為節(jié)點 A 創(chuàng)建一個 clusterNode
結(jié)構(gòu), 并將該結(jié)構(gòu)添加到自己的 clusterState.nodes
字典里面。PONG
消息。PONG
消息, 通過這條 PONG
消息節(jié)點 A 可以知道節(jié)點 B 已經(jīng)成功地接收到了自己發(fā)送的 MEET
消息。PING
消息。PING
消息, 通過這條 PING
消息節(jié)點 B 可以知道節(jié)點 A 已經(jīng)成功地接收到了自己返回的 PONG
消息, 握手完成。圖 IMAGE_HANDSHAKE 展示了以上步驟描述的握手過程。
之后, 節(jié)點 A 會將節(jié)點 B 的信息通過 Gossip 協(xié)議傳播給集群中的其他節(jié)點, 讓其他節(jié)點也與節(jié)點 B 進行握手, 最終, 經(jīng)過一段時間之后, 節(jié)點 B 會被集群中的所有節(jié)點認識。
更多建議: