復(fù)制
復(fù)制功能是讓一臺(tái)Redis服務(wù)器復(fù)制另一臺(tái)服務(wù)器,也就是Master-Slave模式,通常用于實(shí)現(xiàn)讀寫(xiě)分離。該功能有兩種實(shí)現(xiàn),分別對(duì)應(yīng)2.8版本之前的老版本,和2.8(包括)之后的新版本。
2.8版本之前的實(shí)現(xiàn)
復(fù)制功能分為同步和命令傳播兩個(gè)操作。
- 同步:將從服務(wù)器的數(shù)據(jù)庫(kù)更新至主服務(wù)器當(dāng)前所處的數(shù)據(jù)庫(kù)狀態(tài)。
同步的步驟如下:
- 從服務(wù)器發(fā)送
SYNC
命令。 - 主服務(wù)器收到
SYNC
命令,執(zhí)行BGSAVE
在后臺(tái)生成一個(gè)RDB文件,并用緩沖區(qū)記錄從現(xiàn)在開(kāi)始執(zhí)行的所有寫(xiě)命令。 - 當(dāng)RDB文件準(zhǔn)備好時(shí),把該文件發(fā)送給從服務(wù)器。
- 從服務(wù)器阻塞載入RDB文件(這段時(shí)間從服務(wù)器不能處理任何請(qǐng)求)。
- 主服務(wù)器把緩沖區(qū)里的命令發(fā)送給從服務(wù)器
- 命令傳播:主服務(wù)器把寫(xiě)命令傳播到從服務(wù)器,使得主從服務(wù)器的數(shù)據(jù)庫(kù)狀態(tài)一致。比如當(dāng)主服務(wù)器執(zhí)行
DEL key
時(shí),會(huì)異步的把該命令發(fā)送給從服務(wù)器,使兩者狀態(tài)最終一致。
缺點(diǎn):如果從服務(wù)器中途短線,重連后需要重新執(zhí)行一遍同步操作,效率較低(生產(chǎn)RDB文件需要耗費(fèi)大量I/O、CPU資源)。
2.8及之后版本的實(shí)現(xiàn)
新版本使用PSYNC
命令替代SYNC
,該命令具有完整重同步和部分重同步兩種模式。其中完整重同步的步驟跟舊版本的SYNC
的命令類似不再贅述,下面主要講部分重同步功能。
新版本的實(shí)現(xiàn)中,主從服務(wù)器分別維護(hù)一份復(fù)制偏移量,記錄當(dāng)前復(fù)制的進(jìn)度。當(dāng)主服務(wù)器向從服務(wù)器發(fā)送N個(gè)字節(jié)的數(shù)據(jù)時(shí)就把自己的偏移量加上N,當(dāng)從服務(wù)器接收到N個(gè)字節(jié)的數(shù)據(jù)時(shí)就把自己的偏移量也加上N,如果主從服務(wù)器數(shù)據(jù)處于一致,那么它們的偏移量也是一致的。
如果從服務(wù)器出現(xiàn)了斷開(kāi)的狀況,那么復(fù)制偏移量就會(huì)和主服務(wù)器不一致:
為了解決從服務(wù)器意外斷開(kāi)連接后能夠快速恢復(fù)到跟主服務(wù)器一致的狀態(tài)(之所以說(shuō)快速是因?yàn)榕f版本的實(shí)現(xiàn)效率太低),Redis使用了復(fù)制積壓緩沖區(qū)來(lái)記錄最近執(zhí)行的寫(xiě)命令,以便在從服務(wù)器恢復(fù)連接后能通過(guò)緩沖區(qū)把丟失的寫(xiě)命令找回并發(fā)送到從服務(wù)器。該緩沖區(qū)是一個(gè)固定長(zhǎng)度的先進(jìn)先出隊(duì)列,默認(rèn)大小是1MB,當(dāng)緩沖區(qū)大小不夠時(shí)會(huì)將位于隊(duì)首的元素拋棄,隊(duì)列保存了一部分最近傳播的寫(xiě)命令,每個(gè)字節(jié)的偏移量都會(huì)記錄在內(nèi),其構(gòu)造如圖所示:
當(dāng)從服務(wù)器斷線重連后會(huì)發(fā)送自己的復(fù)制偏移量給主服務(wù)器,如果偏移量+1存在主服務(wù)器緩沖區(qū)中,那么主服務(wù)器會(huì)把這部分?jǐn)?shù)據(jù)發(fā)送給從服務(wù)器;反之會(huì)執(zhí)行完整重同步。
這里存在一個(gè)問(wèn)題,從服務(wù)器重連后如何知道是否是之前的那臺(tái)主服務(wù)器?
其實(shí)Redis在啟動(dòng)時(shí)會(huì)生成一個(gè)隨機(jī)的服務(wù)器運(yùn)行ID,當(dāng)從服務(wù)器連接到主服務(wù)器后便會(huì)記下這個(gè)ID,下次連接時(shí)如果發(fā)現(xiàn)ID不能對(duì)應(yīng)就會(huì)執(zhí)行完整重同步。
在命令傳播階段,從服務(wù)器默認(rèn)每秒向服務(wù)器發(fā)送心跳,并帶上自己的復(fù)制偏移量,如果主服務(wù)器超過(guò)1秒沒(méi)有收到心跳,說(shuō)明網(wǎng)絡(luò)出現(xiàn)了問(wèn)題。此外如果主服務(wù)器發(fā)送的寫(xiě)命令意外丟失,那么主服務(wù)器通過(guò)心跳返回的偏移量就可以知道主從服務(wù)器狀態(tài)不一致,然后通過(guò)復(fù)制擠壓緩沖區(qū)補(bǔ)發(fā)缺失的命令,使兩者再次一致。
Redis2.8版本之前沒(méi)有這一機(jī)制,如果主服務(wù)器向從服務(wù)器發(fā)送的命令出現(xiàn)了丟失,兩者都不會(huì)注意到。
Sentinel(哨兵)
上文介紹的復(fù)制功能可以把Master的讀壓力分散到其它Slave上,但是當(dāng)Master發(fā)生故障后,需要手動(dòng)把一臺(tái)Slave提升為Master,客戶端也很有可能需要修改連接地址如果你沒(méi)有服務(wù)發(fā)現(xiàn)這樣的基礎(chǔ)設(shè)施的話,因?yàn)槟悴恢朗悄囊慌_(tái)Slave被提升為Master。
Redis提供了Sentinel來(lái)解決以上問(wèn)題。它會(huì)監(jiān)控所有的Redis節(jié)點(diǎn),并提供故障轉(zhuǎn)移機(jī)智,是一種高可用解決方案。其大致結(jié)構(gòu)如圖所示:
初始化Sentinel
通過(guò)以下命令啟動(dòng)Sentinel:
redis-sentinel /path/to/your/sentinel.config
//等價(jià)于
//redis-server /path/to/your/sentinel.config --sentinel
Sentinel本質(zhì)是一個(gè)特殊的Redis服務(wù)器,因此初始化過(guò)程可以看作是初始化一臺(tái)普通的Redis服務(wù)器,但在某些方面會(huì)有所區(qū)別,比如它不會(huì)使用數(shù)據(jù)庫(kù)因而不會(huì)載入RDB或AOF文件。哨兵使用的命令表也很普通Redis服務(wù)器不同,它只支持7個(gè)命令:PING
、SENTINEL
、INFO
、SUBSCRIBE
、UNSUBSCRIBE
、PSUBSCRIBE
、PUNSUBSCRIBE
。
啟動(dòng)后,服務(wù)器會(huì)初始化一個(gè)sentinelState
結(jié)構(gòu)的對(duì)象,保存了所有和哨兵功能相關(guān)的狀態(tài),其結(jié)構(gòu)如下所示:
struct sentinelState {
//當(dāng)前紀(jì)元,是個(gè)計(jì)數(shù)器,故障轉(zhuǎn)移時(shí)會(huì)用到
uint64_t current_epoch;
//保存了當(dāng)前sentinel監(jiān)控的所有master
//鍵是master的名字,值是一個(gè)指向sentinelRedisInstance結(jié)構(gòu)的指針
dict *masters;
//是否進(jìn)入TILT模式
int tilt;
//目前正在執(zhí)行的腳本數(shù)量
int running_scripts;
//進(jìn)入TILT模式的時(shí)間
mstime_t tilt_start_time;
//最后一次執(zhí)行時(shí)間處理器的時(shí)間
mstime_t previous_time;
//FIFO隊(duì)列,包含所有需要執(zhí)行的用戶腳本
list *scripts_queue;
}
其中sentinelRedisInstance
結(jié)構(gòu)如下所示:
struct sentinelRedisInstance {
//實(shí)例的類型以及狀態(tài)
int flags;
//實(shí)例的名字,master的名字在配置文件中配置,slave的名字由ip:port組成
char *name;
//運(yùn)行ID
char * runid;
//配置紀(jì)元,是個(gè)計(jì)數(shù)器,故障轉(zhuǎn)移時(shí)會(huì)用到
uint64_t config_epoch;
//實(shí)例的地址
sentinelAddr *addr;
//無(wú)響應(yīng)多少毫秒后會(huì)被判斷為主觀下線
mstime_t down_after_period;
//判斷實(shí)例為客觀下線所需要的支持投票數(shù)
int quorum;
//在故障轉(zhuǎn)移時(shí)可以同時(shí)對(duì)新的主服務(wù)器進(jìn)行同步的從服務(wù)器數(shù)量
int parallel_syncs;
//刷新故障遷移狀態(tài)的超時(shí)時(shí)間
mstime_t failover_timeout;
//從服務(wù)器,鍵是ip:port,值是一個(gè)指向sentinelRedisInstance結(jié)構(gòu)的指針
dict *slaves;
//哨兵,鍵是ip:port,值是一個(gè)指向sentinelRedisInstance結(jié)構(gòu)的指針
dict *sentinels;
//其它
...
}
上面的數(shù)據(jù)結(jié)構(gòu)可以和下面的配置文件對(duì)應(yīng)起來(lái):
//sentinel.conf
//監(jiān)控主服務(wù)器的名字為master1,地址是127.0.0.1,端口是6379
//判斷實(shí)例為客觀下線所需要的支持投票數(shù)是2
sentinel monitor master1 127.0.0.1 6379 2
//master1若在30000毫秒內(nèi)無(wú)響應(yīng)則判斷為下線
sentinel down-after-milliseconds master1 30000
//master1發(fā)生故障后,它的從服務(wù)器同時(shí)只有1臺(tái)能從新的master進(jìn)行同步
sentinel parallel-syncs master1 1
//故障遷移超時(shí)時(shí)間900000毫秒
sentinel failover-timeout master1 900000
初始化完成后內(nèi)存中會(huì)有如下所示的對(duì)象關(guān)系:
Sentinel的一些狀態(tài)會(huì)持久化到配置文件中(sentinel.conf),因此無(wú)需擔(dān)心sentinel重啟。
連接Master
初始化后sentinel會(huì)和每一個(gè)被監(jiān)視的master創(chuàng)建兩個(gè)連接,一個(gè)用于收發(fā)命令,一個(gè)用于訂閱master的__sentinel__:hello
頻道。
連接建立后,sentinel會(huì)以10秒一次的頻率向master發(fā)送INFO
命令,通過(guò)返回值可以得到master的運(yùn)行ID、角色(master/slave)以及所有slave(可以看作是服務(wù)發(fā)現(xiàn))。
Sentinel會(huì)把slave關(guān)聯(lián)到對(duì)應(yīng)的master對(duì)象上:
連接Slave
通過(guò)master發(fā)現(xiàn)了所有slave之后,sentinel會(huì)建立和slave的連接,同樣是每個(gè)slave兩個(gè)連接。連接建立后同樣會(huì)以10秒一次的頻率向slave發(fā)送INFO
命令,并且得到運(yùn)行ID、master地址、復(fù)制偏移量等信息,記錄到sentinelRedisInstance
結(jié)構(gòu)中。
連接Sentinel
上面提到Sentinel會(huì)和master和slave保持兩個(gè)連接,一個(gè)用于收發(fā)命令,一個(gè)用于發(fā)布訂閱指定的頻道。Sentinel以2秒一次的頻率通過(guò)第2個(gè)連接向所有監(jiān)控的服務(wù)器發(fā)送以下格式的命令:
PUBLISH __sentinel__:hello "<sentinel_ip>,<sentinel_port>,<sentinel_runid>,<sentinel_epoch>,<master_name>,<master_ip>,<master_port>,<master_epoch>"
- 以sentinel開(kāi)頭的參數(shù)是sentinel本身的信息。
- 以master開(kāi)頭的參數(shù)是主服務(wù)器的信息,如果sentinel發(fā)送命令的對(duì)象是主服務(wù)器,那么就是該主服務(wù)器本身的信息;如果發(fā)送命令的對(duì)象是從服務(wù)器,那么就是該從服務(wù)器正在復(fù)制的主服務(wù)器的信息。
每一個(gè)sentinel既是__sentinel__:hello
頻道的發(fā)布者,同時(shí)也是訂閱者。這種設(shè)計(jì)的作用是,當(dāng)一個(gè)服務(wù)器被多個(gè)sentinel監(jiān)控時(shí),任意一個(gè)sentinel發(fā)送的消息都會(huì)被其它sentinel接收到。當(dāng)其它sentinel接收到消息后就會(huì)發(fā)現(xiàn)新的sentinel,并且更新master對(duì)應(yīng)的sentinelRedisInstance
結(jié)構(gòu)的sentinels
屬性,如下圖所示:
通過(guò)這種方式,每個(gè)sentinel都知道它監(jiān)控的某個(gè)master還在被哪些sentinel監(jiān)控。
Sentinel會(huì)和其它sentinel建立1個(gè)連接,最終同一個(gè)master的所有sentinel互聯(lián)。
判斷下線
一個(gè)sentinel會(huì)以每秒1次的頻率向所有建立連接的服務(wù)器發(fā)送PING
命令,包括主從服務(wù)器和其它sentinel。如果在一段時(shí)間內(nèi)(由配置的down-after-milliseconds指定)一直收到無(wú)效回復(fù)(有效回復(fù)有3種,+PONG
、-LOADING
、-MASTERDOWN
,此外都是無(wú)效回復(fù),沒(méi)有回復(fù)也是無(wú)效回復(fù))那么sentinel就會(huì)認(rèn)為該實(shí)例已經(jīng)下線,sentinel會(huì)修改該實(shí)例對(duì)應(yīng)的sentinelRedisInstance
結(jié)構(gòu)的flags
屬性,將其標(biāo)為主觀下線(SDOWN,Subjectively Down)。
同一個(gè)master被多個(gè)sentinel監(jiān)控時(shí),因?yàn)槊總€(gè)sentinel的主觀下線時(shí)長(zhǎng)可能配置了不同的值,因此不同的sentinel對(duì)于同一個(gè)master的下線狀態(tài)可能有不同的判斷。
當(dāng)sentinel認(rèn)為一個(gè)master已經(jīng)下線后,它會(huì)發(fā)送以下格式的命令詢問(wèn)該服務(wù)器的其它sentinel是否也認(rèn)為該服務(wù)器已經(jīng)下線:
SENTINEL is-master-down-by-addr <master_ip> <master_port> <sentinel_epoch> *
//e.g. SENTINEL is-master-down-by-addr 127.0.0.1 6379 0 *
當(dāng)另一個(gè)sentinel接收并且檢查master是否下線后會(huì)回復(fù)一條包含三個(gè)參數(shù)的消息:
1) <down_state> //1表示下線,0表示未下線
2) *
3) 0
如果得到的確認(rèn)數(shù)量超過(guò)了配置的quorum
的值,sentinel就會(huì)把master標(biāo)為客觀下線(ODOWN,Objectively Down)。
Sentinel僅會(huì)對(duì)master進(jìn)行故障轉(zhuǎn)移,如果是slave下線了,sentinel會(huì)把它標(biāo)為SDOWN,并且不會(huì)詢問(wèn)其它的sentinel。
Leader選舉
當(dāng)一個(gè)master被標(biāo)為客觀下線時(shí),監(jiān)視這個(gè)服務(wù)器的各個(gè)sentinel會(huì)協(xié)商選舉出一個(gè)leader,由leader執(zhí)行故障轉(zhuǎn)移。
我們假設(shè)有3個(gè)sentinel組成哨兵系統(tǒng),為了選出leader,3個(gè)sentinel再次向其它兩個(gè)sentinel發(fā)送命令,區(qū)別是這次會(huì)帶上sentinel自己的運(yùn)行ID,表示要求對(duì)方把自己設(shè)為leader:
SENTINEL is-master-down-by-addr <master_ip> <master_port> <sentinel_epoch> <sentinel_runid>
如果接收到命令的sentinel還沒(méi)有設(shè)置過(guò)leader的話就會(huì)把接收到的sentinel設(shè)置為leader,并返回:
1) <down_state> //1表示下線,0表示未下線
2) <leader_runid>
3) <leader_epoch>
接收到回復(fù)的sentinel就可以知道有多少sentinel選舉自己當(dāng)leader,如果獲得了半數(shù)以上(大于等于sentinel數(shù)量/2+1)的投票,那么就算選舉成功。
在選舉過(guò)程中有幾個(gè)規(guī)則:
- 不論選舉是否成功,所有sentinel的配置紀(jì)元都會(huì)自增一次。
- 在一個(gè)紀(jì)元里只能設(shè)置一次leader,設(shè)置的優(yōu)先級(jí)是先到先得。
- 如果在給定時(shí)間內(nèi)選舉失敗,那么會(huì)在一段時(shí)間后重新選舉直到選出leader為止。
Leader選舉算法請(qǐng)參考Raft算法。
故障轉(zhuǎn)移
Leader會(huì)從下線主服務(wù)器的所有從服務(wù)器中選出一臺(tái)并轉(zhuǎn)換為主服務(wù)器,有以下幾個(gè)篩選條件:
- slave處于在線狀態(tài)。
- 最近5秒內(nèi)回復(fù)過(guò)leader發(fā)出的
INFO
命令(來(lái)保證leader和該slave最近成功進(jìn)行過(guò)通訊)。 - slave與已經(jīng)下線的master連接斷開(kāi)時(shí)間不超過(guò)
down-after-milliseconds * 10
毫秒(確保slave沒(méi)有過(guò)早和master斷開(kāi)連接,slave保存的數(shù)據(jù)是相對(duì)較新的)。
篩選完成后,如果沒(méi)有可用的slave那么就終止此次故障轉(zhuǎn)移,否則Leader會(huì)根據(jù)slave的優(yōu)先級(jí)進(jìn)行排序,選出優(yōu)先級(jí)最高的slave。
Slave的優(yōu)先級(jí)可以在配置文件中通過(guò)
slave-priority
屬性進(jìn)行修改,默認(rèn)是100,該值越低,優(yōu)先級(jí)越高,如果設(shè)成0,那么永遠(yuǎn)不會(huì)被當(dāng)選master。可以通過(guò)INFO
命令查看slave的優(yōu)先級(jí)。
如果有多個(gè)slave優(yōu)先級(jí)相同,那么選出復(fù)制偏移量最大的slave;如果多個(gè)slave復(fù)制偏移量相同,那么選出運(yùn)行ID最小的slave。
Slave被選中后,Leader會(huì)向它發(fā)送SLAVEOF no one
命令,同時(shí)以1秒1次的頻率發(fā)送INFO
命令,觀察slave的角色從slave
變成master
。此時(shí)leader就知道該slave已經(jīng)提升為master。如果這一步超時(shí)了,就終止此次故障轉(zhuǎn)移。
下一步,Leader向已下線master的其它slave發(fā)送SLAVEOF
命令,讓它們復(fù)制新的master。
最后一步,當(dāng)已下線的master重新上線后,Leader會(huì)向它發(fā)送SLAVEOF
命令讓它成為新master的從服務(wù)器。
TILT模式
由于sentinel系統(tǒng)依賴機(jī)器時(shí)間,比如需要知道多長(zhǎng)時(shí)間沒(méi)有跟某個(gè)實(shí)例進(jìn)行過(guò)通訊,因此一旦機(jī)器的時(shí)間功能發(fā)生錯(cuò)誤,Redis就會(huì)進(jìn)入TILT模式,直到正常運(yùn)行超過(guò)30秒。在該模式下它不會(huì)執(zhí)行任何操作,比如故障轉(zhuǎn)移,當(dāng)其它sentinel發(fā)來(lái)SENTINEL is-master-down-by-addr
命令詢問(wèn)實(shí)例的在線狀態(tài)時(shí)它會(huì)返回負(fù)值,告訴對(duì)方它的下線判斷不再準(zhǔn)確。
判定時(shí)間功能發(fā)生錯(cuò)誤的依據(jù)是:sentinel定時(shí)器每100毫秒執(zhí)行一次,如果兩次時(shí)間差值是負(fù)值(時(shí)間出現(xiàn)了倒退)或者過(guò)大(超過(guò)了2秒),Redis就會(huì)進(jìn)入TILT模式。
客戶端處理流程
官方推薦的客戶端處理流程是:
- 當(dāng)客戶端嘗試連接到一個(gè)sentinel系統(tǒng)時(shí),依次嘗試連接sentinel實(shí)例,并發(fā)送
SENTINEL get-master-addr-by-name master-name
命令獲得master信息,如果連接sentinel失敗或sentinel返回的master信息為null,那么繼續(xù)連接下一個(gè)sentinel,直到成功獲取master信息。 - 向master發(fā)送
ROLE
命令確認(rèn)該實(shí)例是master,否則重復(fù)步驟1。 - 客戶端向sentinel訂閱頻道,當(dāng)master被切換后可以收到新的master信息。
- 如果有讀寫(xiě)分離的需求,那么可以通過(guò)
SENTINEL slaves master-name
命令獲取slave列表。
集群
相比上文提到的哨兵模式,集群模式主要提供了數(shù)據(jù)分片的功能,因?yàn)橐慌_(tái)服務(wù)器總有物理容量的限制。分片功能支持把數(shù)據(jù)分散的存儲(chǔ)在多臺(tái)實(shí)例上,突破了單個(gè)節(jié)點(diǎn)的物理限制。
* 快速搭建集群
Linux上可以使用docker快速搭建一個(gè)有三個(gè)master節(jié)點(diǎn)的集群:
# 以下腳本須在linux中執(zhí)行
# 因?yàn)閐ocker的host模式問(wèn)題,mac下需要安裝linux虛擬機(jī)下面的腳本才能正常執(zhí)行
# 創(chuàng)建3個(gè)redis-server
docker run --name redis1 --net=host -itd redis redis-server --port 6379 --cluster-enabled yes --cluster-node-timeout 60000
docker run --name redis2 --net=host -itd redis redis-server --port 6380 --cluster-enabled yes --cluster-node-timeout 60000
docker run --name redis3 --net=host -itd redis redis-server --port 6381 --cluster-enabled yes --cluster-node-timeout 60000
# 使用redis-trib工具啟動(dòng)集群
docker run --rm --net=host -it zvelo/redis-trib create 127.0.0.1:6379 127.0.0.1:6380 127.0.0.1:6381
啟動(dòng)
Redis服務(wù)器在啟動(dòng)時(shí)會(huì)通過(guò)cluster-enabled
參數(shù)決定是否開(kāi)啟集群模式。集群模式跟普通單機(jī)模式用到的模塊大部分都一樣(如RDB模塊),除此之外用到了一些集群專有的功能,比如serverCron
方法會(huì)調(diào)用clusterCron
發(fā)送Gossip消息、檢查節(jié)點(diǎn)狀態(tài)。
除了單機(jī)模式用到的redisServer
結(jié)構(gòu)體,集群模式下還會(huì)用到clusterNode
、clusterLink
、clusterState
來(lái)存儲(chǔ)集群的一些狀態(tài)。
每個(gè)實(shí)例都有一個(gè)clusterState
類型的對(duì)象來(lái)記錄當(dāng)前集群的狀態(tài):
typedef struct clusterState{
// 指向當(dāng)前節(jié)點(diǎn)的指針
clusterNode *myself;
// 配置紀(jì)元
uint64_t currentEpoch;
// 集群的狀態(tài):上線還是下線
int state;
// 集群中至少處理一個(gè)槽的節(jié)點(diǎn)的數(shù)量
int size;
// 集群所有的節(jié)點(diǎn)(包含自己),字典的鍵是節(jié)點(diǎn)名字,值是節(jié)點(diǎn)對(duì)應(yīng)的clusterNode對(duì)象
dict *nodes;
// 槽
// clusterNode *slots[16384];
} clusterState;
每個(gè)clusterState
都存儲(chǔ)了集群內(nèi)所有節(jié)點(diǎn)的信息,如IP、角色等。下面是一個(gè)更直觀的結(jié)構(gòu)圖:
握手
建立集群的第一步是握手。通過(guò)以下命令讓遠(yuǎn)程實(shí)例加入當(dāng)前實(shí)例所在的集群:
CLUSTER MEET <ip> <port>
# 當(dāng)前實(shí)例 127.0.0.1 6379
# CLUSTER MEET 127.0.0.1 6380
# CLUSTER MEET 127.0.0.1 6381
# 以上三個(gè)節(jié)點(diǎn)形成集群
當(dāng)實(shí)例A向?qū)嵗鼴發(fā)送MEET
消息并握手成功后,A會(huì)把B的信息以Gossip協(xié)議傳播給集群中的其它節(jié)點(diǎn),最終所有節(jié)點(diǎn)都會(huì)知道B的存在。
槽
集群模式下,整個(gè)數(shù)據(jù)庫(kù)被劃分為16384個(gè)槽,每個(gè)鍵占用其中的一個(gè)槽,每個(gè)節(jié)點(diǎn)可以處理0個(gè)或多個(gè)槽。只有當(dāng)所有的槽都有節(jié)點(diǎn)在處理時(shí),集群才處于上線狀態(tài)。因此即使用CLUSTER MEET
命令建立了集群,集群仍然處于下線狀態(tài)。
使用以下命令分配槽:
CLUSTER ADDSLOTS <slot>
# 分配0-5槽
# CLUSTER ADDSLOTS 0 1 2 3 4 5
以上命令可以配合shell腳本分配全部的16384個(gè)槽。推薦使用redis-trib工具自動(dòng)建立集群并分配槽。
clusterNode
結(jié)構(gòu)里使用一個(gè)unsigned char slots[2048]
屬性記錄節(jié)點(diǎn)處理的槽。
2048 = 16384 / 8 , C語(yǔ)言中的
char
占1個(gè)字節(jié)。
一個(gè)char
類型變量占1個(gè)字節(jié)(8位),位如果是1則表示節(jié)點(diǎn)處理該槽,0表示不處理。其結(jié)構(gòu)如下:
每個(gè)Redis節(jié)點(diǎn)除了記錄自己處理的槽外也會(huì)記錄其它節(jié)點(diǎn)處理的槽,這些狀態(tài)以slots
數(shù)組存儲(chǔ)在clusterState
結(jié)構(gòu)體中,每個(gè)元素指向一個(gè)clusterNode
結(jié)構(gòu)體。在CLUSTER ADDSLOTS
命令執(zhí)行完畢后,節(jié)點(diǎn)會(huì)把自己的slots
數(shù)組發(fā)送給其它的節(jié)點(diǎn)告訴他們處理槽的狀態(tài)。
鍵
集群模式下,每個(gè)鍵都?xì)w屬于一個(gè)槽,一個(gè)槽可以對(duì)應(yīng)多個(gè)鍵。Redis使用CRC16算法將鍵映射到一個(gè)槽,算法如下:
CRC16(key) & 16383
可以通過(guò)以下命令查看鍵所屬的槽:
CLUSTER KEYSLOT <key>
當(dāng)節(jié)點(diǎn)計(jì)算出鍵屬于哪個(gè)槽后,它會(huì)檢查所屬槽是不是自己負(fù)責(zé)處理,如果是,那么就執(zhí)行客戶端發(fā)來(lái)的命令;否則節(jié)點(diǎn)會(huì)查找處理該槽的節(jié)點(diǎn)并向客戶端返回MOVED
錯(cuò)誤指引它轉(zhuǎn)向正確的節(jié)點(diǎn)。
MOVED
錯(cuò)誤的格式為:
MOVED <slot> <ip>:<port>
客戶端收到MOVED
錯(cuò)誤后會(huì)根據(jù)ip和端口信息轉(zhuǎn)向目標(biāo)節(jié)點(diǎn)并重新發(fā)送命令。
客戶端也區(qū)分集群模式和單機(jī)模式。單機(jī)模式會(huì)直接打印出
MOVED
錯(cuò)誤而不會(huì)重定向。集群模式需要加上-c
參數(shù)。
數(shù)據(jù)庫(kù)
集群模式和單機(jī)模式下的數(shù)據(jù)庫(kù)一個(gè)重要的區(qū)別是:集群模式只能使用0號(hào)數(shù)據(jù)庫(kù)。
集群模式下的鍵值對(duì)以及過(guò)期時(shí)間的存儲(chǔ)和普通模式下的一樣。除此之外集群模式下的節(jié)點(diǎn)會(huì)用一個(gè)跳躍表存儲(chǔ)槽和鍵的關(guān)系,用于對(duì)某些槽的鍵進(jìn)行批量操作。跳躍表里的分值就是槽號(hào),值就是鍵。下面是一個(gè)例子:
重新分片
當(dāng)新增或移除節(jié)點(diǎn)時(shí),需要對(duì)集群進(jìn)行重新分片。我們使用redis-trib工具對(duì)集群進(jìn)行在線重新分片操作,其主要步驟是:
- 向目標(biāo)節(jié)點(diǎn)發(fā)送
CLUSTER SETSLOT <slot> IMPORTING <source_id>
命令,讓其做好準(zhǔn)備。 - 向源節(jié)點(diǎn)發(fā)送
CLUSTER SETSLOT <slot> MIGRATING <target_id>
命令,讓其做好遷移的準(zhǔn)備。 - 向源節(jié)點(diǎn)發(fā)送
CLUSTER GETKEYSINSLOT <slot> <count>
命令,獲得最多count個(gè)位于slot槽的鍵。 - 對(duì)于步驟3中返回的每一個(gè)鍵,向源節(jié)點(diǎn)發(fā)送一個(gè)
MIGRATE <target_ip> <target_port> <key_name> 0 <timeout>
命令將選中的鍵原子地遷移到目標(biāo)節(jié)點(diǎn)。 - 重復(fù)步驟3和4直到所有的鍵都已被遷移。
- 向集群中任意一個(gè)節(jié)點(diǎn)發(fā)送
CLUSTER SETSLOT <slot> NODE <target_id>
命令,將slot指派給目標(biāo)節(jié)點(diǎn),這一消息隨后會(huì)發(fā)送到所有的節(jié)點(diǎn)。 - 如果有多個(gè)slot需要遷移,那么繼續(xù)對(duì)剩下的slot進(jìn)行上面的操作。
ASK
在遷移過(guò)程中,當(dāng)客戶端訪問(wèn)源節(jié)點(diǎn)數(shù)據(jù)庫(kù)的某個(gè)鍵時(shí),源節(jié)點(diǎn)會(huì)先在自己的數(shù)據(jù)庫(kù)中查找,如果沒(méi)有找到則檢查該鍵所屬槽的遷移狀態(tài)。slot的遷移狀態(tài)在源節(jié)點(diǎn)和目標(biāo)節(jié)點(diǎn)上各有一個(gè)clusterNode
指針數(shù)組存儲(chǔ)(importing_slots_from
和migrating_slots_to
),其結(jié)構(gòu)如下:
如果發(fā)現(xiàn)該鍵所屬的槽正在遷移,那么它會(huì)向客戶端返回ASK <target_ip>:<target:port>
指引向目標(biāo)節(jié)點(diǎn)。客戶端收到ASK
命令后會(huì)向目標(biāo)節(jié)點(diǎn)發(fā)送ASKING
消息然后再重新執(zhí)行之前發(fā)給源節(jié)點(diǎn)的命令。
ASKING
命令的用處是打開(kāi)客戶端的REDIS_ADKING
標(biāo)志以讓目標(biāo)節(jié)點(diǎn)強(qiáng)制執(zhí)行命令。如果按正常流程,此時(shí)槽還沒(méi)有完成遷移,仍舊屬于源節(jié)點(diǎn),所以如果不打開(kāi)特殊標(biāo)志,目標(biāo)節(jié)點(diǎn)會(huì)返回MOVED
錯(cuò)誤。
當(dāng)命令執(zhí)行完畢后,
REDIS_ASKING
標(biāo)志會(huì)被移除。這是一個(gè)一次性的標(biāo)志,如果下一次客戶端訪問(wèn)時(shí)遷移仍未完成,那么仍舊會(huì)出現(xiàn)上面ASK
的流程。
* 例子
# 新增1個(gè)redis節(jié)點(diǎn)
docker run --name redis4 --net=host -itd redis redis-server --port 6382 --cluster-enabled yes --cluster-node-timeout 60000
# 使用redis-trib把新的節(jié)點(diǎn)加入到集群中
docker run --rm --net=host -it zvelo/redis-trib add-node 127.0.0.1:6382 127.0.0.1:6369
# 重新分片,分配1000個(gè)slot到新的節(jié)點(diǎn)上
# --from可以指定節(jié)點(diǎn),為了均勻分配,可以使用all,從所有舊的節(jié)點(diǎn)上平均的取出一部分slot遷移到新的節(jié)點(diǎn)上
# --slots指定本次遷到新節(jié)點(diǎn)的slot數(shù)量
docker run --rm --net=host -it zvelo/redis-trib reshard --from all --to <new_node_id> --slots 1000 --yes 127.0.0.1:6379
復(fù)制
集群模式下節(jié)點(diǎn)也分為主節(jié)點(diǎn)和從節(jié)點(diǎn),從節(jié)點(diǎn)復(fù)制主節(jié)點(diǎn)并在主節(jié)點(diǎn)下線時(shí)接替它繼續(xù)處理請(qǐng)求。
通過(guò)向節(jié)點(diǎn)發(fā)送CLUSTER REPLICATE <master_id>
讓接收命令的節(jié)點(diǎn)成為主節(jié)點(diǎn)的從節(jié)點(diǎn)。從節(jié)點(diǎn)會(huì)把clusterState.myself.slaveof
指向主節(jié)點(diǎn)對(duì)應(yīng)的clusterNode
結(jié)構(gòu)體,關(guān)閉REDIS_NODE_MASTER
標(biāo)識(shí)并打開(kāi)REDIS_NODE_SLAVE
標(biāo)識(shí),表示節(jié)點(diǎn)已從主節(jié)點(diǎn)變成從節(jié)點(diǎn)。最后調(diào)用復(fù)制代碼開(kāi)始復(fù)制,這部分代碼就是單機(jī)模式下的復(fù)制代碼。
節(jié)點(diǎn)間的復(fù)制關(guān)系會(huì)通過(guò)消息發(fā)送給集群中的其它節(jié)點(diǎn),最后所有節(jié)點(diǎn)都會(huì)了解到其它節(jié)點(diǎn)間的復(fù)制關(guān)系,并保存在對(duì)應(yīng)的clusterNode
結(jié)構(gòu)體中。
故障轉(zhuǎn)移
節(jié)點(diǎn)之間會(huì)定期向集群中其它節(jié)點(diǎn)發(fā)送PING
消息檢測(cè)在線狀態(tài),如果對(duì)方在一定時(shí)間內(nèi)沒(méi)有回復(fù)PONG
消息,那么節(jié)點(diǎn)就會(huì)把對(duì)方標(biāo)為疑似下線REDIS_NODE_PFAIL
狀態(tài)。各個(gè)節(jié)點(diǎn)之間會(huì)通過(guò)消息交換節(jié)點(diǎn)狀態(tài),當(dāng)某個(gè)主節(jié)點(diǎn)收到其它主節(jié)點(diǎn)發(fā)來(lái)的下線報(bào)告時(shí),會(huì)把該報(bào)告存在目標(biāo)下線節(jié)點(diǎn)對(duì)應(yīng)的clusterNode
結(jié)構(gòu)體的fail_reposts
鏈表中。
如果在下線報(bào)告鏈表中有半數(shù)以上的主節(jié)點(diǎn)都認(rèn)為某個(gè)主節(jié)點(diǎn)疑似下線,那么就把它標(biāo)記為已下線,并向集群廣播一條FAIL
的消息,收到該消息的節(jié)點(diǎn)立刻也把該節(jié)點(diǎn)標(biāo)記為已下線。
當(dāng)下線主節(jié)點(diǎn)的一個(gè)從節(jié)點(diǎn)收到FAIL
消息后就開(kāi)始對(duì)下線主節(jié)點(diǎn)進(jìn)行故障轉(zhuǎn)移,步驟如下:
- 從節(jié)點(diǎn)會(huì)向集群廣播一條
CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST
的消息,要求所有收到消息并有投票權(quán)(集群中負(fù)責(zé)處理slots的主節(jié)點(diǎn)才有投票權(quán))的主節(jié)點(diǎn)向該從節(jié)點(diǎn)投票。 - 主節(jié)點(diǎn)收到投票請(qǐng)求后,如果在當(dāng)前配置紀(jì)元尚未投票給其它節(jié)點(diǎn),那么返回一條
CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK
消息投票支持。 - 如果獲得半數(shù)以上主節(jié)點(diǎn)的支持票,那么從節(jié)點(diǎn)就當(dāng)選為leader。
- Leader節(jié)點(diǎn)執(zhí)行
SLAVEOF no one
命令成為新的主節(jié)點(diǎn),然后撤銷所有對(duì)已下線主節(jié)點(diǎn)的slot,并把這些slot指派給自己。 - 新的主節(jié)點(diǎn)向集群廣播一條
PONG
消息,讓其它節(jié)點(diǎn)意識(shí)它已經(jīng)成為了主節(jié)點(diǎn),并接管了下線節(jié)點(diǎn)處理的slot。下線主節(jié)點(diǎn)的其它從節(jié)點(diǎn)會(huì)調(diào)整為復(fù)制新的主節(jié)點(diǎn)。 - 下線的主節(jié)點(diǎn)重新上線后會(huì)成為新主節(jié)點(diǎn)的從節(jié)點(diǎn)。
參考/圖片出處:
1. 機(jī)械工業(yè)出版社 -《Redis設(shè)計(jì)與實(shí)現(xiàn)》