無論是單機(jī)還是主從結(jié)構(gòu)蚁孔,都無法解決數(shù)據(jù)量過大導(dǎo)致內(nèi)存/虛擬內(nèi)存無法村下所有數(shù)據(jù)的問題问欠。redis集群通過分片根據(jù)key計算出數(shù)據(jù)所屬的槽(slot)唧席,通過指派使每個集群節(jié)點負(fù)責(zé)一部分槽內(nèi)數(shù)據(jù)的處理,從而成為一種分布式數(shù)據(jù)庫除嘹。
redis集群一共有16384個槽怎憋,集群內(nèi)每個節(jié)點可以負(fù)責(zé)處理其中一部分槽又碌。下圖表示集群內(nèi)有3個節(jié)點九昧,分別在端口7000,7001毕匀,7002铸鹰。其中0-5000號槽被指派給7000端口所在的節(jié)點,...
1.節(jié)點
1.1 構(gòu)建集群
redis集群的節(jié)點在初始時都是互相獨立的皂岔,要組建集群只需在某臺主機(jī)上使用CLUSTER MEET <ip> <port>
命令蹋笼,當(dāng)前主機(jī)就會與目的主機(jī)進(jìn)行連接,連接成功后就會將對方添加到自己所在的集群內(nèi)凤薛。
為了使7000-7002端口所在的3個服務(wù)主機(jī)構(gòu)成一個[圖片上傳中...(image.png-891ba7-1632229388314-0)]
集群姓建,只需要在7000端口的客戶端分別輸入
CLUSTER MEET <127.0.0.1> <7001>
CLUSTER MEET <127.0.0.1> <7002>
即可。
1.2 集群對應(yīng)的數(shù)據(jù)結(jié)構(gòu)
集群內(nèi)每一個主機(jī)都對應(yīng)一個clusterNode類型的數(shù)據(jù)缤苫。
typedef struct clusterNode {
mstime_t ctime; /* Node object creation time. */
char name[CLUSTER_NAMELEN]; /* Node name, hex string, sha1-size */
int flags; /* CLUSTER_NODE_... */
...
unsigned char slots[CLUSTER_SLOTS/8]; /* slots handled by this node */
int numslots; /* Number of slots handled by this node */
int numslaves; /* Number of slave nodes, if this is a master */
struct clusterNode **slaves; /* pointers to slave nodes */
struct clusterNode *slaveof; /* pointer to the master node. Note that it
may be NULL even if the node is a slave
if we don't have the master node in our
tables. */
...
char ip[NET_IP_STR_LEN]; /* Latest known IP address of this node */
int port; /* Latest known clients port of this node */
int cport; /* Latest known cluster port of this node. */
clusterLink *link; /* TCP/IP link with this node */
...
} clusterNode;
clusterNode中比較關(guān)鍵的幾個屬性分別是: slots數(shù)組,長度為16384/8字節(jié)(16384 = 2^^14位墅拭,計算key應(yīng)位于哪個slot時取余數(shù)方便)活玲,以一個bitmap的形式表示了當(dāng)前節(jié)點處理的slot; 一個指向slave數(shù)組的指針(如果當(dāng)前主機(jī)為master)谍婉;一個指向master主機(jī)的指針(如果當(dāng)前為slave)舒憾;另外在clusterLink中包含了當(dāng)前節(jié)點的tcp/ip連接信息,如文件描述符穗熬,輸入镀迂、輸出緩沖區(qū)等。
整個集群的信息由clusterState表示唤蔗。 在每個節(jié)點中都會保存一個clusterState結(jié)構(gòu)探遵,表示在當(dāng)前主機(jī)的視角下集群所處的狀態(tài)。
typedef struct clusterState {
// 指向表示當(dāng)前節(jié)點自身的clusterNode
clusterNode *myself; /* This node */
uint64_t currentEpoch;
// 集群當(dāng)前狀態(tài):在線 || 下線
int state;
// 集群中有多少節(jié)點至少處理一個slot
int size;
// 集群中所有節(jié)點的字典妓柜,以name : clusterNode形式存儲
dict *nodes; /* Hash table of name -> clusterNode structures */
...
clusterNode *slots[CLUSTER_SLOTS];
...
} clusterState;
在上述3個節(jié)點創(chuàng)建完集群后的初始狀態(tài)下箱季,每個節(jié)點都沒有被分配任何負(fù)責(zé)處理的槽,在7000節(jié)點視角下狀態(tài)如下圖
1.3 槽指派
通過在某個節(jié)點的客戶端發(fā)送CLUSTER ADDSLOTS <slot> [slot ...]
命令棍掐,可以將命令中的slot分派給當(dāng)前節(jié)點負(fù)責(zé)處理藏雏。分派主要需要完成這樣幾件事情:
- 記錄節(jié)點的槽指派信息。 即將節(jié)點對應(yīng)的clusterNode內(nèi)的slots數(shù)組相應(yīng)位置為1作煌, 同時使clusterState中的slots數(shù)組的對應(yīng)位存放的指針指向當(dāng)前clusterNode掘殴。
- 傳播節(jié)點的槽指派信息。當(dāng)前節(jié)點不僅要自己記錄一份指派信息粟誓,還需要將指派信息發(fā)送給其他節(jié)點奏寨。其他節(jié)點在收到后,修改自己的clusterState努酸,將slots數(shù)組的對應(yīng)位指向被分派的節(jié)點服爷,同時修改該節(jié)點的slots數(shù)組。
在所有節(jié)點指派完成之后,每個節(jié)點內(nèi)clusterState的slots數(shù)組狀態(tài)大致如下圖仍源。同時心褐,各個clusterNode內(nèi),slots數(shù)組中被該節(jié)點負(fù)責(zé)的槽被置位為1.
此時笼踩,想要知道某個slot被哪個節(jié)點負(fù)責(zé)處理逗爹,只需要通過clusterState的slots數(shù)組對應(yīng)位的指針,即可訪問到該節(jié)點的信息嚎于。而如果想要知道當(dāng)前節(jié)點所負(fù)責(zé)的slot信息(例如發(fā)送當(dāng)前節(jié)點負(fù)責(zé)處理的slot信息給其他節(jié)點)掘而,只需要取出myself指向的clusterNode,拿到它的slots數(shù)組于购,取出被置位1的位就行了袍睡。
2. 執(zhí)行命令
clusterState.slots可以快速定位到slot被哪個節(jié)點負(fù)責(zé)處理,因此在集群中執(zhí)行命令也就相對容易了:客戶端連接集群中任一節(jié)點肋僧,向其發(fā)送對key的處理命令斑胜。收到命令的節(jié)點現(xiàn)根據(jù)key計算出該key應(yīng)該配分配到哪個slot上,比如slot = CRC16(key)%16834
根據(jù)clusterState.slots[slot]就能快速定位到對應(yīng)的節(jié)點N嫌吠。然后將節(jié)點N與myself指向的節(jié)點對比止潘,如果是同一個,那么就可以直接處理辫诅。如果不是凭戴,那么說明應(yīng)當(dāng)由其他節(jié)點處理。此時向客戶端返回MOVED錯誤炕矮,根據(jù)節(jié)點N的信息么夫,指引客戶端去請求對應(yīng)的主機(jī)。
其流程如下圖:
3.重新分片
重新分片可以將已經(jīng)指派給某節(jié)點的任意數(shù)量的槽吧享,重新分派給另外一個節(jié)點魏割。重新分派后,槽和槽所屬的鍵值對會被從源節(jié)點移動到目標(biāo)節(jié)點钢颂。重新分片可以在線進(jìn)行钞它,并且再重新分片過程中,源節(jié)點和目標(biāo)節(jié)點都可以繼續(xù)處理命令請求殊鞭。
重新分片由redis-trib負(fù)責(zé)執(zhí)行遭垛,通過向源節(jié)點和目標(biāo)節(jié)點發(fā)送命令來完成該操作。
對每個槽:
- 向目標(biāo)節(jié)點發(fā)送
CLUSTER SETSLOT <slot> IMPORTING <source_id>
命令操灿,讓其準(zhǔn)備好從源節(jié)點導(dǎo)入屬于slot的鍵值對數(shù)據(jù) - 向源節(jié)點發(fā)送
CLUSTER SETSLOT <slot> MIGRATING <target_id>
命令锯仪,讓其準(zhǔn)備好將slot內(nèi)的鍵值對遷移到target - 向源節(jié)點發(fā)送
CLUSTER GETKEYSINSLOT <slot> <count>
命令,獲得最多count個key name - 對于每個key_name, redis-trib向源節(jié)點發(fā)送
MIGRATE <target_ip> <target_port> <key_name> 0 <timeout>
,將key原子地遷移至目標(biāo)節(jié)點 - 重復(fù)3和4趾盐,直到slot內(nèi)所有的key都被遷移完成庶喜。
-
向集群內(nèi)任一節(jié)點發(fā)送``CLUSTER SETSLOT <slot> NODE <target_id>```小腊,將slot指派給target,然后通過消息發(fā)送到整個集群久窟,每個節(jié)點更新自己的slotState秩冈。
遷移slot內(nèi)的鍵
4.復(fù)制與故障轉(zhuǎn)移
1.集群設(shè)置從節(jié)點
向某個服務(wù)器發(fā)送 CLUSTER REPLICATE <node_id>
命令,使之復(fù)制node_id對應(yīng)的節(jié)點斥扛,成為其從節(jié)點入问。收到命令的服務(wù)器需要做以下幾件事:
- 在自己的clusterState.nodes中找到node_id對應(yīng)的clusterNode,將自己的slaveof指向該node稀颁。
- 修改自身flag芬失, 由master 變?yōu)閟lave
3.調(diào)用復(fù)制代碼,根據(jù)master的ip:port發(fā)起復(fù)制請求匾灶。(這一過程和之前講的主從復(fù)制相同棱烂,類似于SLAVEOF <master_ip> <master_port>
) - 通知集群內(nèi)其他節(jié)點,這些節(jié)點收到通知后修改自己的clusterState
2.故障檢測
- 集群內(nèi)每個節(jié)點定期向集群內(nèi)其他節(jié)點發(fā)送PING消息阶女,如果某節(jié)點回復(fù)超時垢啼,那么發(fā)送的節(jié)點將其標(biāo)記為疑似下線。(修改clusterState.nodes中對于clusterNode的標(biāo)志)
- 各節(jié)點間通過消息交換集群中各節(jié)點的狀態(tài)信息张肾。
- 當(dāng)某節(jié)點A得知B認(rèn)為C疑似下線時,向C對應(yīng)的clusterNode.fail_reports添加該報告锚扎。
如果集群內(nèi)半數(shù)以上負(fù)責(zé)處理槽的主節(jié)點都向節(jié)點A報告C為疑似下線吞瞪,那么X就被A標(biāo)記為已下線。A將C已下線的消息廣播到集群內(nèi)驾孔,所有收到消息的節(jié)點都將該節(jié)點標(biāo)記為已下線芍秆。
3.故障轉(zhuǎn)移
當(dāng)已下線節(jié)點C的從節(jié)點C1收到C已下線時,開始進(jìn)行故障轉(zhuǎn)移翠勉。
- 選舉新的主節(jié)點妖啥。 (基于raft領(lǐng)導(dǎo)選舉算法)
1.1 某個從節(jié)點開始故障轉(zhuǎn)移時,先將自己內(nèi)部clusterState.epoch加1对碌,然后發(fā)起投票
1.2 負(fù)責(zé)處理槽的主節(jié)點具有投票權(quán)荆虱,每個投票者根據(jù)發(fā)過來的epoch和自己當(dāng)前的epoch對比,如果當(dāng)前epoch小于請求的epoch朽们,則投票給他怀读,并更新自己的epoch。
1.3 勝出規(guī)則: 得票數(shù)不小于 N/2 +1 則當(dāng)選為新的主節(jié)點骑脱。
1.4 如果沒人勝出菜枷,則進(jìn)行下一輪投票 - 將已下線主節(jié)點負(fù)責(zé)處理的槽指派給自己。
- 將自己成為主節(jié)點的消息廣播給集群內(nèi)的其他節(jié)點叁丧。
- 轉(zhuǎn)移完成啤誊,C1開始處理落在原來由C負(fù)責(zé)的槽內(nèi)的請求岳瞭。