Redis 專題:深入 Redis Cluster 集羣容錯機制
哨兵模式的自動故障轉移能力爲其提供高可用保障,同樣的,爲了提供集羣的可用性,Redis Cluster 提供了自動故障檢測及故障轉移能力。兩者在設計思想上有很大的相似之處,本節將圍繞這個話題進行剖析。
心跳機制
Redis Cluster 作爲無中心的分佈式系統,集羣容錯機制依靠各個節點共同協作,在節點檢測到某個節點故障時,通過傳播節點故障並達成共識,然後觸發一系列的從節點選舉及故障轉移工作。這一工作完成的基礎是節點之間通過心跳機制對集羣狀態的維護。
下圖是從節點 A 視角來看集羣的狀態示意圖(僅繪製與集羣容錯有關的內容),myself 指向 A 節點本身,它是節點 A 對自身狀態的描述;nodes[B] 指向節點 B,它是從 A 節點來看 B 節點的狀態;還有集羣當前紀元、哈希槽與節點映射關係等。PING
和PONG
兩種類型的消息保持心跳,由前文可知這兩種消息採用完全相同的結構(消息頭和消息體都相同),僅消息頭中的type
字段不同,我們稱這個消息對爲心跳消息。
-
PING
/PONG
消息頭包含了消息源節點的配置紀元(configEpoch)、複製偏移量(offset)、節點名稱(sender)、負責的哈希槽(myslots)、節點狀態(flags)等,這些內容與目標節點所維護的 nodes 中節點信息一一對應;另外還包含在源節點看來集羣紀元(currentEpoch)、集羣狀態(state)。 -
PING
/PONG
消息體包含若干clusterMsgDataGossip
,每個clusterMsgDataGossip
對應一個集羣節點狀態,它描述了源節點與之的心跳狀態及源節點對它運行狀態的判斷。
心跳消息在集羣節點兩兩之間以 “我知道的給你,你知道的給我” 這樣 “瘟疫傳播” 的方式傳播、交換信息,可以保證在短時間內節點狀態達成一致。我們從心跳觸發的時機、消息體的構成、應用幾個方面深入理解心跳機制。
觸發時機
在集羣模式下,心跳動作是由週期性函數clusterCron()
觸發的,該函數每個 100 毫秒執行一次。爲了控制集羣內消息的規模,同時兼顧與節點之間心跳的時效性,Redis Cluster 採取了不同的處理策略。
正常情況下,clusterCron()
每隔一秒(該函數每執行 10 次)向一個節點發送PING
消息。這個節點的選擇是隨機的,隨機方式爲:
-
隨機 5 次,每次從節點列表 nodes 中隨機選擇一個節點,如果節點滿足條件(集羣總線鏈接存在、非等待
PONG
回覆、非握手狀態、非本節點),則作爲備選節點; -
如果備選節點不爲空,則從備選節點中選擇
PONG
回覆最早的節點;
補充說明 Redis Cluster 心跳消息發送與接收的檢查依據,這對後續故障檢測也是非常重要的:
當源節點向目標節點發送
PING
命令後,將設置目標節點的ping_sent
爲當前時間。當源節點接收到目標節點的
PONG
回覆後,將設置目標節點的ping_sent
爲 0,同時更新pong_received
爲當前時間。
也就是說,
ping_sent
爲 0,說明已收到PONG
回覆並等待下次發送;ping_sent
不爲 0,說明正在等待PONG
回覆。
我們在集羣配置文件中設置了超時參數cluster-node-timeout
,對應變量NODE_TIMEOUT
,節點將以此參數作爲目標節點心跳超時的依據。爲了確保 PING-PONG 消息不超時並保留重試餘地,Redis Cluster 將以NODE_TIMEOUT/2
爲界限進行心跳補償。
clusterCron()
每次執行時(100 毫秒)會依次檢查每個節點:
-
如果在收到目標節點
PONG
消息NODE_TIMEOUT/2
還未發送PING
,源節點會立刻向目標節點發送一次PING
。 -
如果已經向目標節點發送
PING
消息,但是在NODE_TIMEOUT/2
內未收到目標節點的 PONG 回覆,源節點會嘗試斷開網絡鏈接,通過重連排除網絡鏈接故障對心跳的影響。
總體來講,集羣內每秒的心跳消息收發數量是穩定的,即使集羣有很多節點也不會導致瞬時網絡 I/O 過大,給集羣帶來負擔。集羣中每兩個節點之間都在保持心跳,按照 N 個節點的有向完全圖,整個集羣會有N*(N-1)
個鏈接,每個鏈接都需要保持心跳,心跳消息成對出現。
假如集羣有 100 個節點,NODE_TIMEOUT
爲 60 秒,那就意味着每個節點在 30 秒內要發送 99 條 PING 消息,平均每秒發送 3.3 條。100 個節點每秒發送總計 330 條消息,這個數量級的網絡流量壓力還是可以接受的。
不過,我們需要注意節點數確定的情況下,需要合理設置NODE_TIMEOUT
參數。如果過小,會導致心跳消息對網絡帶來較大壓力;如果太大,可能會影響及時發現節點故障。
消息構成
PING
/PONG
消息採用一致的數據結構。其中,消息頭的內容來自集羣狀態的myself
,這點很容易理解;而消息體需要追加若干節點的狀態,但是集羣中有很多節點,到底應該添加哪幾個節點呢?
按照 Redis Cluster 的設計,每個消息體將會包含正常節點和PFAIL
狀態節點,具體獲取方式如下(該部分源碼位於cluster.c
函數clusterSendPing
中):
-
確定心跳消息需要包含正常節點的理論最大值(之所以是理論值,是因爲接下來的流程還需要考慮節點狀態,會移除在握手中或宕機的節點):
-
條件 1:最大數量=集羣節點數 - 2,“減 2” 是指去掉源節點與目標節點;
-
條件 2:最大數量爲節點數量的 10 分之一且不小於 3;
-
以上結果兩者取最小值,得到所需的理論數量
wanted
。 -
確定心跳信息需要包含
PFAIL
狀態節點的數量pfail_wanted
:集羣狀態中獲取所有PFAIL
節點的數量(server.cluster->stats_pfail_nodes
)。 -
添加正常節點:從集羣節點列表隨機獲取
wanted
個節點,創建 gossip 消息片段,加入消息體。其中節點需要滿足以下條件: -
不是源節點自身;
-
不是
PFAIL
狀態,因爲後面單獨添加PFAIL
節點; -
節點不是握手狀態及無地址狀態,或者節點集羣總線鏈接存在且負責的哈希槽數量不爲 0;
-
添加
PFAIL
狀態節點:遍歷獲取PFAIL
狀態節點,創建 gossip 片段,加入消息體,此時節點需要滿足處於PFAIL
狀態、不是握手狀態、不是無地址狀態。
消息應用
節點接收到 PING 或 PONG 消息後,將按照消息頭及消息體中的內容對本地維護的節點狀態進行更新。順着源碼說明的話,其中涉及的字段和邏輯還是比較複雜的,我將從應用場景角度來說明消息的處理過程(結合源碼函數clusterProcessPacket
)。
集羣紀元和配置紀元
心跳消息頭包含了源節點的配置紀元(configEpoch)及在他看來的集羣當前紀元(currentEpoch),目標節點接收後將檢查自身維護的源節點的配置紀元和集羣當前紀元。具體方式爲:
-
若目標節點緩存的源節點配置紀元(緩存配置紀元)小於源節點心跳消息中聲明的配置紀元(聲明配置紀元),則修改緩存配置紀元爲聲明配置紀元;
-
若目標節點緩存的集羣當前紀元(緩存當前紀元)小於源節點心跳消息中聲明的集羣當前紀元(聲明當前紀元),則修改緩存當前紀元爲聲明當前紀元;
哈希槽變更檢測
消息頭包含了源節點當前負責的哈希槽列表,目標節點會檢查本地緩存的哈希槽與節點的映射關係,看是否存在與映射關係不一致的哈希槽。當發現不一致的映射關係時,將按照以下情況進行處理:
-
如果源節點爲主節點,並且其聲明的配置紀元大於目標節點緩存的配置紀元,則按照聲明的哈希槽修改本地哈希槽與節點的映射關係;
-
如果本地緩存的節點配置紀元大於源節點聲明的配置紀元,則通過
UPDATE
消息告知源節點更新本地的哈希槽配置; -
如果本地緩存的節點配置紀元與源節點聲明的配置紀元相同,並且源節點和目標節點都是主節點,則處理配置紀元衝突:目標節點升級本地集羣當前紀元和自身的配置紀元(與集羣當前紀元保持一致)。這樣在後續心跳中,將會再次調整哈希槽衝突,最終達到一致。
新節點發現
在節點握手過程中,我們知道,新節點加入集羣僅需與集羣中任意一個節點通過握手加入集羣,但是其他節點並不知道有新節點加入,新節點也不知道其他節點的存在;對於兩者而言,都是新節點的發現過程。
在心跳過程中,源節點會把對方未知的新節點信息加入消息體,通知目標節點。目標節點將執行以下流程:
-
目標節點發現消息體中存在本地緩存不存在的節點,將會爲其創建
clusterNode
對象,並加入集羣節點列表 nodes。 -
目標節點在
clusterCron
函數中創建與其的網絡鏈接,兩者通過兩次心跳交互完成新節點的發現。
節點故障發現
節點故障發現是心跳的核心功能,該部分在下一節單獨介紹。
故障發現與轉移
PFAIL 與 FAIL 概念
Redis Cluster 使用兩個概念PFAIL
、FAIL
來描述節點的運行狀態,這與哨兵模式中的SDOWN
、ODOWN
類似。
- PFAIL:可能宕機
當一個節點在超過 NODE_TIMEOUT
時間後仍無法訪問某個節點,那麼它會用 PFAIL
來標識這個不可達的節點。無論節點類型是什麼,主節點和從節點都能標識其他的節點爲 PFAIL
。
Redis 集羣節點的不可達性是指:源節點向目標節點發送 PING 命令後,超過
NODE_TIMEOUT
時間仍未得到它的PONG
回覆,那麼就認爲目標節點具有不可達性。
這是由心跳引出的一個概念。爲了讓PFAIL
儘可能,NODE_TIMEOUT
必須比兩節點間的網絡往返時間大;爲了確保可靠性,當在經過一半 NODE_TIMEOUT
時間還沒收到目標節點對於 PONG
命令的回覆時,源節點就會馬上嘗試重連接該目標節點。
所以,PFAIL
是從源節點對目標節點心跳檢測的結果,具有一定的主觀性。
- FAIL:宕機
PFAIL
狀態具有一定的主觀性,此時不代表目標節點真正的宕機,只有達到FAIL
狀態,才意味着節點真正宕機。
不過我們已經知道,在心跳過程中,每個節點都會把檢測到 PFAIL 的節點告知其他節點。所以,如果某個節點宕機是客觀存在的,那其他節點也必然會檢測到 PFAIL 狀態。
在一定的時間窗口內,當集羣中一定數量的節點都認爲目標節點爲PFAIL
狀態時,節點就會將該節點的狀態提升爲FAIL
(宕機)狀態。
節點故障檢測
本節將詳細說明節點故障檢測的實現原理,還是以下圖爲例(A、B、C 節點爲主節點,以 B 節點宕機爲例),重點關注集羣狀態節點列表(nodes)的ping_sent
、pong_received
、fail_reports
幾個字段。
每個節點維護的集羣狀態中包含節點列表,節點信息如上圖節點 B 所示,其中字段ping_sent
代表了 B 節點對 A 節點的心跳狀態:如果值爲 0,說明 A 節點與 B 節點心跳正常;如果值不是 0,說明 A 節點已經向 B 節點發送了PING
,正在等待 B 節點回復PONG
。
集羣節點每隔 100 毫秒執行一次clusterCron()
函數,其中會檢查與每個節點的心跳及數據交互狀態,若 A 節點在NODE_TIMEOUT
時間內未收到 B 節點的任何數據,則視爲 B 節點發生故障,A 節點設置節點狀態爲PFAIL
。具體代碼如下所示:
void clusterCron(void) {
/* 省略…… */
// ping消息已經發送的時間
mstime_t ping_delay = now - node->ping_sent;
// 已經多久沒有收到節點的數據了
mstime_t data_delay = now - node->data_received;
// 兩者取較早的
mstime_t node_delay = (ping_delay < data_delay) ? ping_delay : data_delay;
// 判斷超時
if (node_delay > server.cluster_node_timeout) {
/* 節點超時,如果當前節點不是PFAIL或FAIL狀態,則設置爲PFAIL狀態 */
if (!(node->flags & (CLUSTER_NODE_PFAIL|CLUSTER_NODE_FAIL))) {
serverLog(LL_DEBUG,"*** NODE %.40s possibly failing", node->name);
node->flags |= CLUSTER_NODE_PFAIL;
update_state = 1;
}
}
/* 省略…… */
}
PFAIL 狀態傳播
由 “心跳機制——消息構成” 可知,PFAIL
狀態的節點將會隨着心跳傳播至集羣內所有可達節點,不再贅述。
PFAIL 狀態切換至 FAIL 狀態
PFAIL
到FAIL
的狀態切換需要集羣內過半數主節點的認可,集羣節點通過心跳消息收集節點的PFAIL
的標誌。如果 B 節點發生故障,A、C 節點都將檢測到 B 節點故障並標記 B 節點爲PFAIL
;那麼 A、C 節點之間的心跳消息都會包含 B 節點已經PFAIL
的狀態。以 A 節點來看,Redis Cluster 是如何處理。
由於其他節點的狀態在心跳消息的消息體內,消息接收方通過clusterProcessGossipSection
函數進行處理,C 節點是主節點,並且聲明 B 節點爲PFAIL
狀態。從源碼可知,將執行以下流程:
- 爲 B 節點添加故障報告節點,即把 C 節點添加到 B 節點的
fail_reports
內。
fail_reports
爲clusterNodeFailReport
列表,保存了所有認爲 B 節點故障的節點列表。結構如下所示,其中time
字段代表其被加入的時間,即聲明該節點故障的最新時間,當再次報告該節點狀態時,僅刷新time
字段。
typedef struct clusterNodeFailReport {
/* 報告節點故障的節點 */
struct clusterNode *node; /* Node reporting the failure condition. */
/* 故障報告的時間 */
mstime_t time; /* Time of the last report from this node. */
} clusterNodeFailReport;
-
檢查是否達到
FAIL
的條件:Redis Cluster 規定,若超過半數的主節點認爲某個節點爲PFAIL
狀態,則設置節點狀態爲FAIL
狀態。接着上面的例子,具體過程爲: -
計算達到
FAIL
的法定節點數:此時集羣中包含 3 個主節點,則至少需要 2 個節點認可; -
在 A 節點集羣狀態中,C 節點已經被加入 B 節點的
fail_reports
列表,並且 A 節點已經標記 B 節點故障。即 2 兩個節點確認 B 節點發生故障,所以可以設置 B 節點爲FAIL
狀態。 -
A 節點取消 B 節點的
PFAIL
狀態,設置其FAIL
狀態,然後向所有可達節點發送關於 B 節點的FAIL
消息。
詳細的代碼過程如下函數所示:
void markNodeAsFailingIfNeeded(clusterNode *node) {
int failures;
// 計算判定節點宕機的法定數量
int needed_quorum = (server.cluster->size / 2) + 1;
// 判斷當前節點是否認爲該節點已經超時
if (!nodeTimedOut(node)) return; /* We can reach it. */
if (nodeFailed(node)) return; /* Already FAILing. */
failures = clusterNodeFailureReportsCount(node);
/* 當前節點也認可該節點宕機 */
if (nodeIsMaster(myself)) failures++;
if (failures < needed_quorum) return; /* No weak agreement from masters. */
serverLog(LL_NOTICE, "Marking node %.40s as failing (quorum reached).", node->name);
/* 設置節點爲FAIL狀態 */
node->flags &= ~CLUSTER_NODE_PFAIL;
node->flags |= CLUSTER_NODE_FAIL;
node->fail_time = mstime();
/* 向所有可達節點廣播節點的FAIL狀態,所有節點接收後將被強制接收認可 */
clusterSendFail(node->name);
clusterDoBeforeSleep(CLUSTER_TODO_UPDATE_STATE|CLUSTER_TODO_SAVE_CONFIG);
}
需要注意的是:fail_reports
中的記錄是有有效期的,默認是 2 倍的NODE_TIMEOUT
,超過該時間限制記錄會被移除。也就是說,必須在一定的時間窗口內收集足夠的記錄才能完成PFAIL
到FAIL
的狀態轉移。如果某個主節點對該節點的心跳恢復正常,會立刻從fail_reports
移除。
主節點故障後,關於它的FAIL
消息被傳播至集羣內的所有可達節點,這些節點標記其爲FAIL
狀態。爲了保證集羣的可用性,該主節點的從節點們將啓動故障轉移動作,選擇最優的從節點提升爲主節點,Redis Cluster 的故障轉移包含兩個關鍵過程:從節點選舉和從節點提升。
從節點選舉
若主節點故障,該主節點的所有從節點都會啓動一個選舉流程,在其他主節點的投票表決下,只有投票勝出的從節點纔有機會提升爲主節點。從節點選舉的準備與執行過程是在 clusterCron 中進行的。
發起選舉的條件與時機
從節點發起選舉流程必須滿足以下條件(選舉流程發起前的檢查工作位於函數clusterHandleSlaveFailover(void)
):
-
從節點的主節點處於
FAIL
狀態; -
從節點主節點負責的哈希槽不爲空;
-
爲了保證從節點數據的時效性,從節點與主節點之間斷聯的時間必須小於指定時間。關於這個指定的時間,我從代碼中提取了出來,如下所示:
/* 數據有效時間 */
mstime_t data_age;
/* 取從節點與主節點斷開的時間間隔 */
if (server.repl_state == REPL_STATE_CONNECTED) {
data_age = (mstime_t)(server.unixtime - server.master->lastinteraction) * 1000;
} else {
data_age = (mstime_t)(server.unixtime - server.repl_down_since) * 1000;
}
/* */
if (data_age > server.cluster_node_timeout)
data_age -= server.cluster_node_timeout;
data_age >
(((mstime_t)server.repl_ping_slave_period * 1000)
+ (server.cluster_node_timeout * server.cluster_slave_validity_factor)
如果FAIL
狀態的主節點擁有多個從節點,Redis Cluster 總是希望數據最完整的從節點被提升爲新的主節點。然而,假如所有從節點同時啓動選舉流程,所有從節點公平競爭,無法保證數據最完整的節點優先被提升。爲了提高該節點的優先級,Redis Cluster 在啓動選舉流程時引入了延遲啓動機制。結合源碼,每個從節點會計算一個延遲值並據此計算該節點選舉流程啓動的時間,計算公式如下:
/* 選舉流程啓動延遲值 */
DELAY = 500 + random(0,500) + SLAVE_RANK * 1000
/* 計算得出從節點啓動選舉流程的時間 */
server.cluster->failover_auth_time = DELAY + mstime()
解釋一下這個公式:
-
固定的延遲值 500,是爲了保證主節點的 FAIL 狀態在集羣內傳播完成,防止發起選舉時主節點拒絕;
-
隨機延遲值 random(0,500):爲了確保從節點不再同一時間啓動選舉流程;
-
SLAVE_RANK:該值取決於從節點數據的完整程度。當主節點變爲
FAIL
狀態後,從節點之間會通過PONG
命令交換狀態,以便建立最優的 rank 排序;從節點 rank 排序規則爲:從節點repl_offset
最大的從節點,rank = 0;其次,rank = 1,以此類推。
所以,數據完整程度最高的節點將最先啓動選舉流程,如果後續一切順利,它將被提升爲新的主節點。
設置failover_auth_time
後,當clusterCron()
再次運行時,如果系統時間達到這個預設值(並且failover_auth_sent=0
)就會進入選舉流程。
從節點選舉流程
從節點啓動選舉流程,先把自身維護的集羣當前紀元(currentEpoch
)加 1,並設置failover_auth_sent=1
以表示已經啓動選舉流程;然後通過FAILOVER_AUTH_REQUEST
類型的消息向集羣內的所有主節點發起選舉請求,並在 2 倍NODE_TIMEOUT
(至少 2 秒)時間內等待主節點的投票回覆。
集羣內的其他主節點是從節點選舉的決策者,投票前需要做出嚴格的檢查。爲了避免多個從節點在選舉中同時勝出,並且保證選舉過程合法性,主節點接收到FAILOVER_AUTH_REQUEST
命令消息後,將會做以下條件校驗:
-
發起選舉流程的從節點所屬主節點必須處於
FAIL
狀態(前面 DELAY 中的固定值 500 毫秒,就是爲了保證 FAIL 消息在集羣內傳播充分); -
針對一個給定的
currentEpoch
,主節點只會投票一次,並且保存在 lastVoteEpoch 中,其他較老的紀元選取申請都會拒絕。另外,如果從節點投票請求中的 currentEpoch 小於當前主節點的currentEpoch
,投票請求會被拒絕。 -
主節點一旦通過
FAILOVER_AUTH_ACK
類型的消息投贊成票給指定的從節點,該主節點在 2 倍NODE_TIMEOUT
時間內將不再投票給同主節點下的其他從節點。
主節點投票完成將記錄信息,並安全持久化保存到配置文件:
保存上次投票的集羣當前紀元:
lastVoteEpoch
。保存投票時間,存儲在集羣節點列表的
voted_time
中。
爲了避免把上一輪投票計入本輪投票,從節點會檢查FAILOVER_AUTH_ACK
消息所聲明的currentEpoch
,若該值小於從節點的集羣當前紀元,該選票會被丟棄。確認投票有效,從節點將通過cluster->failover_auth_count
進行計數。
在得到大多數主節點的投票認可後,從節點將從選舉中勝出。如果在 2 倍NODE_TIMEOUT
(至少 2 秒)時間內未得到大多數節點的投票認可,選舉流程將會終止,並且在 4 倍NODE_TIMEOUT
(至少 4 秒)時間後啓動新的選舉流程。
從節點提升
從節點獲得有效選票後,將把投票計數器failover_auth_count
加 1,並通過從節點選舉與提升處理函數clusterHandleSlaveFailover
進行週期性檢查,如果從節點得到大多數(法定數量)主節點的認可,將觸發從節點提升流程。
這裏選舉通過法定數量與觸發FAIL
狀態的法定數量一致,即(server.cluster->size / 2) + 1
,半數以上的主節點。
從節點啓動提升流程,將會對自身的狀態信息進行一系列的修改,最終把自己提升爲主節點,具體內容如下:
-
從節點把節點配置紀元
configEpoch
加 1; -
切換自己的角色爲主節點,並且重置主從複製關係(這個與哨兵模式從節點提升類似);
-
複製原主節點所負責的哈希槽,改爲自己負責;
-
保存並持久化以上配置變更信息;
-
其他節點:在從節點提升前,集羣內與當前節點無主從關係、非同一主節點從節點的其他節點。
-
兄弟從節點:在從節點提升前,同一主節點的從節點。
-
舊主節點:在故障恢復後如何重新加入集羣。
通用處理邏輯
新晉主節點被提升後,向集羣內所有可達節點發送了 PONG 消息。其他節點收到該PONG
消息,除了進行通用的處理邏輯(如提升配置紀元等)外,會檢測到該節點的角色變化(從節點提升爲主節點),從而進行本地集羣狀態cluterState
更新。具體的更新內容爲:
-
更新配置紀元和集羣當前紀元,因爲從節點提升時升級了;
-
把原主節點的從節點列表中移除該從節點;
-
更新節點角色:設置從節點爲主節點,取消從節點標誌;
-
哈希槽衝突處理:新晉主節點接管了舊主節點的全部哈希槽,把原主節點負責的哈希槽變更爲新的主節點;
兄弟從節點切換主從複製
以上過程是 “其他節點”、“兄弟從節點” 通用的處理過程,“舊主節點”暫時失聯,無法被通知到。基於此PONG
消息,“其他節點”已經認可新晉主節點的角色變更信息。但是 “兄弟節點” 仍然是把舊的主節點作爲自己的主節點,按照故障遷移的思想,它應該以新晉主節點作爲自己的主從複製對象,怎麼實現呢?
在哈希槽衝突處理過程中,“兄弟從節點” 會發現,衝突的哈希槽是原來它的主節點負責的,“兄弟從節點” 檢測到這一變化,就會把新晉主節點作爲自己的主節點,並以它爲新的主節點進行主從複製。
舊主節點重新加入
舊主節點恢復後,將以宕機前的配置信息(集羣當前紀元、配置紀元、哈希槽等等)與其他節點保持心跳。
當集羣內任一節點收到它的PING
消息後,會發現它的配置信息已經過時(節點配置紀元),並且哈希槽的分配情況存在衝突,此時節點將通過UPDATE
消息通知它更新配置。
UPDATE 消息包含了衝突哈希槽的負責權節點信息,舊主節點接收後會發現自身的節點配置紀元已經過時,從而把 UPDATE 消息的節點作爲自己的主節點,並切換自己的身份爲從節點,然後更新本地的哈希槽映射關係。
在後續的心跳中,其他節點將把舊主節點作爲新晉主節點的從節點進行更新。
至此,故障轉移完成。
容錯有關的其他話題
從節點遷移
爲了提供集羣系統的可用性,Redis Cluster 實現了從節點遷移機制:集羣建立時,每個主節點都有若干從節點,如果在運行過程中因爲幾次獨立的節點故障事件,導致某個主節點沒有正常狀態的從節點(被孤立),那麼該主節點一旦宕機,集羣將無法工作。Redis Cluster 會及時發現被孤立的主節點和從節點數量最大的主節點,然後挑選合適的從節點遷移至被孤立的主節點,使得其能夠再抵禦一次宕機事件,從而提高整個系統的可用性。
以下圖爲例進行說明:初始狀態時,集羣有 7 個節點,其中 A、B、C 爲主節點,A 有兩個從節點 A1、A2,B、C 各有一個從節點,分別時 B1、C1。
-
集羣在運行過程中,由於 B 節點發生故障,B1 通過選舉被提升爲新的主節點,導致 B1 成爲無從節點的孤立狀態,此時如果 B1 再發生故障,集羣將不可用。
-
但是此時 A 有兩個從節點,Redis Cluster 將啓動從節點遷移機制,把 A1 轉移至 B1 的從節點,使得 B1 不再被孤立。
-
此時即使 B1 再發生故障,那麼 A1 可以提升爲新的主節點,集羣可以繼續工作。
集羣腦裂
作爲分佈式系統,必須解決網絡分區帶來的各種複雜問題。在 Redis Cluster 中,由於網絡分區問題,導致集羣節點分佈在兩個分區,使得集羣發生 “腦裂”。此時從節點的選舉與提升在兩個網絡分區是如何工作的呢?
- 多數節點網絡分區
該分區內的節點將檢測到節點 A 的PFAIL
狀態,然後經過傳播確認節點 A 達到FAIL
狀態;A2 節點將觸發選舉流程並勝出,提升爲新的主節點,繼續工作。經過故障轉移,含有大部分節點的網絡分區可以繼續工作。
- 少數節點分區
位於少數節點分區的節點 A、A1,會檢測到其他節點 B、C 的PFAIL
狀態,但是由於無法得到大多數主節點的確認,B、C 無法達到FAIL
狀態,進而導致不能發生後續的故障轉移工作。
Redis Cluster 總結
本文從三個主要部分介紹了 Redis Cluster 的工作原理:集羣結構、數據分片、容錯機制,差不多覆蓋了 Redis Cluster 的所有內容,希望能夠給大家學習 Redis Cluster 帶來幫助。
在研究官方文檔、系統源碼的過程中,確實遇到了好多不解的內容,通過反覆梳理代碼流程,逐個揭開各個謎底,最終建立起了整個知識體系。
參考資料
-
《Redis Cluster Specification》:https://redis.io/topics/cluster-spec
-
《Redis cluster tutorial》:https://redis.io/topics/cluster-tutorial
-
《Redis 源碼 6.0.10》
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/3nGjFSHaImIjSVMBxVA9gQ