Redis Cluster實現原理


一、Redis Cluster主要特性和設計

    集群目標

    1)高性能和線性擴展,最大可以支撐到1000個節點;Cluster架構中無Proxy層,Master與slave之間使用異步replication,且不存在操作的merge。(即操作不能跨多個nodes,不存在merge層)

    2)一定程度上保證writes的安全性,需要客戶端容忍一定程度的數據丟失:集群將會盡可能(best-effort)保存客戶端write操作的數據;通常在failover期間,會有短暫時間內的數據丟失(因為異步replication引起);當客戶端與少數派的節點處於網絡分區時(network partition),丟失數據的可能性會更高。(因為節點有效性檢測、failover需要更長的時間)

    3)可用性:只要集群中大多數master可達、且失效的master至少有一個slave可達時,集群都可以繼續提供服務;同時“replicas migration”可以將那些擁有多個slaves的master的某個slave,遷移到沒有slave的master下,即將slaves的分布在整個集群相對平衡,盡力確保每個master都有一定數量的slave備份。

 

    (Redis Cluster集群有多個shard組成,每個shard可以有一個master和多個slaves構成,數據根據hash slots配額分布在多個shard節點上,節點之間建立雙向TCP鏈接用於有效性檢測、Failover等,Client直接與shard節點進行通訊;Cluster集群沒有Proxy層,也沒有中央式的Master用於協調集群狀態或者state存儲;集群暫不提供動態reblance策略)

    備注:下文中提到的query、查詢等語義,泛指redis的讀寫操作。

 

    Mutli-key操作

    Redis單實例支持的命令,Cluster也都支持,但是對於“multi-key”操作(即一次RPC調用中需要進行多個key的操作)比如Set類型的交集、並集等,則要求這些key必須屬於同一個node。Cluster不能進行跨Nodes操作,也沒有nodes提供merge層代理。

    Cluster中實現了一個稱為“hash tags”的概念,每個key都可以包含一個自定義的“tags”,那么在存儲時將根據tags計算此key應該分布在哪個nodes上(而不是使用key計算,但是存儲層面仍然是key);此特性,可以強制某些keys被保存在同一個節點上,以便於進行“multikey”操作,比如“foo”和“{foo}.student”將會被保存在同一個node上。不過在人工對slots進行resharding期間,multikey操作可能不可用。

    我們在Redis單例中,偶爾會用到“SELECT”指令,即可以將key保存在特定的database中(默認database索引號為0);但是在Cluster環境下,將不支持SELECT命令,所有的key都將保存在默認的database中。

 

    客戶端與Server角色

       集群中nodes負責存儲數據,保持集群的狀態,包括keys與nodes的對應關系(內部其實為slots與nodes對應關系)。nodes也能夠自動發現其他的nodes,檢測失效的節點,當某個master失效時還應該能將合適的slave提升為master。  

       為了達成這些行為,集群中的每個節點都通過TCP與其他所有nodes建立連接,它們之間的通信協議和方式稱為“Redis Cluster Bus”。Nodes之間使用gossip協議(參見下文備注)向其他nodes傳播集群信息,以達到自動發現的特性,通過發送ping來確認其他nodes工作正常,也會在合適的時機發送集群的信息。當然在Failover時(包括人為failover)也會使用Bus來傳播消息。

     gossip:最終一致性,分布式服務數據同步算法,node首選需要知道(可以讀取配置)集群中至少一個seed node,此node向seed發送ping請求,此時seed節點pong返回自己已知的所有nodes列表,然后node解析nodes列表並與它們都建立tcp連接,同時也會向每個nodes發送ping,並從它們的pong結果中merge出全局nodes列表,並逐步與所有的nodes建立連接.......數據傳輸的方式也是類似,網絡拓撲結構為full mesh

     

       因為Node並不提供Proxy機制,當Client將請求發給錯誤的nodes時(此node上不存在此key所屬的slot),node將會反饋“MOVED”或者“ASK”錯誤信息,以便Client重新定向到合適的node。理論上,Client可以將請求發送給任意一個nodes,然后根據在根據錯誤信息轉發給合適的node,客戶端可以不用保存集群的狀態信息,當然這種情況下性能比較低效,因為Client可能需要2次TCP調用才能獲取key的結果,通常客戶端會緩存集群中nodes與slots的映射關系,並在遇到“Redirected”錯誤反饋時,才會更新本地的緩存。

 

    安全寫入(write safety)

    在Master-slaves之間使用異步replication機制,在failover之后,新的Master將會最終替代其他的replicas(即slave)。在出現網絡分區時(network partition),總會有一個窗口期(node timeout)可能會導致數據丟失;不過,Client與多數派的Master、少數派Master處於一個分區(網絡分區,因為網絡阻斷問題,導致Clients與Nodes被隔離成2部分)時,這兩種情況下影響並不相同。

    1)write提交到master,master執行完畢后向Client反饋“OK”,不過此時可能數據還沒有傳播給slaves(異步replication);如果此時master不可達的時間超過閥值(node timeout,參見配置參數),那么將觸發slave被選舉為新的Master(即Failover),這意味着那些沒有replication到slaves的writes將永遠丟失了!

    2)還有一種情況導致數據丟失:

        A)因為網絡分區,此時master不可達,且Master與Client處於一個分區,且是少數派分區。

        B)Failover機制,將其中一個slave提升為新Master。

        C)此后網絡分區消除,舊的Master再次可達,此時它將被切換成slave。

        D)那么在網絡分區期間,處於少數派分區的Client仍然將write提交到舊的Master,因為它們覺得Master仍然有效;當舊的Master再次加入集群,切換成slave之后,這些數據將永遠丟失。



 

    在第二種情況下,如果Master無法與其他大多數Masters通訊的時間超過閥值后,此Master也將不再接收Writes,自動切換為readonly狀態。當網絡分區消除后,仍然會有一小段時間,客戶端的write請求被拒絕,因為此時舊的Master需要更新本地的集群狀態、與其他節點建立連接、角色切換為slave等等,同時Client端的路由信息也需要更新。

    只有當此master被大多數其他master不可達的時間達到閥值時,才會觸發Failover,這個時間稱為NODE_TIMEOUT,可以通過配置設定。所以當網絡分區在此時間被消除的話,writes不會有任何丟失。反之,如果網絡分區持續時間超過此值,處於“小分區”(minority)端的Master將會切換為readonly狀態,拒絕客戶端繼續提交writes請求,那么“大分區”端將會進行failover,這意味着NODE_TIMEOUT期間發生在“小分區”端的writes操作將丟失(因為新的Master上沒有同步到那些數據)。 

 

    可用性

    處於“小分區”的集群節點是不可用的;“大分區”端必須持有大多數Masters,同時每個不可達的Master至少有一個slave也在“大分區”端,當NODE_TIMEOUT時,觸發failover,此后集群才是可用的。Redis Cluster在小部分nodes失效后仍然可以恢復有效性,如果application希望大面積節點失效仍然有效,那么Cluster不適合這種情況。

 

    比如集群有N個Master,且每個Master都有一個slave,那么集群的有效性只能容忍一個節點(master)被分區隔離(即一個master處於小分區端,其他處於大分區端),當第二個節點被分區隔離之前仍保持可用性的概率為1 - (1/(N * 2 - 1))(解釋:當第一個節點失效后,剩余N * 2 -1個節點,此時沒有slave的Master失效的概率為1/(N * 2 -1))。比如有5個Master,每個Master有一個slave,當2個nodes被隔離出去(或者失效)后,集群可用性的概率只有1/(5 * 2 - 1) = 11.11%,因此集群不再可用。

    幸好Redis Cluster提供了“replicas migration”機制,在實際應用方面,可以有效的提高集群的可用性,當每次failover發生后,集群都會重新配置、平衡slaves的分布,以更好的抵御下一次失效情況的發生。(具體參見下文)

 

    性能

    Redis Cluster並沒有提供Proxy層,而是告知客戶端將key的請求轉發給合適的nodes。Client保存集群中nodes與keys的映射關系(slots),並保持此數據的更新,所以通常Client總能夠將請求直接發送到正確的nodes上。因為采用異步replication,所以master不會等待slaves也保存成功后才向客戶端反饋結果,除非顯式的指定了WAIT指令。multi-key指令僅限於單個節點內,除了resharding操作外,節點的數據不會在節點間遷移。每個操作只會在特定的一個節點上執行,所以集群的性能為master節點的線性擴展。同時,Clients與每個nodes保持鏈接,所以請求的延遲等同於單個節點,即請求的延遲並不會因為Cluster的規模增大而受到影響。高性能和擴展性,同時保持合理的數據安全性,是Redis Cluster的設計目標。

 

    Redis Cluster沒有Proxy層,Client請求的數據也無法在nodes間merge;因為Redis核心就是K-V數據存儲,沒有scan類型(sort,limit,group by)的操作,因此merge操作並不被Redis Cluster所接受,而且這種特性會極大增加了Cluster的設計復雜度。(類比於mongodb)

 

二、Cluster主要組件

    keys分布模型

    集群將key分成16384個slots(hash 槽),slot是數據映射的單位,言外之意,Redis Cluster最多支持16384個nodes(每個nodes持有一個slot)。集群中的每個master持有16384個slots中的一部分,處於“stable”狀態時,集群中沒有任何slots在節點間遷移,即任意一個hash slot只會被單個node所服務(master,當然可以有多個slave用於replicas,slave也可以用來擴展read請求)。keys與slot的映射關系,是按照如下算法計算的:HASH_SLOT = CRC16(key) mod 16384。其中CRC16是一種冗余碼校驗和,可以將字符串轉換成16位的數字。

 

    hash tags

    在計算hash slots時有一個意外的情況,用於支持“hash tags”;hash tags用於確保多個keys能夠被分配在同一個hash slot中,用於支持multi-key操作。hash tags的實現比較簡單,key中“{}”之間的字符串就是當前key的hash tags,如果存在多個“{}”,首個符合規則的字符串作為hash tags,如果“{}”存在多級嵌套,那么最內層首個完整的字符串作為hash tags,比如“{foo}.student”,那么“foo”是hash tags。如果key中存在合法的hash tags,那么在計算hash slots時,將使用hash tags,而不再使用原始的key。即“foo”與“{foo}.student”將得到相同的slot值,不過“{foo}.student”仍作為key來保存數據,即redis中數據的key仍為“{foo}.student”。

 

    集群節點的屬性

    集群中每個節點都有唯一的名字,稱之為node ID,一個160位隨機數字的16進制表示,在每個節點首次啟動時創建。每個節點都將各自的ID保存在實例的配置文件中,此后將一直使用此ID,或者說只要配置文件不被刪除,或者沒有使用“CLUSTER RESET”指令重置集群,那么此ID將永不會修改。

    集群通過node ID來標識節點,而不是使用IP + port,因為node可以修改它的IP和port,不過如果ID不變,我們仍然認定它是集群中合法一員。集群可以在cluster Bus中通過gossip協議來探測IP、port的變更,並重新配置。

    node ID並不是與node相關的唯一信息,不過是唯一一個全局一致的。每個node還持有如下相關的信息,有些信息是關系集群配置的,其他的信息比如最后ping時間等。每個node也保存其他節點的IP、Port、flags(比如flags表示它是master還是slave)、最近ping的時間、最近pong接收時間、當前配置的epoch、鏈接的狀態,最重要的是還包含此node上持有的hash slots。這些信息均可通過“CLUSTER NODES”指令開查看。

 

    Cluster Bus

    每個Node都有一個特定的TCP端口,用來接收其他nodes的鏈接;此端口號為面向Client的端口號 + 10000,比如果客戶端端口號為6379,那么次node的BUS端口號為16379,客戶端端口號可以在配置文件中聲明。由此可見,nodes之間的交互通訊是通過Bus端口進行,使用了特定的二進制協議,此端口通常應該只對nodes可用,可以借助防火牆技術來屏蔽其他非法訪問。

 

    集群拓撲

    Redis Cluster中每個node都與其他nodes的Bus端口建立TCP鏈接(full mesh,全網)。比如在由N各節點的集群中,每個node有N-1個向外發出的TCP鏈接,以及N-1個其他nodes發過來的TCP鏈接;這些TCP鏈接總是keepalive,不是按需創建的。如果ping發出之后,node在足夠長的時間內仍然沒有pong響應,那么次node將會被標記為“不可達”,那么與此node的鏈接將會被刷新或者重建。Nodes之間通過gossip協議和配置更新的機制,來避免每次都交互大量的消息,最終確保在nodes之間的信息傳送量是可控的。

 

    節點間handshake

    Nodes通過Bus端口發送ping、pong;如果一個節點不屬於集群,那么它的消息將會被其他nodes全部丟棄。一個節點被認為是集群成員的方式有2種:

    1)如果此node在“Cluster meet”指令中引入,此命令的主要意義就是將指定node加入集群。那么對於當前節點,將認為指定的node為“可信任的”。(此后將會通過gossip協議傳播給其他nodes)

    2)當其他nodes通過gossip引入了新的nodes,這些nodes也是被認為是“可信任的”。

 

    只要我們將一個節點加入集群,最終此節點將會與其他節點建立鏈接,即cluster可以通過信息交換來自動發現新的節點,鏈接拓撲仍然是full mesh。

 

三、重定向與resharding

    MOVED重定向

    理論上,Client可以將請求隨意發給任何一個node,包括slaves,此node解析query,如果可以執行(比如語法正確,multiple keys都應該在一個node slots上),它會查看key應該屬於哪個slot、以及此slot所在的nodes,如果當前node持有此slot,那么query直接執行即可,否則當前node將會向Client反饋“MOVED”錯誤:

 

GET X  
-MOVED 3999 127.0.0.1:6381   

 

    錯誤信息中包括此key對應的slot(3999),以及此slot所在node的ip和port,對於Client 而言,收到MOVED信息后,它需要將請求重新發給指定的node。不過,當node向Client返回MOVED之前,集群的配置也在變更(節點調整、resharding、failover等,可能會導致slot的位置發生變更),此時Client可能需要等待更長的時間,不過最終node會反饋MOVED信息,且信息中包含指定的新的node位置。雖然Cluster使用ID標識node,但是在MOVED信息中盡可能的暴露給客戶端便於使用的ip + port。

 

    當Client遇到“MOVED”錯誤時,將會使用“CLUSTER NODES”或者“CLUSTER SLOTS”指令獲取集群的最新信息,主要是nodes與slots的映射關系;因為遇到MOVED,一般也不會僅僅一個slot發生的變更,通常是一個或者多個節點的slots發生了變化,所以進行一次全局刷新是有必要的;我們還應該明白,Client將會把集群的這些信息在被緩存,以便提高query的性能。

    還有一個錯誤信息:“ASK”,它與“MOVED”都屬於重定向錯誤,客戶端的處理機制基本相同,只是ASK不會觸發Client刷新本地的集群信息。

 

    集群運行時重新配置(live reconfiguration)

    我們可以在Cluster運行時增加、刪除nodes,這兩種操作都會導致:slots在nodes的遷移;當然這種機制也可用來集群數據的rebalance等等。

    1)集群中新增一個node,我們需要將其他nodes上的部分slots遷移到此新nodes上,以實現數據負載的均衡分配。

    2)集群中移除一個node,那么在移除節點之前,必須將此節點上(如果此節點沒有任何slaves)的slots遷移到其他nodes。

    3)如果數據負載不均衡,比如某些slots數據集較大、負載較大時,我們需要它們遷移到負載較小的nodes上(即手動resharding),以實現集群的負載平衡。

 

    Cluster支持slots在nodes間移動;從實際的角度來看,一個slot只是一序列keys的邏輯標識,所以Cluster中slot的遷移,其實就是一序列keys的遷移,不過resharding操作只能以slot為單位(而不能僅僅遷移某些keys)。Redis提供了如下幾個操作:

    1)CLUSTER ADDSLOTS [slot] ....

    2)CLUSTER DELSLOTS [slot] ...

    3)CLUSTER SETSLOT [slot] NODE [node]

    4)CLUSTER SETSLOT [slot] MIGRATING [destination-node]

    5)CLUSTER SETSLOT [slot] IMPORTING [source-node]

 

    前兩個指令:ADDSLOTS和DELSLOTS,用於向當前node分配或者移除slots,指令可以接受多個slot值。分配slots的意思是告知指定的master(即此指令需要在某個master節點執行)此后由它接管相應slots的服務;slots分配后,這些信息將會通過gossip發給集群的其他nodes。

    ADDSLOTS指令通常在創建一個新的Cluster時使用,一個新的Cluster有多個空的Masters構成,此后管理員需要手動為每個master分配slots,並將16384個slots分配完畢,集群才能正常服務。簡而言之,ADDSLOTS只能操作那些尚未分配的(即不被任何nodes持有)slots,我們通常在創建新的集群或者修復一個broken的集群(集群中某些slots因為nodes的永久失效而丟失)時使用。為了避免出錯,Redis Cluster提供了一個redis-trib輔助工具,方便我們做這些事情。

 

    DELSLOTS就是將指定的slots刪除,前提是這些slots必須在當前node上,被刪除的slots處於“未分配”狀態(當然其對應的keys數據也被clear),即尚未被任何nodes覆蓋,這種情況可能導致集群處於不可用狀態,此指令通常用於debug,在實際環境中很少使用。那些被刪除的slots,可以通過ADDSLOTS重新分配。

 

    SETSLOT是個很重要的指令,對集群slots進行reshard的最重要手段;它用來將單個slot在兩個nodes間遷移。根據slot的操作方式,它有兩種狀態“MIGRATING”、“IMPORTING”(或者說遷移的方式)

    1)MIGRATING:將slot的狀態設置為“MIGRATING”,並遷移到destination-node上,需要注意當前node必須是slot的持有者。在遷移期間,Client的查詢操作仍在當前node上執行,如果key不存在,則會向Client反饋“-ASK”重定向信息,此后Client將會把請求重新提交給遷移的目標node。

    2)IMPORTING:將slot的狀態設置為“IMPORTING”,並將其從source-node遷移到當前node上,前提是source-node必須是slot的持有者。Client交互機制同上。

 

    假如我們有兩個節點A、B,其中slot 8在A上,我們希望將8從A遷移到B,可以使用如下方式:

    1)在B上:CLUSTER SETSLOT 8 IMPORTING A

    2)在A上:CLUSTER SETSLOT 8 MIGRATING B

    在遷移期間,集群中其他的nodes的集群信息不會改變,即slot 8仍對應A,即此期間,Client查詢仍在A上:

    1)如果key在A上存在,則有A執行。

    2)否則,將向客戶端返回ASK,客戶端將請求重定向到B。

    這種方式下,新key的創建就不會在A上執行,而是在B上執行,這也就是ASK重定向的原因(遷移之前的keys在A,遷移期間created的keys在B上);當上述SETSLOT執行完畢后,slot的狀態也會被自動清除,同時將slot遷移信息傳播給其他nodes,至此集群中slot的映射關系將會變更,此后slot 8的數據請求將會直接提交到B上。

 

    ASK重定向

    在上文中,我們已經介紹了MOVED重定向,ASK與其非常相似。在resharding期間,為什么不能用MOVED?MOVED意思為hash slots已經永久被另一個node接管、接下來的相應的查詢應該與它交互,ASK的意思是當前query暫時與指定的node交互;在遷移期間,slot 8的keys有可能仍在A上,所以Client的請求仍然需要首先經由A,對於A上不存在的,我們才需要到B上進行嘗試。遷移期間,Redis Cluster並沒有粗暴的將slot 8的請求全部阻塞、直到遷移結束,這種方式盡管不再需要ASK,但是會影響集群的可用性。

    1)當Client接收到ASK重定向,它僅僅將當前query重定向到指定的node;此后的請求仍然交付給舊的節點。

    2)客戶端並不會更新本地的slots映射,仍然保持slot 8與A的映射;直到集群遷移完畢,且遇到MOVED重定向。

 

    一旦slot 8遷移完畢之后(集群的映射信息也已更新),如果Client再次在A上訪問slot 8時,將會得到MOVED重定向信息,此后客戶端也更新本地的集群映射信息。

 

    客戶端首次鏈接以及重定向處理

    可能有些Cluster客戶端的實現,不會在內存中保存slots映射關系(即nodes與slots的關系),每次請求都從聲明的、已知的nodes中,隨機訪問一個node,並根據重定向(MOVED)信息來尋找合適的node,這種訪問模式,通常是非常低效的。

    當然,Client應該盡可能的將slots配置信息緩存在本地,不過配置信息也不需要絕對的實時更新,因為在請求時偶爾出現“重定向”,Client也能兼容此次請求的正確轉發,此時再更新slots配置。(所以Client通常不需要間歇性的檢測Cluster中配置信息是否已經更新)客戶端通常是全量更新slots配置:

    1)首次鏈接到集群的某個節點

    2)當遇到MOVED重定向消息時

    遇到MOVED時,客戶端僅僅更新特定的slot是不夠的,因為集群中的reshard通常會影響到多個slots。客戶端通過向任意一個nodes發送“CLUSTER NODES”或者“CLUSTER SLOTS”指令均可以獲得當前集群最新的slots映射信息;“CLUSTER SLOTS”指令返回的信息更易於Client解析。如果集群處於broken狀態,即某些slots尚未被任何nodes覆蓋,指令返回的結果可能是不完整的。

 

    Multikeys操作

    前文已經介紹,基於hash tags機制,我們可以在集群中使用Multikeys操作。不過,在resharding期間,Multikeys操作將可能不可用,比如這些keys不存在於同一個slot(遷移會導致keys被分離);比如Multikeys邏輯上屬於同一個slot,但是因為resharding,它們可能暫時不處於同一個nodes,有些可能在遷移的目標節點上(比如Multikeys包含a、b、c三個keys,邏輯上它們都屬於slot 8,但是其中c在遷移期間創建,它被存儲在節點B上,a、b仍然在節點A),此時將會向客戶端返回“-TRYAGAIN”錯誤,那么客戶端此后將需要重試一次,或者直接返回錯誤(如果遷移操作被中斷),無論如何最終Multikeys的訪問邏輯是一致的,slots的狀態也是最終確定的。

 

    slaves擴展reads請求

    通常情況下,read、write請求都將有持有slots的master節點處理;因為redis的slaves可以支持read操作(前提是application能夠容忍stale數據),所以客戶端可以使用“READONLY”指令來擴展read請求。

    “READONLY”表明其可以訪問集群的slaves節點,能夠容忍stale數據,而且此次鏈接不會執行writes操作。當鏈接設定為readonly模式后,Cluster只有當keys不被slave的master節點持有時才會發送重定向消息(即Client的read請求總是發給slave,只有當此slave的master不持有slots時才會重定向,很好理解):

    1)此slave的master節點不持有相應的slots

    2)集群重新配置,比如reshard或者slave遷移到了其他master上,此slave本身也不持有此slot。

 

    此時Client更新本地的slot配置信息,同上文所述。(目前很多Client實現均基於連接池,所以不能非常便捷的設置READLONLY選項,非常遺憾)

 

四、容錯(Fault Tolerance)

    心跳與gossip消息

    集群中的nodes持續的交換ping、pong數據,這兩種數據包的結構一樣,同樣都能攜帶集群的配置信息,唯一不同的就是message中的type字段。

    通常,一個node發送ping消息,那么接收者將會反饋pong消息;不過有時候並非如此,或許接收者將pong信息發給其他的nodes,而不是直接反饋給發送者,比如當集群中添加新的node時。

    通常一個node每秒都會隨機向幾個nodes發送ping,所以無論集群規模多大,每個nodes發送的ping數據包的總量是恆定的。每個node都確保盡可能的向那些在半個NODE_TIMEOUT時間內,尚未發送過ping或者接收到它們的pong消息的nodes發送ping。在NODE_TIMEOUT逾期之前,nodes也會嘗試與那些通訊異常的nodes重新建立TCP鏈接,確保不能僅僅因為當前鏈接異常而認為它們就是不可達的。

 

    當NODE_TIMEOUT值較小、集群中nodes規模較大時,那么全局交換的信息量也會非常龐大,因為每個node都盡力在半個NODE_TIMEOUT時間內,向其他nodes發送ping。比如有100個nodes,NODE_TIMEOUT為60秒,那么每個node在30秒內向其他99各nodes發送ping,平均每秒3.3個消息,那么整個集群全局就是每秒330個消息。這些消息量,並不會對集群的帶寬帶來不良問題。

 

    心跳數據包的內容

    1)node ID

    2)currentEpoch和configEpoch

    3)node flags:比如表示此node是maste、slave等

    4)hash slots:發送者持有的slots

    5)如果發送者是slave,那么其master的ID

    6)其他..

 

    ping和pong數據包中也包含gossip部分,這部分信息包含sender持有的集群視圖,不過它只包含sender已知的隨機幾個nodes,nodes的數量根據集群規模的大小按比例計算。gossip部分包含了nodes的ID、ip+port、flags,那么接收者將根據sender的視圖,來判定節點的狀態,這對故障檢測、節點自動發現非常有用。

 

    失效檢測

    集群失效檢測就是,當某個master或者slave不能被大多數nodes可達時,用於故障遷移並將合適的slave提升為master。當slave提升未能有效實施時,集群將處於error狀態且停止接收Client端查詢。

    如上所述,每個node有持有其已知nodes的列表包括flags,有2個flag狀態:PFAIL和FAIL;PFAIL表示“可能失效”,是一種尚未完全確認的失效狀態(即某個節點或者少數masters認為其不可達)。FAIL表示此node已經被集群大多數masters判定為失效(大多數master已認定為不可達,且不可達時間已達到設定值,需要failover)。

 

    PFAIL:

    一個被標記為PFAIL的節點,表示此node不可達的時間超過NODE_TIMEOUT,master和slave有可以被標記為PFAIL。所謂不可達,就是當“active ping”(發送ping且能受到pong)尚未成功的時間超過NODE_TIMEOUT,因此我們設定的NODE_TIMEOUT的值應該比網絡交互往返的時間延遲要大一些(通常要大的多,以至於交互往返時間可以忽略)。為了避免誤判,當一個node在半個NODE_TIMEOUT時間內仍未能pong,那么當前node將會盡力嘗試重新建立連接進行重試,以排除pong未能接收是因為當前鏈接故障的問題。

 

    FAIL:

    PFAIL只是當前node有關於其他nodes的本地視圖,可能每個node對其他nodes的本地視圖都不一樣,所以PFAIL還不足以觸發Failover。處於PFAIL狀態下的node可以被提升到FAIL狀態。如上所述,每個node在向其他nodes發送gossip消息時,都會包含本地視圖中幾個隨機nodes的狀態信息;每個node最終都會從其他nodes發送的消息中獲得一組nodes的flags。因此,每個node都可以通過這種機制來通知其他nodes,它檢測到的故障情況。

    PFAIL被上升為FAIL的集中情況:

    1)比如A節點,認為B為PFAIL

    2)那么A通過gossip信息,收集集群中大多數masters關於B的狀態視圖。

    3)多數master都認為B為PFAIL,或者PFAIL情況持續時間為NODE_TIMEOUT * FAIL_REPORT_VALIDITY_MULT(此值當前為2)

 

    如果上述條件成立,那么A將會:

    1)將B節點設定為FAIL

    2)將FAIL信息發送給其所有能到達的所有節點。

 

    每個接收到FAIL消息的節點都會強制將此node標記為FAIL狀態,不管此節點在本地視圖中是否為PFAIL。FAIL狀態是單向的,即PFAIL可以轉換為FAIL,但是FAIL狀態只能清除,不能回轉為PFAIL:

    1)當此node已經變的可達,且為slave,這種情況下FAIL狀態將會被清除,因為沒有發生failover。

    2)此node已經可達,且是一個沒有服務任何slots的master(空的master);這種情況下,FAIL將會被清除,因為master沒有持有slots,所以它並沒有真正參與到集群中,需要等到重新配置以便它加入集群。

    3)此node已經可達,且是master,且在較長時間內(N倍的NODE_TIMEOUT)沒有檢測到slave的提升,即沒有slave發生failover(比如此master下沒有slave),那么它只能重新加入集群且仍為master。

 

    需要注意的是PFAIL->FAIL的轉變,使用了“協議”(agreement)的形式:

    1)nodes會間歇性的收集其他nodes的視圖,即使大多數masters都“agree”,事實上這個狀態,僅僅是我們從不同的nodes、不同的時間收集到的,我們無法確認(也不需要)在特定時刻大多數masters是否“agree”。我們丟棄較舊的故障報告,所以此故障(FAIL)是有大多數masters在一段時間內的信號。

    2)雖然每個node在檢測到FAIL情況時,都會通過FAIL消息發送給其他nodes,但是無法保證消息一定會到達所有的nodes,比如可能當前節點(發送消息的node)因為網絡分區與其他部分nodes隔離了。

 

    如果只有少數master認為某個node為FAIL,並不會觸發相應的slave提升,即failover,因為可能是因為網絡分區導致。FAIL標記只是用來觸發slave 提升;在原理上,當master不可達時將會觸發slave提升,不過當master仍然被大多數可達時,它會拒絕提供相應的確認。

 

五、Failover相關的配置

    集群currentEpoch

    Redis Cluster使用了類似於Raft算法“term”(任期)的概念,那么在redis Cluster中term稱為epoch,用來給events增量版本號。當多個nodes提供了信息有沖突時,它可以作為node來知道哪個狀態是最新的。currentEpoch為一個64位無簽名數字。

    在集群node創建時,master和slave都會將各自的currentEpoch設置為0,每次從其他node接收到數據包時,如果發現發送者的epoch值比自己的大,那么當前node將自己的currentEpoch設置為發送者的epoch。由此,最終所有的nodes都會認同集群中最大的epoch值;當集群的狀態變更,或者node為了執行某個行為需求agreement時,都將需要epoch(傳遞或者比較)。

 

    當前來說,只有在slave提升期間發生;currentEpoch為集群的邏輯時鍾(logical clock),指使持有較大值的獲勝。(currentEpoch,當前集群已達成認同的epoch值,通常所有的nodes應該一樣)

 

    configEpoch

    每個master總會在ping、pong數據包中攜帶自己的configEpoch以及它持有的slots列表。新創建的node,其configEpoch為0,slaves通過遞增它們的configEpoch來替代失效的master,並嘗試獲得其他大多數master的授權(認同)。當slave被授權,一個新的configEpoch被生成,slave提升為master且使用此configEpoch。

    接下來介紹configEpoch幫助解決沖突,當不同的nodes宣稱有分歧的配置時。

    slaves在ping、pong數據包中也會攜帶自己的configEpoch信息,不過這個epoch為它與master在最近一次數據交換時,master的configEpoch。

    每當節點發現configEpoch值變更時,都會將新值寫入nodes.conf文件,當然currentEpoch也也是如此。這兩個變量在寫入文件后會伴隨磁盤的fsync,持久寫入。嚴格來說,集群中所有的master都持有唯一的configEpoch值。同一組master-slaves持有相同的configEpoch。

 

    slave選舉與提升

    在slaves節點中進行選舉,在其他masters的幫助下進行投票,選舉出一個slave並提升為master。當master處於FAIL狀態時,將會觸發slave的選舉。slaves都希望將自己提升為master,此master的所有slaves都可以開啟選舉,不過最終只有一個slave獲勝。當如下情況滿足時,slave將會開始選舉:

    1)當此slave的master處於FAIL狀態

    2)此master持有非零個slots

    3)此slave的replication鏈接與master斷開時間沒有超過設定值,為了確保此被提升的slave的數據是新鮮的,這個時間用戶可以配置。

 

       為了選舉,第一步,就是slave自增它的currentEpoch值,然后向其他masters請求投票(需求支持,votes)。slave通過向其他masters傳播“FAILOVER_AUTH_REQUEST”數據包,然后最長等待2倍的NODE_TIMEOUT時間,來接收反饋。一旦一個master向此slave投票,將會響應“FAILOVER_AUTH_ACK”,此后在2 * NODE_TIMOUT時間內,它將不會向同一個master的slaves投票;雖然這對保證安全上沒有必要,但是對避免多個slaves同時選舉時有幫助的。slave將會丟棄那些epoch值小於自己的currentEpoch的AUTH_ACK反饋,即不會對上一次選舉的投票計數(只對當前輪次的投票計數)。一旦此slave獲取了大多數master的ACKs,它將在此次選舉中獲勝;否則如果大多數master不可達(在2 * NODE_TIMEOUT)或者投票額不足,那么它的選舉將會被中斷,那么其他的slave將會繼續嘗試。

    

    slave rank(次序)

    當master處於FAIL狀態時,slave將會隨機等待一段時間,然后才嘗試選舉,等待的時間:

    DELAY = 500ms + random(0 ~ 500ms) + SLAVE_RANK * 1000ms

    一定的延遲確保我們等待FAIL狀態在集群中傳播,否則slave立即嘗試選舉(不進行等待的話),不過此時其他masters或許尚未意識到FAIL狀態,可能會拒絕投票。

 

    延遲的時間是隨機的,這用來“去同步”(desynchronize),避免slaves同時開始選舉。SLAVE_RANK表示此slave已經從master復制數據的總量的rank。當master失效時,slaves之間交換消息以盡可能的構建rank,持有replication offset最新的rank為0,第二最新的為1,依次輪推。這種方式下,持有最新數據的slave將會首先發起選舉(理論上)。當然rank順序也不是嚴格執行的,如果一個持有較小rank的slave選舉失敗,其他slaves將會稍后繼續。

 

    一旦,slave選舉成功,它將獲取一個新的、唯一的、自增的configEpoch值,此值比集群中任何masters持有的都要大,它開始宣稱自己是master,並通過ping、pong數據包傳播,並提供自己的新的configEpoch以及持有的slots列表。為了加快其他nodes的重新配置,pong數據包將會在集群中廣播。當前node不可達的那些節點,它們可以從其他節點的ping或者pong中獲知信息(gossip),並重新配置。

 

    其他節點也會檢測到這個新的master和舊master持有相同的slots,且持有更高的configEpoch,此時也會更新自己的配置(epoch,以及master);舊master的slaves不僅僅更新配置信息,也會重新配置並與新的master跟進(slave of)。

 

    Masters響應slave的投票請求

    當Master接收到slave的“FAILOVER_AUTH_REQUEST”請求后,開始投票,不過需要滿足如下條件:

    1)此master只會對指定的epoch投票一次,並且拒絕對舊的epoch投票:每個master都持有一個lastVoteEpoch,將會拒絕AUTH_REQUEST中currentEpoch比lastVoteEpoch小的請求。當master響應投票時,將會把lastVoteEpoch保存在磁盤中。

    2)此slave的master處於FAIL狀態時,master才會投票。

    3)如果slave的currentEpoch比此master的currentEpoch小,那么AUTH_REQUEST將會被忽略。因為master只會響應那些與自己的currentEpoch相等的請求。如果同一個slave再此請求投票,持有已經增加的currentEpoch,它(slave)將保證舊的投票響應不能參與計票。

 

    比如master的currentEpoch為5,lastVoteEpoch為1:

    1)slave的currentEpoch為3

    2)slave在選舉開始時,使用epoch為4(先自增),因為小於master的epoch,所以投票響應被延緩。

    3)slave在一段時間后將重新選舉,使用epoch為5(4 + 1,再次自增),此時master上延緩的響應發給slave,接收后視為有效。

 

    1)master在2 * NODE_TIMEOUT超時之前,不會對同一個master的slave再次投票。這並不是嚴格需要,因為也不太可能兩個slave在相同的epoch下同時贏得選舉。不過,它確保當一個slave選舉成功后,它(slave)有一段緩沖時間來通知其他的slaves,避免另一個slave贏得了新的一輪的選擇,避免不必要的二次failover。

    2)master並不會盡力選舉最合適的slave。當slave的master處於FAIL狀態,此master在當前任期(term)內並不投票,只是批准主動投票者(即master不發起選舉,只批准別人的投票)。最合適的slave應該在其他slaves之前,首先發起選舉。

    3)當master拒絕一個slave投票,並不會發出一個“否決”響應,而是簡單的忽略。

    4)slave發送的configEpoch是其master的,還包括其master持有的slots;master不會向持有相同slots、但configEpoch只較低的slave投票。

 

    Hash Slots配置傳播

    Redis Cluster中重要的一部分就是傳播集群中哪些節點上持有的哪些hash slots信息;無論是啟動一個新的集群,還是當master失效其slave提升后更新配置,這對它們都至關重要。有2種方式用於hash slot配置的傳播:

    1)heartbeat 消息:發送者的ping、pong消息中,總是攜帶自己目前持有的slots信息,不管自己是master還是slave。

    2)UPDATE 消息:因為每個心跳消息中會包含發送者的configEpoch和其持有的slots,如果接收者發現發送者的信息已經stale(比如發送者的configEpoch值小於持有相同slots的master的值),它會向發送者反饋新的配置信息(UPDATE),強制stale節點更新它。

 

    當一個新的節點加入集群,其本地的hash slots映射表將初始為NULL,即每個hash slot都沒有與任何節點綁定。

    Rule 1:如果此node本地視圖中一個hash slot尚未分配(設置為NULL),並且有一個已知的node聲明持有它,那么此node將會修改本地hash slot的映射表,將此slot與那個node關聯。slave的failover操作、reshard操作都會導致hash slots映射的變更,新的配置信息將會通過心跳在集群中傳播。

    Rule 2:如果此node的本地視圖中一個hash slot已經分配,並且一個已知的node也聲明持有它,且此node的configEpoch比當前slot關聯的master的configEpoch值更大,那么此node將會把slot重新綁定到新的node上。根據此規則,最終集群中所有的nodes都贊同那個持有聲明持有slot、且configEpoch最大值的nodes為slot的持有者。

 

    nodes如何重新加入集群

    node A被告知slot 1、2現在有node B接管,假如這兩個slots目前有A持有,且A只持有這兩個slots,那么此后A將放棄這2個slots,成為空的節點;此后A將會被重新配置,成為其他新master的slave。這個規則可能有些復雜,A離群一段時間后重新加入集群,此時A發現此前自己持有的slots已經被其他多個nodes接管,比如slot 1被B接管,slot 2被C接管。

    在重新配置時,最終此節點上的slots將會被清空,那個竊取自己最后一個slot的node,將成為它的新master。

    節點重新加入集群,通常發生在failover之后,舊的master(也可以為slave)離群,然后重新加入集群。

 

    Replica遷移

    Redis Cluster實現了一個成為“Replica migration”的概念,用來提升集群的可用性。比如集群中每個master都有一個slave,當集群中有一個master或者slave失效時,而不是master與它的slave同時失效,集群仍然可以繼續提供服務。

    1)master A,有一個slave A1

    2)master A失效,A1被提升為master

    3)一段時間后,A1也失效了,那么此時集群中沒有其他的slave可以接管服務,集群將不能繼續服務。

 

    如果masters與slaves之間的映射關系是固定的(fixed),提高集群抗災能力的唯一方式,就是給每個master增加更多的slaves,不過這種方式開支很大,需要更多的redis實例。

    解決這個問題的方案,我們可以將集群非對稱,且在運行時可以動態調整master-slaves的布局(而不是固定master-slaves的映射),比如集群中有三個master A、B、C,它們對應的slave為A1、B1、C1、C2,即C節點有2個slaves。“Replica遷移”可以自動的重新配置slave,將其遷移到某個沒有slave的master下。

    1)A失效,A1被提升為master

    2)此時A1沒有任何slave,但是C仍然有2個slave,此時C2被遷移到A1下,成為A1的slave

    3)此后某刻,A1失效,那么C2將被提升為master。集群可以繼續提供服務。

 

    Replica遷移算法

    遷移算法並沒有使用“agree”形式,而是使用一種算法來避免大規模遷移,這個算法確保最終每個master至少有一個slave即可。起初,我們先定義哪個slave是良好的:一個良好的slave不能處於FAIL狀態。觸發時機為,任何一個slave檢測到某個master沒有一個良好slave時。參與遷移的slave必須為,持有最多slaves的master的其中一個slave,且不處於FAIL狀態,且持有最小的node ID。

    比如有10個masters都持有一個slave,有2個masters各持有5個slaves,那么遷移將會發生在持有5個slaves的masters中,且node ID最小的slave node上。我們不再使用“agreement”,不過也有可能當集群的配置不夠穩定時,有一種競爭情況的發生,即多個slaves都認為它們自己的ID最小;如果這種情況發生,結果就是可能多個slaves會遷移到同一個master下,不過這並沒有什么害處,但是最壞的結果是導致原來的master遷出了所有的slaves,讓自己變得單一。但是遷移算法(進程)會在遷移完畢之后重新判斷,如果尚未平衡,那么將會重新遷移。

    最終,每個master最少持有一個slave;這個算法由用戶配置的“cluster-migration-barrier”,此配置參數表示一個master至少保留多少個slaves,其他多余的slaves可以被遷出。此值通常為1,如果設置為2,表示一個master持有的slaves個數大於2時,多余的slaves才可以遷移到持有更少slaves的master下。

 

    configEpoch沖突解決算法

    在slave failover期間,會生成新的configEpoch值,需要保證唯一性。不過有2種不同的event會導致configEpoch的創建是不安全的:僅僅自增本地的currentEpoch並希望它不會發生沖突。這兩個事件有系統管理員觸發:

    1)CLUSTER FAILOVER:這個指令,就是人為的將某個slave提升為master,而不需要要求大多數masters的投票參與。

    2)slots的遷移,用於平衡集群的數據分布(reshard);此時本地的configEpoch也會修改,因為性能的考慮,這個過程也不需要“agreement”。

 

    在手動reshard期間,當一個hash slot從A遷移到B,resharding程序將強制B更新自己的配置信息、epoch值也修改為集群的最大值 + 1(除非B的configEpoch已經是最大值),這種變更則不需要其他nodes的agreement(注意與failover的原理不同)。通常每次resharding都會遷移多個slots,且有多個nodes參與,如果每個slots遷移都需要agreement,才能生成新的epoch,這種性能是很差的,也不可取。我們在首個slots遷移開始時,只會生成一個新的configEpoch,在遷移完畢后,將新的配置傳播給集群即可,這種方式在生產環境中更加高效。

 

    因為上述兩個情況,有可能(雖然概率極小)最終多個nodes產生了相同的configEpoch;比如管理員正在進行resharding,但是此時failover發生了...無論是failover還是resharding都是將currentEpoch自增,而且resharding不使用agreement形式(即其他nodes或許不知道,而且網絡傳播可能延遲),這就會發生epoch值的沖突問題。

 

    當持有不同slots的masters持有相同的configEpoch,這並不會有什么問題。比較遺憾的是,人工干預或者resharding會以不同的方式修改了集群的配置,Cluster要求所有的slots都應該被nodes覆蓋,所以在任何情況下,我們都希望所有的master都持有不同的configEpoch。避免沖突的算法,就是用來解決當2個nodes持有相同的configEpoch:

    1)如果一個master節點發現其他master持有相同的configEpoch。

    2)並且此master邏輯上持有較小的node ID(字典順序)

    3)然后此master將自己的currentEpoch加1,並作為自己新的configEpoch。

 

    如果有多個nodes持有相同的congfigEpoch,那么除了持有最大ID的節點外,其他的nodes都將往前推進(+1,直到沖突解決),最終保證每個master都持有唯一的configEpoch(slave的configEpoch與master一樣)。對於新創建的cluster也是同理,所有的nodes都初始為不同的configEpoch。

 

    Node resets

    所有的nodes都可以進行軟件級的reset(不需要重啟、重新部署它們),reset為了重用集群(重新設定集群),必須需要將某個(些)節點重置后添加到其他集群。我們可以使用“CLUSTER RESET”指令:

    1)CLUSTER RESET SOFT

    2)CLUSTER RESET HARD

 

    指令必須直接發給需要reset的節點,如果沒有指定reset類型,默認為SOFT。

    1)soft和hard:如果節點為slave,那么節點將會轉換為master,並清空其持有的數據,成為一個空的master。如果此節點為master,且持有slots數據,那么reset操作將被中斷。

    2)soft和hard:其上持有的slots將會被釋放

    3)soft和hard:此節點上的nodes映射表將會被清除,此后此node將不會知道其他節點的存在與狀態。

    4)hard:currentEpoch、configEpoch、lastVoteEpoch值將被重置為0。

    5)hard:此nodeID將會重新生成。

 

    持有數據的(slot映射不為空的)master不能被reset(除非現將此master上的slot手動遷移到其他nodes上,或者手動failover,將其切換成slave);在某些特定的場景下,在執行reset之前,或許需要執行FLUSHALL來清空原有的數據。

 

    集群中移除節點

    我們已經知道,將node移除集群之前,首先將其上的slots遷移到其他nodes上(reshard),然后關閉它。不過這似乎還並未結束,因為其他nodes仍然記住了它的ID,仍然不會嘗試與它建立連接。因此,當我們確定將節點移除集群時,可以使用“CLUSTER FORGET <node-ID>”指令:

    1)將此node從nodes映射表中移除。

    2)然后設定一個60秒的隔離時間,阻止持有相同ID的node再次加入集群。因為FORGET指令將會通過gossip協議傳播給其他nodes,集群中所有的節點都收到消息是需要一定的時間延遲。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM