Redis 緩存作為使用最多的緩存工具被各大廠商爭(zhēng)相使用。通常我們會(huì)使用單體的 Redis 應(yīng)用作為緩存服務(wù),為了保證其高可用還會(huì)使用主從模式(Master-Slave),又或者是讀寫分離的設(shè)計(jì)。
圖片來自 Pexels
但是當(dāng)緩存數(shù)據(jù)量增加以后,無(wú)法用單體服務(wù)器承載緩存服務(wù)時(shí),就需要對(duì)緩存服務(wù)進(jìn)行擴(kuò)展。
將需要緩存的數(shù)據(jù)切分成不同的分區(qū),將數(shù)據(jù)分區(qū)放到不同的服務(wù)器中,用分布式的緩存來承載高并發(fā)的緩存訪問。恰好 Redis Cluster 方案剛好支持這部分功能。
今天就來一起看看 Redis Cluster 的核心原理和實(shí)踐:
Redis Cluster 實(shí)現(xiàn)數(shù)據(jù)分區(qū) 分布式緩存節(jié)點(diǎn)之間的通訊 請(qǐng)求分布式緩存的路由 緩存節(jié)點(diǎn)的擴(kuò)展和收縮 故障發(fā)現(xiàn)和恢復(fù)Redis Cluster 實(shí)現(xiàn)數(shù)據(jù)分區(qū)
正如開篇中提到的,分布式數(shù)據(jù)庫(kù)要解決的就是將整塊數(shù)據(jù),按照規(guī)則分配到多個(gè)緩存節(jié)點(diǎn),解決的是單個(gè)緩存節(jié)點(diǎn)處理數(shù)量大的問題。
如果要將這些數(shù)據(jù)進(jìn)行拆分,并且存放必須有一個(gè)算法。例如:哈希算法和哈希一致性算法,這些比較經(jīng)典的算法。
Redis Cluster 則采用的是虛擬槽分區(qū)算法。其中提到了槽(Slot)的概念。這個(gè)槽是用來存放緩存信息的單位,在 Redis 中將存儲(chǔ)空間分成了 16384 個(gè)槽,也就是說 Redis Cluster 槽的范圍是 0 -16383(2^4 * 2^10)。
緩存信息通常是用 Key-Value 的方式來存放的,在存儲(chǔ)信息的時(shí)候,集群會(huì)對(duì) Key 進(jìn)行 CRC16 校驗(yàn)并對(duì) 16384 取模(slot = CRC16(key)%16383)。
得到的結(jié)果就是 Key-Value 所放入的槽,從而實(shí)現(xiàn)自動(dòng)分割數(shù)據(jù)到不同的節(jié)點(diǎn)上。然后再將這些槽分配到不同的緩存節(jié)點(diǎn)中保存。
圖1:Redis 集群中的數(shù)據(jù)分片
如圖 1 所示,假設(shè)有三個(gè)緩存節(jié)點(diǎn)分別是 1、2、3。Redis Cluster 將存放緩存數(shù)據(jù)的槽(Slot)分別放入這三個(gè)節(jié)點(diǎn)中:
緩存節(jié)點(diǎn) 1 存放的是(0-5000)Slot 的數(shù)據(jù)。 緩存節(jié)點(diǎn) 2 存放的是(5001-10000)Slot 的數(shù)據(jù)。 緩存節(jié)點(diǎn) 3 存放的是(10000-16383)Slot 的數(shù)據(jù)。此時(shí) Redis Client 需要根據(jù)一個(gè) Key 獲取對(duì)應(yīng)的 Value 的數(shù)據(jù),首先通過 CRC16(key)%16383 計(jì)算出 Slot 的值,假設(shè)計(jì)算的結(jié)果是 5002。
將這個(gè)數(shù)據(jù)傳送給 Redis Cluster,集群接受到以后會(huì)到一個(gè)對(duì)照表中查找這個(gè) Slot=5002 屬于那個(gè)緩存節(jié)點(diǎn)。
發(fā)現(xiàn)屬于“緩存節(jié)點(diǎn) 2”,于是順著紅線的方向調(diào)用緩存節(jié)點(diǎn) 2 中存放的 Key-Value 的內(nèi)容并且返回給 Redis Client。
分布式緩存節(jié)點(diǎn)之間的通訊
如果說 Redis Cluster 的虛擬槽算法解決的是數(shù)據(jù)拆分和存放的問題,那么存放緩存數(shù)據(jù)的節(jié)點(diǎn)之間是如何通訊的,就是接下來我們要討論的。
緩存節(jié)點(diǎn)中存放著緩存的數(shù)據(jù),在 Redis Cluster 的分布式部署下,緩存節(jié)點(diǎn)會(huì)被分配到一臺(tái)或者多臺(tái)服務(wù)器上。
圖 2:新上線的緩存節(jié)點(diǎn) 2 和緩存節(jié)點(diǎn) 1 進(jìn)行通訊
緩存節(jié)點(diǎn)的數(shù)目也有可能根據(jù)緩存數(shù)據(jù)量和支持的并發(fā)進(jìn)行擴(kuò)展。如圖 2 所示,假設(shè) Redis Cluster 中存在“緩存節(jié)點(diǎn) 1”,此時(shí)由于業(yè)務(wù)擴(kuò)展新增了“緩存節(jié)點(diǎn) 2”。
新加入的節(jié)點(diǎn)會(huì)通過 Gossip 協(xié)議向老節(jié)點(diǎn),發(fā)出一個(gè)“Meet 消息”。收到消息以后“緩存節(jié)點(diǎn) 1”,會(huì)禮貌地回復(fù)一個(gè)“Pong 消息”。
此后“緩存節(jié)點(diǎn) 2”會(huì)定期發(fā)送給“緩存節(jié)點(diǎn) 1” 一個(gè)“Ping 消息”,同樣的“緩存節(jié)點(diǎn) 1”每次都會(huì)回復(fù)“Pong 消息”。
上面這個(gè)例子說明了,在 Redis Cluster 中緩存節(jié)點(diǎn)之間是通過 Gossip 協(xié)議進(jìn)行通訊的。
其實(shí)節(jié)點(diǎn)之間通訊的目的是為了維護(hù)節(jié)點(diǎn)之間的元數(shù)據(jù)信息。這個(gè)元數(shù)據(jù)就是每個(gè)節(jié)點(diǎn)包含哪些數(shù)據(jù),是否出現(xiàn)故障。
節(jié)點(diǎn)之間通過 Gossip 協(xié)議不斷相互交互這些信息,就好像一群人在一起八卦一樣,沒有多久每個(gè)節(jié)點(diǎn)就知道其他所有節(jié)點(diǎn)的情況了,這個(gè)情況就是節(jié)點(diǎn)的元數(shù)據(jù)。
整個(gè)傳輸過程大致分為以下幾點(diǎn):
Redis Cluster 的每個(gè)緩存節(jié)點(diǎn)都會(huì)開通一個(gè)獨(dú)立的 TCP 通道,用于和其他節(jié)點(diǎn)通訊。 有一個(gè)節(jié)點(diǎn)定時(shí)任務(wù),每隔一段時(shí)間會(huì)從系統(tǒng)中選出“發(fā)送節(jié)點(diǎn)”。這個(gè)“發(fā)送節(jié)點(diǎn)”按照一定頻率,例如:每秒 5 次,隨機(jī)向最久沒有通訊的節(jié)點(diǎn)發(fā)起 Ping 消息。 接受到 Ping 消息的節(jié)點(diǎn)會(huì)使用 Pong 消息向“發(fā)送節(jié)點(diǎn)”進(jìn)行回復(fù)。不斷重復(fù)上面行為,讓所有節(jié)點(diǎn)保持通訊。他們之間通訊是通過 Gossip 協(xié)議實(shí)現(xiàn)的。
從類型上來說其分為了四種,分別是:
Meet 消息,用于通知新節(jié)點(diǎn)加入。就好像上面例子中提到的新節(jié)點(diǎn)上線會(huì)給老節(jié)點(diǎn)發(fā)送 Meet 消息,表示有“新成員”加入。 Ping 消息,這個(gè)消息使用得最為頻繁,該消息中封裝了自身節(jié)點(diǎn)和其他節(jié)點(diǎn)的狀態(tài)數(shù)據(jù),有規(guī)律地發(fā)給其他節(jié)點(diǎn)。 Pong 消息,在接受到 Meet 和 Ping 消息以后,也將自己的數(shù)據(jù)狀態(tài)發(fā)給對(duì)方。同時(shí)也可以對(duì)集群中所有的節(jié)點(diǎn)發(fā)起廣播,告知大家的自身狀態(tài)。 Fail 消息,如果一個(gè)節(jié)點(diǎn)下線或者掛掉了,會(huì)向集群中廣播這個(gè)消息。圖 3:Gossip 協(xié)議結(jié)構(gòu)
Gossip 協(xié)議的結(jié)構(gòu)如圖 3 所示,有其中 type 定義了消息的類型,例如:Meet、Ping、Pong、Fail 等消息。
另外有一個(gè) myslots 的數(shù)組定義了節(jié)點(diǎn)負(fù)責(zé)的槽信息。每個(gè)節(jié)點(diǎn)發(fā)送 Gossip 協(xié)議給其他節(jié)點(diǎn)最重要的就是將該信息告訴其他節(jié)點(diǎn)。另外,消息體通過 clusterMsgData 對(duì)象傳遞消息征文。
請(qǐng)求分布式緩存的路由
對(duì)內(nèi),分布式緩存的節(jié)點(diǎn)通過 Gossip 協(xié)議互相發(fā)送消息,為了保證節(jié)點(diǎn)之間了解對(duì)方的情況。
那么對(duì)外來說,一個(gè) Redis 客戶端如何通過分布式節(jié)點(diǎn)獲取緩存數(shù)據(jù),就是分布式緩存路由要解決的問題了。
上文提到了 Gossip 協(xié)議會(huì)將每個(gè)節(jié)點(diǎn)管理的槽信息發(fā)送給其他節(jié)點(diǎn),其中用到了 unsigned char myslots[CLUSTER_SLOTS/8] 這樣一個(gè)數(shù)組存放每個(gè)節(jié)點(diǎn)的槽信息。
myslots 屬性是一個(gè)二進(jìn)制位數(shù)組(bit array),其中 CLUSTER_SLOTS 為 16384。
這個(gè)數(shù)組的長(zhǎng)度為 16384/8=2048 個(gè)字節(jié),由于每個(gè)字節(jié)包含 8 個(gè) bit 位(二進(jìn)制位),所以共包含 16384 個(gè) bit,也就是 16384 個(gè)二進(jìn)制位。
每個(gè)節(jié)點(diǎn)用 bit 來標(biāo)識(shí)自己是否擁有某個(gè)槽的數(shù)據(jù)。如圖 4 所示,假設(shè)這個(gè)圖表示節(jié)點(diǎn) A 所管理槽的情況。
圖 4:通過二進(jìn)制數(shù)組存放槽信息
0、1、2 三個(gè)數(shù)組下標(biāo)就表示 0、1、2 三個(gè)槽,如果對(duì)應(yīng)的二進(jìn)制值是 1,表示該節(jié)點(diǎn)負(fù)責(zé)存放 0、1、2 三個(gè)槽的數(shù)據(jù)。同理,后面的數(shù)組下標(biāo)位 0 就表示該節(jié)點(diǎn)不負(fù)責(zé)存放對(duì)應(yīng)槽的數(shù)據(jù)。
用二進(jìn)制存放的優(yōu)點(diǎn)是,判斷的效率高,例如對(duì)于編號(hào)為 1 的槽,節(jié)點(diǎn)只要判斷序列的第二位,時(shí)間復(fù)雜度為 O(1)。
圖 5:接受節(jié)點(diǎn)把節(jié)點(diǎn)槽的對(duì)應(yīng)信息保存在本地
如圖 5 所示,當(dāng)收到發(fā)送節(jié)點(diǎn)的節(jié)點(diǎn)槽信息以后,接受節(jié)點(diǎn)會(huì)將這些信息保存到本地的 clusterState 的結(jié)構(gòu)中,其中 Slots 的數(shù)組就是存放每個(gè)槽對(duì)應(yīng)哪些節(jié)點(diǎn)信息。
圖 6:ClusterStatus 結(jié)構(gòu)以及槽與節(jié)點(diǎn)的對(duì)應(yīng)
如圖 6 所示,ClusterState 中保存的 Slots 數(shù)組中每個(gè)下標(biāo)對(duì)應(yīng)一個(gè)槽,每個(gè)槽信息中對(duì)應(yīng)一個(gè) clusterNode 也就是緩存的節(jié)點(diǎn)。
這些節(jié)點(diǎn)會(huì)對(duì)應(yīng)一個(gè)實(shí)際存在的 Redis 緩存服務(wù),包括 IP 和 Port 的信息。
Redis Cluster 的通訊機(jī)制實(shí)際上保證了每個(gè)節(jié)點(diǎn)都有其他節(jié)點(diǎn)和槽數(shù)據(jù)的對(duì)應(yīng)關(guān)系。
Redis 的客戶端無(wú)論訪問集群中的哪個(gè)節(jié)點(diǎn)都可以路由到對(duì)應(yīng)的節(jié)點(diǎn)上,因?yàn)槊總€(gè)節(jié)點(diǎn)都有一份 ClusterState,它記錄了所有槽和節(jié)點(diǎn)的對(duì)應(yīng)關(guān)系。
下面來看看 Redis 客戶端是如何通過路由來調(diào)用緩存節(jié)點(diǎn)的:
圖 7:MOVED 重定向請(qǐng)求
如圖 7 所示,Redis 客戶端通過 CRC16(key)%16383 計(jì)算出 Slot 的值,發(fā)現(xiàn)需要找“緩存節(jié)點(diǎn) 1”讀/寫數(shù)據(jù),但是由于緩存數(shù)據(jù)遷移或者其他原因?qū)е逻@個(gè)對(duì)應(yīng)的 Slot 的數(shù)據(jù)被遷移到了“緩存節(jié)點(diǎn) 2”上面。
那么這個(gè)時(shí)候 Redis 客戶端就無(wú)法從“緩存節(jié)點(diǎn) 1”中獲取數(shù)據(jù)了。
但是由于“緩存節(jié)點(diǎn) 1”中保存了所有集群中緩存節(jié)點(diǎn)的信息,因此它知道這個(gè) Slot 的數(shù)據(jù)在“緩存節(jié)點(diǎn) 2”中保存,因此向 Redis 客戶端發(fā)送了一個(gè) MOVED 的重定向請(qǐng)求。
這個(gè)請(qǐng)求告訴其應(yīng)該訪問的“緩存節(jié)點(diǎn) 2”的地址。Redis 客戶端拿到這個(gè)地址,繼續(xù)訪問“緩存節(jié)點(diǎn) 2”并且拿到數(shù)據(jù)。
上面的例子說明了,數(shù)據(jù) Slot 從“緩存節(jié)點(diǎn) 1”已經(jīng)遷移到“緩存節(jié)點(diǎn) 2”了,那么客戶端可以直接找“緩存節(jié)點(diǎn) 2”要數(shù)據(jù)。
那么如果兩個(gè)緩存節(jié)點(diǎn)正在做節(jié)點(diǎn)的數(shù)據(jù)遷移,此時(shí)客戶端請(qǐng)求會(huì)如何處理呢?
圖 8:ASK 重定向請(qǐng)求
如圖 8 所示,Redis 客戶端向“緩存節(jié)點(diǎn) 1”發(fā)出請(qǐng)求,此時(shí)“緩存節(jié)點(diǎn) 1”正向“緩存節(jié)點(diǎn) 2”遷移數(shù)據(jù),如果沒有命中對(duì)應(yīng)的 Slot,它會(huì)返回客戶端一個(gè) ASK 重定向請(qǐng)求并且告訴“緩存節(jié)點(diǎn) 2”的地址。
客戶端向“緩存節(jié)點(diǎn) 2”發(fā)送 Asking 命令,詢問需要的數(shù)據(jù)是否在“緩存節(jié)點(diǎn) 2”上,“緩存節(jié)點(diǎn) 2”接到消息以后返回?cái)?shù)據(jù)是否存在的結(jié)果。
緩存節(jié)點(diǎn)的擴(kuò)展和收縮
作為分布式部署的緩存節(jié)點(diǎn)總會(huì)遇到緩存擴(kuò)容和緩存故障的問題。這就會(huì)導(dǎo)致緩存節(jié)點(diǎn)的上線和下線的問題。
由于每個(gè)節(jié)點(diǎn)中保存著槽數(shù)據(jù),因此當(dāng)緩存節(jié)點(diǎn)數(shù)出現(xiàn)變動(dòng)時(shí),這些槽數(shù)據(jù)會(huì)根據(jù)對(duì)應(yīng)的虛擬槽算法被遷移到其他的緩存節(jié)點(diǎn)上。
圖 9:分布式緩存擴(kuò)容
如圖 9 所示,集群中本來存在“緩存節(jié)點(diǎn) 1”和“緩存節(jié)點(diǎn) 2”,此時(shí)“緩存節(jié)點(diǎn) 3”上線了并且加入到集群中。
此時(shí)根據(jù)虛擬槽的算法,“緩存節(jié)點(diǎn) 1”和“緩存節(jié)點(diǎn) 2”中對(duì)應(yīng)槽的數(shù)據(jù)會(huì)應(yīng)該新節(jié)點(diǎn)的加入被遷移到“緩存節(jié)點(diǎn) 3”上面。
針對(duì)節(jié)點(diǎn)擴(kuò)容,新建立的節(jié)點(diǎn)需要運(yùn)行在集群模式下,因此新建節(jié)點(diǎn)的配置最好與集群內(nèi)其他節(jié)點(diǎn)配置保持一致。
新節(jié)點(diǎn)加入到集群的時(shí)候,作為孤兒節(jié)點(diǎn)是沒有和其他節(jié)點(diǎn)進(jìn)行通訊的。因此,其會(huì)采用 cluster meet 命令加入到集群中。
在集群中任意節(jié)點(diǎn)執(zhí)行 cluster meet 命令讓新節(jié)點(diǎn)加入進(jìn)來。假設(shè)新節(jié)點(diǎn)是 192.168.1.1 5002,老節(jié)點(diǎn)是 192.168.1.1 5003,那么運(yùn)行以下命令將新節(jié)點(diǎn)加入到集群中。
192.168.1.1 5003> cluster meet 192.168.1.1 5002這個(gè)是由老節(jié)點(diǎn)發(fā)起的,有點(diǎn)老成員歡迎新成員加入的意思。新節(jié)點(diǎn)剛剛建立沒有建立槽對(duì)應(yīng)的數(shù)據(jù),也就是說沒有緩存任何數(shù)據(jù)。
如果這個(gè)節(jié)點(diǎn)是主節(jié)點(diǎn),需要對(duì)其進(jìn)行槽數(shù)據(jù)的擴(kuò)容;如果這個(gè)節(jié)點(diǎn)是從節(jié)點(diǎn),就需要同步主節(jié)點(diǎn)上的數(shù)據(jù)??傊褪且綌?shù)據(jù)。
圖 10:節(jié)點(diǎn)遷移槽數(shù)據(jù)的過程
如圖 10 所示,由客戶端發(fā)起節(jié)點(diǎn)之間的槽數(shù)據(jù)遷移,數(shù)據(jù)從源節(jié)點(diǎn)往目標(biāo)節(jié)點(diǎn)遷移:
客戶端對(duì)目標(biāo)節(jié)點(diǎn)發(fā)起準(zhǔn)備導(dǎo)入槽數(shù)據(jù)的命令,讓目標(biāo)節(jié)點(diǎn)準(zhǔn)備好導(dǎo)入槽數(shù)據(jù)。這里使用 cluster setslot {slot} importing {sourceNodeId} 命令。 之后對(duì)源節(jié)點(diǎn)發(fā)起送命令,讓源節(jié)點(diǎn)準(zhǔn)備遷出對(duì)應(yīng)的槽數(shù)據(jù)。使用命令 cluster setslot {slot} importing {sourceNodeId}。 此時(shí)源節(jié)點(diǎn)準(zhǔn)備遷移數(shù)據(jù)了,在遷移之前把要遷移的數(shù)據(jù)獲取出來。通過命令 cluster getkeysinslot {slot} {count}。Count 表示遷移的 Slot 的個(gè)數(shù)。 然后在源節(jié)點(diǎn)上執(zhí)行,migrate {targetIP} {targetPort} “” 0 {timeout} keys{keys} 命令,把獲取的鍵通過流水線批量遷移到目標(biāo)節(jié)點(diǎn)。 重復(fù) 3 和 4 兩步不斷將數(shù)據(jù)遷移到目標(biāo)節(jié)點(diǎn)。目標(biāo)節(jié)點(diǎn)獲取遷移的數(shù)據(jù)。 完成數(shù)據(jù)遷移以后目標(biāo)節(jié)點(diǎn),通過 cluster setslot {slot} node {targetNodeId} 命令通知對(duì)應(yīng)的槽被分配到目標(biāo)節(jié)點(diǎn),并且廣播這個(gè)信息給全網(wǎng)的其他主節(jié)點(diǎn),更新自身的槽節(jié)點(diǎn)對(duì)應(yīng)表。既然有緩存服務(wù)器的上線操作,那么也有下線的操作。下線操作正好和上線操作相反,將要下線緩存節(jié)點(diǎn)的槽數(shù)據(jù)分配到其他的緩存主節(jié)點(diǎn)中。
遷移的過程也與上線操作類似,不同的是下線的時(shí)候需要通知全網(wǎng)的其他節(jié)點(diǎn)忘記自己,此時(shí)通過命令 cluster forget{downNodeId} 通知其他的節(jié)點(diǎn)。
當(dāng)節(jié)點(diǎn)收到 forget 命令以后會(huì)將這個(gè)下線節(jié)點(diǎn)放到僅用列表中,那么之后就不用再向這個(gè)節(jié)點(diǎn)發(fā)送 Gossip 的 Ping 消息了。
不過這個(gè)僅用表的超時(shí)時(shí)間是 60 秒,超過了這個(gè)時(shí)間,依舊還會(huì)對(duì)這個(gè)節(jié)點(diǎn)發(fā)起 Ping 消息。
不過可以使用 redis-trib.rb del-node{host:port} {donwNodeId} 命令幫助我們完成下線操作。
尤其是下線的節(jié)點(diǎn)是主節(jié)點(diǎn)的情況下,會(huì)安排對(duì)應(yīng)的從節(jié)點(diǎn)接替主節(jié)點(diǎn)的位置。
故障發(fā)現(xiàn)和恢復(fù)
前面在談到緩存節(jié)點(diǎn)擴(kuò)展和收縮是提到,緩存節(jié)點(diǎn)收縮時(shí)會(huì)有一個(gè)下線的動(dòng)作。
有些時(shí)候是為了節(jié)約資源,或者是計(jì)劃性的下線,但更多時(shí)候是節(jié)點(diǎn)出現(xiàn)了故障導(dǎo)致下線。
針對(duì)下線故障來說有兩種下線的確定方式:
主觀下線:當(dāng)節(jié)點(diǎn) 1 向節(jié)點(diǎn) 2 例行發(fā)送 Ping 消息的時(shí)候,如果節(jié)點(diǎn) 2 正常工作就會(huì)返回 Pong 消息,同時(shí)會(huì)記錄節(jié)點(diǎn) 1 的相關(guān)信息。
同時(shí)接受到 Pong 消息以后節(jié)點(diǎn) 1 也會(huì)更新最近一次與節(jié)點(diǎn) 2 通訊的時(shí)間。
如果此時(shí)兩個(gè)節(jié)點(diǎn)由于某種原因斷開連接,過一段時(shí)間以后節(jié)點(diǎn) 1 還會(huì)主動(dòng)連接節(jié)點(diǎn) 2,如果一直通訊失敗,節(jié)點(diǎn) 1 中就無(wú)法更新與節(jié)點(diǎn) 2 最后通訊時(shí)間了。
此時(shí)節(jié)點(diǎn) 1 的定時(shí)任務(wù)檢測(cè)到與節(jié)點(diǎn) 2 最好通訊的時(shí)間超過了 cluster-node-timeout 的時(shí)候,就會(huì)更新本地節(jié)點(diǎn)狀態(tài),把節(jié)點(diǎn) 2 更新為主觀下線。
這里的 cluster-node-timeout 是節(jié)點(diǎn)掛掉被發(fā)現(xiàn)的超時(shí)時(shí)間,如果超過這個(gè)時(shí)間還沒有獲得節(jié)點(diǎn)返回的 Pong 消息就認(rèn)為該節(jié)點(diǎn)掛掉了。
這里的主觀下線指的是,節(jié)點(diǎn) 1 主觀的認(rèn)為節(jié)點(diǎn) 2 沒有返回 Pong 消息,因此認(rèn)為節(jié)點(diǎn) 2 下線。
只是節(jié)點(diǎn) 1 的主觀認(rèn)為,有可能是節(jié)點(diǎn) 1 與節(jié)點(diǎn) 2 之間的網(wǎng)絡(luò)斷開了,但是其他的節(jié)點(diǎn)依舊可以和節(jié)點(diǎn) 2 進(jìn)行通訊,因此主觀下線并不能代表某個(gè)節(jié)點(diǎn)真的下線了。
客觀下線:由于 Redis Cluster 的節(jié)點(diǎn)不斷地與集群內(nèi)的節(jié)點(diǎn)進(jìn)行通訊,下線信息也會(huì)通過 Gossip 消息傳遍所有節(jié)點(diǎn)。
因此集群內(nèi)的節(jié)點(diǎn)會(huì)不斷收到下線報(bào)告,當(dāng)半數(shù)以上持有槽的主節(jié)點(diǎn)標(biāo)記了某個(gè)節(jié)點(diǎn)是主觀下線時(shí),便會(huì)觸發(fā)客觀下線的流程。
也就是說當(dāng)集群內(nèi)的半數(shù)以上的主節(jié)點(diǎn),認(rèn)為某個(gè)節(jié)點(diǎn)主觀下線了,才會(huì)啟動(dòng)這個(gè)流程。
這個(gè)流程有一個(gè)前提,就是直針對(duì)主節(jié)點(diǎn),如果是從節(jié)點(diǎn)就會(huì)忽略。也就是說集群中的節(jié)點(diǎn)每次接受到其他節(jié)點(diǎn)的主觀下線是都會(huì)做以下的事情。
將主觀下線的報(bào)告保存到本地的 ClusterNode 的結(jié)構(gòu)中,并且針對(duì)主觀下線報(bào)告的時(shí)效性進(jìn)行檢查,如果超過 cluster-node-timeout*2 的時(shí)間,就忽略這個(gè)報(bào)告。
否則就記錄報(bào)告內(nèi)容,并且比較被標(biāo)記下線的主觀節(jié)點(diǎn)的報(bào)告數(shù)量大于等于持有槽的主節(jié)點(diǎn)數(shù)量的時(shí)候,將其標(biāo)記為客觀下線。
同時(shí)向集群中廣播一條 Fail 消息,通知所有的節(jié)點(diǎn)將故障節(jié)點(diǎn)標(biāo)記為客觀下線,這個(gè)消息指包含故障節(jié)點(diǎn)的 ID。
此后,群內(nèi)所有的節(jié)點(diǎn)都會(huì)標(biāo)記這個(gè)節(jié)點(diǎn)為客觀下線,通知故障節(jié)點(diǎn)的從節(jié)點(diǎn)出發(fā)故障轉(zhuǎn)移的流程,也就是故障的恢復(fù)。
說白了,客觀下線就是整個(gè)集群中有一半的節(jié)點(diǎn)都認(rèn)為某節(jié)點(diǎn)主觀下線了,那么這個(gè)節(jié)點(diǎn)就被標(biāo)記為客觀下線了。
如果某個(gè)主節(jié)點(diǎn)被認(rèn)為客觀下線了,那么需要從它的從節(jié)點(diǎn)中選出一個(gè)節(jié)點(diǎn)替代主節(jié)點(diǎn)的位置。
此時(shí)下線主節(jié)點(diǎn)的所有從節(jié)點(diǎn)都擔(dān)負(fù)著恢復(fù)義務(wù),這些從節(jié)點(diǎn)會(huì)定時(shí)監(jiān)測(cè)主節(jié)點(diǎn)是否下線。
一旦發(fā)現(xiàn)下線會(huì)走如下的恢復(fù)流程:
①資格檢查,每個(gè)節(jié)點(diǎn)都會(huì)檢查與主節(jié)點(diǎn)斷開的時(shí)間。如果這個(gè)時(shí)間超過了 cluster-node-timeout*cluster-slave-validity-factor(從節(jié)點(diǎn)有效因子,默認(rèn)為 10),那么就沒有故障轉(zhuǎn)移的資格。
也就是說這個(gè)從節(jié)點(diǎn)和主節(jié)點(diǎn)斷開的太久了,很久沒有同步主節(jié)點(diǎn)的數(shù)據(jù)了,不適合成為新的主節(jié)點(diǎn),因?yàn)槌蔀橹鞴?jié)點(diǎn)以后其他的從節(jié)點(diǎn)回同步自己的數(shù)據(jù)。
②觸發(fā)選舉,通過了上面資格的從節(jié)點(diǎn)都可以觸發(fā)選舉。但是出發(fā)選舉是有先后順序的,這里按照復(fù)制偏移量的大小來判斷。
這個(gè)偏移量記錄了執(zhí)行命令的字節(jié)數(shù)。主服務(wù)器每次向從服務(wù)器傳播 N 個(gè)字節(jié)時(shí)就會(huì)將自己的復(fù)制偏移量+N,從服務(wù)在接收到主服務(wù)器傳送來的 N 個(gè)字節(jié)的命令時(shí),就將自己的復(fù)制偏移量+N。
復(fù)制偏移量越大說明從節(jié)點(diǎn)延遲越低,也就是該從節(jié)點(diǎn)和主節(jié)點(diǎn)溝通更加頻繁,該從節(jié)點(diǎn)上面的數(shù)據(jù)也會(huì)更新一些,因此復(fù)制偏移量大的從節(jié)點(diǎn)會(huì)率先發(fā)起選舉。
③發(fā)起選舉,首先每個(gè)主節(jié)點(diǎn)會(huì)去更新配置紀(jì)元(clusterNode.configEpoch),這個(gè)值是不斷增加的整數(shù)。
在節(jié)點(diǎn)進(jìn)行 Ping/Pong 消息交互式也會(huì)更新這個(gè)值,它們都會(huì)將最大的值更新到自己的配置紀(jì)元中。
這個(gè)值記錄了每個(gè)節(jié)點(diǎn)的版本和整個(gè)集群的版本。每當(dāng)發(fā)生重要事情的時(shí)候,例如:出現(xiàn)新節(jié)點(diǎn),從節(jié)點(diǎn)精選。都會(huì)增加全局的配置紀(jì)元并且賦給相關(guān)的主節(jié)點(diǎn),用來記錄這個(gè)事件。
說白了更新這個(gè)值目的是,保證所有主節(jié)點(diǎn)對(duì)這件“大事”保持一致。大家都統(tǒng)一成一個(gè)配置紀(jì)元(一個(gè)整數(shù)),表示大家都知道這個(gè)“大事”了。
更新完配置紀(jì)元以后,會(huì)想群內(nèi)發(fā)起廣播選舉的消息(FAILOVER_AUTH_REQUEST)。并且保證每個(gè)從節(jié)點(diǎn)在一次配置紀(jì)元中只能發(fā)起一次選舉。
④投票選舉,參與投票的只有主節(jié)點(diǎn),從節(jié)點(diǎn)沒有投票權(quán),超過半數(shù)的主節(jié)點(diǎn)通過某一個(gè)節(jié)點(diǎn)成為新的主節(jié)點(diǎn)時(shí)投票完成。
如果在 cluster-node-timeout*2 的時(shí)間內(nèi)從節(jié)點(diǎn)沒有獲得足夠數(shù)量的票數(shù),本次選舉作廢,進(jìn)行第二輪選舉。
這里每個(gè)候選的從節(jié)點(diǎn)會(huì)收到其他主節(jié)點(diǎn)投的票。在第2步領(lǐng)先的從節(jié)點(diǎn)通常此時(shí)會(huì)獲得更多的票,因?yàn)樗|發(fā)選舉的時(shí)間更早一些。
獲得票的機(jī)會(huì)更大,也是由于它和原主節(jié)點(diǎn)延遲少,理論上數(shù)據(jù)會(huì)更加新一點(diǎn)。
⑤當(dāng)滿足投票條件的從節(jié)點(diǎn)被選出來以后,會(huì)觸發(fā)替換主節(jié)點(diǎn)的操作。新的主節(jié)點(diǎn)別選出以后,刪除原主節(jié)點(diǎn)負(fù)責(zé)的槽數(shù)據(jù),把這些槽數(shù)據(jù)添加到自己節(jié)點(diǎn)上。
并且廣播讓其他的節(jié)點(diǎn)都知道這件事情,新的主節(jié)點(diǎn)誕生了。
總結(jié)
本文通過 Redis Cluster 提供了分布式緩存的方案為出發(fā)點(diǎn),針對(duì)此方案中緩存節(jié)點(diǎn)的分區(qū)方式進(jìn)行了描述。
虛擬槽的分區(qū)算法,將整塊數(shù)據(jù)分配到了不同的緩存節(jié)點(diǎn),通過 Slot 和 Node 的對(duì)應(yīng)關(guān)系讓數(shù)據(jù)找到節(jié)點(diǎn)的位置。
對(duì)于分布式部署的節(jié)點(diǎn),需要通過 Gossip 協(xié)議進(jìn)行 Ping、Pong、Meet、Fail 的通訊,達(dá)到互通有無(wú)的目的。
當(dāng)客戶端調(diào)用緩存節(jié)點(diǎn)數(shù)據(jù)的時(shí)候通過 MOVED 和 ASKED 重定向請(qǐng)求找到正確的緩存節(jié)點(diǎn)。
并且介紹了在緩存擴(kuò)容和收縮時(shí)需要注意的處理流程,以及數(shù)據(jù)遷移的方式。
最后,講述如何發(fā)現(xiàn)故障(主觀下線和客觀下線)以及如何恢復(fù)故障(選舉節(jié)點(diǎn))的處理流程。
責(zé)任編輯: