【譯】Redis集群規(guī)范 (Redis Cluster Specification)

Redis Cluster Specification

1 設計目標和理由

1.1 Redis Cluster goals

  • 高性能可線性擴展至最多1000節(jié)點。集群中沒有代理,(集群節(jié)點間)使用異步復制,沒有歸并操作(merge operations on values)
  • 可接受的寫入安全:系統(tǒng)嘗試(采用best-effort方式)保留所有連接到master節(jié)點的client發(fā)起的寫操作。通常會有一個小的時間窗,時間窗內(nèi)的已確認寫操作可能丟失(即,在發(fā)生failover之前的小段時間窗內(nèi)的寫操作可能在failover中丟失)。而在(網(wǎng)絡)分區(qū)故障下,對少數(shù)派master的寫入,發(fā)生寫丟失的時間窗會很大。
  • 可用性:Redis Cluster在以下場景下集群總是可用:大部分master節(jié)點可用,并且對少部分不可用的master,每一個master至少有一個當前可用的slave。更進一步,通過使用 replicas migration 技術,當前沒有slave的master會從當前擁有多個slave的master接受到一個新slave來確??捎眯?。

1.2 Clients and Servers roles in the Redis Cluster protocol

  • Redis Cluster的節(jié)點負責維護數(shù)據(jù),和獲取集群狀態(tài),這包括將keys映射到正確的節(jié)點。集群節(jié)點同樣可以自動發(fā)現(xiàn)其他節(jié)點、檢測不工作節(jié)點、以及在發(fā)現(xiàn)故障發(fā)生時晉升slave節(jié)點到master
  • 所有集群節(jié)點通過由TCP和二進制協(xié)議組成的稱為 Redis Cluster Bus 的方式來實現(xiàn)集群的節(jié)點自動發(fā)現(xiàn)、故障節(jié)點探測、slave升級為master等任務。每個節(jié)點通過cluster bus連接所有其他節(jié)點。節(jié)點間使用gossip協(xié)議進行集群信息傳播,以此來實現(xiàn)新節(jié)點發(fā)現(xiàn),發(fā)送ping包以確認對端工作正常,以及發(fā)送cluster消息用來標記特定狀態(tài)。cluster bus還被用來在集群中創(chuàng)博Pub/Sub消息,以及在接收到用戶請求后編排手動failover。

1.3 Write safety

  • Redis Cluster在節(jié)點間采用了異步復制,以及 last failover wins 隱含合并功能(implicit merge function)(【譯注】不存在合并功能,而是總是認為最近一次failover的節(jié)點是最新的)。這意味著最后被選舉出的master所包含的數(shù)據(jù)最終會替代(同一前master下)所有其他備份(replicas/slaves)節(jié)點(包含的數(shù)據(jù))。當發(fā)生分區(qū)問題時,總是會有一個時間窗內(nèi)會發(fā)生寫入丟失。然而,對連接到多數(shù)派master(majority of masters)的client,以及連接到少數(shù)派master(mimority of masters)的client,這個時間窗是不同的。
  • 相比較連接到少數(shù)master(minority of masters)的client,對連接到多數(shù)master(majority of masters)的client發(fā)起的寫入,Redis cluster會更努力地嘗試將其保存。 下面的場景將會導致在主分區(qū)的master上,已經(jīng)確認的寫入在故障期間發(fā)生丟失:
    1. 寫入請求達到master,但是當master執(zhí)行完并回復client時,寫操作可能還沒有通過異步復制傳播到它的slave。如果master在寫操作抵達slave之前掛了,并且master無法觸達(unreachable)的時間足夠長而導致了slave節(jié)點晉升,那么這個寫操作就永遠地丟失了。通常很難直接觀察到,因為master嘗試回復client(寫入確認)和傳播寫操作到slave通常幾乎是同時發(fā)生。然而,這卻是真實世界中的故障方式。(【譯注】不考慮返回后宕機的場景,因為宕機導致的寫入丟失,在單機版redis上同樣存在,這不是redis cluster引入的目的及要解決的問題
    2. 另一種理論上可能發(fā)生寫入丟失的模式是:
      • master因為分區(qū)原因不可用(unreachable)
      • 該master被某個slave替換(failover)
      • 一段時間后,該master重新可用
      • 在該old master變?yōu)閟lave之前,一個client通過過期的路由表對該節(jié)點進行寫入。
  • 上述第二種失敗場景通常難以發(fā)生,因為:1)少數(shù)派master(minority master)無法與多數(shù)派master(majority master)通信達到一定的時間后,它將拒絕寫入,并且當分區(qū)恢復后,該master在重新與多數(shù)派master建立連接后,還將保持拒絕寫入狀態(tài)一小段時間來感知集群配置變化。留給client可寫入的時間窗很小。2)發(fā)生這種錯誤還有一個前提是,client一直都在使用過期的路由表(而實際上集群因為發(fā)生了failover,已有slave發(fā)生了晉升)。
  • 寫入少數(shù)派master(minority side of a partition)會有一個更長的時間窗會導致數(shù)據(jù)丟失。因為如果最終導致了failover,則寫入少數(shù)派master的數(shù)據(jù)將會被多數(shù)派一側(cè)(majority side)覆蓋(在少數(shù)派master作為slave重新接入集群后)。
  • 特別地,如果要發(fā)生failover,master必須至少在NODE_TIMEOUT時間內(nèi)無法被多數(shù)masters(majority of maters)連接,因此如果分區(qū)在這一時間內(nèi)被修復,則不會發(fā)生寫入丟失。當分區(qū)持續(xù)時間超過NODE_TIMEOUT時,所有在這段時間內(nèi)對少數(shù)派master(minority side)的寫入將會丟失。然而少數(shù)派一側(cè)(minority side)將會在NODE_TIMEOUT時間之后如果還沒有連上多數(shù)派一側(cè),則它會立即開始拒絕寫入,因此對少數(shù)派master而言,存在一個進入不可用狀態(tài)的最大時間窗。在這一時間窗之外,不會再有寫入被接受或丟失。

1.4 可用性(Availability)

  • Redis Cluster在少數(shù)派分區(qū)側(cè)不可用。在多數(shù)派分區(qū)側(cè),假設由多數(shù)派masters存在并且不可達的master有一個slave,cluster將會在NODE_TIMEOUT外加重新選舉所需的一小段時間(通常1~2秒)后恢復可用。
  • 這意味著,Redis Cluster被設計為可以忍受一小部分節(jié)點的故障,但是如果需要在大網(wǎng)絡分裂(network splits)事件中(【譯注】比如發(fā)生多分區(qū)故障導致網(wǎng)絡被分割成多塊,且不存在多數(shù)派master分區(qū))保持可用性,它不是一個合適的方案(【譯注】比如,不要嘗試在多機房間部署redis cluster,這不是redis cluster該做的事)。
  • 假設一個cluster由N個master節(jié)點組成并且每個節(jié)點僅擁有一個slave,在多數(shù)側(cè)只有一個節(jié)點出現(xiàn)分區(qū)問題時,cluster的多數(shù)側(cè)(majority side)可以保持可用,而當有兩個節(jié)點出現(xiàn)分區(qū)故障時,只有 1-(1/(N*2-1)) 的可能性保持集群可用。
  • 也就是說,如果有一個由5個master和5個slave組成的cluster,那么當兩個節(jié)點出現(xiàn)分區(qū)故障時,它有 1/(5*2-1)=11.11%的可能性發(fā)生集群不可用。
  • Redis cluster提供了一種成為 Replicas Migration 的有用特性特性,它通過自動轉(zhuǎn)移備份節(jié)點到孤master節(jié)點,在真實世界的常見場景中提升了cluster的可用性。在每次成功的failover之后,cluster會自動重新配置slave分布以盡可能保證在下一次failure中擁有更好的抵御力。

2.1.5 性能(Performance)

  • Redis Cluster不會將命令路由到其中的key所在的節(jié)點,而是向client發(fā)一個重定向命令 (- MOVED) 引導client到正確的節(jié)點。
  • 最終client會獲得一個最新的cluster(hash slots分布)展示,以及哪個節(jié)點服務于命令中的keys,因此clients就可以獲得正確的節(jié)點并用來繼續(xù)執(zhí)行命令。
  • 因為master和slave之間使用異步復制,節(jié)點不需要等待其他節(jié)點對寫入的確認(除非使用了WAIT命令)就可以回復client。
  • 同樣,因為multi-key命令被限制在了臨近的key(near keys)(【譯注】即同一hash slot內(nèi)的key,或者從實際使用場景來說,更多的是通過hash tag定義為具備相同hash字段的有相近業(yè)務含義的一組keys),所以除非觸發(fā)resharding,數(shù)據(jù)永遠不會在節(jié)點間移動。
  • 普通的命令(normal operations)會像在單個redis實例那樣被執(zhí)行。這意味著一個擁有N個master節(jié)點的Redis Cluster,你可以認為它擁有N倍的單個Redis性能。同時,query通常都在一個round trip中執(zhí)行,因為client通常會保留與所有節(jié)點的持久化連接(連接池),因此延遲也與客戶端操作單臺redis實例沒有區(qū)別。
  • 在對數(shù)據(jù)安全性、可用性方面提供了合理的弱保證的前提下,提供極高的性能和可擴展性,這是Redis Cluster的主要目標

1.6 為何要避免合并(merge)操作

  • Redis Cluster設計上避免了在多個擁有相同key-value對的節(jié)點上的版本沖突(及合并/merge),因為在redis數(shù)據(jù)模型下這是不需要的。Redis的值同時都非常大;一個擁有數(shù)百萬元素的list或sorted set是很常見的。同樣,數(shù)據(jù)類型的語義也很復雜。傳輸和合并這類值將會產(chǎn)生明顯的瓶頸,并可能需要對應用側(cè)的邏輯做明顯的修改,比如需要更多的內(nèi)存來保存meta-data等。
  • 這里(【譯注】刻意避免了merge)并沒有嚴格的技術限制。CRDTs或同步復制狀態(tài)機可以塑造與redis類似的復雜的數(shù)據(jù)類型。然而,這類系統(tǒng)運行時的行為與Redis Cluster其實是不一樣的。Redis Cluster被設計用來支持非集群redis版本無法支持的一些額外的場景。

2 Redis Cluster主要模塊介紹

2.1 分布式Keys模型

  • key空間被分為16384個slot,有效地設置了一個集群的最大上限為16384個master(然而一般建議最大節(jié)點數(shù)少于1000)
  • 每個master節(jié)點處理16384個hash slot中的一部分。在沒有集群重新配置的任務(比如,正在執(zhí)行將一個slot hash從一個節(jié)點轉(zhuǎn)移到另一個的任務)時,cluster是穩(wěn)定的。當cluster處于穩(wěn)定狀態(tài)時,每個hash slot只會由一個節(jié)點提供服務(當然服務節(jié)點可以有一個或多個slave,用于在網(wǎng)絡分裂或單點故障是替代它,或是在允許讀取過期數(shù)據(jù)的前提下用來擴展讀操作)。
  • 將key映射到hash slot的算法如下:
    • HASH_SLOT = CRC16(key) mod 16384
  • 具體算法介紹及示例代碼請參考原文

2.2 Keys hash tags

  • Hash tags提供了一種途徑,用來將多個(相關的)key分配到相同的hash slot中。這時Redis Cluster中實現(xiàn)multi-key操作的基礎。
  • hash tag規(guī)則如下,如果:
    • key包含一個{字符
    • 并且 如果在這個{的右面有一個}字符
    • 并且 如果在{}之間存在至少一個字符
  • 那么,{}之間的字符將用來計算HASH_SLOT,以保證這樣的key保存在同一個slot中。
  • 例如:
    • {user1000}.following{user1000}.followers這兩個key會被hash到相同的hash slot中,因為只有user1000會被用來計算hash slot值。
    • foo{}{bar}這個key不會啟用hash tag因為第一個{}之間沒有字符。
    • foo{{bar}}zap這個key中的{bar部分會被用來計算hash slot
    • foo{bar}{zap}這個key中的bar會被用來計算計算hash slot,而zap不會

2.3 Cluster nodes屬性

  • 每個節(jié)點在cluster中有一個唯一的名字。這個名字由160bit隨機十六進制數(shù)字表示,并在節(jié)點啟動時第一次獲得(通常通過/dev/urandom)。節(jié)點在配置文件中保留它的ID,并永遠地使用這個ID,直到被管理員使用CLUSTER RESET HARD命令hard reset這個節(jié)點。
  • 節(jié)點ID被用來在整個cluster中標識每個節(jié)點。一個節(jié)點可以修改自己的IP地址而不需要修改自己的ID。Cluster可以檢測到IP /port的改動并通過運行在cluster bus上的gossip協(xié)議重新配置該節(jié)點。
  • 節(jié)點ID不是唯一與節(jié)點綁定的信息,但是他是唯一的一個總是保持全局一致的字段。每個節(jié)點都擁有一系列相關的信息。一些信息時關于本節(jié)點在集群中配置細節(jié),并最終在cluster內(nèi)部保持一致的。而其他信息,比如節(jié)點最后被ping的時間,是節(jié)點的本地信息。
  • 每個節(jié)點維護著集群內(nèi)其他節(jié)點的以下信息:node id, 節(jié)點的IP和port,節(jié)點標簽,master node id(如果這是一個slave節(jié)點),最后被掛起的ping的發(fā)送時間(如果沒有掛起的ping則為0),最后一次收到pong的時間,當前的節(jié)點configuration epoch ,鏈接狀態(tài),以及最后是該節(jié)點服務的hash slots。
  • 對節(jié)點字段更詳細的描述,可以參考對命令 CLUSTER NODES的描述。
  • CLUSTER NODES命令可以被發(fā)送到集群內(nèi)的任意節(jié)點,他會提供基于該節(jié)點視角(view)下的集群狀態(tài)以及每個節(jié)點的信息。
  • 下面是一個發(fā)送到一個擁有3個節(jié)點的小集群的master節(jié)點的CLUSTER NODES輸出的例子。

    $ redis-cli cluster nodes

    d1861060fe6a534d42d8a19aeb36600e18785e04 127.0.0.1:6379 myself - 0 1318428930 1 connected 0-1364
    3886e65cc906bfd9b1f7e7bde468726a052d1dae 127.0.0.1:6380 master - 1318428930 1318428931 2 connected 1365-2729
    d289c575dcbc4bdd2931585fd4339089e461a27d 127.0.0.1:6381 master - 1318428931 1318428931 3 connected 2730-4095
  • 在上面的例子中,按順序列出了不同的字段:
    node id, address:port, flags, last ping sent, last pong received, configuration epoch, link state, slots.

2.4 Cluster總線

  • 每個Redis Cluster節(jié)點有一個額外的TCP端口用來接受其他節(jié)點的連接。這個端口與用來接收client命令的普通TCP端口有一個固定的offset。該端口等于普通命令端口加上10000.例如,一個Redis街道口在端口6379堅挺客戶端連接,那么它的集群總線端口16379也會被打開。
  • 節(jié)點到節(jié)點的通訊只使用集群總線,同時使用集群總線協(xié)議:有不同的類型和大小的幀組成的二進制協(xié)議。集群總線的二進制協(xié)議沒有被公開文檔話,因為他不希望被外部軟件設備用來預計群姐點進行對話。當然你可以通過Redis Cluster的源碼中的cluster.hcluster.c獲得更多的細節(jié)。

2.5 集群拓撲

  • Redis Cluster是一張全網(wǎng)拓撲,節(jié)點與其他每個節(jié)點之間都保持著TCP連接。
  • 在一個擁有N個節(jié)點的集群中,每個節(jié)點由N-1個TCP傳出連接,和N-1個TCP傳入連接。
  • 這些TCP連接總是保持活性(be kept alive)。當一個節(jié)點在集群總線上發(fā)送了ping請求并期待對方回復pong,(如果沒有得到回復)在等待足夠成時間以便將對方標記為不可達之前,它將先嘗試重新連接對方以刷新與對方的連接。
  • 而在全網(wǎng)拓撲中的Redis Cluster節(jié)點,節(jié)點使用gossip協(xié)議和配置更新機制來避免在正常情況下節(jié)點之間交換過多的消息,因此集群內(nèi)交換的消息數(shù)目(相對節(jié)點數(shù)目)不是指數(shù)級的。

2.6 節(jié)點握手

  • 節(jié)點總是接受集群總線端口的鏈接,并且總是會回復ping請求,即使ping來自一個不可信節(jié)點。然而,如果發(fā)送節(jié)點被認為不是當前集群的一部分,所有其他包將被拋棄。
  • 節(jié)點認定其他節(jié)點是當前集群的一部分有兩種方式:
    • 如果一個節(jié)點出現(xiàn)在了一條MEET消息中。一條meet消息非常像一個PING消息,但是它會強制接收者接受一個節(jié)點作為集群的一部分。節(jié)點只有在接收到系統(tǒng)管理員的如下命令后,才會向其他節(jié)點發(fā)送MEET消息:
      CLUSTER MEET ip port
    • 如果一個被信任的節(jié)點gossip了某個節(jié)點,那么接收到gossip消息的節(jié)點也會那個節(jié)點標記為集群的一部分。也就是說,如果在集群中,A知道B,而B知道C,最終B會發(fā)送gossip消息到A,告訴A節(jié)點C是集群的一部分。這時,A會把C注冊未網(wǎng)絡的一部分,并嘗試與C建立連接。
  • 這意味著,一旦我們把某個節(jié)點加入了連接圖(connected graph),它們最終會自動形成一張全連接圖(fully connected graph)。這意味著只要系統(tǒng)管理員強制加入了一條信任關系(在某個節(jié)點上通過meet命令加入了一個新節(jié)點),集群可以自動發(fā)現(xiàn)其他節(jié)點。

3 Redirection and resharding

3.1 MOVED Redirection

  • 一個redis client可以隨意地向集群里的任意節(jié)點發(fā)送查詢請求,包括slave節(jié)點。節(jié)點將會分析請求,如果它是可以接受的(也就是說,請求只涉及一個key,或是涉及了多個屬于同一hash slot的keys),它會查找哪個節(jié)點負責命令中的key所屬的hash slot。
  • 如果hash slot正好由當前節(jié)點服務,那么請求會直接被執(zhí)行,否則節(jié)點會檢查它內(nèi)部的hash slot到節(jié)點的映射,并會恢復client一個MOVED錯誤,就像下面的例子:

    $ redis-cli -p 7000 get foo10449
    (error) MOVED 4995 127.0.0.1:7003

  • 錯誤包含了key所屬的hash slot(3999)和為該hash slot服務的節(jié)點ip:port。客戶端需要想指定節(jié)點的IP地址和端口補發(fā)該請求。即使client在補發(fā)請求前等待了很長一段時間,并且在此期間集群的配置發(fā)生了變化,目標節(jié)點依舊會再次發(fā)送一個 MOVED 錯誤告知3999 hash slot的所有權(quán)現(xiàn)在已經(jīng)轉(zhuǎn)移到了另一個節(jié)點。
  • 此外,盡管從集群角度看節(jié)點是通過ID被標記的,為了簡化與client的接口,我們還是向客戶端暴露了一個hash slot到ip:port指定的redis節(jié)點。
  • 規(guī)范沒有對client做出強制要求,但是redis client (在收到了MOVED錯誤之后)應該能記錄下目前hash slot 3999由節(jié)點127.0.0.1:6381提供服務。這樣如果一旦有一個新的命令需要發(fā)送,它就可以計算出目標key的hash slot并有很大的機會選擇一個正確的節(jié)點。
  • 另一個可以選擇的方式是每當接收到一個MOVED重定向,客戶端就通過CLUSTER NODES或 CLUSTER SLOTS`命令刷新客戶端側(cè)的全部集群信息。當client遇到一個重定向錯誤,那么更有可能有多個hash slots被重新配置而不是僅僅這一個,所以盡快更新客戶端配置通常是最有效的策略。
  • 注意到當cluster時穩(wěn)定的(沒有正在執(zhí)行的配置變化),最終所有的客戶端可以會的一個hash slots -> nodes的映射表,這樣clieng就可以直接定位到正確的節(jié)點而不需要重定向、路由或其它單點故障,從而使整個集群更有效率。
  • Redis client必須能夠正確處理 ASK 重定向,否則就不是一個完整的Redis Cluster client。

3.2 Cluster live reconfiguration

  • Redis cluster支持在運行時添加和刪除節(jié)點。這可以抽象成如下操作:將一個hash slot從一個節(jié)點轉(zhuǎn)移到另一個節(jié)點。這意味著,同樣的機制可以被用作集群rebalance、添加節(jié)點、刪除節(jié)點等。
    • 添加節(jié)點:將一個新的空節(jié)點添加到集群病從已有節(jié)點轉(zhuǎn)移一系列slot set到該新節(jié)點。
    • 刪除節(jié)點:將待刪除節(jié)點的所有hash slot轉(zhuǎn)移到其他節(jié)點。
    • 節(jié)點rebalance:將給定的一系列hash slots在節(jié)點間移動。
  • 該實現(xiàn)的核心是為集群提供移動hash slots的能力。從一個特別的角度來看,一個hash slot就是一系列的keys,因此在resharding期間,Redis Cluster真正做的就是將keys從一個實例轉(zhuǎn)移到另一個。移動一個hash slot意味著將那些正好hash到該hash slot的所有keys進行移動。
  • 為了更好的理解這一過程,我們先看一看我們用來操作slots轉(zhuǎn)移的 CLUSTER 子命令。
  • 以下cluster子命令可以被用來達到上述目的:
    • CLUSTER ADDSLOTS slot1 [slot2] ... [slotN]
      • 將指定的slot分配給redis節(jié)點
      • 所有指定的slot必須是集群內(nèi)未分配的,否則命令執(zhí)行失敗
      • 主要用于:
        • 新建集群的hash slot配置
        • 修復被損壞的集群時分配未指定的slots
      • 不建議直接使用,應該在集群編排應用程序中使用,比如redis-trib.rb
    • CLUSTER DELSLOTS slot1 [slot2] ... [slotN]
      • 使 指定的cluster節(jié)點 忘記某個主節(jié)點正在負責指定的hash slots
      • 刪除成功的slot在該節(jié)點將進入unbound狀態(tài)等待分配
      • 在該 指定的cluster節(jié)點 刪除hash slots后將使該節(jié)點進入cluster_state:fail狀態(tài),所有針對該節(jié)點的redis操作都將失敗。
        • 每個節(jié)點必須知道所有16384個hash slot的分配,才能對外提供服務。
        • 多節(jié)點之間的slot分配信息可能因為delslots/addslots而不同,造成 集群失步?。。?/strong>
      • 當從其他節(jié)點接收到一個心跳包并得知該hash slot已被其他節(jié)點負責后,會重新建立關系
      • 極少被使用,建議只用在debug場景中
      • 目前redis-trib.rb中沒有使用該命令
    • CLUSTER SETSLOT slot NODE node
    • CLUSTER SETSLOT slot MIGRATING node
    • CLUSTER SETSLOT slot IMPORTING node
  • 前兩個命令,ADDSLOTSDELSLOTS,只是用來簡單地在redis節(jié)點上分配或移除slot。分配slot意味著告訴一個master node他講負責保存和服務指定hash slot的內(nèi)容。
  • 在分配了hash slots之后,節(jié)點會通過gossip協(xié)議在集群中傳播這些信息。
  • 命令ADDSLOTS通常用在創(chuàng)建一個新cluster時為每個master節(jié)點指定16384個hash slots的一個子集。
  • 命令DELSLOTS主要用在手動修改集群配置,或者用在調(diào)試相關的任務:在實際場景中這個命令極少被用到。
    • 當一個slot被設置為MIGRATING,如果某個命令的keys在當前節(jié)點存在,那么節(jié)點將會接受對該slot的所有操作;如果有節(jié)點不存在,就會使用 -ASK 重定向到遷移的目標節(jié)點。
    • 當一個slot被設置為IMPORTING,則如果某個命令緊跟著一個ASKING命令,那么該命令將會被執(zhí)行。如果client沒有給出ASKING命令,該操作將會被-MOVED重定向到它真正的hash slot所有者。
  • 為了更好地理解這一過程,我們看看下面的hash slot遷移的例子。假設我們有兩個Redis master節(jié)點,稱為 A 和 B。我們希望將hash slot 8從A轉(zhuǎn)移到B,因此我們發(fā)出如下指令:
    • 向B發(fā)送: CLUSTER SETSLOT 8 IMPORINT A
    • 向A發(fā)送: CLUSTER SETSLOT 8 MIGRATING B
  • 所有其他節(jié)點在收到一個對屬于hash slot 8的key的查詢時,將會繼續(xù)向client指出重定向到A。
    • 所有對現(xiàn)有keys的查詢將會被"A"處理
    • 所有在"A"上對不存在的keys的查詢都會被"B"處理,因為"A"會將client重定向到"B"。
  • 這樣,我們不會再在"A"上創(chuàng)建新keys。同時,有一個稱為"redis-trib"的特別的程序,它通常被用來resharding以及Redis Cluster配置,會對所有hash slot 8中已經(jīng)存在的keys進行遷移。這會用到如下命令:

    CLUSTER GETKEYSINSLOT slot count

  • 上述命令將會返回指定hash slot中的 count 個keys。對返回的每個key,"redis-trib"向"A"發(fā)送一個MIGRATE命令,這會將指定的key從"A"自動遷移到"B"。在遷移過程中,"A"和"B"兩個實例都會被鎖定很短的時間以確保沒有競爭條件。這是MIGRATE的工作方式:

    MIGRATE target_host target_port key target_datebase id timeout

  • 命令MIGRATE將會連接對端實例,序列化并發(fā)送該key,一旦收到對方的OK回復,就會在本地刪除舊的key。從外部client角度看,在任意給定時間,一個key只會存在于A或者B。
  • 在Redis Cluster中,沒有必要指定一個0以外的database,但是MIGRATE是一個通用命令,它也用于非cluster環(huán)境。MIGRATE命令針對移動復雜key,比如很大的list,做過優(yōu)化,以便能夠盡可能快地在集群間移動key。盡管如此,如果使用redis的應用對延時由要求,對存在大key的集群進行重新配置仍然被認為是個不明智的舉動。
  • 當歉意過程最終結(jié)束,SETSLOT <slot> NODE <node-id>命令將會被發(fā)送到遷移涉及的兩個節(jié)點,以便將他們的slots狀態(tài)設置回普通狀態(tài)。同樣的命令通常也會被發(fā)送到集群中所有其他節(jié)點,以避免新配置在集群間自然傳播的等待時間。

3.2.1 CLUSTER SETSLOT命令

  • MIGRATING
    • CLUSTER SETSLOT <slot> MIGRATING <destination-node-id>
    • 將一個hash slot設置為migrating狀態(tài)
    • 當一個slot處于migrating狀態(tài)時,
      • 如果一個命令的key存在,則執(zhí)行該命令
      • 如果一個命令的key不存在,則產(chǎn)生一個ASK重定向
      • 如果一個命令包含多個key
        • 如果所有key都存在,則執(zhí)行
        • 如果只有部分key存在,則產(chǎn)生一個TRYAGAIN錯誤提示等遷移完成后再試 (在筆者4.0本地測試環(huán)境中,只會產(chǎn)生ASK錯誤,留待繼續(xù)研究)
  • IMPORTING
    • CLUSTER SETSLOT <slot> IMPORTING <source-node-id>
    • 將一個hash slot設置為importing狀態(tài)
    • 當一個hash slot處于importing狀態(tài)時,
      • 對所有命令,都會產(chǎn)生一個 MOVED 重定向
      • 如果命令緊跟在ASKING命令之后的命令,則會被按照如下規(guī)則執(zhí)行:
        • 如果在某個key遷移前,在importing方通過asking插入該key,則會造成后續(xù)migrating失敗
        • 新key只能在target(imporing一方)創(chuàng)建
        • 對于已經(jīng)完成遷移的key,命令可以背正確執(zhí)行并保證一致性
  • STABLE
    • CLUSTER SETSLOT <slot> STABLE
    • 清除當前hash slot的migrating/importing狀態(tài)。
    • 所有已經(jīng)通過MIGRATE命令轉(zhuǎn)移過的key無法恢復
  • NODE
    • 該子命令的語法最為復雜。它將hash slot與特定節(jié)點相關聯(lián),但是該命令只能在特定條件下才會被執(zhí)行并且根據(jù)slot狀態(tài)會產(chǎn)生不同的結(jié)果:
      • 如果當前hash slot的owner是接受命令的節(jié)點,此時嘗試通過本命令將slot分配給一個不同的節(jié)點,則
        • 如果該slot為空,執(zhí)行成功
        • 如果該slot含有任何key,澤返回錯誤
      • 如果slot處于migrating狀態(tài)的節(jié)點接收到該命令,當該slot被assign給其他key之后,清除migrating狀態(tài)
      • 如果slot處于importing狀態(tài)的節(jié)點接收到該命令,則并且將這個slotassign給自己(通常發(fā)生在針對某個hash slot的resharding結(jié)束時),則該命令產(chǎn)生如下影響:
        • 清除imporing狀態(tài)
        • 如果本節(jié)點的config epoch在及群眾不是最大的,澤產(chǎn)生一個心的值并將這個config epoch分配給自己。這樣相對之前由failover或slot遷移產(chǎn)生的配置,本節(jié)點能確保它贏得這個新hash slot的擁有權(quán)。
  • Redis Cluster live resharding過程
    • 在source node上把該hash slot設置為IMPORTING狀態(tài)
      CLUSTER SETSLOT <slot> IMPORTING <source-node-id>
    • 在destination node上把該hash slot設置為IMPORTING狀態(tài)
      CLUSTER SETSLOT <slot> MIGRATING <destination-node-id>
    • 在source node上通過以下命令遷移所有key
      • CLUSTER GETKEYSINSLOT <slot> <count>
      • MIGRATE host port "" destination-db timeout [KEYS key [key ...]]
    • 在source或destination節(jié)點執(zhí)行以下命令assign slot到指定節(jié)點
      • CLUSTER SETSLOT <slot> NODE <destination-node-id>

3.3 ASK重定向

  • 在過去的章節(jié),我們已經(jīng)提及了 ASK 重定向。為何我們不能簡單地使用 MOVED 重定向?因為使用 MOVED 意味著我們認為hash slot已經(jīng)永久的服務于一個不同的節(jié)點,并且下一次查詢應該嘗試那個指定節(jié)點,而 AKS 意味著只是下一次查詢需要發(fā)往指定的節(jié)點。
  • 這是有必要的,因為下一次我們可能會對一個屬于hash slot 8但目前仍然在"A"上的key進行操作,因此我們總是希望client先嘗試訪問"A"然后在必要的時候再訪問"B"。因為這只會偶爾(resharding或reconfiguring期間)發(fā)生在16384個hash slot中的一個上,因此對cluster的性能影響是可以接受的。
  • 我們需要強制規(guī)定client的行為,以確保client只有在A已經(jīng)嘗試過之后再嘗試B。如果client在查詢前發(fā)送了ASKING命令,節(jié)點B只有在該key所屬的slot處于IMPORTING狀態(tài)時才會接受改命令。
  • 一般來說,ASKING命令會為client設置一個單次標簽(one-time flag),以允許該client可以訪問一個IMPORTING的slot一次。
  • 從client視角,ASK重定向的語義如下:
    • 如果收到了 ASK 重定向命令,僅僅將這條查詢重定向到某個節(jié)點的命令發(fā)送到指定的新節(jié)點,之后的命令還是繼續(xù)發(fā)送給老的節(jié)點。
    • 重定向查詢必須以一條ASKING命令開始
    • (目前)不要在本地將hash slot 8對應的服務節(jié)點指向B。
  • 一旦hash slot 8的遷移完成,A會發(fā)送一個MOVED消息,而client會永久更新hash slot 8的映射到新的IP:port。逐一,如果一個有bug的client提前修改了本地映射,這也不會成為問題,因為這個client不會在查詢前帶上ASKING命令,這樣B會通過MOVED將client重定向回A節(jié)點。

3.4 客戶端首次連接和對重定向的處理

  • 一個Redis Cluster client是可以不將slots配置(slot號到服務節(jié)點的地址的映射)記錄在本地內(nèi)存中的,這樣它只需要隨機找一個節(jié)點訪問,并根據(jù)回復的重定向找到正確的服務節(jié)點,當然這樣的client是非常沒有效率的。

  • Redis Cluster client應該通過緩存slots配置而變得盡可能聰明。當然,這個配置并非必須總是最新的。因為跟錯誤的節(jié)點通信后會簡單地獲得一個重定向,并且這會出發(fā)一次客戶端視圖的更新。

  • Clients通常需要在下面兩個場景中獲取一次全量的slots/nodes映射信息:

    • 在啟動階段為了生成slots配置的初始化信息
    • 在接收到一個MOVED重定向信息時
  • 逐一client在接收到MOVED重定向時,可以只更新單個slot,當然這通常不是很有效率,因為一般來說,每次配置變動通常會設計多個slots(比如發(fā)生了一次slave晉升,則所有該節(jié)點服務的slots都會被重新映射)。而在收到MOVED重定向時重新獲取所有slots的映射處理起來更為簡單。

  • 為了獲取slots配置,Redis Cluster除了提供了CLUSTER NODES命令外,還提供了一個新選擇,這個新的命令只提供了client需要的信息,并且不需要client(對接收到的數(shù)據(jù))進行解析。

  • 這個新命令就是CLUSTER SLOTS,它提供了一個slots范圍數(shù)組,并關聯(lián)了服務于對應范圍的master和slave節(jié)點。

  • 下面是CLUSTER SLOTS輸出的例子:

    127.0.0.1:7000> cluster slots
    1) 1) (integer) 5461
      2) (integer) 10922
      3) 1) "127.0.0.1"

    2) (integer) 7001
      4) 1) "127.0.0.1"
        2) (integer) 7004
    2) 1) (integer) 0
      2) (integer) 5460
      3) 1) "127.0.0.1"
        2) (integer) 7000
      4) 1) "127.0.0.1"
        2) (integer) 7003
    3) 1) (integer) 10923
      2) (integer) 16383
      3) 1) "127.0.0.1"
        2) (integer) 7002
      4) 1) "127.0.0.1"
        2) (integer) 7005 d

  • 更多對該命令的解釋請參考 CLUSTER SLOTS

  • 該命令(CLUSTER SLOTS)不保證可以反悔16384 slots中的所有信息,如果slots配置缺失,clients應該將其初始化未NULL,并在用戶嘗試在這些未分配slots上執(zhí)行命令式上報錯誤。

  • 在返回一個錯誤給調(diào)用者之前,當一個slot被發(fā)現(xiàn)沒有分配,client應該再次嘗試獲取slots配置以檢查當前cluster已經(jīng)正確地配置了。

3.5 多key操作

  • 對擁有相同hash tag的key,總是可以使用multi-key操作
  • 當resharding時:
    • 如果此時所有的key都正好處于相同的節(jié)點,則可以成功進行multi-key操作
    • 否則,multi-key操作不可用,產(chǎn)生一個 "-TRYAGAIN" 錯誤。

3.6 用slave節(jié)點擴展讀操作

  • slave節(jié)點默認不可讀,對slave的讀操作將產(chǎn)生 MOVED 錯誤
  • 通過READONLY命令,可以將slave節(jié)點設置為可讀
  • 通過READWRITE命令,可以清除該節(jié)點的只讀flags值

4 容錯

4.1 心跳包和gossip消息

  • Redis Cluster節(jié)點持續(xù)地交換ping和pong包。這兩種包擁有相同的結(jié)構(gòu),并且都會攜帶重要的配置信息。它們唯一的不同就是消息的類型字段。我們通常把ping和pong包統(tǒng)稱為心跳包(heartbeat packets)。
  • 通常節(jié)點發(fā)送的ping包將會觸發(fā)接收者回復一個pong包。當然這并不總是必須的。也有可能節(jié)點會向其他節(jié)點直接發(fā)送包含重要配置信息的pong包,而不需要觸發(fā)回復。這是有用的,例如,在將自己的心配置盡可能快地廣播出去。
  • 通常,每一秒里,一個節(jié)點會隨機挑選一些節(jié)點并發(fā)送ping包,因此每個節(jié)點(指定時間內(nèi))發(fā)送的ping包(以及接收到的pong包)的總是是一個常熟,不管集群中有多少節(jié)點。
  • 當然,每個節(jié)點一定會主動ping那些自己在NODE_TIMEOUT/2時間內(nèi)沒有發(fā)送過ping或從之接收過pong的節(jié)點。在NODE_TIMEOUT耗盡之前,節(jié)點同樣會嘗試重新連接哪個節(jié)點,以確保這不是由于當前TCP連接問題造成的。
  • 如果NODE_TIMEOUT被設置為一個較小的數(shù)字,同時節(jié)點數(shù)目又很大,從全局看,交換的消息數(shù)目會是很可觀的,因為在NODE_TIMEOUT一半的時間內(nèi),每個節(jié)點會嘗試ping其他所有節(jié)點。
  • 例如,在一個擁有100個節(jié)點的集群里,如果把節(jié)點過期設置為60秒,那么每個節(jié)點在30秒內(nèi)將會發(fā)送99個ping,也就是每秒3.3個ping??紤]到有100個節(jié)點,也就是集群內(nèi)部每秒會生成330個ping包。
  • 還是有一些辦法來降低消息總數(shù)的,然而到目前為止還沒有人報告因為Redis Cluster故障檢測而導致的帶寬問題。也必須注意,即使在上面的例子中,每秒330個包交換也是被平分到100個不同的節(jié)點上的,因此對每個節(jié)點來說接收到的流量是可以接受的。

4.2 心跳包內(nèi)容

  • ping和pong除了包含一個同樣會被用于所有其他類型的包(例如請求failover選票的包)的包頭(header)外,還有一個特別的Gossip Section。Gossip Section只存在于Ping和Pong包中。
  • 通用包頭包含了以下信息:
    • 發(fā)送者的Node ID——這是一個160bit隨機字符串它在一個節(jié)點第一次被創(chuàng)建時生成,并在該Redis Cluster節(jié)點的整個生命周期中保持不變。
    • 發(fā)送者的currentEpochconfigEpoch字段——由Redis Cluster用來加載分布式算法(下一節(jié)中會詳細描述)。對slave,configEpoch就是它的master的configEpoch。
    • 發(fā)送者的node flags——用來表明節(jié)點是slave,還是master,以及其他一些由單bit表示的信息。
    • 發(fā)送者的hash slots的bitmap——如果是一個slave,則代表其master的hash slots的bitmap
    • 發(fā)送者的TCP和port——(port指的是接收普通命令的端口,即+10000就是redis cluster bus端口)
    • 發(fā)送者視角下的集群狀態(tài)——down或者ok
    • 發(fā)送者的master節(jié)點ID(如果這是一個slave)
  • ping和pong包同樣包含一個gossip段。該段為接收方提供了這樣一個視圖——發(fā)送方節(jié)點視角的集群中其他節(jié)點的狀態(tài)。Gossip段只包含了發(fā)送方知道的一系列其他節(jié)點中的隨機幾個節(jié)點的信息。Gossip段中包含的節(jié)點的數(shù)目與集群大小成正比。
  • 每個被放入gossip段的節(jié)點包含了如下字段:
    - Node ID
    - 該節(jié)點的IP和port
    - 該節(jié)點的Node flags
  • Gossip段允許接收方可以得到發(fā)送方視角的集群狀態(tài)。這在故障檢測和節(jié)點發(fā)現(xiàn)時很有用處。

4.3 故障檢測

  • Redisu Cluster故障檢測用來識別合適一個master或slave節(jié)點對集群中多數(shù)節(jié)點來說不再可達(no longer reachable)并在之后提升一個slave節(jié)點為master。當無法進行slave晉升時,集群將會被設置為error狀態(tài),并停止接受客戶端的命令。
  • 之前已經(jīng)提及,每個節(jié)點持有每個已知的其他節(jié)點的一系列標簽。有兩個標簽用于故障檢測,他們是PFAILFAILPFAIL標簽意味著 可能故障 ,是一個不需要確認的故障類型。FAIL意味著一個節(jié)點已經(jīng)失敗,他必須在一段固定的時間內(nèi)由多數(shù)master進行確認。
  • PFAIL flag (possible failure)
    • 當節(jié)點發(fā)現(xiàn)某個節(jié)點失聯(lián)超過NODE_TIMEOUT時間后,就會將該節(jié)點標記為PFAIL。無論master還是slave都可以姜其他節(jié)點標記為PFAIL,而不管對方的類型。
    • 對一個Redis Cluster節(jié)點來說,不可達概念的定義為,我們有一個 活躍的ping(指的是我們發(fā)送出但是沒有接收到對方回復的ping)掛起超過了 NODE_TIMEOUT。為了讓這一機制正確工作,NODE_TIMEOUT必須大于一個網(wǎng)絡正常往返的時間。為了增加可靠性,在NODE_TIMEOUT時間過去一半時,如果節(jié)點還沒有得到回復,它會嘗試重新連接其他節(jié)點。這一機制確保了連接保持活躍,因此損壞的鏈接通常不會到之錯誤的在節(jié)點間報告失敗。
  • FAIL flag
    • PFAIL flag只是每個節(jié)點針對其他節(jié)點狀態(tài)的本地信息,它不足以被用來觸發(fā)slave晉升。為了確認一個節(jié)點確實down了,PFAIL條件必須升級為FAIL條件。
    • 每個節(jié)點發(fā)出的gossip消息中會隨機包含一部分自己已知的其他節(jié)點的狀態(tài)信息,每一個節(jié)點最終會接收到每個其他節(jié)點的一系列node flags。這為每個節(jié)點提供了一種機制來向其他節(jié)點發(fā)送自己檢測到的節(jié)點失效事件。
  • 當如下一系列條件滿足時,PFAIL條件就會升級稱為FAIL條件:
    • 某個節(jié)點(我們稱之為A),已經(jīng)將另一個節(jié)點B標識為PFAIL
    • 節(jié)點A通過gossip段收集集群中多數(shù)master(majority master)對該節(jié)點的標注
    • 多數(shù)master在 NODE_TIMEOUT * FAIL_REPORT_VALIDITY_MULT (在當前redis實現(xiàn)中,FAIL_REPORT_VALIDITY_MULTI被設置為2且不可配置) 這段時間內(nèi)將節(jié)點A標注為 PFAILFAIL。
  • 如果上述條件滿足,則節(jié)點A將:
    • 把失聯(lián)節(jié)點標記為FAIL
    • 發(fā)送一個FAIL消息給所有可達節(jié)點
  • FAIL消息將會強制所有接收到的節(jié)點將失聯(lián)節(jié)點(節(jié)點B)標記為FAIL,而不管當前自己是否已將其標記為PFAIL
  • 【注意】FAIL flag總是單向的,即一個節(jié)點可以從PFAIL變?yōu)?code>FAIL,但是不能反向轉(zhuǎn)變。FLAG標簽只有在以下情況下才會被清除:
    • 節(jié)點是slave,并且重新可達。這種情況下 FAIL 標簽可以清除因為slave節(jié)點不會發(fā)生故障轉(zhuǎn)移。
    • 節(jié)點是master且沒有服務于任何slot,重新可達。這種情況下,FAIL標簽可以被清除,該master繼續(xù)等待被配置后加入集群
    • 節(jié)點是master,重新可達,且在一段較長時間 (N倍NODE_TIMEOUT) 內(nèi) 沒有被檢測到有slave節(jié)點被晉升。顯然此時它應該被作為master將重新加入集群。
  • 在從 PFAIL -> FAIL 轉(zhuǎn)變的過程中,使用了弱一致機制(weak agreeement):
    1. 節(jié)點在一段時間內(nèi)收集其他節(jié)點的視圖(views),所以即使多數(shù)派master需要達成一致,事實上這只是說明我們在不同的時間,從不同的節(jié)點收集到了這一結(jié)果,我們既無法確定,也無法要求,在什么時刻獲得了多數(shù)master的一致結(jié)果。然而,因為我們會拋棄老舊(過期)的失敗報告,所以多數(shù)派master一定是在某個時間窗內(nèi)對某個節(jié)點的失敗達成了一致。
    2. 即使每個節(jié)點檢測到了 FAIL 條件并通過 FAIL 消息強制集群中其他節(jié)點接受該條件,還是無法保證消息可以倍所有節(jié)點接收到,因為此時可能因為分區(qū)問題導致某些節(jié)點不可達。
  • 當然,redis cluster的失敗檢測還有一個活性需求(liveness requirement):最終所有的節(jié)點需要(should)對一個給定節(jié)點的狀態(tài)達成一致。下面是兩個可能由(集群)腦裂引起的場景(case):一些少數(shù)派節(jié)點認為某個節(jié)點已經(jīng) FAIL,或是一些少數(shù)派節(jié)點認定某個節(jié)點不在FAIL狀態(tài)。這兩種狀態(tài)下集群對某個節(jié)點最終一定會(在集群全局)有一個唯一的視圖(view)。
    • 場景1: 如果多數(shù)派masters通過失敗檢測及其產(chǎn)生的影響鏈,最終將一個節(jié)點標記為FAIL,所有其他節(jié)點將最終將會把這個master標記為FAIL,因為在指定的時間窗內(nèi),集群里會有足夠多的失敗被報告。
    • 場景2: 如果只是少數(shù)派master將一個節(jié)點標記為FAIL,slave晉升將不會發(fā)生,所有節(jié)點將會根據(jù)上述的FAIL狀態(tài)清除規(guī)則清除該節(jié)點的FAIL狀態(tài)(例如通過"在N倍NODE_TIMEOUT內(nèi)沒有晉升動作"這一條規(guī)則)。
  • FAIL flag只是用來作為一個觸發(fā)機制,它將觸發(fā)執(zhí)行slave晉升算法的安全部分,以便將slave晉升。理論上slave獨立地運作并在發(fā)現(xiàn)它的master不可達后啟動一次晉升,并在多數(shù)masters可以觸達該master時等待其他master拒絕認可。然而,PFAIL -> FAIL 狀態(tài)變遷、弱一致、 在cluster可達節(jié)點間通過FAIL消息強制狀態(tài)的生成,這些額外的復雜度的引入,在實踐上是有它的優(yōu)勢的。因為這些機制的引入,使得集群(可以意識到自己)在處于一個error狀態(tài)下,所有節(jié)點可以拒絕寫入操作。從從使用redis cluster的應用的角度看,這是一個必要的特性。同時這樣也可以避免由于slave自己的問題導致無法連接master,進而導致錯誤的選舉嘗試。

5 配置執(zhí)行,傳播,和故障轉(zhuǎn)移 (Configuration handling,propagation, and failovers)

5.1 集群當前代(cluster current epoch)

  • Redis Cluster使用了一個類似Raft算法的"term"的概念,稱為"epoch"(代)。它用來為事件提供遞增的版本號。當多個節(jié)點提供了相互沖突的信息,它讓其他節(jié)點可以正確的理解哪一個狀態(tài)是最新的。
  • currentEpoch是一個64bit無符號整數(shù)。
  • 在節(jié)點創(chuàng)建時,所有的Redis Cluster節(jié)點,包括slave和master節(jié)點,都把自己的currentEpoch設置為0。
  • 每當從其他節(jié)點接收到一個包時,如果發(fā)送方的epoch(包含在cluster總線消息頭中)大于本地節(jié)點的epoch,則本地節(jié)點將自己的currentEpoch更新為發(fā)送方的epoch。
  • 因為這一語義,最終所有節(jié)點將會認同集群中擁有最大configEpoch的節(jié)點(提出的主張)。(【譯注】current epoch是用來標識集群epoch的,集群epoch取自所有節(jié)點中configEpoch最大的那個節(jié)點的configEpoch
  • 該信息被用在:當集群狀態(tài)發(fā)生變化,并且一個節(jié)點正在請求其他節(jié)點的同意來執(zhí)行一些操作時(比如slave晉升)。
  • 當前的實現(xiàn)里,currentEpoch僅僅被用在slave晉升。簡單來說,epoch是集群的一個本地時鐘,擁有大epoch的消息總是能贏得擁有相對小的epoch的消息。

5.2 配置代(Configuration epoch)

  • 每個master總是在ping和pong包中向它的slave廣播自己的configEpoch,和其服務的hash slots的bitmap信息。
  • 當一個新master節(jié)點被創(chuàng)建時,configEpoch被設置為0.
  • 一個新的configEpoch將會在slave選舉中被創(chuàng)建。在嘗試替代失敗的master時,slave會增加他的epoch并嘗試得到多數(shù)masters的授權(quán)。當一個slave被選中,一個新的唯一的configEpoch會被創(chuàng)建,同時該slave會使用這個新的configEpoch并轉(zhuǎn)變?yōu)橐粋€master。
  • 后續(xù)章節(jié)將會解釋,當不同的節(jié)點主張存在分歧時(可能由于網(wǎng)絡分區(qū)或節(jié)點失敗導致),configEpoch時如何幫助解決沖突的。
  • slave節(jié)點同樣會在ping和pong包中聲明configEpoch,此時的configEpoch是他的master在上一次包交換中攜帶的configEpoch。這允許其他實例檢測到這個slave有一個老的配置并且需要更新(master節(jié)點將不會向一個擁有舊配置的slave授權(quán)選票)。
  • 每當一些已知節(jié)點的configEpoch發(fā)生變化,它就會被所有收到這條信息的節(jié)點永久地保存在各自的node.conf文件里。同理,currentEpoch也會被保存。Redis保證會在執(zhí)行下一個操作前保存這兩個變量并同步到磁盤。
  • configEpoch的值在failover時使用一個簡單的算法來保證產(chǎn)生一個新的、遞增的、唯一的值。

5.3 Slave選舉和晉升

  • 選舉和晉升總是由slave節(jié)點發(fā)起和處理,在這期間,master節(jié)點會提供一些幫助,它們會為晉升哪個slave而進行投票。當一個master在至少一個slave的視圖中處于FAIL狀態(tài),并且該slave已經(jīng)要求晉升為master時,slave選舉就會發(fā)生。
  • 為了將自己晉升為master,slave需要開啟一個選舉并贏得該選舉。如果一個master處于FAIL狀態(tài),那么它的所有slave都可以開始一個選舉,但是只有一個slave會贏得最終的選舉并晉升為master。
  • 當以下條件符合時,slave將開始一個選舉:
    • 該slave的master處于FAIL狀態(tài)
    • master正在為至少一個以上slot服務
    • slave與master的復制連接斷開時間少于一個給定值,這用來保證晉升的slave的數(shù)據(jù)是盡可能新的。這個值可以由用戶配置。
  • 為了被選中,一個slave首先要做的就是增加自己的currentEpoch計數(shù),并向master實例請求選票。
  • Slave通過向集群的每一個master節(jié)點廣播一個 FAILOVER_AUTH_REQUEST 包請求選票。然后它會在不超過2倍NODE_TIMEOUT時間內(nèi)等待所有master的回復(一般至少等2秒)。
  • 一旦一個master將選票投給了某個slave,主動回復了 FAILOVER_AUTH_ACK,在時間窗 NODE_TIMEOUT * 2 以內(nèi),它就再也不能向該master的任何其他slave投票了。從安全性保證上來說,這一規(guī)則不是必須的,但是它有助于避免多個slave在幾乎相同的時間內(nèi)同時被選中(哪怕它們的configEpoch是不同的),而這顯然不是期望得到的結(jié)果。
  • slave拋棄那些epoch比自己在發(fā)送選票請求時的currentEpoch小的AUTH_ACK。這保證了它不會計算用于上一次選舉的選票。
  • 一旦slave受到了多數(shù)master的ACK,它就贏得了選舉。否則如果在 2 * NODE_TIMEOUT 時間窗(至少2s)內(nèi)沒有得到多數(shù)的回復,選舉就會中止,而一次新的嘗試將在 NODE_TIMEOUT * 4 (至少4s)之后嘗試。

5.4 Slave排序

  • 一旦一個master處于FAIL狀態(tài),一個slave會在嘗試選舉前等待一小段時間。等待的時間按照如下公式計算:
    DELAY = 500 milliseconds + random delay between 0 and 500 milliseconds + SLAVE_RANK * 1000 milliseconds
  • 固定的DELAY用來保證FAIL狀態(tài)擴散到整個集群,否則slave可能會在多數(shù)master不知道該FAIL時請求選舉并被拒絕投票。
  • 隨機的DELAY用來使slave之間異步,避免在同一時間同時發(fā)起選舉。
  • SLAVE_RANK是該slave針對它從master獲得的備份數(shù)據(jù)的總量的排序。在master失敗之后,slave之間通過交換消息來創(chuàng)建一個(最大努力)排序:擁有最新備份offset的slave獲得排序0,第二個更新為1,以此類推。這樣最新的slave會嘗試最先開始選舉。
  • 排序的順序并沒有嚴格強制。如果一個擁有最高排序的slave再選劇終失敗了,其他slave會很快進行重試。
  • 一旦某個slave贏得了選舉,它就獲得了一個新的唯一的遞增的configEPoch,這個configEpoch會比所有其他現(xiàn)存的master更大。它會在ping和pong包中作為master廣播自己,同時提供自己的服務slot和比之前的master更大的configEpoch。
  • 為了加速重配置,新master會向集群內(nèi)所有節(jié)點直接發(fā)送pong包。
  • 當前不可達的節(jié)點,最終也將會被重新配置,比如它重新連接后接收到了其他節(jié)點發(fā)來的ping和pong包,或者通過它自己發(fā)送的心跳包被其他節(jié)點檢測到已過期并回復了UPDATE包之后。
  • 其他節(jié)點會檢測到有一個新的master服務于之前master相同的slots,但是擁有一個更大的configEpoch,之后它們會更新自己的配置。舊master的其他slave(包括重新接入的舊master自己),不但會更新配置,而且會重新從新的master同步所有數(shù)據(jù)。

5.5 Masters回復slave選舉請求

  • Master接收到slave的FAILOVER_AUTH_REQUEST后就會開始一次選舉。
  • 只有符合如下條件,master才會授予選票:
    • master針對每一個epoch只會投票一次,一旦投票后就會拒絕所有更小的epoch:每個master有一個lastVoteEpoch字段,并且會拒絕對currentEpoch小于該值的請求投票。一旦master對投票請求回復確認,lasterVoteEpoch就會同步更新并安全地保存到磁盤。
    • 只有當slave所屬的master被標記為FAIL時,master才會投票給該slave
    • 如果一個FAIL_AUTH_REQUESTcureentEpoch的值小于master的currentEpoch,那么該選舉請求將被忽略。因此,master的回復總是與FAIL_AUTH_REQUEST擁有相同的currentEpoch。如果同樣的slave再次請求選票,并增加了currentEpoch,這可以保證針對舊請求的DELAY的投票不會在新投票請求中被接受。
  1. 如果一個master在上一輪選舉中投票過,那么它在NODE_TIMEOUT*2時間窗內(nèi),不會為該master的任意slave再次投票。這不是嚴格的需求,因為兩個slave不可能在一個相同的epoch中同時獲勝。然而,在實踐中,它保證了當一個slave被選舉后,它擁有足夠的時間通知其他slave并避免其他slave贏得新一輪選舉的可能性,否則這會造成有一次沒有必要的failover。
  2. master不會做任何嘗試來保證選出最好的slave。如果slave的master處于FAIL狀態(tài),且master沒有在當前的term(任期,代?)中投票過,那么它一定會將授予自己的投票。最好的slave總是更可能啟動一次選舉并在其他slave之前贏得選舉,因為由于它擁有更高的排名,它總是會先于其他slave發(fā)起選舉。
  3. 當一個master拒絕為某個slave投票,那么它會簡單地忽略該請求,而不會發(fā)出一個負面的響應。
  4. master不會投票給這樣的slaves,它們發(fā)送的configEpoch小于master表中為slave宣稱的slot服務的master的configEpoch。記得之前提起過,slave發(fā)送的消息中使用它的master的configEpoch,以及它的master服務的slots。這意味著請求選票的slave必須擁有它打算failover的master的slot配置,并且這個配置需要比授權(quán)選票的master更新或至少相等。

5.6 一個分區(qū)問題中epoch配置有效性實際例子

  • 本節(jié)解釋了epoch概念如何使slave在晉升過程中更加能夠容忍分區(qū)(partitions)錯誤
    • 一個master無限期失聯(lián)。它擁有三個slave:A,B,C
    • Slave A贏得了選舉并晉升為master
    • 一個網(wǎng)絡分區(qū)問題導致A對集群的多數(shù)節(jié)點不可用
    • SLave B贏得了選舉并晉升為master
    • 一個分區(qū)問題導致B對集群多數(shù)節(jié)點不可用
    • 網(wǎng)絡問題修復,并且A也恢復可用。
  • 此時,B忽然down機并且A恰好以master身份恢復可用(實際上UPDATE消息會及時重新配置它,但我們假設所有的UPDATE消息也丟失了)。這時,slave C嘗試選圖來替代B。接下來:
    1. C嘗試選舉并成功,因為對多數(shù)master來說,它的master的確掛了。它將獲得一個新的增加了的configEpoch。
    2. A不能生成自己時這些hash slots的master,因為其他服務于相同hash slots的節(jié)點已經(jīng)有了一個更大的configuration epoch。
    3. 所以,所有的節(jié)點將會更新它們的配置表并將這些hash slots分配給C,集群可以繼續(xù)運行。
  • 在下一節(jié)中,你會看到,一個舊節(jié)點重新加入cluster,它將立刻被通知到配置的變更,因為一旦它主動ping其他節(jié)點,接收方就會檢測到它有一個陳舊的集群信息,并向它發(fā)送一條UPDATE消息。

5.7 Hash slots配置傳播

  • Redis Cluster中很重要的部分就是提供一種機制,用來傳播集群中哪個節(jié)點服務于哪些hash slot信息。這無論是在集群啟動還是在slave晉升時都非常重要。

  • 同樣的機制保證了節(jié)點在不限期遇到分區(qū)問題后能以一種明智的方式重新加入集群。

  • 有兩種生成hash slots配置的方式:

    • heartbeat消息。ping和pong消息的發(fā)送者總是添加自己或自己的master所服務的hash slots信息。
    • UPDATE消息。因為每個heartbeat包都有發(fā)送方的configEpoch和其服務的hash slots,如果接收方發(fā)現(xiàn)發(fā)送方的信息過期,它姜發(fā)送一個包含了新信息的包,強制過期節(jié)點更新自己的信息。
  • 心跳包消息或UPDATE消息的接收方使用一種簡單的規(guī)則來更新表映射中的hash slots到對應的節(jié)點。當一個新的Redis Cluster 節(jié)點被創(chuàng)建,它的本地hash slot表被初始化為NULL,這樣每個hash slot就不會被綁定到任何節(jié)點。它看上去就像這樣:

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

  • 配置傳播的規(guī)則如下:

    • Rule 1: 如果一個hash slot沒有被分配(NULL),這時如果有一個已知節(jié)點聲明了該slot,則修改本地hash slot table并將聲明的hash slots關聯(lián)到該節(jié)點。
      • 當一個新的cluster被創(chuàng)建,系統(tǒng)管理員需要手動分配(使用命令CLUSTER ADDSLOTS,一般通過redis-trib命令行工具,或其它類似的工具)所有的slots給它的master,這一信息會很快在整個集群中傳播。
    • Rule 2: 如果一個hash slot已經(jīng)被分配,并且一個已知節(jié)點廣播消息中的configEpoch比當前擁有該slot的master的configEpoch更大,則重新綁定hash slot到新節(jié)點。
    • 因為rule 2,最終所有節(jié)點一定會通過節(jié)點間的消息廣播就configEpoch最大的節(jié)點獲得slot的擁有權(quán)達成一致。
    • 這一機制被稱為 last failover wins(最后故障轉(zhuǎn)移者勝)
    • 同樣的情況也發(fā)生在resharding(重分片)。當一個節(jié)點importing一個hash slot并完成,它的configuration epoch將被增加以確保改動會被擴散到整個集群。

5.8 UPDATE消息,a closer look

  • Node A在一段時間后重新加入集群。它將發(fā)送heartbeat包并聲明自己服務于hash slots1和2,而configuration epoch為3。所有更新了最新集群信息的接收者卻看到相同的hash slots已經(jīng)被關聯(lián)到了節(jié)點B,并且節(jié)點B擁有更高的configuration epoch。因此它們會發(fā)送一條UPDATE消息給A,同時帶上這些slots的心配置。A將會根據(jù)rule2更新自己的配置。

5.9 節(jié)點如何重新加入集群

  • 同樣的機制也被用于一個節(jié)點重新加入集群。繼續(xù)上面的例子,節(jié)點A會被通知hash slots1和2現(xiàn)在被節(jié)點B服務。假設A之前只服務這兩個hash slots,那么目前A服務的slots數(shù)目就會將為0。因此A將會重新配置為新master的slave。
  • 實際上遵循的規(guī)則比上述場景更復雜一些。同上這在A經(jīng)過很長時間的斷開后重新加入集群時更容易發(fā)生,這時A發(fā)現(xiàn)它之前服務的hash slots目前被多個節(jié)點服務,例如hash slot 1由節(jié)點B服務,而hash slot 2被節(jié)點C服務。
  • 所以真是場景中Redis Cluster節(jié)點角色切換的規(guī)則是:
    • 一個master節(jié)點(在重新加入集群后)將會修改自己的配置為:自動從屬于(be slave of)之前該master服務的最后一個hash slot的新master
  • 通過重新配置,最終該節(jié)點服務的所有hash slots將會被丟棄,且該節(jié)點會被重新配置。
  • 對slave而言也是一樣的:它們會將自己配置為之前master的最后一個hash slot的新master的備份節(jié)點。

5.10 備份遷移(Replica migration)

  • Redis Cluster實現(xiàn)了一個稱為備份遷移(replica migration)的概念(特性),用來提升系統(tǒng)可用性。在一個由master-slave組成的集群中,如果slaves和masters之間的映射關系是固定的,那么集群的可用性隨著時間的推移,姜會因為單個節(jié)點的失敗而變得越來越差。
  • 例如,在一個每個master擁有一個slave的集群中,集群在任意一個master或slave失敗后還能繼續(xù)運作,但是如果master和slave同時失敗,則集群就無法繼續(xù)使用了。不幸的是,由于硬件或軟件的問題,總是會有一類錯誤造成但個節(jié)點的失敗,并且隨著時間而積累。比如:
    • Master A只有一個slave A1
    • Master A發(fā)生了故障了,A1被晉升為新的master
    • 三小時后,由于各種問題,A1也發(fā)生了故障。此時已經(jīng)沒有其他slave可以被晉升了,因為A和A1已經(jīng)全部宕機。Cluster這時會進入error狀態(tài)而不能繼續(xù)提供服務。
  • 如果masters和slaves之間的映射關系是固定的,那么唯一能保證集群更穩(wěn)定的方法就是為每個master添加更多的slave,然而這樣做的成本非常昂貴,因為它需要添加更多的Redis實例,以及更多的內(nèi)存等。
  • 另一個可用方案是在集群中創(chuàng)建不對稱(slave),并允許集群布局隨著時間自動發(fā)生變化。例如,集群可以擁有三個masters:A,B,C。A和B分別有一個slave,A1和B1。而master C與之不同,它擁有兩個slaves:C1和C2。
  • 備份遷移是一種slave自動重配的過程,它用來將備份節(jié)點遷移到當前已經(jīng)沒有可用slave的master上。通過備份遷移,上述場景變?yōu)椋?
    • Master A發(fā)生故障。A1被晉升。
    • C2遷移為A1的slave,否則A1將會沒有任何slave。
    • 三小時后A1也發(fā)生了故障
    • C2被晉升為新的master以替代A1
    • 此時,集群還能正常提供服務。

5.11 備份遷移算法

  • 歉意算法不需要使用任何的協(xié)商(agreement),因為在Redis Cluster中,slave布局不是集群配置的一部分,他不需要通過config epoch提供任何的一致性和/或版本化保證。相反,在master沒有回歸之前,它使用了一種避免slave塊遷移(mass-migration)的算法。這種算法保證了最終(一旦集群配置變得穩(wěn)定后)每個master至少可以擁有一個slave。
  • 這就是該算法的工作方式。我們先從『什么是一個好的slve(a good slave)』的定義開始:一個好的slave指的是,從某個節(jié)點角度,處于非FAIL狀態(tài)的slave節(jié)點。
  • 每個slave一旦檢測到目前至少有一個master沒有好的slave,就會觸發(fā)該算法。然而,在所有檢測到這一情況的slave中,只有一個子集會真正行動(act)。這個子集實際上通常只有一個slave,除非不同的slaves在特定時刻對其他節(jié)點的失敗狀態(tài)有一個略微不同的視圖。
  • 行動slave(acting slave) 是一個這樣的slave:它是集群中擁有最多slave的master的一個ID最小的slave。
  • 例如,對于這樣的一個集群,集群中有10個masters每個擁有1個slave,有2個masters每個擁有5個slaves,那么即將發(fā)生遷移的slave是 —— 在2個擁有5個slaves的master中 - 擁有最小節(jié)點ID的那個。確認這點不需要使用任何協(xié)商,只是有可能在集群配置不穩(wěn)定的時候,發(fā)生競爭條件,這時多個slave會認為自己是擁有更小節(jié)點ID的非失敗節(jié)點 (實際上這很難發(fā)生)。如果發(fā)生了,結(jié)果就是會有多個slaves被遷移到同一master,這本身并沒有害處。如果競爭導致失去slave的master變成孤節(jié)點,一旦集群穩(wěn)定后,該算法會再次被執(zhí)行并轉(zhuǎn)移一個可用slave給該節(jié)點。
  • 最終每個master將會擁有至少一個slave。然而,通常行為都會是,只有一個slave從其擁有多個slaves的master遷移到另一個孤master上。
  • 算法會被一個稱為cluster-migration-barrier的用戶配置參數(shù)控制,該參數(shù)指定了在發(fā)生備份遷移之前,一個master必須擁有的好slave的數(shù)目。例如,如果該參數(shù)被設置為2,那么只有在某個master擁有2個可工作slaves時,其中一個slave才能嘗試遷移(【譯注】通過檢查代碼,應該是擁有2個或2個以上可工作slaves時,其中一個可以發(fā)生遷移)。

5.12 configEpoch沖突解決算法

  • 在failover階段,slave晉升中會產(chǎn)生一個新的configEpoch值,并且這個值被保證是唯一的。
  • 然而如果有兩個不同的events通過不安全的方式分別生成新的configEpoch值,它們將會僅僅把本地的currentEpoch自增并寄希望于同一時間內(nèi)不會有沖突。比如,系統(tǒng)管理員同時觸發(fā)了兩個events:
    1. TAKEOVER選項的CLUSTER FAILOVER會手動晉升一個slave節(jié)點到master,并且不需要多數(shù)master的同意。這是個有用的操作,比如,在多數(shù)據(jù)中心創(chuàng)建時
    2. 為集群rebalancing而遷移slots,為了性能考慮,這同樣會在節(jié)點內(nèi)產(chǎn)生新的configuration epochs而不需要其他節(jié)點的同意。
  • 特別的,在手動重分片(reshardings)期間,當一個hash slot被從節(jié)點A遷移到節(jié)點B時,重分片程序?qū)娭埔驜更新它的configuration epoch為集群中的最大值加1(除非此時B的configuration epoch已經(jīng)是集群中最大的),而不需要請求其他節(jié)點的同意。
  • 通常一次真實世界的resharding會設計數(shù)百hash slot的遷移(尤其在較小的集群里)。在resharding期間,如果每一個slot遷移都需要為生成新的configuration epoch而請求其他節(jié)點的同意,這將會很沒有效率。而且,這回要求集群中的節(jié)點每次都通過fsync來保存這一新的配置(configuration epoch)。因為這些原因,我們采用如下行為處理reshardings時的configEpoch:我們只需要在第一個hash slot遷移時生成一個新的configEpoch,這在生產(chǎn)環(huán)境中會更加有效率。
  • 然而,因為上述兩種情況,有可能發(fā)生(雖然很難)多個節(jié)點擁有相同的configuration epoch的情況。一個resharding操作有管理員發(fā)起,同時一次failover恰巧發(fā)生,外加一點壞運氣,如果各自的epoch沒有在足夠短的時間內(nèi)擴散開,這將會會導致currentEpoch沖突。
  • 更進一步來說,軟件bug和文件系統(tǒng)破壞,這同樣可能導致多個節(jié)點擁有相同的configuration epoch。
  • 當服務于不同hash slots的master擁有相同的configEpoch時,這不會有什么問題。相對而言,更重要的是,當slave故障恢復一個master時,它需要擁有唯一的configuration epoch
    也就是說,手動干預或resharding會用一種不同的方式改變集群配置。Redis Cluster主要的liveness property要求,slot配置總是匯聚的,因此我們總是期望所有場景下,所有的master節(jié)點總是擁有不同的configEpoch。
  • 為了強制達到上述目的,當兩個節(jié)點以相同的configEpoch結(jié)束某個操作時, 沖突解決算法 將會被用來處理這一場景。
    • 如果一個master節(jié)點檢測到其他master節(jié)點正在廣播一個像同的configEpoch
    • 并且 如果本節(jié)點擁有一個按字典排序更小的節(jié)點ID
    • 那么 它會將自己的currentEpoch增加1,并用這一新值作為自己的configEpoch。
  • 如果還有任意數(shù)目的節(jié)點擁有相同的configEpoch,那么除了擁有最大ID的節(jié)點,所有其他節(jié)點都將被前移,來保證最終每個節(jié)點都擁有一個唯一的configEpoch。
    這一機制同樣也保證了,在一個新的集群被創(chuàng)建后,所有的節(jié)點都會以一個不同的configEpoch開始,盡管它們并沒有被用到,因為redis-trib會保證使用CONFIG SET-CONFIG-EPOCH為每一個節(jié)點設置不同的ID。
  • 然而,如果因為某些原因一個節(jié)點沒有被配置,它還是會自動地更新自己的配置到一個不同的configuration epoch(因為有沖突解決算法的保證)。

5.13 節(jié)點reset

  • 節(jié)點可以被軟件重置(software reset),而不需要重啟,以便用于不同的role或不同的cluster。
  • CLUSTER RESET命令包含連個變種:
    • CLUSTER RESET SOFT
    • CLUSTER RESET HARD
  • 命令必須被直接發(fā)送給待reset的節(jié)點。默認使用soft reset。
  • 下面是一個reset命令所做的操作:
    1. SOFT/HARD: 如果節(jié)點是slave,將其轉(zhuǎn)化為master,刪除所有數(shù)據(jù)。如果節(jié)點是master且包含keys,則reset失敗
    2. SOFT/HARD: 釋放所有的slots,重置手動failover狀態(tài)
    3. SOFT/HARD: 刪除該節(jié)點上node table中所有其他節(jié)點,即該節(jié)點不再知道任何其他節(jié)點的信息
    4. HARD only: 重置currentEpoch,configEpoch,和lastVoteEpoch為0
    5. HARD only: Node ID變更為一個新的隨機ID。
  • 擁有非空數(shù)據(jù)集的master節(jié)點不能被reset,因為一般而言你可能希望先reshard數(shù)據(jù)到其他分片?;蛘撸谀承┨囟▓鼍跋?,比如當一個cluster已經(jīng)完全被破壞而需要創(chuàng)建一個新cluster時,可以用FLUSHALl先清空數(shù)據(jù),然后再reset。

5.14 從集群移除節(jié)點

  • 如果希望移除一個節(jié)點,從實踐上來說,resharding它的所有數(shù)據(jù)到其他節(jié)點(如果這是一個master),然后關閉它,這樣是可行的。但是,其他節(jié)點姜還會技術該節(jié)點的node ID和地址,并會嘗試連接它。
    因此,當一個節(jié)點需要被移除時,我們希望把它徹底從其他節(jié)點的node table中一并移除。這可以通過CLUSTER FORGET <node-id>命令來實現(xiàn)。
  • 該命令做了兩件事情:
    1. 它把該節(jié)點從其他節(jié)點的nodes table中移除
    2. 它設置了一個60秒的禁用期,來防止一個節(jié)點使用相同的node id重新加入集群。
  • 上述第二條時必要的,因為Redis Cluster使用gossip來自動發(fā)現(xiàn)節(jié)點,因此從節(jié)點A刪除節(jié)點X,會導致節(jié)點B從節(jié)點A處通過gossip重新發(fā)現(xiàn)該節(jié)點(X)。而通過引入60秒禁用期,Redis Cluster管理工具就可以有60秒的時間來逐一在所有節(jié)點刪除X,并防止它被重新發(fā)現(xiàn)。

6. Publish/Subscribe

  • 在Redis Cluster集群中,client可以在任何節(jié)點上訂閱(subscribe)消息,也可以向任何節(jié)點發(fā)布(publish)消息。如果需要,集群會保證發(fā)布的消息被正確轉(zhuǎn)發(fā)。
  • 當前的實現(xiàn)中,發(fā)布的消息將會被簡單地廣播到所有節(jié)點,但有時候他是可以使用Bloom過濾器或其他算法進行優(yōu)化的。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容