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