手把手帶你搞懂Redis高可用集群

本篇文章給大家?guī)?lái)了關(guān)于redis的相關(guān)知識(shí),其中主要介紹了集群的相關(guān)問(wèn)題,redis集群是一種分布式數(shù)據(jù)庫(kù)方案,集群通過(guò)分片來(lái)進(jìn)行數(shù)據(jù)共享,并提供復(fù)制和故障轉(zhuǎn)移功能,希望對(duì)大家有幫助。

手把手帶你搞懂Redis高可用集群

推薦學(xué)習(xí):redis

幾種 redis 高可用性的解決方案。包括:「主從模式」、「哨兵機(jī)制」以及「哨兵集群」。

  • 「主從模式」具有讀寫(xiě)分離,分擔(dān)讀壓力、數(shù)據(jù)備份,提供多個(gè)副本等優(yōu)點(diǎn)。
  • 「哨兵機(jī)制」在主節(jié)點(diǎn)故障后能自動(dòng)將從節(jié)點(diǎn)提升成主節(jié)點(diǎn),不需要人工干預(yù)操作就能恢復(fù)服務(wù)可用。
  • 「哨兵集群」解決單點(diǎn)故障以及單機(jī)哨兵產(chǎn)生「誤判」問(wèn)題。

Redis 從最簡(jiǎn)單的單機(jī)版,經(jīng)過(guò)數(shù)據(jù)持久化、主從多副本、哨兵集群,通過(guò)這么一番的優(yōu)化,不管是性能還是穩(wěn)定性,都越來(lái)越高。

但是隨著時(shí)間的發(fā)展,公司業(yè)務(wù)體量迎來(lái)了爆炸性增長(zhǎng),此時(shí)的架構(gòu)模型,還能夠承擔(dān)這么大的流量嗎?

比如有這么一個(gè)需求:要用 Redis 保存 5000 萬(wàn)個(gè)鍵值對(duì),每個(gè)鍵值對(duì)大約是 512B,為了能快速部署并對(duì)外提供服務(wù),我們采用云主機(jī)來(lái)運(yùn)行 Redis 實(shí)例,那么,該如何選擇云主機(jī)的內(nèi)存容量呢?

通過(guò)計(jì)算,這些鍵值對(duì)所占的內(nèi)存空間大約是 25GB(5000 萬(wàn) *512B)。

想到的第一個(gè)方案就是:選擇一臺(tái) 32GB 內(nèi)存的云主機(jī)來(lái)部署 Redis。因?yàn)?32GB 的內(nèi)存能保存所有數(shù)據(jù),而且還留有 7GB,可以保證系統(tǒng)的正常運(yùn)行。

同時(shí),還采用 RDB 對(duì)數(shù)據(jù)做持久化,以確保 Redis 實(shí)例故障后,還能從 RDB 恢復(fù)數(shù)據(jù)。

但是,在使用的過(guò)程中會(huì)發(fā)現(xiàn),Redis 的響應(yīng)有時(shí)會(huì)非常慢。通過(guò) INFO命令 查看 Redis 的latest_fork_usec指標(biāo)值(表示最近一次 fork 的耗時(shí)),結(jié)果發(fā)現(xiàn)這個(gè)指標(biāo)值特別高。

這跟 Redis 的持久化機(jī)制有關(guān)系。

在使用 RDB 進(jìn)行持久化時(shí),Redis 會(huì) fork 子進(jìn)程來(lái)完成,fork 操作的用時(shí)和 Redis 的數(shù)據(jù)量是正相關(guān)的,而 fork 在執(zhí)行時(shí)會(huì)阻塞線程。數(shù)據(jù)量越大,fork 操作造成的主線程阻塞的時(shí)間越長(zhǎng)。

所以,在使用 RDB 對(duì) 25GB 的數(shù)據(jù)進(jìn)行持久化時(shí),數(shù)據(jù)量較大,后臺(tái)運(yùn)行的子進(jìn)程在 fork 創(chuàng)建時(shí)阻塞了主線程,于是就導(dǎo)致 Redis 響應(yīng)變慢了。

顯然這個(gè)方案是不可行的,我們必須要尋找其他的方案。

如何保存更多數(shù)據(jù)?

為了保存大量數(shù)據(jù),我們一般有兩種方法:「縱向擴(kuò)展」和「橫向擴(kuò)展」:

  • 縱向擴(kuò)展:升級(jí)單個(gè) Redis 實(shí)例的資源配置,包括增加內(nèi)存容量、增加磁盤(pán)容量、使用更高配置的 CPU;
  • 橫向擴(kuò)展:橫向增加當(dāng)前 Redis 實(shí)例的個(gè)數(shù)。

首先,「縱向擴(kuò)展」的好處是,實(shí)施起來(lái)簡(jiǎn)單、直接。不過(guò),這個(gè)方案也面臨兩個(gè)潛在的問(wèn)題。

  • 第一個(gè)問(wèn)題是,當(dāng)使用 RDB 對(duì)數(shù)據(jù)進(jìn)行持久化時(shí),如果數(shù)據(jù)量增加,需要的內(nèi)存也會(huì)增加,主線程 fork 子進(jìn)程時(shí)就可能會(huì)阻塞。
  • 第二個(gè)問(wèn)題:縱向擴(kuò)展會(huì)受到硬件和成本的限制。 這很容易理解,畢竟,把內(nèi)存從 32GB 擴(kuò)展到 64GB 還算容易,但是,要想擴(kuò)充到 1TB,就會(huì)面臨硬件容量和成本上的限制了。

與「縱向擴(kuò)展」相比,「橫向擴(kuò)展」是一個(gè)擴(kuò)展性更好的方案。這是因?yàn)?,要想保存更多的?shù)據(jù),采用這種方案的話,只用增加 Redis 的實(shí)例個(gè)數(shù)就行了,不用擔(dān)心單個(gè)實(shí)例的硬件和成本限制。

Redis 集群就是基于「橫向擴(kuò)展」實(shí)現(xiàn)的 ,通過(guò)啟動(dòng)多個(gè) Redis 實(shí)例組成一個(gè)集群,然后按照一定的規(guī)則,把收到的數(shù)據(jù)劃分成多份,每一份用一個(gè)實(shí)例來(lái)保存。

Redis 集群

Redis 集群是一種分布式數(shù)據(jù)庫(kù)方案,集群通過(guò)分片(sharding,也可以叫切片)來(lái)進(jìn)行數(shù)據(jù)共享,并提供復(fù)制和故障轉(zhuǎn)移功能。

回到我們剛剛的場(chǎng)景中,如果把 25GB 的數(shù)據(jù)平均分成 5 份(當(dāng)然,也可以不做均分),使用 5 個(gè)實(shí)例來(lái)保存,每個(gè)實(shí)例只需要保存 5GB 數(shù)據(jù)。如下圖所示:

手把手帶你搞懂Redis高可用集群
那么,在切片集群中,實(shí)例在為 5GB 數(shù)據(jù)生成 RDB 時(shí),數(shù)據(jù)量就小了很多,fork 子進(jìn)程一般不會(huì)給主線程帶來(lái)較長(zhǎng)時(shí)間的阻塞。

采用多個(gè)實(shí)例保存數(shù)據(jù)切片后,我們既能保存 25GB 數(shù)據(jù),又避免了 fork 子進(jìn)程阻塞主線程而導(dǎo)致的響應(yīng)突然變慢。

在實(shí)際應(yīng)用 Redis 時(shí),隨著業(yè)務(wù)規(guī)模的擴(kuò)展,保存大量數(shù)據(jù)的情況通常是無(wú)法避免的。而 Redis 集群,就是一個(gè)非常好的解決方案。

下面我們開(kāi)始研究如何搭建一個(gè) Redis 集群?

搭建 Redis 集群

一個(gè) Redis 集群通常由多個(gè)節(jié)點(diǎn)組成,在剛開(kāi)始的時(shí)候,每個(gè)節(jié)點(diǎn)都是相互獨(dú)立地,節(jié)點(diǎn)之間沒(méi)有任何關(guān)聯(lián)。要組建一個(gè)可以工作的集群,我們必須將各個(gè)獨(dú)立的節(jié)點(diǎn)連接起來(lái),構(gòu)成一個(gè)包含多節(jié)點(diǎn)的集群。

我們可以通過(guò) CLUSTER MEET 命令,將各個(gè)節(jié)點(diǎn)連接起來(lái):

CLUSTER MEET <ip> <port>
  • ip:待加入集群的節(jié)點(diǎn) ip
  • port:待加入集群的節(jié)點(diǎn) port

命令說(shuō)明:通過(guò)向一個(gè)節(jié)點(diǎn) A 發(fā)送 CLUSTER MEET 命令,可以讓接收命令的節(jié)點(diǎn) A 將另一個(gè)節(jié)點(diǎn) B 添加到節(jié)點(diǎn) A 所在的集群中。

這么說(shuō)有點(diǎn)抽象,下面看一個(gè)例子。

假設(shè)現(xiàn)在有三個(gè)獨(dú)立的節(jié)點(diǎn) 127.0.0.1:7001、 127.0.0.1:7002、 127.0.0.1:7003。

手把手帶你搞懂Redis高可用集群

我們首先使用客戶端連上節(jié)點(diǎn) 7001:

$ redis-cli -c -p 7001

然后向節(jié)點(diǎn) 7001 發(fā)送命令,將節(jié)點(diǎn) 7002 添加到 7001 所在的集群里:

127.0.0.1:7001> CLUSTER MEET 127.0.0.1 7002

同樣的,我們向 7003 發(fā)送命令,也添加到 7001 和 7002 所在的集群。

127.0.0.1:7001> CLUSTER MEET 127.0.0.1 7003

通過(guò) CLUSTER NODES 命令可以查看集群中的節(jié)點(diǎn)信息。

手把手帶你搞懂Redis高可用集群
現(xiàn)在集群中已經(jīng)包含 7001、 7002 和 7003 三個(gè)節(jié)點(diǎn)。不過(guò),在使用單個(gè)實(shí)例的時(shí)候,數(shù)據(jù)存在哪兒,客戶端訪問(wèn)哪兒,都是非常明確的。但是,切片集群不可避免地涉及到多個(gè)實(shí)例的分布式管理問(wèn)題

要想把切片集群用起來(lái),我們就需要解決兩大問(wèn)題:

  • 數(shù)據(jù)切片后,在多個(gè)實(shí)例之間如何分布?
  • 客戶端怎么確定想要訪問(wèn)的數(shù)據(jù)在哪個(gè)實(shí)例上?

接下來(lái),我們就一個(gè)個(gè)地解決。

數(shù)據(jù)切片和實(shí)例的對(duì)應(yīng)分布關(guān)系

在切片集群中,數(shù)據(jù)需要分布在不同實(shí)例上,那么,數(shù)據(jù)和實(shí)例之間如何對(duì)應(yīng)呢?

這就和接下來(lái)要講的 Redis Cluster 方案有關(guān)了。不過(guò),我們要先弄明白切片集群和 Redis Cluster 的聯(lián)系與區(qū)別。

在 Redis 3.0 之前,官方并沒(méi)有針對(duì)切片集群提供具體的方案。從 3.0 開(kāi)始,官方提供了一個(gè)名為 Redis Cluster 的方案,用于實(shí)現(xiàn)切片集群。

實(shí)際上,切片集群是一種保存大量數(shù)據(jù)的通用機(jī)制,這個(gè)機(jī)制可以有不同的實(shí)現(xiàn)方案。 Redis Cluster 方案中就規(guī)定了數(shù)據(jù)和實(shí)例的對(duì)應(yīng)規(guī)則。

具體來(lái)說(shuō), Redis Cluster 方案采用 哈希槽(Hash Slot),來(lái)處理數(shù)據(jù)和實(shí)例之間的映射關(guān)系。

哈希槽與 Redis 實(shí)例映射

在 Redis Cluster 方案中,一個(gè)切片集群共有 16384 個(gè)哈希槽(2^14),這些哈希槽類(lèi)似于數(shù)據(jù)分區(qū),每個(gè)鍵值對(duì)都會(huì)根據(jù)它的 key,被映射到一個(gè)哈希槽中。

在上面我們分析的,通過(guò) CLUSTER MEET 命令將 7001、7002、7003 三個(gè)節(jié)點(diǎn)連接到同一個(gè)集群里面,但是這個(gè)集群目前是處于下線狀態(tài)的,因?yàn)榧褐械娜齻€(gè)節(jié)點(diǎn)沒(méi)有分配任何槽。

那么,這些哈希槽又是如何被映射到具體的 Redis 實(shí)例上的呢?

我們可以使用 CLUSTER MEET 命令手動(dòng)建立實(shí)例間的連接,形成集群,再使用CLUSTER ADDSLOTS 命令,指定每個(gè)實(shí)例上的哈希槽個(gè)數(shù)。

CLUSTER ADDSLOTS <slot> [slot ...]

Redis5.0 提供 CLUSTER CREATE 命令創(chuàng)建集群,使用該命令,Redis 會(huì)自動(dòng)把這些槽平均分布在集群實(shí)例上。

舉個(gè)例子,我們通過(guò)以下命令,給 7001、7002、7003 三個(gè)節(jié)點(diǎn)分別指派槽。

將槽 0 ~ 槽5000 指派給 給 7001 :

127.0.0.1:7001> CLUSTER ADDSLOTS 0 1 2 3 4 ... 5000

將槽 5001 ~ 槽10000 指派給 給 7002 :

127.0.0.1:7002> CLUSTER ADDSLOTS 5001 5002 5003 5004 ... 10000

將槽 10001~ 槽 16383 指派給 給 7003 :

127.0.0.1:7003> CLUSTER ADDSLOTS 10001 10002 10003 10004 ... 16383

手把手帶你搞懂Redis高可用集群

當(dāng)三個(gè) CLUSTER ADDSLOTS 命令都執(zhí)行完畢之后,數(shù)據(jù)庫(kù)中的 16384 個(gè)槽都已經(jīng)被指派給了對(duì)應(yīng)的節(jié)點(diǎn),此時(shí)集群進(jìn)入上線狀態(tài)。

通過(guò)哈希槽,切片集群就實(shí)現(xiàn)了數(shù)據(jù)到哈希槽、哈希槽再到實(shí)例的分配。

但是,即使實(shí)例有了哈希槽的映射信息,客戶端又是怎么知道要訪問(wèn)的數(shù)據(jù)在哪個(gè)實(shí)例上呢?

客戶端如何定位數(shù)據(jù)?

一般來(lái)說(shuō),客戶端和集群實(shí)例建立連接后,實(shí)例就會(huì)把哈希槽的分配信息發(fā)給客戶端。但是,在集群剛剛創(chuàng)建的時(shí)候,每個(gè)實(shí)例只知道自己被分配了哪些哈希槽,是不知道其他實(shí)例擁有的哈希槽信息的。

那么,客戶端是如何可以在訪問(wèn)任何一個(gè)實(shí)例時(shí),就能獲得所有的哈希槽信息呢?

Redis 實(shí)例會(huì)把自己的哈希槽信息發(fā)給和它相連接的其它實(shí)例,來(lái)完成哈希槽分配信息的擴(kuò)散。當(dāng)實(shí)例之間相互連接后,每個(gè)實(shí)例就有所有哈希槽的映射關(guān)系了。

客戶端收到哈希槽信息后,會(huì)把哈希槽信息緩存在本地。當(dāng)客戶端請(qǐng)求鍵值對(duì)時(shí),會(huì)先計(jì)算鍵所對(duì)應(yīng)的哈希槽,然后就可以給相應(yīng)的實(shí)例發(fā)送請(qǐng)求了。

當(dāng)客戶端向節(jié)點(diǎn)請(qǐng)求鍵值對(duì)時(shí),接收命令的節(jié)點(diǎn)會(huì)計(jì)算出命令要處理的數(shù)據(jù)庫(kù)鍵屬于哪個(gè)槽,并檢查這個(gè)槽是否指派給了自己:

  • 如果鍵所在的槽剛好指派給了當(dāng)前節(jié)點(diǎn),那么節(jié)點(diǎn)會(huì)直接執(zhí)行這個(gè)命令;
  • 如果沒(méi)有指派給當(dāng)前節(jié)點(diǎn),那么節(jié)點(diǎn)會(huì)向客戶端返回一個(gè) MOVED 錯(cuò)誤,然后重定向(redirect)到正確的節(jié)點(diǎn),并再次發(fā)送之前待執(zhí)行的命令。

手把手帶你搞懂Redis高可用集群

計(jì)算鍵屬于哪個(gè)槽

節(jié)點(diǎn)通過(guò)以下算法來(lái)定義 key 屬于哪個(gè)槽:

crc16(key,keylen) & 0x3FFF;
  • crc16:用于計(jì)算 key 的 CRC-16 校驗(yàn)和
  • 0x3FFF:換算成 10 進(jìn)制是 16383
  • & 0x3FFF:用于計(jì)算出一個(gè)介于 0~16383 之間的整數(shù)作為 key 的槽號(hào)。

通過(guò) CLUSTER KEYSLOT 命令可以查看 key 屬于哪個(gè)槽。

判斷槽是否由當(dāng)前節(jié)點(diǎn)負(fù)責(zé)處理

當(dāng)節(jié)點(diǎn)計(jì)算出 key 所屬的 槽 i 之后,節(jié)點(diǎn)會(huì)判斷 槽 i 是否被指派了自己。那么如何判斷呢?

每個(gè)節(jié)點(diǎn)會(huì)維護(hù)一個(gè) 「slots數(shù)組」,節(jié)點(diǎn)通過(guò)檢查 slots[i] ,判斷 槽 i 是否由自己負(fù)責(zé):

  • 如果說(shuō) slots[i] 對(duì)應(yīng)的節(jié)點(diǎn)是當(dāng)前節(jié)點(diǎn)的話,那么說(shuō)明 槽 i 由當(dāng)前節(jié)點(diǎn)負(fù)責(zé),節(jié)點(diǎn)可以執(zhí)行客戶端發(fā)送的命令;
  • 如果說(shuō) slots[i] 對(duì)應(yīng)的不是當(dāng)前節(jié)點(diǎn),節(jié)點(diǎn)會(huì)根據(jù) slots[i] 所指向的節(jié)點(diǎn)向客戶端返回 MOVED 錯(cuò)誤,指引客戶端轉(zhuǎn)到正確的節(jié)點(diǎn)。

MOVED 錯(cuò)誤

格式:

MOVED  <slot> <ip>:<port>
  • slot:鍵所在的槽
  • ip:負(fù)責(zé)處理槽 slot 節(jié)點(diǎn)的 ip
  • port:負(fù)責(zé)處理槽 slot 節(jié)點(diǎn)的 port

比如:MOVED 10086 127.0.0.1:7002,表示,客戶端請(qǐng)求的鍵值對(duì)所在的哈希槽 10086,實(shí)際是在 127.0.0.1:7002 這個(gè)實(shí)例上。

通過(guò)返回的 MOVED 命令,就相當(dāng)于把哈希槽所在的新實(shí)例的信息告訴給客戶端了。

這樣一來(lái),客戶端就可以直接和 7002 連接,并發(fā)送操作請(qǐng)求了。

同時(shí),客戶端還會(huì)更新本地緩存,將該槽與 Redis 實(shí)例對(duì)應(yīng)關(guān)系更新正確。

集群模式的 redis-cli 客戶端在接收到 MOVED 錯(cuò)誤時(shí),并不會(huì)打印出 MOVED 錯(cuò)誤,而是根據(jù) MOVED 錯(cuò)誤自動(dòng)進(jìn)行節(jié)點(diǎn)轉(zhuǎn)向,并打印出轉(zhuǎn)向信息,所以我們是看不見(jiàn)節(jié)點(diǎn)返回的 MOVED 錯(cuò)誤的。而使用單機(jī)模式的 redis-cli 客戶端可以打印MOVED 錯(cuò)誤。

其實(shí),Redis 告知客戶端重定向訪問(wèn)新實(shí)例分兩種情況:MOVED 和 ASK 。下面我們分析下 ASK 重定向命令的使用方法。

重新分片

在集群中,實(shí)例和哈希槽的對(duì)應(yīng)關(guān)系并不是一成不變的,最常見(jiàn)的變化有兩個(gè):

  • 在集群中,實(shí)例有新增或刪除,Redis 需要重新分配哈希槽;
  • 為了負(fù)載均衡,Redis 需要把哈希槽在所有實(shí)例上重新分布一遍。

重新分片可以在線進(jìn)行,也就是說(shuō),重新分片的過(guò)程中,集群不需要下線。

舉個(gè)例子,上面提到,我們組成了 7001、7002、7003 三個(gè)節(jié)點(diǎn)的集群,我們可以向這個(gè)集群添加一個(gè)新節(jié)點(diǎn)127.0.0.1:7004。

$ redis-cli -c -p 7001 127.0.0.1:7001> CLUSTER MEET 127.0.0.1 7004 OK

然后通過(guò)重新分片,將原本指派給節(jié)點(diǎn) 7003 的槽 15001 ~ 槽 16383 改為指派給 7004。
手把手帶你搞懂Redis高可用集群
在重新分片的期間,源節(jié)點(diǎn)向目標(biāo)節(jié)點(diǎn)遷移槽的過(guò)程中,可能會(huì)出現(xiàn)這樣一種情況:如果某個(gè)槽的數(shù)據(jù)比較多,部分遷移到新實(shí)例,還有一部分沒(méi)有遷移咋辦?

在這種遷移部分完成的情況下,客戶端就會(huì)收到一條 ASK 報(bào)錯(cuò)信息。

ASK 錯(cuò)誤

如果客戶端向目標(biāo)節(jié)點(diǎn)發(fā)送一個(gè)與數(shù)據(jù)庫(kù)鍵有關(guān)的命令,并且這個(gè)命令要處理的鍵正好屬于被遷移的槽時(shí):

  • 源節(jié)點(diǎn)會(huì)先在自己的數(shù)據(jù)庫(kù)里查找指定的鍵,如果找到的話,直接執(zhí)行命令;
  • 相反,如果源節(jié)點(diǎn)沒(méi)有找到,那么這個(gè)鍵就有可能已經(jīng)遷移到了目標(biāo)節(jié)點(diǎn),源節(jié)點(diǎn)就會(huì)向客戶端發(fā)送一個(gè) ASK 錯(cuò)誤,指引客戶端轉(zhuǎn)向目標(biāo)節(jié)點(diǎn),并再次發(fā)送之前要執(zhí)行的命令。

看起來(lái)好像有點(diǎn)復(fù)雜,我們舉個(gè)例子來(lái)解釋一下。

手把手帶你搞懂Redis高可用集群

如上圖所示,節(jié)點(diǎn) 7003 正在向 7004 遷移 槽 16383,這個(gè)槽包含 hello 和 world,其中鍵 hello 還留在節(jié)點(diǎn) 7003,而 world 已經(jīng)遷移到 7004。

我們向節(jié)點(diǎn) 7003 發(fā)送關(guān)于 hello 的命令 這個(gè)命令會(huì)直接執(zhí)行:

127.0.0.1:7003> GET "hello" "you get the key 'hello'"

如果我們向節(jié)點(diǎn) 7003 發(fā)送 world 那么客戶端就會(huì)被重定向到 7004:

127.0.0.1:7003>  GET "world" -> (error) ASK 16383 127.0.0.1:7004

客戶端在接收到 ASK 錯(cuò)誤之后,先發(fā)送一個(gè) ASKING 命令,然后在發(fā)送 GET “world” 命令。

ASKING 命令用于打開(kāi)節(jié)點(diǎn)的 ASKING 標(biāo)識(shí),打開(kāi)之后才可以執(zhí)行命令。

ASK 和 MOVED 的區(qū)別

ASK 錯(cuò)誤和 MOVED 錯(cuò)誤都會(huì)導(dǎo)致客戶端重定向,它們的區(qū)別在于:

  • MOVED 錯(cuò)誤代表槽的負(fù)責(zé)權(quán)已經(jīng)從一個(gè)節(jié)點(diǎn)轉(zhuǎn)移到了另一個(gè)節(jié)點(diǎn):在客戶端收到關(guān)于 槽 i 的 MOVED 錯(cuò)誤之后,客戶端每次遇到關(guān)于 槽 i 的命令請(qǐng)求時(shí),都可以直接將命令請(qǐng)求發(fā)送至 MOVED 錯(cuò)誤指向的節(jié)點(diǎn),因?yàn)樵摴?jié)點(diǎn)就是目前負(fù)責(zé) 槽 i的節(jié)點(diǎn)。
  • 而 ASK 只是兩個(gè)節(jié)點(diǎn)遷移槽的過(guò)程中的一種臨時(shí)措施:在客戶端收到關(guān)于 槽 i 的 ASK 錯(cuò)誤之后,客戶端只會(huì)在接下來(lái)的一次命令請(qǐng)求中將關(guān)于 槽 i 的命令請(qǐng)求發(fā)送到 ASK 錯(cuò)誤指向的節(jié)點(diǎn),但是 ,如果客戶端再次請(qǐng)求 槽 i 中的數(shù)據(jù),它還是會(huì)給原來(lái)負(fù)責(zé) 槽 i 的節(jié)點(diǎn)發(fā)送請(qǐng)求。

這也就是說(shuō),ASK 命令的作用只是讓客戶端能給新實(shí)例發(fā)送一次請(qǐng)求,而且也不會(huì)更新客戶端緩存的哈希槽分配信息。而不像 MOVED 命令那樣,會(huì)更改本地緩存,讓后續(xù)所有命令都發(fā)往新實(shí)例。

我們現(xiàn)在知道了 Redis 集群的實(shí)現(xiàn)原理。下面我們?cè)賮?lái)分析下,Redis 集群如何實(shí)現(xiàn)高可用的呢?

復(fù)制與故障轉(zhuǎn)移

Redis 集群中的節(jié)點(diǎn)也是分為主節(jié)點(diǎn)和從節(jié)點(diǎn)。

  • 主節(jié)點(diǎn)用于處理槽
  • 從節(jié)點(diǎn)用于復(fù)制主節(jié)點(diǎn),如果被復(fù)制的主節(jié)點(diǎn)下線,可以代替主節(jié)點(diǎn)繼續(xù)提供服務(wù)。

舉個(gè)例子,對(duì)于包含 7001 ~ 7004 的四個(gè)主節(jié)點(diǎn)的集群,可以添加兩個(gè)節(jié)點(diǎn):7005、7006。并將這兩個(gè)節(jié)點(diǎn)設(shè)置為 7001 的從節(jié)點(diǎn)。

設(shè)置從節(jié)點(diǎn)命令:

CLUSTER REPLICATE <node_id>

如圖:

手把手帶你搞懂Redis高可用集群

如果此時(shí),主節(jié)點(diǎn) 7001 下線,那么集群中剩余正常工作的主節(jié)點(diǎn)將在 7001 的兩個(gè)從節(jié)點(diǎn)中選出一個(gè)作為新的主節(jié)點(diǎn)。

例如,節(jié)點(diǎn) 7005 被選中,那么原來(lái)由節(jié)點(diǎn) 7001 負(fù)責(zé)處理的槽會(huì)交給節(jié)點(diǎn) 7005 處理。而節(jié)點(diǎn) 7006 會(huì)改為復(fù)制新主節(jié)點(diǎn) 7005。如果后續(xù) 7001 重新上線,那么它將成為 7005 的從節(jié)點(diǎn)。如下圖所示:

手把手帶你搞懂Redis高可用集群

故障檢測(cè)

集群中每個(gè)節(jié)點(diǎn)會(huì)定期向其他節(jié)點(diǎn)發(fā)送 PING 消息,來(lái)檢測(cè)對(duì)方是否在線。如果接收消息的一方?jīng)]有在規(guī)定時(shí)間內(nèi)返回 PONG 消息,那么接收消息的一方就會(huì)被發(fā)送方標(biāo)記為「疑似下線」。

集群中的各個(gè)節(jié)點(diǎn)會(huì)通過(guò)互相發(fā)消息的方式來(lái)交換各節(jié)點(diǎn)的狀態(tài)信息。

節(jié)點(diǎn)的三種狀態(tài):

  • 在線狀態(tài)
  • 疑似下線狀態(tài) PFAIL
  • 已下線狀態(tài) FAIL

一個(gè)節(jié)點(diǎn)認(rèn)為某個(gè)節(jié)點(diǎn)失聯(lián)了并不代表所有的節(jié)點(diǎn)都認(rèn)為它失聯(lián)了。在一個(gè)集群中,半數(shù)以上負(fù)責(zé)處理槽的主節(jié)點(diǎn)都認(rèn)定了某個(gè)主節(jié)點(diǎn)下線了,集群才認(rèn)為該節(jié)點(diǎn)需要進(jìn)行主從切換。

Redis 集群節(jié)點(diǎn)采用 Gossip 協(xié)議來(lái)廣播自己的狀態(tài)以及自己對(duì)整個(gè)集群認(rèn)知的改變。比如一個(gè)節(jié)點(diǎn)發(fā)現(xiàn)某個(gè)節(jié)點(diǎn)失聯(lián)了 (PFail),它會(huì)將這條信息向整個(gè)集群廣播,其它節(jié)點(diǎn)也就可以收到這點(diǎn)失聯(lián)信息。

我們都知道,哨兵機(jī)制可以通過(guò)監(jiān)控、自動(dòng)切換主庫(kù)、通知客戶端實(shí)現(xiàn)故障自動(dòng)切換。那么 Redis Cluster 又是如何實(shí)現(xiàn)故障自動(dòng)轉(zhuǎn)移呢?

故障轉(zhuǎn)移

當(dāng)一個(gè)從節(jié)點(diǎn)發(fā)現(xiàn)自己正在復(fù)制的主節(jié)點(diǎn)進(jìn)入了「已下線」?fàn)顟B(tài)時(shí),從節(jié)點(diǎn)將開(kāi)始對(duì)下線主節(jié)點(diǎn)進(jìn)行故障切換。

故障轉(zhuǎn)移的執(zhí)行步驟:

  1. 在復(fù)制下線主節(jié)點(diǎn)的所有從節(jié)點(diǎn)里,選中一個(gè)從節(jié)點(diǎn)
  2. 被選中的從節(jié)點(diǎn)執(zhí)行 SLAVEOF no one 命令,成為主節(jié)點(diǎn)
  3. 新的主節(jié)點(diǎn)會(huì)撤銷(xiāo)所有對(duì)已下線主節(jié)點(diǎn)的槽指派,將這些槽全部指派給自己
  4. 新的主節(jié)點(diǎn)向集群廣播一條 PONG 消息,讓集群中其他節(jié)點(diǎn)知道,該節(jié)點(diǎn)已經(jīng)由從節(jié)點(diǎn)變?yōu)橹鞴?jié)點(diǎn),且已經(jīng)接管了原主節(jié)點(diǎn)負(fù)責(zé)的槽
  5. 新的主節(jié)點(diǎn)開(kāi)始接收自己負(fù)責(zé)處理槽有關(guān)的命令請(qǐng)求,故障轉(zhuǎn)移完成。

選主

這個(gè)選主方法和哨兵的很相似,兩者都是基于 Raft算法 的領(lǐng)頭算法實(shí)現(xiàn)的。流程如下:

  1. 集群的配置紀(jì)元是一個(gè)自增計(jì)數(shù)器,初始值為0;
  2. 當(dāng)集群里的某個(gè)節(jié)點(diǎn)開(kāi)始一次故障轉(zhuǎn)移操作時(shí),集群配置紀(jì)元加 1;
  3. 對(duì)于每個(gè)配置紀(jì)元,集群里每個(gè)負(fù)責(zé)處理槽的主節(jié)點(diǎn)都有一次投票的機(jī)會(huì),第一個(gè)向主節(jié)點(diǎn)要求投票的從節(jié)點(diǎn)將獲得主節(jié)點(diǎn)的投票;
  4. 當(dāng)從節(jié)點(diǎn)發(fā)現(xiàn)自己復(fù)制的主節(jié)點(diǎn)進(jìn)入「已下線」?fàn)顟B(tài)時(shí),會(huì)向集群廣播一條消息,要求收到這條消息,并且具有投票權(quán)的主節(jié)點(diǎn)為自己投票;
  5. 如果一個(gè)主節(jié)點(diǎn)具有投票權(quán),且尚未投票給其他從節(jié)點(diǎn),那么該主節(jié)點(diǎn)會(huì)返回一條消息給要求投票的從節(jié)點(diǎn),表示支持從節(jié)點(diǎn)成為新的主節(jié)點(diǎn);
  6. 每個(gè)參與選舉的從節(jié)點(diǎn)會(huì)計(jì)算獲得了多少主節(jié)點(diǎn)的支持;
  7. 如果集群中有 N 個(gè)具有投票權(quán)的主節(jié)點(diǎn),當(dāng)一個(gè)從節(jié)點(diǎn)收到的支持票 大于等于 N/2 + 1時(shí),該從節(jié)點(diǎn)就會(huì)當(dāng)選為新的主節(jié)點(diǎn);
  8. 如果在一個(gè)配置紀(jì)元里沒(méi)有從節(jié)點(diǎn)收集到足夠多的票數(shù),那么集群會(huì)進(jìn)入一個(gè)新的配置紀(jì)元,并再次進(jìn)行選主。

消息

集群中的各個(gè)節(jié)點(diǎn)通過(guò)發(fā)送和接收消息來(lái)進(jìn)行通信,我們把發(fā)送消息的節(jié)點(diǎn)稱(chēng)為發(fā)送者,接收消息的稱(chēng)為接收者。

節(jié)點(diǎn)發(fā)送的消息主要有五種:

  • MEET 消息
  • PING 消息
  • PONG 消息
  • FAIL 消息
  • PUBLISH 消息

集群中的各個(gè)節(jié)點(diǎn)通過(guò) Gossip 協(xié)議交換不同節(jié)點(diǎn)的狀態(tài)信息, Gossip 是由 MEET、PING、PONG 三種消息組成。

發(fā)送者每次發(fā)送 MEET、PING、PONG 消息時(shí),都會(huì)從自己已知的節(jié)點(diǎn)列表中隨機(jī)選出兩個(gè)節(jié)點(diǎn)(可以是主節(jié)點(diǎn)或者從節(jié)點(diǎn))一并發(fā)送給接收者。

接收者收到 MEET、PING、PONG 消息時(shí),根據(jù)自身是否認(rèn)識(shí)這兩個(gè)節(jié)點(diǎn)來(lái)進(jìn)行不同的處理:

  • 如果被選中的節(jié)點(diǎn)不存在接收著已知的節(jié)點(diǎn)列表,說(shuō)明是第一次接觸,接收者會(huì)根據(jù)選擇節(jié)點(diǎn)的 ip和端口號(hào)進(jìn)行通信;
  • 如果已經(jīng)存在,說(shuō)明之前已經(jīng)完成了通信,然后會(huì)更新原有選中節(jié)點(diǎn)的信息。

推薦學(xué)習(xí):redis

? 版權(quán)聲明
THE END
喜歡就支持一下吧
點(diǎn)贊5 分享
站長(zhǎng)的頭像-小浪學(xué)習(xí)網(wǎng)月度會(huì)員