分散式Redis深度歷險-Clustor

做個好人君發表於2019-03-01

本文為分散式Redis深度歷險系列的第三篇,主要內容為Redis的Clustor,也就是Redis叢集功能。

更多文章見個人部落格:github.com/farmerjohng…

Redis叢集是Redis官方提供的分散式方案,整個叢集通過將所有資料分成16384個槽來進行資料共享。

叢集基礎實現

一個叢集由多個Redis節點組成,不同的節點通過CLUSTOR MEET命令進行連線:

CLUSTOR MEET <ip> <port>

收到命令的節點會與命令中指定的目標節點進行握手,握手成功後目標節點會加入到叢集中,看個例子,圖片來自於Redis的設計與實現

image

image

image

image

image

槽分配

一個叢集的所有資料被分為16384個槽,可以通過CLUSTER ADDSLOTS命令將槽指派給對應的節點。當所有的槽都有節點負責時,叢集處於上線狀態,否則處於下線狀態不對外提供服務。

clusterNode的位陣列slots代表一個節點負責的槽資訊。


struct clusterNode {


    unsigned char slots[16384/8]; /* slots handled by this node */

    int numslots;   /* Number of slots handled by this node */

    ...
}

複製程式碼

看個例子,下圖中1、3、5、8、9、10位的值為1,代表該節點負責槽1、3、5、8、9、10。

每個Redis Server上都有一個ClusterState的物件,代表了該Server所在叢集的資訊,其中欄位slots記錄了叢集中所有節點負責的槽資訊。

typedef struct clusterState {

    // 負責處理各個槽的節點
    // 例如 slots[i] = clusterNode_A 表示槽 i 由節點 A 處理
    // slots[i] = null 代表該槽目前沒有節點負責
    clusterNode *slots[REDIS_CLUSTER_SLOTS];

}
複製程式碼

槽重分配

可以通過redis-trib工具對槽重新分配,重分配的實現步驟如下:

  1. 通知目標節點準備好接收槽
  2. 通知源節點準備好傳送槽
  3. 向源節點傳送命令:CLUSTER GETKEYSINSLOT <slot> <count>從源節點獲取最多count個槽slot的key
  4. 對於步驟3的每個key,都向源節點傳送一個MIGRATE <target_ip> <target_port> <key_name> 0 <timeout>命令,將被選中的鍵原子的從源節點遷移至目標節點。
  5. 重複步驟3、4。直到槽slot的所有鍵值對都被遷移到目標節點
  6. 將槽slot指派給目標節點的資訊傳送到整個叢集。

在槽重分配的過程中,槽中的一部分資料儲存著源節點,另一部分儲存在目標節點。這時如果要客戶端向源節點傳送一個命令,且相關資料在一個正在遷移槽中,源節點處理步驟如圖:

image

當客戶端收到一個ASK錯誤的時候,會根據返回的資訊向目標節點重新發起一次請求。

ASK和MOVED的區別主要是ASK是一次性的,MOVED是永久性的,有點像Http協議中的301和302。

一次命令執行過程

我們來看clustor下一次命令的請求過程,假設執行命令 get testKey

  1. clustor client在執行前需要配置若干個server節點的ip和port。我們稱這些節點為種子節點。

  2. clustor的客戶端在執行命令時,會先通過計算得到key的槽資訊,計算規則為:getCRC16(key) & (16384 - 1),得到槽資訊後,會從一個快取map中獲得槽對應的redis server資訊,如果能獲取到,則調到第4步

  3. 向種子節點傳送slots命令以獲得整個叢集的槽分佈資訊,然後跳轉到第2步重試命令

  4. 向負責該槽的server發起呼叫 server處理如圖:

    image

  5. 客戶端如果收到MOVED錯誤,則根據對應的地址跳轉到第4步重新請求,

  6. 客戶段如果收到ASK錯誤,則根據對應的地址跳轉到第4步重新請求,並在請求前帶上ASKING標識。

以上步驟大致就是redis clustor下一次命令請求的過程,但忽略了一個細節,如果要查詢的資料鎖所在的槽正在重分配怎麼辦?

Redis故障轉移

疑似下線與已下線

叢集中每個Redis節點都會定期的向叢集中的其他節點傳送PING訊息,如果目標節點沒有在有效時間內回覆PONG訊息,則會被標記為疑似下線。同時將該資訊傳送給其他節點。當一個叢集中有半數負責處理槽的主節點都將某個節點A標記為疑似下線後,那麼A會被標記為已下線,將A標記為已下線的節點會將該資訊傳送給其他節點。

比如說有A,B,C,D,E 5個主節點。E有F、G兩個從節點。 當E節點發生異常後,其他節點傳送給A的PING訊息將不能得到正常回復。當過了最大超時時間後,假設A,B先將E標記為疑似下線;之後C也會將E標記為疑似下線,這時C發現叢集中由3個節點(A、B、C)都將E標記為疑似下線,超過叢集複製槽的主節點個數的一半(>2.5)則會將E標記為已下線,並向叢集廣播E下線的訊息。

選取新的主節點

當F、G(E的從節點)收到E被標記已下線的訊息後,會根據Raft演算法選舉出一個新的主節點,新的主節點會將E複製的所有槽指派給自己,然後向叢集廣播訊息,通知其他節點新的主節點資訊。

選舉新的主節點演算法與選舉Sentinel頭節點的過程很像:

  1. 叢集的配置紀元是一個自增計數器,它的初始值為0.

  2. 當叢集裡的某個節點開始一次故障轉移操作時,叢集配置紀元的值會被增一。

  3. 對於每個配置紀元,叢集裡每個負責處理槽的主節點都有一次投票的機會,而第一個向主節點要求投票的從節點將獲得主節點的投票。

  4. 檔從節點發現自己正在複製的主節點進入已下線狀態時,從節點會想叢集廣播一條CLUSTER_TYPE_FAILOVER_AUTH_REQUEST訊息,要求所有接收到這條訊息、並且具有投票權的主節點向這個從節點投票。

  5. 如果一個主節點具有投票權(它正在負責處理槽),並且這個主節點尚未投票給其他從節點,那麼主節點將向要求投票的從節點返回一條CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK訊息,表示這個主節點支援從節點成為新的主節點。

  6. 每個參與選舉的從節點都會接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK訊息,並根據自己收到了多少條這種訊息來同濟自己獲得了多少主節點的支援。

  7. 如果叢集裡有N個具有投票權的主節點,那麼當一個從節點收集到大於等於N/2+1張支援票時,這個從節點就會當選為新的主節點。

  8. 因為在每一個配置紀元裡面,每個具有投票權的主節點只能投一次票,所以如果有N個主節點進行投票,那麼具有大於等於N/2+1張支援票的從節點只會有一個,這確保了新的主節點只會有一個。

  9. 如果在一個配置紀元裡面沒有從節點能收集到足夠多的支援票,那麼叢集進入一個新的配置紀元,並再次進行選舉,知道選出新的主節點為止。

Redis常用分散式實現方案

最後,聊聊redis叢集的其他兩種實現方案。

client做分片

客戶端做路由,採用一致性hash演算法,將key對映到對應的redis節點上。 其優點是實現簡單,沒有引用其他中介軟體。 缺點也很明顯:是一種靜態分片方案,擴容性差。

Jedis中的ShardedJedis是該方案的實現。

proxy做分片

該方案在client與redis之間引入一個代理層。client的所有操作都傳送給代理層,由代理層實現路由轉發給不同的redis伺服器。

image

其優點是: 路由規則可自定義,擴容方便。 缺點是: 代理層有單點問題,多一層轉發的網路開銷

其開源實現有twitter的twemproxy 和豌豆莢的codis

結束

分散式redis深度歷險系列到此為止了,之後一個系列會詳細講講單機Redis的實現,包括Redis的底層資料結構、對記憶體佔用的優化、基於事件的處理機制、持久化的實現等等偏底層的內容,敬請期待~

相關文章