Redis集羣規範(一)

什麼是 Redis 集羣

Redis 集羣是一個分佈式(distributed)、容錯(fault-tolerant)的 Redis 實現, 集羣可以使用的功能是普通單機 Redis 所能使用的功能的一個子集(subset)。

Redis 集羣中不存在中心(central)節點或者代理(proxy)節點, 集羣的其中一個主要設計目標是達到線性可擴展性(linear scalability)。

Redis 集羣爲了保證一致性(consistency)而犧牲了一部分容錯性: 系統會在保證對網絡斷線(net split)和節點失效(node failure)具有有限(limited)抵抗力的前提下, 儘可能地保持數據的一致性。

集羣將節點失效視爲網絡斷線的其中一種特殊情況。

集羣的容錯功能是通過使用主節點(master)從節點(slave)兩種角色(role)的節點(node)來實現的:

  • 主節點和從節點使用完全相同的服務器實現, 它們的功能(functionally)也完全一樣, 但從節點通常僅用於替換失效的主節點。
  • 不過, 如果不需要保證“先寫入,後讀取”操作的一致性(read-after-write consistency), 那麼可以使用從節點來執行只讀查詢。

Redis 集羣不像單機 Redis 那樣支持多數據庫功能, 集羣只使用默認的 0 號數據庫, 並且不能使用 SELECT 命令

Redis 集羣中的節點有以下責任:

  • 持有鍵值對數據。
  • 記錄集羣的狀態,包括鍵到正確節點的映射(mapping keys to right nodes)。
  • 自動發現其他節點,識別工作不正常的節點,並在有需要時,在從節點中選舉出新的主節點。
    爲了執行以上列出的任務, 集羣中的每個節點都與其他節點建立起了“集羣連接(cluster bus)”, 該連接是一個 TCP 連接, 使用二進制協議進行通訊。

節點之間使用 Gossip 協議 來進行以下工作:

  • 傳播(propagate)關於集羣的信息,以此來發現新的節點。
  • 向其他節點發送 PING 數據包,以此來檢查目標節點是否正常運作。
  • 在特定事件發生時,發送集羣信息。
    集羣節點不能代理(proxy)命令請求, 所以客戶端應該在節點返回 -MOVED 或者 -ASK 轉向(redirection)錯誤時, 自行將命令請求轉發至其他節點。

鍵分佈模型

Redis 集羣的鍵空間被分割爲 16384 個槽(slot), 集羣的最大節點數量也是 16384 個

鍵空間:redis是一個鍵值對數據庫,數據庫中的鍵值對就由字典保存:每個數據庫都有一個與之相對應的字典,這個字典被稱爲鍵空間。當用戶添加一個鍵值對到數據庫(不論鍵值對是什麼類型),程序就講該鍵值對添加到鍵空間,刪除同理。

推薦的最大節點數量爲 1000 個左右。

每個主節點都負責處理 16384 個哈希槽的其中一部分。
當我們說一個集羣處於“穩定”(stable)狀態時, 指的是集羣沒有在執行重配置(reconfiguration)操作, 每個哈希槽都只由一個節點進行處理。

重配置指的是將某個/某些槽從一個節點移動到另一個節點。
一個主節點可以有任意多個從節點, 這些從節點用於在主節點發生網絡斷線或者節點失效時, 對主節點進行替換。

以下是負責將鍵映射到槽的算法:
HASH_SLOT = CRC16(key) mod 16384
以下是該算法所使用的參數:

• 算法的名稱: XMODEM (又稱 ZMODEM 或者 CRC-16/ACORN)
• 結果的長度: 16 位
• 多項數(poly): 1021 (也即是 x16 + x12 + x5 + 1)
• 初始化值: 0000
• 反射輸入字節(Reflect Input byte): False
• 發射輸出 CRC (Reflect Output CRC): False
• 用於 CRC 輸出值的異或常量(Xor constant to output CRC): 0000
• 該算法對於輸入 “123456789” 的輸出: 31C3

CRC16 算法所產生的 16 位輸出中的 14 位會被用到。在我們的測試中, CRC16 算法可以很好地將各種不同類型的鍵平穩地分佈到 16384 個槽裏面。

集羣節點屬性

每個節點在集羣中都有一個獨一無二的 ID , 該 ID 是一個十六進制表示的 160 位隨機數, 在節點第一次啓動時由 /dev/urandom 生成。

節點會將它的 ID 保存到配置文件, 只要這個配置文件不被刪除, 節點就會一直沿用這個 ID 。

節點 ID 用於標識集羣中的每個節點。 一個節點可以改變它的 IP 和端口號, 而不改變節點 ID 。 集羣可以自動識別出 IP/端口號的變化, 並將這一信息通過 Gossip 協議廣播給其他節點知道。

以下是每個節點都有的關聯信息, 並且節點會將這些信息發送給其他節點:

  • 節點所使用的 IP 地址和 TCP 端口號。
  • 節點的標誌(flags)。
  • 節點負責處理的哈希槽。
  • 節點最近一次使用集羣連接發送 PING 數據包(packet)的時間。
  • 節點最近一次在回覆中接收到 PONG 數據包的時間。
  • 集羣將該節點標記爲下線的時間。
  • 該節點的從節點數量。
  • 如果該節點是從節點的話,那麼它會記錄主節點的節點 ID 。 如果這是一個主節點的話,那麼主節點 ID 這一欄的值爲 0000000。

以上信息的其中一部分可以通過向集羣中的任意節點(主節點或者從節點都可以)發送 CLUSTER NODES 命令來獲得。

$ redis-cli cluster nodes

d1861060fe6a534d42d8a19aeb36600e18785e04 :0 myself - 0 1318428930 connected 0-1364

3886e65cc906bfd9b1f7e7bde468726a052d1dae 127.0.0.1:6380 master - 1318428930 1318428931 connected 1365-2729

d289c575dcbc4bdd2931585fd4339089e461a27d 127.0.0.1:6381 master - 1318428931 1318428932 connected 2730-4095

在上面列出的三行信息中, 從左到右的各個域分別是:
節點ID IP:port 標誌(flag) 最後發送 PING 的時間 最後接收 PONG 的時間 連接狀態 節點負責處理的槽。

MOVED 轉向

一個 Redis 客戶端可以向集羣中的任意節點(包括從節點)發送命令請求。 節點會對命令請求進行分析, 如果該命令是集羣可以執行的命令, 那麼節點會查找這個命令所要處理的鍵所在的槽。

如果要查找的哈希槽正好就由接收到命令的節點負責處理, 那麼節點就直接執行這個命令。
如果所查找的槽不是由該節點處理的話, 節點將查看自身內部所保存的哈希槽到節點 ID 的映射記錄, 並向客戶端回覆一個 MOVED 錯誤。
以下是一個 MOVED 錯誤的例子:

GET x
-MOVED 3999 127.0.0.1:6381

錯誤信息包含鍵 x 所屬的哈希槽 3999 , 以及負責處理這個槽的節點的 IP 和端口號 127.0.0.1:6381 。 客戶端需要根據這個 IP 和端口號, 向所屬的節點重新發送一次 GET 命令請求。

當重新發送get x命令之前。集羣配置重新發生了變化,首次moved指向的127.0.0.1:6381不再處理哈希槽3999,則再次發送get命令之後仍然返回moved錯誤。並指示當前真正處理哈希槽的節點。

注意, 當集羣處於穩定狀態時, 所有客戶端最終都會保存有一個哈希槽至節點的映射記錄(map of hash slots to nodes), 使得集羣非常高效: 客戶端可以直接向正確的節點發送命令請求, 無須轉向、代理或者其他任何可能發生單點故障(single point failure)的實體(entiy)。
除了 MOVED 轉向錯誤之外, 一個客戶端還應該可以處理稍後介紹的 ASK 轉向錯誤。

ASK 轉向

當節點需要讓一個客戶端長期地(permanently)將針對某個槽的命令請求發送至另一個節點時, 節點向客戶端返回 MOVED 轉向。
另一方面, 當節點需要讓客戶端僅僅在下一個命令請求中轉向至另一個節點時, 節點向客戶端返回 ASK 轉向。(單次請求)

比如說, 在我們上一節列舉的槽 8 的例子中, 因爲槽 8 所包含的各個鍵分散在節點 A 和節點 B 中, 所以當客戶端在節點 A 中沒找到某個鍵時, 它應該轉向到節點 B 中去尋找, 但是這種轉向應該僅僅影響一次命令查詢, 而不是讓客戶端每次都直接去查找節點 B : 在節點 A 所持有的屬於槽 8 的鍵沒有全部被遷移到節點 B 之前, 客戶端應該先訪問節點 A , 然後再訪問節點 B 。
因爲這種轉向只針對 16384 個槽中的其中一個槽, 所以轉向對集羣造成的性能損耗屬於可接受的範圍。

因爲上述原因, 如果我們要在查找節點 A 之後, 繼續查找節點 B , 那麼客戶端在向節點 B 發送命令請求之前, 應該先發送一個ASKING 命令, 否則這個針對帶有 IMPORTING 狀態的槽的命令請求將被節點 B 拒絕執行。

接收到客戶端 ASKING 命令的節點將爲客戶端設置一個一次性的標誌(flag), 使得客戶端可以執行一次針對 IMPORTING 狀態的槽的命令請求。

從客戶端的角度來看, ASK 轉向的完整語義(semantics)如下:

  • 如果客戶端接收到 ASK 轉向, 那麼將命令請求的發送對象調整爲轉向所指定的節點。
  • 先發送一個 ASKING 命令,然後再發送真正的命令請求。
  • 不必更新客戶端所記錄的槽 8 至節點的映射: 槽 8 應該仍然映射到節點 A , 而不是節點 B 。

一旦節點 A 針對槽 8 的遷移工作完成, 節點 A 在再次收到針對槽 8 的命令請求時, 就會向客戶端返回 MOVED 轉向, 將關於槽 8的命令請求長期地轉向到節點 B 。
注意, 即使客戶端出現 Bug , 過早地將槽 8 映射到了節點 B 上面, 但只要這個客戶端不發送 ASKING 命令, 客戶端發送命令請求的時候就會遇上 MOVED 錯誤, 並將它轉向回節點 A 。

Redis 集羣的一致性保證(guarantee)

Redis 集羣不保證數據的強一致性(strong consistency): 在特定條件下, Redis 集羣可能會丟失已經被執行過的寫命令。原因有如下兩點:
(1)、使用異步複製(asynchronous replication)考慮以下這個寫命令的例子:

  • 客戶端向主節點 B 發送一條寫命令。
  • 主節點 B 執行寫命令,並向客戶端返回命令回覆。
  • 主節點 B 將剛剛執行的寫命令複製給它的從節點 B1 、 B2 和 B3 。
    如你所見, 主節點對命令的複製工作發生在返回命令回覆之後先返回客戶端回覆,再執行從節點數據複製, 因爲如果每次處理命令請求都需要等待複製操作完成的話, 那麼主節點處理命令請求的速度將極大地降低 —— 我們必須在性能和一致性之間做出權衡。

(2) 、集羣出現網絡分裂(network partition), 並且一個客戶端與至少包括一個主節點在內的少數(minority)實例被孤立。

假設集羣包含 A 、 B 、 C 、 A1 、 B1 、 C1 六個節點, 其中 A 、B 、C 爲主節點, 而 A1 、B1 、C1 分別爲三個主節點的從節點, 另外還有一個客戶端 Z1 。

集羣中發生網絡分裂, 那麼集羣可能會分裂爲兩方, 大多數(majority)的一方包含節點 A 、C 、A1 、B1 和 C1 , 而少數(minority)的一方則包含節點 B 和客戶端 Z1 。

在網絡分裂期間, 主節點 B 仍然會接受 Z1 發送的寫命令:

  • 如果網絡分裂出現的時間很短, 那麼集羣會繼續正常運行;
  • 如果網絡分裂出現的時間足夠長,從節點 B1被選舉代替原來的主節點 B , 那麼 Z1 發送給主節點 B 的寫命令將丟失。

在網絡分列期間,如果節點B未能在節點超時限制之內從新連接上集羣

  • 該主節點將停止處理寫命令, 並向客戶端報告錯誤。
  • 集羣會將這個主節點視爲下線, 並使用從節點來代替這個主節點繼續工作。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章