閱讀950 返回首頁    go 阿裏雲 go 技術社區[雲棲]


《Redis官方教程》Redis集群規範(二)

心跳數據包內容


Ping 和 Pong 數據包都包含著一個頭部(header),這在這類數據包(比如請求投票的數據包)中是很常見的。一個特殊的 報文片段就是 Ping 包和 Pong 包裏一個特殊部分。

常見頭部會包含以下這些信息:

  • 節點 ID,在節點第一次創建的時候賦值的一個 160 bit 的偽隨機字符串,在Redis 集群節點永遠都保持不變。
  • currentEpoch和 configEpoch 兩個字段,用來掛載 Redis 集群使用的分布式算法(這會在下一節中詳細解釋)。如果節點是slave,configEpoch是上一個已知master的configEpoch。
  • 節點標識,標識一個節點是slave/slave,還有其他隻占用一個 bit 的節點信息。
  • 給定節點負責的哈希槽的位圖(bitmap),如果該節點是slave,就是一個其master節點負責的哈希槽位圖。
  • 發送端的 TCP 基本端口號(也就是,這個端口號是 Redis 用來接收客戶端命令的,加上 10000 就是集群總線端口)。
  • 從發送者的角度看來的集群狀態(down還是ok)。
  • 如果這是個slave節點,那麼會有master節點 ID。

ping 包和 pong 包都包含著一個 gossip 字段。這個字段是用來讓接收者知道發送者是怎麼看待集群中的其他節點。gossip 報文片段隻包含在發送者已知節點集合裏隨機取的一些節點的信息。

提到的gossip報文片段的節點數據與集群大小成比例。

 

每個添加到gossip 報文片段的節點有如下幾個字段

  • 節點 ID。
  • 節點的 IP 和端口號。
  • 節點標識。

從發送者的角度看來,gossip 報文片段是讓接收的節點能獲得其他節點的狀態信息。這對於失效檢測或者發現集群中的其他節點都是非常有用的。

失效檢測


Redis 集群失效檢測是用來識別出大多數節點何時無法訪問某一個master節點或slave節點。然後,就提升級一個slave成master。若如果無法升級slave成master,那麼整個集群就置為錯誤狀態並停止接收客戶端的請求。

正如已經提到的,每個節點都有一份跟其他已知節點相關的標識列表。其中有兩個標識是用於失效檢測,分別是 PFAIL 和FAIL。PFAIL 表示可能失效(Possible failure,這是一個非承認失效類型。FAIL 表示一個節點已經失效,而且這個情況已經被大多數master節點在固定時間內確認過。

PFAIL 標識:

當一個節點在超過 NODE_TIMEOUT 時間後仍無法訪問另一個節點,那麼它會用 PFAIL 來標識另一個節點。master節點和slave節點都能標識其他的節點為 PFAIL,不論節點類型。

Redis 集群節點的不可達性是指,發送給某個節點的一個活躍的 ping  (一個我們發送後要等待其回複)已經等待了超過 NODE_TIMEOUT 時間遲遲不回複。為了讓這個機製能正常工作,NODE_TIMEOUT 必須比網絡往返時間大。節點為了在普通操作中增加可靠性,當在經過一半 NODE_TIMEOUT時間還沒收到目標節點對於 ping 包的回複,就會馬上嚐試重連接該節點。這個機製能保證連接都保持有效,所以節點間的失效連接通常都不會導致錯誤的失效報告。

FAIL 標識:

單獨一個 PFAIL 標識僅僅是每個節點的一些關於其他節點的本地信息,但是不足夠觸發slave節點的升級。要讓一個節點真正被認為down需要讓 PFAIL 狀態上升為 FAIL 狀態。

在本文的節點心跳章節有提到的,每個節點向其他每個節點發送的 gossip 消息中有包含一些隨機的已知節點的狀態。最終每個節點都能收到一份其他每個節點的節點標識。通過這種方法,每個節點都有一套機製去標記檢測到的其他節點的失效狀態。

當下麵的條件滿足的時候,會使用這個機製來讓 PFAIL 狀態升級為 FAIL 狀態:

  • 某個節點,我們稱為節點 A,標記另一個節點 B 為PFAIL。
  • 節點 A 通過 gossip 報文片段收集到集群中大部分master節點標識的節點 B 的狀態信息。
  • 大部分master節點標記節點 B 為PFAIL 狀態,或者在 NODE_TIMEOUT * FAIL_REPORT_VALIDITY_MULT 這個時間內是處於 PFAIL 狀態(在當前的代碼,有效因子FAIL_REPORT_VALIDITY_MULT值為2,所以這個時間是2倍的NODE_TIMEOUT時間)。

如果以上所有條件都滿足了,那麼節點 A 會:

  • 標記節點 B 為FAIL。
  • 向所有可達節點發送一個FAIL 消息。

FAIL 消息會強製每個接收到這消息的節點把節點 B 標記為 FAIL 狀態,不論是否標記過節點PFAIL。

注意,FAIL 標識基本都是單向的,也就是說,一個節點能從 PFAIL 狀態升級到 FAIL 狀態,但要清除FAIL 標識隻有以下兩種可能方法:

  • 節點已經恢複可達的,並且它是一個slave節點。在這種情況下,FAIL標識可以清除掉,當slave節點並沒有被故障轉移。
  • 節點已經恢複可達的,並且它是不負責任何哈希槽,在這種情況下FAIL可以清除,因為master沒有任何哈希槽處在集群中,就等待配置加入到集群中.
  • 節點已經恢複可達的,而且它是一個master節點,但經過了很長時間(N *NODE_TIMEOUT)後也沒有檢測到任何slave節點被提升了,最好重新加入到集群,並繼續這個方法。

值得注意,PFAIL -> FAIL 的轉變使用了一種弱協議:

1) 節點是在一段時間內收集其他節點的信息,所以即使大多數master節點要去”同意”標記某節點為 FAIL,實際上這隻是表明說我們在不同時間裏從不同節點收集了信息,並且我們不確定或不需要,給定的時刻大多數master同意,然後我們舍棄舊的失效報告,所以失效的通知是大多數master在時間窗口內的。

2) 當每個節點檢測到 FAIL 節點的時候會強迫集群裏的其他節點把各自對該節點的記錄更新為 FAIL,但沒有一種方式能保證這個消息能到達所有節點。比如有個節點可能檢測到了 FAIL 的節點,但是因為網絡分區,這個節點無法到達其他任何一個節點。

然而 Redis 集群的失效檢測有一個實時要求:最終所有節點都應該同意給定節點的狀態。有兩種情況是來源於腦裂情況,或者是小部分節點相信該節點處於 FAIL 狀態,或者少數節點相信節點不處於 FAIL 狀態。在這兩種情況中,最後集群都會給節點一個狀態:

 1 種情況: 如果大多數節點都標記了某個節點為 FAIL,由於失效檢測和產生的鏈條反應,這個master節點最終會被標記為FAIL,因為在指定時間窗口內失效將被報告。

 2 種情況: 當隻有小部分的master節點標記某個節點為 FAIL 的時候,slave節點的提升並不會發生(它是使用一個更正式的算法來保證每個節點最終都會知道節點的提升)並且每個節點都會根據上麵的清除規則來清除 FAIL 狀態(即:在經過了一段時間 > N * NODE_TIMEOUT 後仍沒有slave節點升級操作)。

FAIL 標識隻是用來觸發slave節點升級算法的安全部分。理論上一個slave節點會在它的master節點不可達的時候獨立起作用並且啟動slave節點提升程序,如果master節點對大部分節點恢複連接,然後等待master節點來拒絕認可該提升。然後增加複雜性的PFAIL -> FAIL 狀態變化、弱協議、強製在集群的可達部分用最短的時間傳播狀態變更的 FAIL 消息,這些東西增加的複雜性有實際的好處。由於這種機製,如果集群處於錯誤狀態的時候,所有節點都會在同一時間停止接收寫入操作,這從使用 Redis 集群的應用的角度來看是個很好的特性。還錯誤的選舉,是slave節點本地原因無法訪問master節點(該master節點能被其他大多數master節點訪問的話),這個選舉會被拒絕掉。

 

配置處理,傳播,和失效轉移


集群當前epoch


Redis 集群使用一個類似於木筏算法”術語”的概念。在 Redis 集群中這個術語叫做 epoch,它是用來記錄事件的遞增版本號,所以當有多個節點提供了衝突的信息的時候,另外的節點就可以通過這個狀態來了解哪個是最新的。

currentEpoch 是一個 64bit 的 unsigned 數。

Redis 集群中的每個節點,包括master節點和slave節點,都在創建的時候設置了 currentEpoch 為0。

每次當節點接收到來自其他節點的 ping 包或 pong 包的時候,如果發送者的 epoch(集群總線消息頭部的一部分)大於該節點的 epoch,那麼更新currentEpoch成發送者的 epoch 。

由於這個語義,最終所有節點都會支持集群中最大的 epoch。

這個信息在此處是用於,當一個節點的狀態發生改變的時候為了執行一些動作尋求其他節點的同意(agreement)。

目前這個隻發生在slave節點的升級期間,隨後在下一節中詳述。本質上說,epoch 是一個集群裏的邏輯時鍾,並決定一個給定的消息覆蓋另一個帶著更小 epoch 的消息。

epoch配置


每一個master節點總是通過發送 ping 包和 pong 包向別人宣傳它的 configEpoch 和一份表示它負責的哈希槽的位圖。

當一個新節點被創建的時候,master節點中的 configEpoch 設為零。

slave升級的時候創建一個新的configEpoch. slave試圖取代失敗的主人增加他們的epoch,並嚐試從大多數主人獲得授權。當slave被授權,新的唯一configEpoch創建並且alave變成master使用新的configEpoch。

將在下一節解釋,configEpoch 用於在不同節點提出不同的配置信息的時候解決衝突(這種情況或許會在網絡分區和節點失效)。

slave節點也會在 ping 包和 pong 包中向別人宣傳它的 configEpoch 字段,不過slave節點的這字段表示的是上一次跟它的master節點交換數據的時候master節點的 configEpoch 值。這能讓其他節點檢測出slave節點的配置信息是不是需要更新了(master節點不會給一個配置信息過期的slave節點投票)。

所有節點收到信息,每次由於一些已知節點的值比自己的值大而更新 configEpoch 值,都會永久性地存儲在 nodes.conf 文件中。currentEpoch也一樣。這兩人個變量更新時,節點在繼續動作前,保證同步保存到磁盤

configEpoch值使用簡單算法, 在失效轉移生成保證是最新/自增/唯一。

slave節點的選舉和升級


slave節點的選舉和提升都是由slave節點處理的,master節點會投票要提升哪個slave節點。一個slave節點的選舉發生條件:master處理FAIL狀態(至少一個slave有條件成為master)。

要讓一個slave升級成master,需要發起一次選舉並取勝。當master處於FAIL狀態,所有給定master的slave都能發起一次選舉。然後隻有一個slave能贏得選舉升級成為master。一個slave發起選舉,並要滿足如下條件:

  • 該slave節點的master節點處於FAIL 狀態。
  • 這個master節點負責的哈希槽數目不為零。
  • slave節點和master節點之間的連接斷線不超過一段給定的時間,這是為了確保slave節點的數據是可靠的。

一個slave節點想要被推選出來,那麼第一步應該是提高它的 currentEpoch 計數,並且向master節點們請求投票。

slave節點通過廣播一個 FAILOVER_AUTH_REQUEST 數據包給集群裏的每個master節點來請求選票。然後等待回複,最多等 NODE_TIMEOUT 這麼長時間(通常至少2秒)。

一旦一個master節點給這個slave節點投票,會回複一個FAILOVER_AUTH_ACK,並且在 NODE_TIMEOUT * 2 這段時間內不能再給同個master節點的其他slave節點投票。在這段時間內它完全不能回複其他授權請求。這不必保證安全,但用於同時在一輪防止多slave選舉(使用不同的configEpoch),多選舉是不需要的。

slave節點會忽視所有帶有的epoch參數比 currentEpoch 小的回應(ACKs),這樣能避免把之前的投票的算為當前的合理投票。

一旦某個slave節點收到了大多數master節點的回應,那麼它就贏得了選舉。否則,如果無法在 NODE_TIMEOUT 時間內訪問到大多數master節點(通常至少2s),那麼當前選舉會被放棄並在 NODE_TIMEOUT * 4 這段時間後由另一個slave節點嚐試發起選舉(通常至少4s)。

 

slave排名


slave是在master節點一進入 FAIL 狀態,就等一小段時間嚐試發起選舉,這段延遲是這麼計算的:

DELAY = 500 milliseconds + 0-500隨機數 milliseconds +

SLAVE_RANK * 1000 milliseconds.

固定延時確保我們會等到 FAIL 狀態在集群內廣播後,否則若slave節點嚐試發起選舉,master節點們仍然不知道那個master節點已經 FAIL,就會拒絕投票。

隨機延遲用於破壞同步slave,所以他們不太可能在同一時間開始選舉。

該SLAVE_RANK是slave從master複製過來的數據處理數量的排名。slave交換消息時,當master失敗,以(盡力而為)建立一個排名:最新的複製數據偏移量slave排名0,第二最新排名1 ,依此類推。如果靠前排名的slave選舉失敗,其它很快會接著做。

排名順序沒有嚴格執行;如果排名較高的奴隸落選,其他人會嚐試很快。

 

一旦有slave節點贏得選舉,它就會有一個比其它存在的master更大的 唯一自增的configEpoch 。 它開始通過ping 和 pong 數據包向其他節點宣布自己已經是master節點,並提供它負責的哈希槽覆蓋老的,這個數據包帶有configEpoch。

為了加速其他節點的重新配置,該節點會廣播一個 pong 包 給集群裏的所有節點. 那些現在訪問不到的節點,如果它們通過心跳包發布的信息已經過期,最終也會收到一個 ping 包或 pong 包,並且進行重新配置。

其他節點會檢測到有一個更大configEpoch的新master節點在負責處理之前一個舊的master節點負責的哈希槽,然後就升級自己的配置信息。 舊master節點的slave節點(或者是經過故障轉移後重新加入集群的該舊master節點)不僅會升級配置信息,還會配置新master節點的備份。節點怎麼重新加入集群的配置會在下一節解釋。

master節點回複slave節點的投票請求


在上一節中我們討論了slave節點是如何被選舉上的,這一節我們將從master節點的角度解釋在為給定slave節點投票的時候發生了什麼。

master節點接收到來自於slave節點、要求以 FAILOVER_AUTH_REQUEST 請求的格式投票的請求。

要授予一個投票,必須要滿足以下條件:

  • 1)一個master節點隻能投一次票給指定的epoch,並且拒絕更早的epoch:每個master節點都有一個 lastVoteEpoch 域,一旦認證請求數據包裏的currentEpoch小於 lastVoteEpoch,那麼master節點就會拒絕再次投票。當一個master節點積極響應一個投票請求,那麼 lastVoteEpoch 會相應地進行更新,同時安全地存儲到磁盤。
  • 2) 一個master節點投票給某個slave節點隻有當該slave節點的master節點被標記為FAIL。
  • 3) 如果認證請求裏的currentEpoch 小於master節點裏的 currentEpoch 的話,那麼該請求會被忽視掉。因此,master節點的回應總是帶著和認證請求一致的 currentEpoch。如果同一個slave節點在增加currentEpoch 後再次請求投票,那麼保證一個來自於master節點的、舊的延遲回複不會被新一輪選舉接受。

下麵的例子是沒有依據規則3引發的問題:

master節點的 currentEpoch 是 5, lastVoteEpoch 是 1(在幾次失敗的選舉後這也許會發生的)

  • slave節點的currentEpoch 是 3。
  • slave節點嚐試用 epoch 值為 4(3+1)來贏得選票,master節點回複 ok,裏麵的currentEpoch 是 5,可是這個回複延遲了。
  • slave節點嚐試用 epoch 值為 5(4+1)來再次贏得選票,收到的是帶著currentEpoch 值為 5 的延遲回複,這個回複會被當作有效的來接收。
  1. master節點若已經為某個失效master節點的一個slave節點投票後,在經過NODE_TIMEOUT * 2 時間之前不會為同個失效master節點的另一個slave節點投票。這並不是嚴格要求的,因為兩個slave節點用同個 epoch 來贏得選舉的可能性很低,不過在實際中,係統確保正常情況當一個slave節點被選舉上,那麼它有足夠的時間來通知其他slave節點,以避免另一個slave節點發起另一個新的選舉。
  2. master節點不會用任何方式來嚐試選出最好的slave節點,隻要slave節點的master節點處於FAIL 狀態並且投票master節點在這一輪中還沒投票,master節點就能積極投票。

最佳的slave最可能在其它slave前發起一次選舉並贏得它 ,因為它通常能更早發起選舉過程,因為它更高排名(前麵章節提到的slave排名)。

  1. 當master拒絕投票給沒有積極回應的slave,請求簡單的被忽略.
  2. master節點不會投票給那些configEpoch 值比master節點哈希槽表裏的 configEpoch 更小的slave節點。記住,slave節點發送了它的master節點的 configEpoch 值,還有它的master節點負責的哈希槽對應的位圖。這意味著,請求投票的slave節點必須擁有它想要進行故障轉移的哈希槽的配置信息,而且信息應該比它請求投票的master節點的配置信息更新或者一致。

在網絡分區下epoch配置的可用性實例


這一節解釋如何使用 epoch 概念來使得slave節點提升過程對網絡分區更有抵抗力。

  • master節點不是無限期地可達。它擁有三個slave節點 A,B,C。
  • slave節點 A 贏得了選舉並且被推選為master節點。
  • 一個網絡分區操作使得集群中的大多數節點無法訪問節點 A。
  • 節點 B 贏得了選舉並且被推選為master節點。
  • 一個網絡分區操作使得集群中大多數節點無法訪問節點 B。
  • 之前分區操作的問題被修複了,節點 A 又恢複可訪問狀態。

此刻,節點 B 已經down,節點 A 當上master又可訪問了(事實上UPDATE消息可以快速重新配置它,介此處我們假設所有UPDATE消息已經丟失),同時slave C 競選對節點 B 進行故障轉移。

這兩個有同樣的哈希槽的slave節點最終都會請求被提升,然而由於它們發布的 configEpoch 是不一樣的,而且節點 C 的 epoch 比較大,所以所有的節點都會把它們的配置更新為節點 C 的。

過程如下。

1, B就盡量當選而且一定會成功,因為對於大多數master它的master已經down。它會得到一個自增的configEpoch 。

  1. A將無法聲稱自己是master負責它的哈希槽,因為相比A,其他的節點已經具有更高的epoch,關聯了相同的哈希插槽

3。因此,所有的節點都將升級他們的哈希槽分配給C,集群將繼續運作。

正如你下麵章節看到的,一個失聯的節點重新加入集群將盡快通知到配置變更,因為一量它ping任何其它節點,接收者會檢測它已經失效的上並發送UPDATE消息.

哈希槽信息的傳播


Redis 集群很重要的一個部分是用來傳播關於集群節點負責哪些哈希槽的信息的機製。這對於新集群的啟動和提升配置(slave升級前master處理的槽)的能力來說是必不可少的。

同樣的機製讓節點劃分,因無限長的時間通過合理的方式來加入集群。

哈希槽配置傳播有兩種方法:

1.心跳消息。 ping或pong包的發送者總是帶有負責的哈希槽信息(如果是slave,就是對應master的哈希槽信息)。

2.UPDATE消息。因為每一次心跳數據包中有一個關於發送者configEpoch信息和負責的哈希槽,如果心跳包的接收方發現發送者信息是過時的,它會發送新信息的數據包,迫使過時的節點更新其信息。

心跳或UPDATE消息的接收方使用某些簡單的規則,以更新其映射的哈希插槽節點。當創建一個新的Redis集群,其本地哈希槽映射表隻是初始化為NULL的條目,使每個哈希位置沒有綁定或鏈接到任何節點。這看起來類似於以下內容:

0 -> NULL
1 -> NULL
2 -> NULL
...
16383 -> NULL

第一個規則允許節點來更新它的哈希槽映射表如下:

規則一:如果一個哈希槽沒有賦值,(設置為NULL),並且一個已經節點申明它,我們修改哈希槽映射表並關聯申明的哈希槽,因此如果我們收到節點A的心跳包申明負責槽1和2,配置epoch是3,映射表修改如下:

0 -> NULL
1 -> A [3]
2 -> A [3]
...
16383 -> NULL

當創建一個新的集群,一個係統管理員需要手動分配(使用CLUSTER ADDSLOTS命令,通過redis – trib命令行工具,或通過任何其他方式)各主節點對節點本身負責的槽,這些信息將迅速在群集中傳播。

但是這條規則是不夠的。我們知道,哈希槽映射可以因兩個事件改變:

  1. 一個slave在故障轉移中替換它的master
  2. 一個槽從一個節點重新分片到不同節點

到此為止,我們專注在故障轉移。當一個slave故障轉移掉它的master, 它獲得被保證是大於它的主人的配置epoch(更通常大於任何其他先前生成的配置epoch)。例如節點B,是一個A的slave,可以用配置epoch4故障轉移B,它將開始發送心跳數據包(第一次大規模廣播集群範圍內),也因為以下第二個規則,接收者將更新他們的哈希槽映射表:規則2: 如果一個哈希槽已經分配,和已知的節點首播它使用的configEpoch大於目前master關聯槽的configEpoch,我會重新綁定哈希槽到新節點。所以從B接收消息,用配置epoch申明負責槽1和2,接收者將用如下方式更新它們的映射表:

0 -> NULL
1 -> B [4]
2 -> B [4]
...
16383 -> NULL

實時屬性:因為第二個規則,最終所有集群中的節點同意槽的歸屬節點在節點首播時擁有最大configEpoch

這個原理在redis集群中叫做 最後故障轉移取勝

這同樣發生在重新分片。當一個節點導入哈希槽完成導入操作,它的配置epoch會增加來保證這個改變會在集群中傳播。

細看UPDATE 消息


帶著上一節,是比較容易看到更新消息是如何工作的。節點A可能一段時間後重新加入集群。它用配置epoch 3申明負責哈希槽1和2,所有的更新信息接收者將轉而看到相同的哈希插槽關聯著具有較高配置epoch的節點B,將發送心跳包。正因為如此,他們會發送帶有最新哈希槽配置的UPDATE信息到A。 A將更新其配置,因為上麵的規則2。

 

節點怎麼重新加入集群


當一個節點重新加入的集群,應用了相同的原理。繼續上麵的例子,節點A將通知哈希槽1和2現在由B負責.假設這兩個哈希槽以前都由A負責。假設A負責的槽數量隻有兩人個,A負責的槽數降為0!因此A將重新配置成為新master的slave。

 

事實上後麵的規則比這複雜一點。通常可能A會過很長時間再加入集群,與此同時可能開始由A負責的槽由多個節點負責,比如B負責槽1,C負責槽2.

所以真實的redis集群節點角色切換規則是:master節點會改變配置來變成slave, 它的master是拿走它最後一個槽的master.

在重新配置期間,最終負責的槽數會變成0, 節點會相應的重新配置。注意,在這基本條件下意味著老master會成為它slave失效轉移後的slave. 然而通用規則會覆蓋所有可能情況。

 

slave 做完全一樣的事:它們重新配置來做slave , 它們的master是拿走老master最後一個槽的master.

備份遷移


Redis 集群實現了一個叫做備份遷移(replica migration的概念,以提高係統的可用性。在集群中有master-slave的設定,如果主slave節點間的映射關係是固定的,master/slave一定的可用性受限於故障時間,這個時間發生多個單一節點獨立故障。

例如在一個集群中,每個master節點都隻有一個slave節點,任何一對master-slave的一個出故障的時候集群能讓操作繼續執行下去,一對同都出故障就不行。然而這樣長期會積累很多由硬件或軟件問題引起的單一節點獨立故障。例如:

  • master A隻有一個slave A1。
  • master A 失效了。A1 升級成新master。
  • 三個小時後,A1 因為一個獨立事件(跟節點 A 的失效無關)失效了。由於沒有其他slave節點可以升級為master節點,因為節點 A 仍然down著,集群沒法繼續進行正常操作。

如果master-slave的映射關係是固定的,那要讓集群對上述情況更具容災能力 , 唯一方法就是為每個master多加slave。然而這要付出的代價也更昂貴,因為要求 Redis 部署更多的實例、更多的內存等等。

一個候選方案就是在集群中創建不對稱性,然後讓集群布局時隨著時間自動變化。例如,假設集群有三個master A,B,C。節點 A 和 B 都各有一個slave節點,A1 和 B1。節點 C 有兩個slave:C1 和 C2。

備份遷移是slave節點自動重新配置的過程,為了遷移到一個沒有可工作slave節點的master節點上。按上麵提到的方案,備份遷移過程如下:

  • master A 失效。A1 升級成master。
  • 節點 C2 遷移成為節點 A1 的slave,要不然 A1 就沒有任何slave備份。
  • 三個小時後節點 A1 也失效了。
  • C2 被提升為取代 A1 的新master。
  • 集群仍然能繼續正常工作。

備份遷移算法


遷移算法不用任何形式的協議,因為 Redis 集群中的slave節點布局不是集群配置信息的一部分,配置信息要求前後一致並且者用 config epochs 來標記版本號。 當一個master沒有備份時,它使用一個算法避免slave節點大批遷移。這個算法保證,一旦集群配置信息穩定下來,最終每個master節點都至少會有一個slave節點作為備份。

 

這個算法是如何工作的。在開始之前我們需要定義清楚在這個上下文中什麼才算是一個好的slave節點:一個健康的slave節點是指節點不處於 FAIL 狀態。

檢測出有一個master沒有健康slave,那麼就會觸發這個算法的執行。然而在所有檢測出這種情況的slave節點中,隻有一部分slave節點會采取行動。 通常這部分slave隻有一個,除非有不同的slave在給定時刻對其他節點的失效狀態有稍微不同的視角。

 

采取行動的slave節點是屬於那些綁定了最多slave的master節點,並且不處於 FAIL 狀態及擁有最小的節點 ID。

例如,如果有 10 個master節點,它們各有 1 個slave節點,另外還有 2 個master節點,它們各有 5 個slave節點。會發生遷移的slave是-那 2 個擁有 5 個slave的master中的所有slave裏-節點 ID 最小的那個。鑒於沒用到任何協議,在集群配置信息不穩定的情況下,有可能發生一種競爭情況,多個slave節點都認為自己是不處於 FAIL 狀態並且擁有較小節點 ID(實際上這是一種比較難出現的狀況)。如果這種情況發生的話,結果是多個slave節點都會遷移到同個master節點下,不過這種結局是無害的。這種競爭發生的話,有時候會使得割讓出slave的master變成沒有任何備份節點,當集群再次達到穩定狀態的時候,本算法會再次執行,然後把slave節點遷移回它原來的master節點。

最終每個master節點都會至少有一個slave備份。通常表現象是,一個slave從一個擁有多slave的master節點遷移到一個孤立的master節點。

這個算法能通過一個用戶可配置的參數控製 cluster-migration-barrier : 一個master節點在擁有多少個健康slave節點的時候就要割讓一個slave節點出來。例如這個參數設為 2,那麼隻有當一個master節點擁有 2 個可工作的slave節點時,它的一個slave節點會嚐試遷移。

 

configEpoch衝突解決算法


當slave通過故障轉移中升級,會生成新的configEpoch值,它可以保證是唯一的。

但也有兩個不同的事件, 新configEpoch值以不安全的方式創建,隻是遞增本地節點的本地currentEpoch ,希望同一時間不存在衝突。這兩個事件是係統管理員觸發:

  1. 當在多數master不可達,CLUSTER FAILOVER 和TAKEOVER選項是用來手工升級slave成master 。 這很管用,比如,多數據中心的設置 .
  2. 當沒有協議性能問題,集群重新平衡遷移哈希槽時在本地節點也產生新的配置epoch

 

具體來說,在手工重新分片中,當一個槽從A遷移到B,重新分片的程序會迫使B升級它的配置epoch,它是集群中最大的加1(除非節點已經是配置epoch中最大),節點相互之沒有必需的協議。通常實際的重新分片涉及幾百個哈希槽(特別是在小集群中)。 每個哈希槽遷移, 需要一個協議在重新分片過程中來產生新的配置epoch,這是低效的. 另外,它需要集群節點實時同步來存儲最新的配置,因為它的這種運行方式,當第一個槽移動隻需要一個新的配置epoch,使得在生產環境更加高效.

 

然而,因為上述兩種情況的,它是可能的(雖然不太可能) ,最終多個節點有相同的配置epoch。一個重新分片操作由係統管理員執行,並在故障轉移發生在同一時間(加了很多的壞運氣),如果他們不傳播速度不夠快可能會導致currentEpoch碰撞。此外,軟件錯誤和文件係統損壞也可能導致多節點有相同的配置epoch。

當負責不同的槽的master有相同的configEpoch, 這沒有問題. 可能slave故障轉移成master有一個唯一的配置epoch. 也就是說,手工幹預或重新分片可能用不同的方式改變集群配置。redis集群主要的活越性要求槽配置經常匯總,所以在每種環境下,我們想要所有master有不同的configEpoch.

為了增強這個,使用一個衝突解決算法,來保證兩個節點最終有相同configEpoch.

  • 如果一個master發現其它master正在宣傳跟它相同的configEpoch.
  • 並且如果節點有按字典順序比其它節點有更小的節點ID,申明相同的configEpoch.
  • 然後它把currentEpoch加1作為新的configEpoch.

如果有一些節點有相同的configEpoch,除了最大節點 ID的節點都將前進,保無論發生什麼證最終有唯一configEpoch。

這個原理也保證在新集群創建後,節點開始有不同的configEpoch(即使這沒有使用)因為redis-trib保證使用CONFIG SET-CONFIG-EPOCH來起動。然而,如果節點由於某些原因誤配置了,它會自動更新配置成另外一個。

節點重置


節點可以軟重置(不重啟的情況下)來來重新使用不同的角色或加入到不同的集群。 這在普通操作、測試、雲環境(給定的節點可以另行配置加入到一堆集群來擴大或創建新集群)中非常有用。

在集群節點中使用CLUSTER RESET 來重置,命令提供兩個選項:

  • CLUSTER RESET SOFT
  • CLUSTER RESET HARD

命令必需直接發送到節點重置。如果沒有加上重置類型,默認是軟重置.

下麵是reset執行的一係列操作:

  1. 軟重置和硬重置:如果節點是slave,變成了master,數據集被丟棄。如果節點是master,包含了鍵會放棄執行重置
  2. 軟重置和硬重置:所有槽釋放,手工故障轉移也重置
  3. 軟重置和硬重置:節點列表會移除其它節點,節點不再知道其它節點
  4. 單獨硬重置:currentEpoch,configEpoch, 和lastVoteEpoch 設置成 0.
  5. 單獨硬重置:節點ID變成新的隨機ID.

非空數據集的master不能重置(因為通常你想重新分布數據到其它節點)。然後,在特殊條件下,這是合適的(比如當一個集群完全銷毀來創建一個新的),FLUSHALL必需在重置前執行.

集群刪除節點


實際中可能從一個集群中刪除一個節點,把數據重新分布到其它節點(如果它是master),然後關掉它。然而,其它節點仍然記錄了它的節點ID和地址,並且嚐試重連.

因為這個原因,當刪除一個節點,我們想把條目從所有其它節點列表中刪除。這可以使用CLUSTER FORGET <node-id> 命令來實現,這個命令做兩件事:

1.從節點列表中刪除指定的節點ID.

2.在60s間隔內,阻止相同節點ID的節點重新加入.

 

第二項有必要是因為redis集群使用gossip來自動發現節點,因為從節點A移除節點X,會導致B 把節點X又告訴A。 因為這60s間隔,redis集群管理工具有60s在所有節點移除節點,阻止由於重新發現重新加入節點。

 

CLUSTER FORGET 文檔中可以得到更多信息.

發布/訂閱(Publish/Subscribe)


在一個 Redis 集群中,客戶端能訂閱任何一個節點,也能發布消息給任何一個節點。集群會確保發布的消息都會按需進行轉發。

目前的實現方式是單純地向所有節點廣播所有的發布消息,在某些點,將來會用 bloom filters 或其他算法來優化。

附錄 A:CRC16算法的 ANSI C 版本的參考實現

/*
 * Copyright 2001-2010 Georges Menie (www.menie.org)
 * Copyright 2010 Salvatore Sanfilippo (adapted to Redis coding style)
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the University of California, Berkeley nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/* CRC16 implementation according to CCITT standards.
 *
 * Note by @antirez: this is actually the XMODEM CRC 16 algorithm, using the
 * following parameters:
 *
 * Name                       : "XMODEM", also known as "ZMODEM", "CRC-16/ACORN"
 * Width                      : 16 bit
 * Poly                       : 1021 (That is actually x^16 + x^12 + x^5 + 1)
 * Initialization             : 0000
 * Reflect Input byte         : False
 * Reflect Output CRC         : False
 * Xor constant to output CRC : 0000
 * Output for "123456789"     : 31C3
 */

static const uint16_t crc16tab[256]= {
    0x0000,0x1021,0x2042,0x3063,0x4084,0x50a5,0x60c6,0x70e7,
    0x8108,0x9129,0xa14a,0xb16b,0xc18c,0xd1ad,0xe1ce,0xf1ef,
    0x1231,0x0210,0x3273,0x2252,0x52b5,0x4294,0x72f7,0x62d6,
    0x9339,0x8318,0xb37b,0xa35a,0xd3bd,0xc39c,0xf3ff,0xe3de,
    0x2462,0x3443,0x0420,0x1401,0x64e6,0x74c7,0x44a4,0x5485,
    0xa56a,0xb54b,0x8528,0x9509,0xe5ee,0xf5cf,0xc5ac,0xd58d,
    0x3653,0x2672,0x1611,0x0630,0x76d7,0x66f6,0x5695,0x46b4,
    0xb75b,0xa77a,0x9719,0x8738,0xf7df,0xe7fe,0xd79d,0xc7bc,
    0x48c4,0x58e5,0x6886,0x78a7,0x0840,0x1861,0x2802,0x3823,
    0xc9cc,0xd9ed,0xe98e,0xf9af,0x8948,0x9969,0xa90a,0xb92b,
    0x5af5,0x4ad4,0x7ab7,0x6a96,0x1a71,0x0a50,0x3a33,0x2a12,
    0xdbfd,0xcbdc,0xfbbf,0xeb9e,0x9b79,0x8b58,0xbb3b,0xab1a,
    0x6ca6,0x7c87,0x4ce4,0x5cc5,0x2c22,0x3c03,0x0c60,0x1c41,
    0xedae,0xfd8f,0xcdec,0xddcd,0xad2a,0xbd0b,0x8d68,0x9d49,
    0x7e97,0x6eb6,0x5ed5,0x4ef4,0x3e13,0x2e32,0x1e51,0x0e70,
    0xff9f,0xefbe,0xdfdd,0xcffc,0xbf1b,0xaf3a,0x9f59,0x8f78,
    0x9188,0x81a9,0xb1ca,0xa1eb,0xd10c,0xc12d,0xf14e,0xe16f,
    0x1080,0x00a1,0x30c2,0x20e3,0x5004,0x4025,0x7046,0x6067,
    0x83b9,0x9398,0xa3fb,0xb3da,0xc33d,0xd31c,0xe37f,0xf35e,
    0x02b1,0x1290,0x22f3,0x32d2,0x4235,0x5214,0x6277,0x7256,
    0xb5ea,0xa5cb,0x95a8,0x8589,0xf56e,0xe54f,0xd52c,0xc50d,
    0x34e2,0x24c3,0x14a0,0x0481,0x7466,0x6447,0x5424,0x4405,
    0xa7db,0xb7fa,0x8799,0x97b8,0xe75f,0xf77e,0xc71d,0xd73c,
    0x26d3,0x36f2,0x0691,0x16b0,0x6657,0x7676,0x4615,0x5634,
    0xd94c,0xc96d,0xf90e,0xe92f,0x99c8,0x89e9,0xb98a,0xa9ab,
    0x5844,0x4865,0x7806,0x6827,0x18c0,0x08e1,0x3882,0x28a3,
    0xcb7d,0xdb5c,0xeb3f,0xfb1e,0x8bf9,0x9bd8,0xabbb,0xbb9a,
    0x4a75,0x5a54,0x6a37,0x7a16,0x0af1,0x1ad0,0x2ab3,0x3a92,
    0xfd2e,0xed0f,0xdd6c,0xcd4d,0xbdaa,0xad8b,0x9de8,0x8dc9,
    0x7c26,0x6c07,0x5c64,0x4c45,0x3ca2,0x2c83,0x1ce0,0x0cc1,
    0xef1f,0xff3e,0xcf5d,0xdf7c,0xaf9b,0xbfba,0x8fd9,0x9ff8,
    0x6e17,0x7e36,0x4e55,0x5e74,0x2e93,0x3eb2,0x0ed1,0x1ef0
};

uint16_t crc16(const char *buf, int len) {
    int counter;
    uint16_t crc = 0;
    for (counter = 0; counter < len; counter++)
            crc = (crc<<8) ^ crc16tab[((crc>>8) ^ *buf++)&0x00FF];
    return crc;
}

最後更新:2017-05-22 10:03:04

  上一篇:go  《Redis官方文檔》Redis集群教程
  下一篇:go  《Redis官方教程》Redis集群規範(一)