分享:Redis叢集詳述(面試官再怎麼問也能輕輕鬆鬆!)

Luson發表於2021-10-12

1、簡介

Redis叢集是Redis提供的分散式資料庫方案,叢集通過分片(sharding)進行資料共享,Redis叢集主要實現了以下目標:

  • 在1000個節點的時候仍能表現得很好並且可擴充套件性是線性的。
  • 沒有合併操作(多個節點不存在相同的鍵),這樣在 Redis 的資料模型中最典型的大資料值中也能有很好的表現。
  • 寫入安全,那些與大多數節點相連的客戶端所做的寫入操作,系統嘗試全部都儲存下來。但是Redis無法保證資料完全不丟失,非同步同步的主從複製無論如何都會存在資料丟失的情況。
  • 可用性,主節點不可用,從節點能替換主節點工作。

關於Redis叢集的學習,如果沒有任何經驗的弟兄們建議先看下這三篇文章(中文系列):
Redis叢集教程

redis.cn/topics/clus…

Redis叢集規範

redis.cn/topics/clus…

Redis3主3從偽叢集部署

blog.csdn.net/qq_41125219…

下文內容依賴下圖三主三從結構開展:

8624ecbdf7f691d78ef48bc5afdb6d18.png

資源清單:

節點 IP 槽(slot)範圍
Master[0] 192.168.211.107:6319 Slots 0 - 5460
Master[1] 192.168.211.107:6329 Slots 5461 - 10922
Master[2] 192.168.211.107:6339 Slots 10923 - 16383
Slave[0] 192.168.211.107:6369
Slave[1] 192.168.211.107:6349
Slave[2] 192.168.211.107:6359

Redis叢集.png

2、叢集內部

Redis 叢集沒有使用一致性hash, 而是引入了 雜湊槽的概念。Redis 叢集有16384個雜湊槽,每個key通過CRC16校驗後對16384取模來決定放置哪個槽,這種結構很容易新增或者刪除節點。叢集的每個節點負責一部分hash槽,比如上面資源清單的叢集有3個節點,其槽分配如下所示:

  • 節點 Master[0] 包含 0 到 5460 號雜湊槽
  • 節點 Master[1] 包含5461 到 10922 號雜湊槽
  • 節點 Master[2] 包含10923到 16383 號雜湊槽

深入學習Redis叢集之前,需要了解叢集中Redis例項的內部結構。當某個Redis服務節點通過cluster_enabled配置為yes開啟叢集模式之後,Redis服務節點不僅會繼續使用單機模式下的伺服器元件,還會增加custerState、clusterNode、custerLink等結構用於儲存叢集模式下的特殊資料。

如下三個資料承載物件一定要認真看,尤其是結構中的註釋,看完之後叢集大體上怎麼工作的,心裡就有數了,嘿嘿嘿;

2.1 clsuterNode

clsuterNode用於儲存節點資訊,比如節點的名字、IP地址、埠資訊和配置紀元等等,以下程式碼列出部分非常重要的屬性:

typedef struct clsuterNode {

    // 建立時間
    mstime_t ctime;

    // 節點名字,由40位隨機16進位制的字元組成(與sentinel中講的伺服器執行id相同)
    char name[REDIS_CLUSTER_NAMELEN];

    // 節點標識,可以標識節點的角色和狀態
    // 角色 -> 主節點或從節點 例如:REDIS_NODE_MASTER(主節點) REDIS_NODE_SLAVE(從節點)
    // 狀態 -> 線上或下線 例如:REDIS_NODE_PFAIL(疑似下線) REDIS_NODE_FAIL(下線) 
    int flags;

    // 節點配置紀元,用於故障轉移,與sentinel中用法類似
    // clusterState中的代表叢集的配置紀元
    unit64_t configEpoch;

    // 節點IP地址
    char ip[REDIS_IP_STR_LEN];

    // 節點埠
    int port;

    // 連線節點的資訊
    clusterLink *link;

    // 一個2048位元組的二進位制位陣列
    // 位陣列索引值可能為0或1
    // 陣列索引i位置值為0,代表節點不負責處理槽i
    // 陣列索引i位置值為1,代表節點負責處理槽i
    unsigned char slots[16384/8];

    // 記錄當前節點處理槽的數量總和
    int numslots;

    // 如果當前節點是從節點
    // 指向當前從節點的主節點
    struct clusterNode *slaveof;

    // 如果當前節點是主節點
    // 正在複製當前主節點的從節點數量
    int numslaves;

    // 陣列——記錄正在複製當前主節點的所有從節點
    struct clusterNode **slaves;

} clsuterNode;
複製程式碼

上述程式碼中可能不太好理解的是slots[16384/8],其實可以簡單的理解為一個16384大小的陣列,陣列索引下標處如果為1表示當前槽屬於當前clusterNode處理,如果為0表示不屬於當前clusterNode處理。clusterNode能夠通過slots來識別,當前節點處理負責處理哪些槽。
初始clsuterNode或者未分配槽的叢集中的clsuterNode的slots如下所示:
初始slots[16384_8].png

假設叢集如上面我給出的資源清單,此時代表Master[0]的clusterNode的slots如下所示:
Master[0]的clusterNode的slots.png

2.2 clusterLink

clusterLink是clsuterNode中的一個屬性,用於儲存連線節點所需的相關資訊,比如套接字描述符、輸入輸出緩衝區等待,以下程式碼列出部分非常重要的屬性:

typedef struct clusterState {

    // 連線建立時間
    mstime_t ctime;

    // TCP 套接字描述符
    int fd;

    // 輸出緩衝區,需要傳送給其他節點的訊息快取在這裡
    sds sndbuf;

    // 輸入緩衝區,接收打其他節點的訊息快取在這裡
    sds rcvbuf;

    // 與當前clsuterNode節點代表的節點建立連線的其他節點儲存在這裡
    struct clusterNode *node;
} clusterState;
複製程式碼

2.3 custerState

每個節點都會有一個custerState結構,這個結構中儲存了當前叢集的全部資料,比如叢集狀態、叢集中的所有節點資訊(主節點、從節點)等等,以下程式碼列出部分非常重要的屬性:

typedef struct clusterState {

    // 當前節點指標,指向一個clusterNode
    clusterNode *myself;

    // 叢集當前配置紀元,用於故障轉移,與sentinel中用法類似
    unit64_t currentEpoch;

    // 叢集狀態 線上/下線
    int state;

    // 叢集中處理著槽的節點數量總和
    int size;

    // 叢集節點字典,所有clusterNode包括自己
    dict *node;

    // 叢集中所有槽的指派資訊
    clsuterNode *slots[16384];

    // 用於槽的重新分配——記錄當前節點正在從其他節點匯入的槽
    clusterNode *importing_slots_from[16384];

    // 用於槽的重新分配——記錄當前節點正在遷移至其他節點的槽
    clusterNode *migrating_slots_to[16384];

    // ...

} clusterState;
複製程式碼

在custerState有三個結構需要認真瞭解的,第一個是slots陣列,clusterState中的slots陣列與clsuterNode中的slots陣列是不一樣的,在clusterNode中slots陣列記錄的是當前clusterNode所負責的槽,而clusterState中的slots陣列記錄的是整個叢集的每個槽由哪個clsuterNode負責,因此叢集正常工作的時候clusterState的slots陣列每個索引指向負責該槽的clusterNode,叢集槽未分配之前指向null。

如圖展示資源清單中的叢集clusterState中的slots陣列與clsuterNode中的slots陣列:

clusterState中Slots陣列.png

Redis叢集中使用兩個slots陣列的原因是出於效能的考慮:

  • 當我們需要獲取整個叢集中clusterNode分別負責什麼槽時,只需要查詢clusterState中的slots陣列即可。如果沒有clusterState的slots陣列,則需要遍歷所有的clusterNode結構,這樣顯然要慢一些
  • 此外clusterNode中的slots陣列也有存在的必要,因為叢集中任意一個節點之間需要知道彼此負責的槽,此時節點之間只需要互相傳輸clusterNode中的slots陣列結構就行。

第二個需要認真瞭解的結構是node字典,該結構雖然簡單,但是node字典中儲存了所有的clusterNode,這也是Redis叢集中的單個節點獲取其他主節點、從節點資訊的主要位置,因此我們也需要注意一下。
第三個需要認真瞭解的結構是importing_slots_from[16384]陣列和migrating_slots_to[16384],這兩個陣列在叢集重新分片時需要使用,需要重點了解,後面再說吧,這裡說的話順序不太對。

3、叢集工作

3.1 槽(slot)如何指派?

Redis叢集一共16384個槽,如上資源清單我們在三主三從的叢集中,每個主節點負責自己相應的槽,而在上面的三主三從部署的過程中並未看到我指定槽給對應的主節點,這是因為Redis叢集自己內部給我們劃分了槽,但是如果我們想自己指派槽該如何整呢?
我們可以向節點傳送如下命令,將一個或多個槽指派給當前節點負責:

CLUSTER ADDSLOTS

比如我們想把0和1槽指派給Master[0],我們只需要想Master[0]節點傳送如下命令即可:

CLUSTER ADDSLOTS 0 1

當節點被指派了槽後,會將clusterNode的slots陣列更新,節點會將自己負責處理的槽也就是slots陣列通過訊息傳送給叢集中的其他節點,其他節點在接收當訊息後會更新對應clusterNode的slots陣列以及clusterState的solts陣列。

3.2 ADDSLOTS 在Redis叢集內部是如何實現的呢?

這個其實也比較簡單,當我們向Redis叢集中的某個節點傳送CLUSTER ADDSLOTS命令時,當前節點首先會通過clusterState中的slots陣列來確認指派給當前節點的槽是否沒有指派給其他節點,如果已經指派了,那麼會直接丟擲異常,返回錯誤給指派的客戶端。如果指派給當前節點的所有槽都未指派給其他節點,那麼當前節點會將這些槽指派給自己。
指派主要有三個步驟:

  1. 更新clusterState的slots陣列,將指定槽slots[i]指向當前clusterNode
  2. 更新clusterNode的slots陣列,將指定槽slots[i]處的值更新為1
  3. 向叢集中的其他節點傳送訊息,將clusterNode的slots陣列傳送給其他節點,其他節點接收到訊息後也更新對應的clusterState的slots陣列和clusterNode的slots陣列

3.3 叢集這麼多節點,客戶端怎麼知道請求哪個節點?

在瞭解這個問題之前先要知道一個點,Redis叢集是怎麼計算當前這個鍵屬於哪個槽的呢?根據官網的介紹,Redis其實並未使用一致性hash演算法,而是將每個請求的key通過CRC16校驗後對16384取模來決定放置到哪個槽中。

HASH_SLOT = CRC16(key) mod 16384

此時,當客戶端連線向某個節點傳送請求時,當前接收到命令的節點首先會通過演算法計算出當前key所屬的槽i,計算完後當前節點會判斷clusterState的槽i是否由自己負責,如果恰好由自己負責那麼當前節點就會之間響應客戶端的請求,如果不由當前節點負責,則會經歷如下步驟:

  1. 節點向客戶端返回MOVED重定向錯誤,MOVED重定向錯誤中會將計算好的正確處理該key的clusterNode的ip和port返回給客戶端
  2. 客戶端接收到節點返回的MOVED重定向錯誤時,會根據ip和port將命令轉發給正確的節點,整個處理過程對程式設計師來說透明,由Redis叢集的服務端和客戶端共同負責完成。

3.4 如果我想將已經分配給A節點的槽重新分配給B節點,怎麼整?

這個問題其實涵括了很多問題,比如移除Redis叢集中的某些節點,增加節點等都可以概括為把雜湊槽從一個節點移動到另外一個節點。並且Redis叢集非常牛逼的一點也在這裡,它支援線上(不停機)的分配,也就是官方說叢集線上重配置(live reconfiguration )。

在將實現之前先來看下CLUSTER的指令,指令會了操作就會了:

  • CLUSTER ADDSLOTS slot1 [slot2] … [slotN]
  • CLUSTER DELSLOTS slot1 [slot2] … [slotN]
  • CLUSTER SETSLOT slot NODE node
  • CLUSTER SETSLOT slot MIGRATING node
  • CLUSTER SETSLOT slot IMPORTING node

CLUSTER 用於槽分配的指令主要有如上這些,ADDSLOTS 和DELSLOTS主要用於槽的快速指派和快速刪除,通常我們在叢集剛剛建立的時候進行快速分配的時候才使用。CLUSTER SETSLOT slot NODE node也用於直接給指定的節點指派槽。如果叢集已經建立我們通常使用最後兩個來重分配,其代表的含義如下所示:

  • 當一個槽被設定為 MIGRATING,原來持有該雜湊槽的節點仍會接受所有跟這個雜湊槽有關的請求,但只有當查詢的鍵還存在原節點時,原節點會處理該請求,否則這個查詢會通過一個 -ASK 重定向(-ASK redirection)轉發到遷移的目標節點。
  • 當一個槽被設定為 IMPORTING,只有在接受到 ASKING 命令之後節點才會接受所有查詢這個雜湊槽的請求。如果客戶端一直沒有傳送 ASKING 命令,那麼查詢都會通過 -MOVED 重定向錯誤轉發到真正處理這個雜湊槽的節點那裡。

上面這兩句話是不是感覺不太看的懂,這是官方的描述,不太懂的話我來給你通俗的描述,整個流程大致如下步驟:

  1. redis-trib(叢集管理軟體redis-trib會負責Redis叢集的槽分配工作),向目標節點(槽匯入節點)傳送CLUSTER SETSLOT slot IMPORTING node命令,目標節點會做好從源節點(槽匯出節點)匯入槽的準備工作。
  2. redis-trib隨即向源節點傳送CLUSTER SETSLOT slot MIGRATING node命令,源節點會做好槽匯出準備工作
  3. redis-trib隨即向源節點傳送CLUSTER GETKEYSINSLOT slot count命令,源節點接收命令後會返回屬於槽slot的鍵,最多返回count個鍵
  4. redis-trib會根據源節點返回的鍵向源節點依次傳送MIGRATE ip port key 0 timeout命令,如果key在源節點中,將會遷移至目標節點。
  5. 遷移完成之後,redis-trib會向叢集中的某個節點傳送CLUSTER SETSLOT slot NODE node命令,節點接收到命令後會更新clusterNode和clusterState結構,然後節點通過訊息傳播槽的指派資訊,至此叢集槽遷移工作完成,且叢集中的其他節點也更新了新的槽分配資訊。

3.5 如果客戶端訪問的key所屬的槽正在遷移怎麼辦?

優秀的你總會想到這種併發情況,牛皮呀!大佬們!

u=79087421,2199932123&fm=26&fmt=auto.webp

這個問題官方也考慮了,還記得我們在聊clusterState結構的時候麼?importing_slots_from和migrating_slots_to就是用來處理這個問題的。

typedef struct clusterState {

    // ...

    // 用於槽的重新分配——記錄當前節點正在從其他節點匯入的槽
    clusterNode *importing_slots_from[16384];

    // 用於槽的重新分配——記錄當前節點正在遷移至其他節點的槽
    clusterNode *migrating_slots_to[16384];

    // ...

} clusterState;
複製程式碼
  • 當節點正在匯出某個槽,則會在clusterState中的migrating_slots_to陣列對應的下標處設定其指向對應的clusterNode,這個clusterNode會指向匯入的節點。
  • 當節點正在匯入某個槽,則會在clusterState中的importing_slots_from陣列對應的下標處設定其指向對應的clusterNode,這個clusterNode會指向匯出的節點。

有了上述兩個相互陣列,就能判斷當前槽是否在遷移了,而且從哪裡遷移來,要遷移到哪裡去?搞笑不就是這麼簡單……

此時,回到問題中,如果客戶端請求的key剛好屬於正在遷移的槽。那麼接收到命令的節點首先會嘗試在自己的資料庫中查詢鍵key,如果這個槽還沒遷移完成,且當前key剛好也還沒遷移完成,那就直接響應客戶端的請求就行。如果該key已經不在了,此時節點會去查詢migrating_slots_to陣列對應的索引槽,如果索引處的值不為null,而是指向了某個clusterNode結構,那說明這個key已經被遷移到這個clusterNode了。這個時候節點不會繼續在處理指令,而是返回ASKING命令,這個命令也會攜帶匯入槽clusterNode對應的ip和port。客戶端在接收到ASKING命令之後就需要將請求轉向正確的節點了,不過這裡有一點需要注意的地方 (因此我放個表情包在這裡,方便讀者注意)。

前面說了,當節點發現當前槽不屬於自己處理時會返回MOVED指令,那麼在遷移中的槽時怎麼處理的呢?這個Redis叢集是這個玩的。
節點發現槽正在遷移則向客戶端返回ASKING命令,客戶端會接收到ASKING命令,其中包含了槽遷入的clusterNode的節點ip和port。那麼客戶端首先會向遷入的clusterNode傳送一條ASKING命令,這個命令必須要發目的是告訴當前節點,你要破例處理這次請求,因為這個槽已經遷移到你這裡了,你不能直接拒絕我(因此如果Redis未接收到ASKING命令,會直接查詢節點的clusterState,而正在遷移中的槽還沒有更新到clusterState中,那麼只能直接返回MOVED,這樣不就會一直迴圈很多次……),接收到ASKING命令的節點會強制執行一次這個請求(只執行一次,下次再來需要重新提前傳送ASKING命令)。

4、叢集故障

Redis叢集故障比較簡單,這個和sentinel中主節點當機或者在指定最長時間內未響應,重新在從節點中選舉新的主節點的方式其實差不多。當然前提是Redis叢集中的每個主節點,我們提前設定了從節點,要不就嘿嘿嘿……沒戲。其大致步驟如下:

  1. 正常工作的叢集,每個節點之間會定期向其他節點傳送PING命令,如果接收命令的節點未在規定時間內返回PONG訊息 ,當前節點會將接收命令的節點的clusterNode的flags設定為REDIS_NODE_PFAIL,PFAIL並不是下線,而是疑似下線。
  2. 叢集節點會通過傳送訊息的方式來告知其他節點,叢集中各個節點的狀態資訊
  3. 如果叢集中半數以上負責處理槽的主節點都將某個主節點設定為疑似下線,那麼這個節點將會被標記位下線狀態,節點會將接收命令的節點的clusterNode的flags設定為REDIS_NODE_FAIL,FAIL表示已下線
  4. 叢集節點通過傳送訊息的方式來告知其他節點,叢集中各個節點的狀態資訊,此時下線節點的從節點在發現自己的主節點已經被標記為下線狀態了,那麼是時候挺身而出了
  5. 下線主節點的從節點,會選舉出一個從節點作為最新的主節點,執行被選中的節點指向SLAVEOF no one成為新的主節點
  6. 新的主節點會撤銷掉原主節點的槽指派,並將這些槽指派修改為自己,也就是修改clusterNode結構和clusterState結構
  7. 新的主節點向叢集廣播一條PONG指令,其他節點將會知道有新的主節點產生,並更新clusterNode結構和clusterState結構
  8. 新的主節點如果會向原主節點剩餘的從節點傳送新的SLAVEOF指令,使其成為自己的從節點
  9. 最後新的主節點將會負責原主節點的槽的響應工作

作者:李子捌
連結:juejin.cn/post/7016865316240097287

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章