Redis集羣進階之路

Redis集羣規範node

本文檔基於Redis 3.X或更高版本,講解Redis集羣算法以及設計原理。此官方文檔長期更新且隨着Redis新版本特性的變化變更,詳細請留意官網。redis

 

官網地址:https://redis.io/topics/cluster-spec算法

 

主要特性和設計原理數據庫

  Redis集羣目標數組

  Redis集羣做爲Redis的一個分佈式實現,主要實現如下目標(按重要性排序):緩存

    ·高性能,以及高達的1000個節點的線性可擴展性(linear scalability ),並且是在沒有使用代理,異步複製,在值(value)上不執行合併操做的狀況下安全

    ·寫安全(write safety)控制在可接受的範圍:當客戶端與大多數master創建鏈接,Redis架構會盡可能保持來自客戶端的寫入請求。其中會存在已確認的寫入請求可能會丟失,網絡隔離場景下,較小的網絡分區中丟失的已確認的寫入請求會更大。服務器

    ·可用性(Availability):在大部分master節點可達,且每一個不可達的master節點都只少有一個他的slave節點可達的狀況下,Redis集羣仍能進行正常運行。此外,基於備份遷移功能,當一個master節點沒有slave副本節點時,集羣會從有多個slave副本節點的master遷移必定數量的slave節點給它網絡

 

  已實現的部分功能數據結構

  Redis集羣實現了全部Redis非分佈式版本提供的單鍵命令,只要鍵存儲在相同節點就能夠執行復雜的多鍵操做命令(像set類型的合集或交集的命令)。

  Redis集羣實現了「哈希標籤」的概念,可強制某些鍵存儲在同一節點。但在手動從新綁按期間,多鍵操做可能不可用,但單鍵操做始終可用。

  Redis集羣中只有數據庫0(db0),且不支持SELECT命令去選擇數據庫,會報錯「(error) ERR SELECT is not allowed in cluster mode」。

 

  Redis集羣協議中客戶端和服務器的角色

  Redis集羣中,節點負責保存數據、集羣狀態信息,以及負責將鍵映射到正確的節點。集羣節點能自動發現其餘節點,並檢查其是否正常,必要時將slave提高爲master。

  在執行任務時,集羣節點使用TCP總線和二進制協議組成的Redis集羣總線(Redis Cluster Bus)進行互相鏈接。節點使用gossip協議來傳播和集羣信息,這樣能夠:發現新節點、發送ping包(用來確保全部節點都處於正常狀態),以及在發生特殊狀況時發送集羣消息。集羣總線也用於在集羣匯中傳播PUB/SUB消息,如管理員手動執行故障轉移時。

  因爲集羣節點不能代理請求,因此客戶端在接收到重定向錯誤「-MOVED」和「-ASK」時,會將命令重定向到其餘節點。理論上客戶端可自由地向集羣中全部節點發送請求,必要時會將請求重定向到其餘節點,因此客戶端不須要保存集羣狀態。不過客戶端能夠緩存鍵值和節點間的映射關係,如此可提升命令執行效率

 

  寫入的安全性

  Redis集羣節點間使用異步複製(asynchronous replication)傳輸數據,最後一次故障轉移執行隱式合併動做(last failover wins implicit merge function)。這意味着最後被選舉出來的master的數據集最終會覆蓋到其餘slave副本節點。

  使用異步複製的缺點就是在故障期間總會丟失一點數據,但鏈接到大多數master節點的客戶端與鏈接到極少部分master節點的客戶端狀況徹底不一樣。Redis集羣會盡可能保存全部與大多數master節點鏈接的客戶端執行的寫入,但如下兩種狀況除外:

    一、一個寫入操做能到達一個master節點,但當master節點準備回覆客戶端時,此寫入可能還未經過異步複製到它的slave複製節點。若master節點在寫入還未複製到slave節點時掛掉,那麼這次寫入就會丟失,若master節點不可達,就會有一個合適的slave被提高爲新master。

    二、理論上另外一種寫入可能的丟失的狀況:

      a、網絡故障形成網絡分區,導致master節點不可達

      b、故障轉移致使slave節點被提高爲master

      c、Master節點再次可達

      d、網絡分區後master節點隔離,使用過期路由信息(out-of-date routing table)的客戶端寫入數據到舊master節點

 

  第二種狀況發生的可能性比較小,主要由於master節點不可達必定時間,將不會再接收任何寫入請求,且會有其餘新master替代它,當網絡恢復後仍然會在一小段時間內拒絕寫入請求,以便其餘節點收到配置更新的通知。處於這種情況也須要客戶端路由信息還未更新。

  一般全部節點都會嘗試經過非阻塞鏈接嘗試(non-blocking connection attempt)儘快去訪問一個再次加入到集羣裏的節點,一旦跟該節點創建一個新的鏈接就會發送一個ping包過去(這足夠升級節點配置信息)。這保證了一個節點在恢復可寫入狀態以前先更新知配置信息。Redis集羣在擁有少數master節點和至少一個客戶端的分區上容易丟失爲數很多的寫入操做,這是由於若master節點被故障轉移到集羣中多數節點那邊的節點上, 那麼全部發送到這些master節點的寫入操做都會永久性丟失。

  一個master節點被故障轉移,必須大多數master節點至少「NODE_TIMEOUT」時間沒法訪問該節點,因此若網絡問題在這段時間內恢復,就不會有寫入操做丟失。在網絡故障形成網絡分區狀況下,當分區持續超過「NODE_TIMEOUT」時間,集羣節點較少的分區的全部寫入可能會丟失,但此分區也會禁止客戶端的全部寫入請求,所以在少數節點所在的分區變得不可用(寫)後,會產生一個最大寫入損失量(網絡分區產生後直至禁止寫入時刻),這個量的數據會在網絡恢復,舊master成爲新master後丟棄。

 

  可用性

  Redis集羣在節點數比較少分區的不可用。假設集羣多數節點所在分區裏有大多數可達的master節點,且對於每一個不可達的master都至少有一個slave節點可達,在通過「NODE_TIMEOUT」時間後,就會有合適的slave節點被選舉出來而後故障轉移掉他的master節點,此時集羣會再次可用(故障轉移一般在1~2s內完成)。

  由此可看出Redis集羣只能容忍少數節點故障,對於大面積網絡故障甚至形成網絡分區的狀況來講(the event of large net splits),Redis並不能提供很好的可用性。如,一個集羣由N個master節點組成,每一個master都有一個slave複製節點。當有一個節點由於網絡問題被隔離出去,多數節點所在分區仍然能可用。當兩個節點在相同問題下被隔離出去集羣仍可用過的機率是1-(1/(N*2-1))(在第一個節點故障後還剩下N*2-1個節點,那麼失去slave節點只剩master節點的出錯的機率是1/(N*2-1))

  如一個集羣有5個master節點,每一個節點都只有一個slave節點,那麼在兩個節點被隔離分割出去集羣再也不可用的機率是1/(5*2-1) = 0.1111,即約11%的機率。

  Redis集羣的備份遷移功能很好的下降了這個機率,備份遷移的主要功能就是避免孤立的master出現,或者儘可能保證每一個master都一個slave複製節點。所以每當有孤立的master出現,redis集羣的備份遷移機制就會啓動。但若一組master和slave節點同時所有故障,備份遷移就沒用了,集羣也會有部分數據不可用,或者集羣直接不可用。所以,在資源足夠的狀況下,儘可能將master和slave部署在不一樣的虛擬機,甚至物理機,數據中心等等

 

  性能

  Redis集羣中節點並非把命令轉發到管理所給出鍵值的正確的節點上,而是把客戶端重定向到服務必定範圍內鍵值的節點上。最終客戶端得到一份最新的集羣路由表,記錄哪些節點服務哪些範圍的鍵,所以在正常操做中客戶端是直接鏈接到對應的節點來發送指令

  Redis使用異步複製,節點不會等待其餘節點的寫入確認(若未使用「WAIT」明確指出)

  多鍵指令僅用於相鄰的鍵,不是從新分片,數據是永遠不會在節點間移動的。單鍵操做等普通操做和Redis單實例同樣。這意味着因爲線性擴展性的設計,在一個擁有N個master節點的redis集羣和Redis獨立實例上執行相同的操做,前者的性能將是Redis單實例的N倍。但請求老是能從客戶端到達服務器,並從服務器返回數據回覆客戶端,且客戶端會與節點保持長鏈接,因此延遲問題二者同樣。

  很是高的性能和可擴展性,同時保持弱但合理的數據安全性和可用性是Redis集羣的主要目標。

 

  爲何要避免使用合併操做

  Redis集羣設計原理是避免在多個節點中存在同個鍵值對致使衝突,但這並不老是理想的。Redis中的值一般比較大,列表或有序集合中存儲數以萬計的元素是比較常見的。數據類型的語義也很複雜。傳輸和合並這類值可能會造成架構主要瓶頸,另外可能須要應用層使用大量的邏輯,以及額外的內存用來存儲元數據等等。

  There are no strict technological limits here. CRDTs or synchronously replicated state machines can model complex data types similar to Redis. However, the actual run time behavior of such systems would not be similar to Redis Cluster. Redis Cluster was designed in order to cover the exact use cases of the non-clustered Redis version.

 

Redis集羣主要組件概述

  鍵分佈模型(Keys distribution model)

  鍵空間(key space)被切割成16384個哈希槽,每一個哈希槽存在於一個master節點,那麼一個集羣最多有16384個master節點,可是一個集羣建議的最大master節點數是1000個

  集羣中每一個master負責16384個哈希槽中的一小部分。當集羣沒有從新配置(哈希槽從一個節點移動到另外一節點)時,集羣是穩定的。當集羣處於穩定狀態,一個哈希槽只被一個master節點負責,但master節點能夠有一個或多個slave複製節點,能夠在master故障時替換之,且這樣能夠用來水平擴展讀操做(這些讀操做不要求實時數據))。

  用於將鍵映射到哈希槽的是本算法以下(下一段落,除了哈希標籤之外就是按照這個規則):

 

    HASH_SLOT = CRC16(key) mod 16384

 

    其中,CRC16的定義以下:

      ·名稱:XMODEM(也能夠稱爲 ZMODEM 或 CRC-16/ACORN)

      ·輸出長度:16 bit

      ·多項數(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位會被使用(這也是爲何上面的式子中有一個對16384取餘的操做)。在測試中,CRC16能至關好地把不一樣的鍵均勻地分配到16384個槽中。

    注意: 在本文檔的附錄A中有CRC16算法的實現。

 

  鍵哈希標籤(Keys hash tags)

  爲了實現哈希標籤而使用的hash槽計算有一個例外,哈希標籤是確保兩個鍵都在同一個哈希槽裏的一種方式,主要用來實現集羣中多鍵操做(multi-key operations)。

  爲了實現哈希標籤,在某些條件下,鍵的哈希槽以另外一種不一樣的方式計算。若鍵是「{...}」模式,那只有「{」和「}」間的字符串用作哈希計算以獲取哈希槽。但同時出現多個「{」和「}」是可能的,詳細計算方法以下:

    ·當鍵包含一個「{」

    ·且當「{」右邊有一個「}」

    ·且當第一次出現和第一次出現「}」間有一個或多個字符

 

    然而不是直接計算哈希,而是拿出第一個「{」和它右邊第一個「}」 間的字符串計算哈希值。

 

    示例:

      ·{user1000}.following和{user1000}.followers這兩個鍵會被哈希到同一個哈希槽,由於只有「user1000」這個子串會被用來計算哈希值

      ·foo{}{bar}這個鍵,「foo{}{bar} 」整個字符串都被用來計算哈希值,由於第一個出現的「{」和它右邊第一個出現的「}」間沒有任何字符

      ·foo{{bar}}zap這個鍵,「{bar」這個字符串會被用來計算哈希值,由於它是第一個出現的「{」和它右邊第一個出現的「}」間的字符

      ·foo{bar}{zap}這個鍵,「bar」這個字符串會被用來計算哈希值,由於算法會在第一次有效或無效(中間沒有任何字符)地匹配到第一個「{」和它右邊第一個「}」時中止

      ·如此,若一個鍵是以「{}」開頭,那麼整個鍵字符會被用來計算哈希值。當使用二進制數據做爲鍵名稱時很是有用。

 

  加上哈希標籤的特殊處理,下面是用Ruby和C語言實現的HASH_SLOT函數。

 

  Ruby 樣例代碼:

    

def HASH_SLOT(key)

    s = key.index "{"

    if s

        e = key.index "}",s+1

        if e && e != s+1

            key = key[s+1..e-1]

        end

    end

    crc16(key) % 16384

End

 

 

C 樣例代碼:
unsigned
int HASH_SLOT(char *key, int keylen) { int s, e; /* start-end indexes of { and } */ /* Search the first occurrence of '{'. */ for (s = 0; s < keylen; s++) if (key[s] == '{') break; /* No '{' ? Hash the whole key. This is the base case. */ if (s == keylen) return crc16(key,keylen) & 16383; /* '{' found? Check if we have the corresponding '}'. */ for (e = s+1; e < keylen; e++) if (key[e] == '}') break; /* No '}' or nothing between {} ? Hash the whole key. */ if (e == keylen || e == s+1) return crc16(key,keylen) & 16383; /* If we are here there is both a { and a } on its right. Hash * what is in the middle between { and }. */ return crc16(key+s+1,e-s-1) & 16383; }

 

  集羣節點屬性

  每一個集羣節點都有惟一名稱,節點ID(名稱)是一個十六進制表示的160bit隨機數進制,這個隨機數是節點第一次啓動時生成的(一般是用/dev/urandom)。節點會把ID保存在配置文件,只要節點沒被管理員刪掉,就會一直使用此ID。

  「CLUSTER RESET」可強制重置節點ID

  節點ID在集羣中表明着節點身份,節點改變IP鏈接信息,不須要更新節點ID。集羣能檢測到鏈接信息的變化,而後使用在集羣上通訊的gossip協議發佈廣播消息,通知配置變動。

  節點ID不只是關聯節點的所需信息,在整個Redis集羣也是全局惟一的。每一個節點也會保存一些關聯信息,像具體集羣節點的配置詳情(此配置在集羣中保證最終一致性),還有其餘信息,如節點最後ping的時間,此時間都是節點本地時間。

 

  每一個節點都維護其餘節點的信息:節點ID,節點的IP和端口,一組標誌,若標識爲slave對應的master節點,上一次發送ping包的時間,上一次收到pong包的時間,節點配置版本號,鏈路狀態和節點負責的哈希範圍等。

 

  使用CLUSTER NODES(關於全部字段的詳細說明)命令能夠得到以上的一些信息,這個命令能夠發送到集羣中的全部節點。如下示例是在一個只有4個節點的小集羣中發送CLUSTER NODES命令到一個master節點獲得的輸出:

  [root@hd4 7003]# redis-cli -p 7000 cluster nodes

  a466e88499423858c5f53de9be640500d9fb3e5b 127.0.0.1:7000@17000 myself,master - 0 1529050482000 7 connected 2365-5961 10923-11421

  5055b631a9b310417fa75948a5e473e2e2e1cfee 127.0.0.1:7001@17001 master - 0 1529050484054 12 connected 1616-1990 7202-10922

  d1ce7d9db6086c41f13ef0ce3753f82a3bfc420f 127.0.0.1:7002@17002 master - 0 1529050484054 11 connected 1991-2364 12662-16383

  bde6fc14465ecdbc71c6630edb5f9a3ab0c45cf0 127.0.0.1:7006@17006 master - 0 1529050484000 9 connected 0-1615 5962-7201 11422-12661

 

 

  在上面羅列出來的信息中,各個域依次表示的是:節點ID,IP地址:端口號,標識信息,上一次發送ping包的時間,上一次收到pong包的時間,配置文件版本號,鏈接狀態,節點負責的哈希槽範圍

 

  集羣總線

  每一個Redis集羣節點使用一個額外的TCP端口接收來自其餘節點的傳入鏈接。通常來講此端口比客戶端(如redis-cli)端口大10000,如客戶端端口是6379,集羣總線端口則是16379。在配置端口時,只需指明客戶端端口便可。

 

  集羣拓撲圖

  集羣是一個網狀結構,每一個節點使用TCP協議鏈接到其餘節點。N個節點的集羣中,每一個節點都有N-1個對外的TCP鏈接和N-1個對內鏈接。這些鏈接一經建立就會永久保持。當節點在集羣中等待ping響應,足夠長時間後在標記某節點不可達前,都將嘗試刷新與該節點的最近通訊時間。Redis集羣節點間造成網狀結構後,節點使用gossip協議和配置更新機制,避免節點間在正常狀況下交換過多消息,所以交換的消息數量不會以指數形式增加的。

 

  節點間的握手

  節點間經過集羣總線TCP端口鏈接,也經過該端口進行ping的發送與接收(不管ping的發起者是否可信)。可是若消息發送來源不被認爲是集羣的一部分,則全部其餘的包將會被集羣節點丟棄。

  只在兩種狀況下,一個節點會認爲另外一節點是集羣的一部分:

    ·若一個節點使用MEET消息廣播本身。MEET消息和PING消息徹底同樣,但它會強制讓接收者接受本身爲集羣中的一部分。只有管理員使用如下命令請求時,節點纔會發送MEET消息給其餘節點:

      CLUSTER MEET IP PORT

 

    ·一個已經被信任的節點能經過傳播gossip消息讓另外一節點被註冊爲集羣中的一部分。即A知道B,B知道C,最終B將發送gossip消息「知道C」給A,接着A收到後會把C註冊爲網絡拓撲中的一部分,並嘗試直接鏈接C。這意味着只要在網絡拓撲中指定任意節點接入新節點,最終它們會自動徹底連通。即集集羣節點能自動發現其餘節點,但只有系統管理員強制指定信任關係才能實現。這種機制能防止不一樣的Redis集羣由於IP地址變動或者其餘網絡事件而意外混合起來,從而使得集羣更加健壯,以及可用性更強。

 

重定向和從新分片(Redirection and resharding)

  MOVED 重定向

  客戶端能夠自由地向集羣每一個節點(包括slave節點)發送查詢請求。接收請求的節點會分析請求,若命令是能夠執行的(即單鍵查詢,或者同一哈希槽的多鍵查詢),則節點將找出這些/個鍵是哪一個節點的哪一個哈希槽所服務的。

  若哈希槽在這個節點上,那麼這個請求執行就很是簡單,不然將查看節點上哈希槽與節點的映射表,且返回客戶端MOVED錯誤:

    127.0.0.1:7000> get  foo856761

    (error) MOVED 87 127.0.0.1:7006

 

 

  錯誤信息中指明瞭存儲鍵的哈希槽編號,以及能處理此查詢的IP:PORT。客戶端須要從新發送請求到該IP:PORT。注意:只要查詢的鍵不在命令接收節點上,都會有與MOVED相關的報錯。

集羣中節點是以ID來標識的,爲簡化接口,因此只向客戶端返回哈希槽和IP:PORT之間的映射關係

 

  儘管沒有要求,但客戶端應該記住哈希槽87被 127.0.0.1:7006服務,如此一旦有新的命令需發送,它能計算所處目標鍵的哈希槽,提升找到正確節點的機率。

  另外一種方法是在收到MOVED重定向時,使用「CLUSTER NODES」或「CLUSTER SLOTS」命令刷新客戶端鏈接的節點。遇到重定向時,可能多個哈希槽須要從新配置,而不是一個,所以最好是儘快刷新客戶端配置信息。

 

  注意:在集羣穩按期間(配置沒有持續變化),全部客戶端將得到一份「哈希槽->節點」的映射表,以提高集羣的請求效率,而不用重定向或代理,減小錯誤。

 

  另外,客戶端也要能處理後文將提到的-ASK重定向錯誤,不然此客戶端是不完整的集羣客戶端。

 

  集羣在線重配置(Cluster live reconfiguration)

  Redis集羣支持在線添加或刪除節點。實際上,添加和刪除被抽象爲同一操做,也就是把哈希槽從一個節點遷移到另外一節點。這意味着可使用相同的機制去平衡集羣哈希槽,增長/刪除節點等。

    ·向集羣添加新節點就是把一個空節點添加到集羣,而後從現有節點(能夠是多個節點,也能夠是單個節點)遷移部分哈希槽到該節點上。

    ·刪除集羣節點就是將該節點的哈希槽遷移到其餘現有節點

    ·平衡集羣就是將一組哈希槽在節點間移動。

 

  從實際角度看,哈希槽就是一堆鍵,因此Redis集羣在從新分片(reshard)時作的就是把鍵從一個節點移動到另外一節點。

 

  爲了理解這是怎麼工做的,咱們須要理解用來操做redis集羣節點上哈希曹轉換表(slots translation table)的CLUSTER子命令:

    ·CLUSTER ADDSLOTS slot1 [slot2] ... [slotN]

    ·CLUSTER DELSLOTS slot1 [slot2] ... [slotN]

    ·CLUSTER SETSLOT slot NODE node

    ·CLUSTER SETSLOT slot MIGRATING node

    ·CLUSTER SETSLOT slot IMPORTING node

 

  前兩個子命令:ADDSLOTS和DELSLOTS用來給Redis節點指派或移除哈希槽。指派一個哈希槽意味着告訴目標master,它將負責這個/些哈希槽的存儲與服務。在哈希槽被指派後,節點會將這個消息經過gossip協議想整個集羣傳播。(協議在後續「哈希槽配置傳播」章節說明)

 

  ADDSLOTS子命令一般用於新創建Redis集羣時用於給全部master節點指派16384個哈希槽中的部分。

  DELSLOSTS子命令主要在手工配置集羣或調試,實際中用的少。

  SETSLOT子命令使用「SETSLOT <slot> NODE」形式將哈希槽指派到指定ID的節點。此外哈希槽還能設置爲兩種特殊狀態:MIGRATING and IMPORTING。這兩種狀態用於將哈希槽從一個節點遷移到另外一節點。

    ·當一個槽被設置爲MIGRATING,服務該哈希槽的節點會接受全部查詢這個哈希槽的請求,但僅當查詢的鍵還存在原節點上時,原節點會處理該請求,不然此查詢會經過一個-ASK重定向(-ASK redirection)轉發到遷移的目標節點上

    ·當一個槽被設置爲IMPORTING,僅當接受到ASKING命令後節點纔會接受全部查詢這個哈希槽的請求。若客戶端一直未發送ASKING命令,那麼查詢會經過-MOVED重定向錯誤轉發到真正處理這個哈希槽的節點上。

 

  如下實例來解釋上述。假設有兩個master節點A、B。目的是將哈希槽8slave節點A移動到節點B,所以發送命令:

    一、在節點B執行:CLUSTER SETSLOT 8 IMPORTING A

    二、在節點A執行:CLUSTER SETSLOT 8 MIGRATING B

  其餘全部節點在每次請求的一個鍵是屬於哈希槽8是,都會把客戶端指向節點A:

    ·全部關於已存在的鍵的查詢都由節點A處理

    ·全部關於不存在於節點A的鍵都由節點B處理,由於節點A將重定向客戶端到節點B

  這種方式讓咱們能夠不用再節點A中建立新的鍵。另外用於集羣配置的「redis-trib」腳本也能夠把已存在的屬於哈希槽8的鍵slave節點A移動到節點B。可經過命令實現:

    CLUSTER GETKEYSINSLOT slot count

  上述命令會返回指定的哈希槽中count個鍵。對於每一個返回的鍵,redis-trib向節點A發送一個「MIGRATE」命令,該命令將以原子性的方式把指定的鍵slave節點A移動到節點B。如下是MIGRATE的工做原理:

    MIGRATE target_host target_port key target_database id timeout

  執行MIGRATE命令的節點會鏈接目標節點,將序列化後的鍵發送過去,一旦收到「OK」回覆就會將本身數據集中的老鍵(old key)刪除。所以對於一個外部客戶端而言,一個鍵要麼存在於節點A要麼節點B。

Redis集羣中僅有一個數據庫(db0),不能切換到其餘數據庫,但MIGRATE命令能用於其餘與Redis集羣無關的任務。如遷移比較複雜的鍵像長列表,都被優化的很是快速。但在調整一個擁有不少鍵,且鍵的數據量都很大的集羣時,若使用它的應用程序有延時問題的限制,再使用此命令就不明智了。

當最終遷移完成,在集羣中全部節點上執行命令「SETSLOT <slot> NODE <node-id>」以便將槽設置爲正常狀態

 

  ASK重定向

  前面有提到關於ASK重定向(ASK redirection),那麼爲啥不直接使用MOVED重定向呢?由於當使用MOVED時,意味着將哈希槽永久地遷移到另外一節點,且但願接下來的全部查詢都發到這個指定節點上去。而ASK意味着只要下一個查詢發送到指定節點上去

  這個命令是必要的,由於下一個關於哈希槽8的查詢須要的鍵或許還在節點A中,所以咱們但願客戶端嘗試在節點A中查找,若須要的話也在節點B中查找。因爲這是發生在16384個槽的其中一個槽,因此對於集羣的性能影響是在可接受的範圍。

  然而咱們須要強制客戶端的行爲,以確保客戶端會在嘗試節點A中查找後去嘗試在節點B中查找。若客戶端在發送查詢前發送了ASKING命令,那麼節點B只會接受被設爲IMPORTING的槽的查詢。 本質上來講,ASKING命令在客戶端設置了一個一次性標識(one-time flag),強制一個節點能夠執行一次關於帶有IMPORTING狀態的槽的查詢。

 

  所以從客戶端角度看,ASK重定向的完整語義以下:

    ·若接收到ASK重定向,那麼把查詢的對象調整爲指定的節點。

    ·先發送ASKING命令,再開始發送查詢。

    ·如今不要更新本地客戶端的映射表把哈希槽8映射到節點B。

  一旦完成了哈希槽8的轉移,節點A會發送一個MOVED消息,客戶端會把哈希槽8映射到新的IP:PORT上。注意,即便客戶端出現bug,過早地執行這個映射更新,也是沒有問題的,由於它不會在查詢前發送ASKING命令,節點B會用MOVED重定向錯誤把客戶端重定向到節點A上。

  CLUSTER SETSLOT命令文檔中,哈希槽遷移以相似的術語進行解釋,但使用不一樣的措辭(爲了文檔中的冗餘)。

 

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

  儘管有些Redis集羣客戶端可能不在內存中哈希槽編號與服務節點地址間的映射列表,且只能經過鏈接到隨機節點而後在須要時進行客戶端鏈接的重定向。

  實現的客戶端應該保存哈希槽編號與服務節點地址間的映射列表,此信息也不要求是最新的,由於請求時若鏈接到的節點沒法獲取所需數據,會重定向到其餘節點,此時會觸發客戶端更新映射信息。

  客戶端一般在兩種狀況下獲取哈希槽與節點映射列表:

    ·啓動時保存初始化的列表信息

    ·當收到MOVED重定向

 

    請注意:客戶端可能根據MOVED重定向更新變更的哈希槽,但這種方法僅適用於一個哈希槽更新,當遇到大量哈希槽映射信息更新,如slave被提高爲master,全部映射到舊master的哈希槽會從新映射到新master。所以更合適的對MOVED重定向作出迴應的方法是:從新獲取哈希槽節點的映射信息

 

  爲了獲取哈希槽與節點的映射信息,集羣提供了一種命令「CLUSTER NODES」的替代方案,且此方案只提供客戶端須要的信息。這個命令是「CLUSTER SLOTS」,命令顯示每組哈希槽範圍是由哪一個master、slave節點服務的。如下是「CLUSTER SLOTS」輸出實例

    

    [root@hd4 ~]# redis-cli -c -p 7000 cluster slots

    1) 1) (integer) 2365

      2) (integer) 5961

      3) 1) "127.0.0.1"

        2) (integer) 7000

        3) "a466e88499423858c5f53de9be640500d9fb3e5b"

      4) 1) "127.0.0.1"

        2) (integer) 7008
    
        3) "948addb812fe9322a25fbbdac9de940bab09f9f7"

    2) 1) (integer) 10923

      2) (integer) 11421

      3) 1) "127.0.0.1"

        2) (integer) 7000
    
        3) "a466e88499423858c5f53de9be640500d9fb3e5b"

      4) 1) "127.0.0.1"

        2) (integer) 7008

        3) "948addb812fe9322a25fbbdac9de940bab09f9f7"

    3) 1) (integer) 1616

      2) (integer) 1990

      3) 1) "127.0.0.1"

        2) (integer) 7001

        3) "5055b631a9b310417fa75948a5e473e2e2e1cfee"

      4) 1) "127.0.0.1"

        2) (integer) 7005

        3) "406bda57ed591c2bd3b15955f687a57b03a653c0"

      5) 1) "127.0.0.1"

        2) (integer) 7003

        3) "9b1d9c3e7bbcc955afce649f439cd2d094957313"

    4) 1) (integer) 7202

      2) (integer) 10922

      3) 1) "127.0.0.1"

        2) (integer) 7001

        3) "5055b631a9b310417fa75948a5e473e2e2e1cfee"

       4) 1) "127.0.0.1"

        2) (integer) 7005

        3) "406bda57ed591c2bd3b15955f687a57b03a653c0"

      5) 1) "127.0.0.1"

        2) (integer) 7003

        3) "9b1d9c3e7bbcc955afce649f439cd2d094957313"

    5) 1) (integer) 1991

      2) (integer) 2364

      3) 1) "127.0.0.1"

        2) (integer) 7002

        3) "d1ce7d9db6086c41f13ef0ce3753f82a3bfc420f"

      4) 1) "127.0.0.1"

        2) (integer) 7004
    
        3) "b36883be3b39692f71a441a67277ab23dff80afb"

      5) 1) "127.0.0.1"

        2) (integer) 7009

        3) "5837a7c77a04b5100222dca1d226e4980764a97f"

    6) 1) (integer) 12662

      2) (integer) 16383

      3) 1) "127.0.0.1"

        2) (integer) 7002

        3) "d1ce7d9db6086c41f13ef0ce3753f82a3bfc420f"

        4) 1) "127.0.0.1"

        2) (integer) 7004

        3) "b36883be3b39692f71a441a67277ab23dff80afb"

      5) 1) "127.0.0.1"

        2) (integer) 7009

        3) "5837a7c77a04b5100222dca1d226e4980764a97f"

    7) 1) (integer) 0

      2) (integer) 1615

      3) 1) "127.0.0.1"

        2) (integer) 7006

        3) "bde6fc14465ecdbc71c6630edb5f9a3ab0c45cf0"

      4) 1) "127.0.0.1"

         2) (integer) 7007

         3) "382b8977ccb4523495bed7ebdbab866f5ada4930"

    8) 1) (integer) 5962

      2) (integer) 7201

      3) 1) "127.0.0.1"

        2) (integer) 7006

        3) "bde6fc14465ecdbc71c6630edb5f9a3ab0c45cf0"

       4) 1) "127.0.0.1"

         2) (integer) 7007

         3) "382b8977ccb4523495bed7ebdbab866f5ada4930"

    9) 1) (integer) 11422

      2) (integer) 12661

      3) 1) "127.0.0.1"

        2) (integer) 7006

        3) "bde6fc14465ecdbc71c6630edb5f9a3ab0c45cf0"

      4) 1) "127.0.0.1"

        2) (integer) 7007

        3) "382b8977ccb4523495bed7ebdbab866f5ada4930"

 

 

    返回數組的每一個元素的前兩個子元素是該範圍的始末哈希槽。附加元素表示地址端口對(address-port pairs)。第一個地址端口對是服務該範圍哈希槽的master節點,如下的都是該master節點的正處於正常狀態的slave複製節點

 

    如輸出的第一個元素表示,槽從2365至5961(開始和結束哈希槽)由127.0.0.1:7000服務,且可經過127.0.0.1:7008水平擴展讀負載。

 

    集羣配置不正確時,CLUSTER SLOTS不能保證返回的哈希槽範圍覆蓋16384個哈希槽,所以客戶初始化哈希槽信息時,若用戶執行有關鍵的命令屬於未分配的哈希槽,應當用NULL填充空節點,並報告一個錯誤。

 

    當一個哈希槽被發現未被分配鍵,返回一個錯誤給客戶端前,客戶端應嘗試獲取哈希槽映射信息,已檢查集羣配置是否正確。

 

  多鍵操做

  客戶端可經過哈希標籤任意進行多鍵操做。如如下有效操做:

  MSET {user:1000}.name Angela {user:1000}.surname White

 

 

  當鍵所屬哈希槽正在進行從新分片時,多鍵操做可能不可用。詳細的說,在從新分片期間,針對相同節點(源節點和目標節點)全部已存在鍵的多鍵操做是可用的。

  在從新分片時,操做的鍵不存在或鍵在源節點和目的節點之間,將產生-TRYAGAIN錯誤。客戶端能夠一段時間後再嘗試操做,或報錯(Operations on keys that don't exist or are - during the resharding - split between the source and destination nodes, will generate a -TRYAGAIN error)

  一旦指定的哈希槽的遷移操做結束,全部多鍵操做可再次用於該哈希槽。

 

  使用slave節點擴展讀取功能

  通常slave節點會將客戶端重定向到給定命令中涉及的哈希槽的master節點上,但客戶端也可以使用命令「READONLY」在slave節點上擴展讀性能。命令「READONLY」告知slave節點,容許不在意數據是不是最新的、沒有寫請求的客戶端鏈接它

  當鏈接處於只讀模式,請求涉及到不是slave的master節點服務的鍵時,集羣將發送一個重定向到客戶端。發生這種狀況多是由於:

    ·客戶端發送了一個關於哈希槽的命令,但該哈希槽並非由這個slave的master節點提供服務

    ·集羣哈希槽通過從新分配,slave節點再也不爲給定哈希槽提供服務

 

  當發生這些狀況,客戶端應更新哈希槽與節點的映射表。

 

  總結,「READONLY」設置slave的只讀模式,鏈接的只讀狀態能用命令「READWRITE」清除

 

容錯性(Fault Tolerance)

  節點心跳和gossip消息

  Redis集羣節點間週期性交換ping和pong的數據包。兩種數據包具備相同的數據結構,都攜帶重要的配置信息。惟一不一樣是消息類型字段。通常將ping包和pong包合稱爲心跳數據包。

  通常節點發送ping數據包,接收到的節點會回覆pong數據包,但並不是全部狀況下都會有回覆。在傳播配置時,節點經過PONG包發送配置信息,而不會觸發一個回覆。

 

  一般集羣節點會每秒隨機ping幾個節點,這樣發送的ping包(和接收到的pong包)的總數會是一個跟集羣節點數量無關的常數。

  在過去的一半NODE_TIMEOUT時間裏都沒有發送ping包過去或接收從那節點發來的pong包的節點,會保證去ping每個其餘節點。在NODE_TIMEOUT過去前,若當前TCP鏈接有問題,節點會嘗試去重鏈接另外一個節點,以確保不會被看成不可達的節點。

 

  若NODE_TIMEOUT被設置得很小而節點很是多,那麼集羣內交互的消息會很是多。由於每一個節點都會嘗試去ping每一個在過去一半NODE_TIMEOUT時間裏都沒更新信息的節點。如在一個NODE_TIMEOUT設置爲60s的100節點集羣中,每一個節點將每30s發送99個ping數據包,那麼每秒發送的ping數據包就是3.3個,乘以100個節點就是整個集羣每秒有330個ping數據包在發送中。但因爲每秒330個包被均勻分部在100個不一樣的節點,所以每一個節點接收的流量仍是能夠接受的

 

  心跳數據包內容

  PING和PONG數據包包含很常見的header信息,以及一個PING和PONG的gossip協議特殊數據包。

  Header信息包含如下方面:

    ·節點ID,一個160位的僞隨機字符串,在首次將節點加入節點時分配,在Redis集羣生命週期內永久不變,除非刪除從新添加。

    ·currentEpoch和configEpoch兩個字段,用來掛載Redis集羣使用的分佈式算法(後續會有說明)。若節點是slave角色,configEpoch是上個已知master的configEpoch。

    ·節點標識,標明節點是master仍是slave身份,以及其餘只佔用1個bit的節點信息。

    ·數據包發送節點所服務的哈希槽範圍(bitmap,轉義成範圍),或者該節點是slave節點,則該範圍是它的master節點所服務的哈希槽範圍

    ·數據包發送節點TCP的IP:PORT信息(集羣總線端口,即Redis客戶端端口加上10000)

    ·數據包發送節點角度看集羣狀態(OK仍是down)

    ·master節點ID,若數據包發送節點是slave角色

 

  PING和PONG都包含一個gossip字段,向接收者提供發送節點對集羣中其餘節點的見解。Gossip字段僅包含發送者已知的一組節點中的幾個隨機節點的信息。Gossip字段提到的節點數量與羣集大小成正比。

  每一個在gossip字段中的節點都包含一下信息:

    ·節點ID

    ·節點IP:PORT

    ·節點標識

 

  從數據包發送節點看來,gossip字段用於讓接收節點能得到其餘節點的狀態信息。這對於失效檢測或者發現集羣中的其餘節點都是很是有用。

 

  故障檢測

  Redis集羣故障檢測用於在master再也不可達時將一個合適的slave提高爲master,若沒法提高則整個集羣將處於錯誤狀態,且中止接收客戶端的請求。

  如前所述,每一個節點都會獲取一份其餘已知節點的標識列表。其中有兩個標識用於故障檢測,分別是:PFAIL和FAILPFAIL表示可能失效(possible failure),這是一個未確認的故障類型。FAIL表示節點故障,這是已經被大多數節點在必定時間內確認過的故障類型

 

  ·PFAIL標識:

  在節點超過NODE_TIMEOUT時間不可達時,節點將用PFAIL標識另外一節點不可達,無論另外一節點是什麼節點類型。

  Redis集羣節點不可達指發送給某節點的ping包在超過NODE_TIMEOUT時間未收到回覆。爲了此機制能正常工做,NODE_TIMEOUT必須比網絡往返時間(network round trip time)長。爲增長正常工做時的集羣可靠性,節點在通過一半NODE_TIMEOUT時間還沒收到目標節點對ping的回覆,則會嘗試從新鏈接該節點,此機制下的鏈接會一直處於有效的活躍狀態,所以節點斷開鏈接一般不會致使錯誤的故障報告。

 

  ·FAIL標識

  單獨一個PFAIL標識只是每一個節點的一些關於其餘節點的本地信息,此不足以觸發slave節點身份的提高。要讓一個節點真正被認爲故障,那須要PFAIL狀態上升爲FAIL狀態。在前文有提到,每一個節點向其餘全部節點發送gossip消息中又包含一些隨機的已知節點的狀態,最終每一個節點都能收到一份其餘全部節點的節點標識。這樣,每一個節點都有一個機制去標記他們檢查到的其餘節點的故障狀態。

 

  知足一下條件會觸發PFAIL狀態升級爲FAIL狀態:

    一、節點A,B中的A標記B爲PFAIL

    二、節點A經過gossip字段收集到集羣中大部分master節點標識節點B的狀態信息

    三、大部分master節點標記節點PFAIL狀態,或在NODE_TIMEOUT * FAIL_REPORT_VALIDITY_MULT這個時間內是處於PFAIL狀態(實際中,有效因子通常設置爲2,因此爲NODE_TIMEOUT的兩倍)

 

  若以上條件都知足,那麼節點A:

    一、標記節點B爲FAIL

    二、向全部可達節點發送一個FAIL消息

 

  FAIL消息會強制每一個接收到此消息的節點把節點B標記爲FAIL狀態。

 

  注意:FAIL標識基本都是單向的,即一個節點能從PFAIL狀態升級爲FAIL狀態,但FAIL標識只能在下列狀況中被清除:

    ·節點已從新可達,且它是一個slave節點。此時FAIL標識能夠清除,由於slave節點並無被故障轉移。

    ·節點已從新可達,且它是一個master節點,但不服務任何範圍的哈希槽,此時master節點並無真正參與到集羣,正等待加入服務哈希槽中。

    ·節點已從新可達,且它是一個master節點,但通過很長時間(N * NODE_TIMEOUT)後也沒有檢測到任何slave節點被提高。

 

  PFAIL狀態到FAIL狀態的轉變使用一種弱協議(agreement):

    一、節點在某時間段內收集其餘節點的信息,所以即便大多數master節點須要「贊成」標記某個節點爲FAIL狀態,實際上這也只是代表節點在不一樣時間不一樣節點上收集了節點不肯定、也不須要大多數master贊成的信息;然而,在大多數節點都贊成後,每一個節點將丟棄舊的PFAIL狀態,最終統一指定節點的狀態(However we discard failure reports which are old, so the failure was signaled by the majority of masters within a window of time)。

    二、檢測到節點FAIL狀態的每一個節點都會將FAIL消息強加於羣中的其餘節點時,但沒法確保消息到達全部節點。如節點檢測到FAIL狀態,但由於網絡分區問題沒法將消息發送到任何節點。

 

  然而Redis集羣故障檢測有活躍性要求:最終全部節點都應該對給定節點的狀態達成一致。腦裂可能形成兩種狀況:要麼少數節點認爲指定節點處於FAIL狀態,要麼少數節點認爲指定節點不處FAIL狀態。針對這兩種狀況,集羣最終會就狀態達成一致:

    第一種狀況,若大對數節點已經標記某個master節點爲FAIL狀態,因爲故障檢測和鏈式效應(chain effect),剩餘其餘節點最終也將標記這個master節點爲FAIL狀態。

    第二種狀況,當只有少數節點標記了某個master節點爲FAIL狀態,slave節點不會被提高(使用一個更正式的算法保證每一個節點最終收到節點被提高的消息)。且每一個節點將依據文前所述清楚規則清除FAIL狀態(通過很長時間「N * NODE_TIMEOUT」後也沒有檢測到任何slave節點被提高)。

  FAIL標識僅用於觸發slave節點提高(slave promotion)算法的安所有分。理論上一個slave節點會在他的master節點不可達時獨立工做用且啓動slave節點提高程序,而後等待master節點來拒絕該提高(若master節點對大部分節點恢復鏈接)。PFAIL到FAIL的狀態變化、弱協議、強制在集羣的可達部分用最短的時間傳播狀態變動的 FAIL 消息,這些東西增長的複雜性有實際的好處。因爲這些機制,若集羣處於錯誤狀態,全部節點都會在同一時間中止接收寫入操做,這從Redis集羣客戶端角度來看是個很好的特性。還有非必要的選舉,是slave節點在沒法訪問master節點的時候發起的,若該master節點能被其餘大多數master節點訪問的話,這個選舉會被拒絕掉。

 

配置處理、傳播,以及故障轉移

  集羣當前配置版本(Cluster current epoch)

  Redis集羣使用相似於Raft算法的「術語」。在Redis集羣中該術語被稱爲EPOCH,用於對事件進行增量版本控制。當多個節點提供衝突的信息,另外一節點可經過這個EPOCH來判斷哪一個是最新的。

  currentEpoch是一個64位的無符號數字(unsigned)。
  在節點剛加入集羣時,無論是slave仍是master節點,currentEpoch都是0。

  每當節點接收到來自其餘節點的數據包時,若發送方的epoch(包含在集羣總線消息頭中)大於本地的epoch,那將更新發送方的epoch爲本地節點的currentEpoch。基於此邏輯,集羣全部節點最終都將更新爲這個最大的epoch。

  這個信息在此處是用於,當一個節點的狀態發生改變時爲了執行一些動做尋求其餘節點的贊成(agreement)。

  目前這種狀況只發生在slave被提高過程當中(將在下一節表述)。本質上,epoch 是集羣裏的邏輯時鐘,並決定一個給定的消息覆蓋另外一個帶着更小epoch的消息。

 

  配置版本號(Configuration epoch)

  每一個master節點老是經過發送ping和pong數據包向其餘節點傳播本身的configEpoch,以及一份本身維護的哈希槽範圍列表。

  當新節點被建立是,master節點中的configEpoch爲0。

  在slave選舉中configEpoch會被更新slave節點因爲故障轉移事件被提高爲master節點時,爲了取代它那失效的master節點,會把 configEpoch 設置爲它贏得選舉的時候的 configEpoch 值。

  下一節會解釋,configEpoch用於在不一樣節點提出不一樣的配置信息時解決衝突(可能發生在網絡分區的產生和節點故障的狀況中)。

  Slave節點也會在ping和pong數據包中廣播configEpoch信息,不過slave節點的configEpoch表示上次它與master節點交換數據時master節點的configEpoch。如此能讓其餘節點檢測slave節點的配置是否應該更新(master節點都不會向一個配置過期的slave節點投票,任何狀況下)。

  每當可達節點的configEpoch更新,節點都會將之更新到nodes.conf中。currentEpoch也是如此。這兩個變量會在節點繼續操做前保證更新以及刷新到nodes.conf。

  在故障轉移期間使用簡單算法生成的configEpoch保證最新、自增、惟一。

 

  Slave節點的選舉與提高

  在master節點的投票幫助下,slave節點的選舉與提高由故障master的全部slave節點自行處理。一個slave節點的選舉是在master節點被至少一個具備成爲master節點必備條件的slave節點標記爲FAIL狀態時發生

  要讓一個slave提高爲master,須要發起一次選舉並獲勝。當master處於FAIL狀態,全部給定master的slave都能發起一次選舉。然而只有一個slave能贏得選舉升級成爲master。

  當知足如下條件,slave節點就能夠發起選舉:

    ·slave的master節點處於FAIL狀態。

    ·slave的master節點負責的哈希槽數量不爲0。

    ·slave節點和master節點間複製進程(replication link)的鏈接斷開不超過給定的時間(可配置),以確保被升級的slave節點的數據是可靠的、比較新的。

 

  slave節點爲了在選舉中當選,第一步應該自增他的currentEpoch,且向集羣中master節點請求投票。

  slave節點經過廣播一個FAILOVER_AUTH_REQUEST數據包向集羣每一個master節點請求投票。而後等待回覆,最大等待時間是NODE_TIMEOUT*2,但至少2秒。

  一旦一個master投票給一個給定的slave,會回覆一個FAILOVER_AUTH_ACK,且在NODE_TIMEOUT*2時間內不能再給同個master節點的其餘slave節點投票,期間它將徹底不能回覆其餘slave節點的受權請求。主要防止多個slave在同一時間被選舉。

  發出投票請求後,slave節點會丟棄全部帶有epoch參數比currentEpoch小的迴應(ACKs),這確保了它不計入先前選舉的票數。

  一旦slave節點從大多數master節點得到ACKs迴應,那他就贏得選舉,不然若沒法在NODE_TIMEOUT*2(至少2s)時間內獲得大多數master節點的回覆,則終止選舉,在NODE_TIMEOUT*4時間後,會有另外的slave節點嘗試發起選舉。

 

  Slave 排名(slave rank)

  並不是master節點一進入FAIL狀態slave節點就會開始選舉,slave開始選舉前會等待一點時間,延時時間的計算方法以下:

    DELAY = 500 milliseconds + random delay between 0 and 500 milliseconds + SLAVE_RANK * 1000 milliseconds

 

  固定延時(fixed delay)確保master節點的FAIL狀態在集羣內廣播後slave節點再發起選舉,不然master節點們在不知道那個master已經故障的狀況下,會拒絕投票。

  隨機延時(random delay)用於減小同時多個slave節點發起選舉的可能性。由於若同時多個slave節點發起選舉或許會致使沒有任何節點贏得選舉,要再次發起另外一個選舉的話會使集羣在當時變得不可用。

  SLAVE_RANK是故障master的slave從master複製過來的數據處理數量的排名(越多的越靠前)。Slave交換消息時,當master故障會盡力生成一個排名:數據複製處理量最多的排名爲0,第二的爲1。總之數據複製處理量越多越靠前,SLAVE_RANK就越小,所增長的延時就越少。若靠前排名的slave選舉失敗,其餘人會很快嘗試。其餘人嘗試時,不會嚴格按照slave排名來從新發起選舉。

  一旦有slave節點在選舉中獲勝,他就會自增出一個比其餘已存在的master更大的configEpoch。 它開始經過ping和pong數據包向其餘節點廣播本身master的身份,該數據包中會包含它服務的哈希槽範圍,以及configEpoch。

  爲加速其餘節點配置的調整,將向集羣全部節點廣播一個pong數據包。那些如今訪問不到的節點最終也會收到一個ping包或pong包,而且進行配置調整。

  其餘節點將檢測到有一個新的master節點(帶着更大的configEpoch)在服務舊的哈希槽範圍,而後更新本身的配置。舊master的全部slave(或故障後恢復正常從新加入集羣的舊master)將更新調整配置,接着重新master複製數據。節點如何從新加入集羣會在下一節解釋。

 

  Master節點回復slave節點的投票請求

  上節討論了slave如何在選舉中當選,本節將從master節點的角度解析爲給定slave節點投票時發生的事情。

  Master節點接收來自slave節點、要求以FAILOVER_AUTH_REQUEST請求的形式的投票。要授予一個投票,必須知足如下條件:

    · 在一個給定的時段(epoch版本)裏,一個master節點只能投一次票,且拒絕給之前時段(epoch版本)投票:每一個master節點都有一個lastVoteEpoch字段,只要認證請求數據包(auth request packet)裏面的currentEpoch小於lastVoteEpoch,那麼master節點就會拒絕再次投票。當一個master節點積極響應一個投票請求,那麼lastVoteEpoch就會相應的更新,且持久化至nodes.conf。

    ·只有某個slave的master節點被標記爲FAIL狀態後,其他master節點纔會給該slave投票

    ·若認證請求裏的currentEpoch小於master的currentEpoch,該請求會被忽略。由於master節點的迴應老是帶着和認證請求一致的currentEpoch。若同一slave節點再次請求投票,會先遞增currentEpoch,以此保證master節點在收到相同slave節點的不一樣版本currentEpoch的認證請求時會選擇新版本而丟棄舊版本的請求。

 

  若不知足最後一條會出現如下例子中的問題:

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

    ·slave節點的currentEpoch是3。

    ·slave節點嘗試用epoch值爲4(3+1)來贏得選票,master節點回復ok,數據包裏currentEpoch是5,但此回覆延遲了。

    ·slave節點嘗試用epoch值爲5(4+1)來再次贏得選票,收到的是帶着currentEpoch值爲5的延遲迴復,此回覆會被看成有效的來接收。

 

    一、 master節點若已經爲某個失效master節點的一個slave節點投票後,在通過 NODE_TIMEOUT * 2 時間以前不會爲同個失效master節點的另外一個slave節點投票。這並非嚴格要求的,由於兩個slave節點用同個 epoch 來贏得選舉的可能性很低,不過在實際中,系統確保正常狀況當一個slave節點被選舉上,那麼它有足夠的時間來通知其餘slave節點,以免另外一個slave節點發起另外一個新的選舉。

    二、 master節點不會用任何方式來嘗試選出最好的slave節點,只要slave節點的master節點處於 FAIL 狀態而且投票master節點在這一輪中還沒投票,master節點就能進行積極投票。

    三、 若一個master節點拒絕爲給定slave節點投票,它不會給任何負面的迴應,只是單純忽略掉這個投票請求。

    四、 master節點不會授予投票給那些configEpoch值比master節點哈希槽表裏的configEpoch更小的slave節點。記住,slave節點發送了它的master節點的configEpoch值,還有它的master節點負責的哈希槽範圍信息。本質上來講,這意味着,請求投票的slave節點必須擁有它想要進行故障轉移的哈希槽的配置信息,並且信息應該比它請求投票的master節點的配置信息更新或者一致。

 

  configEpoch使得slave節點在提高過程當中對網絡分區更具備抵抗力

  本節說明如何使用config epoch來使得slave節點在提高過程當中對網絡分區更具備抵抗力。

    ·master節點並不是永久可達。Master節點有三個slave節點A,B,C

    ·在master節點故障後,slave節點A贏得選舉被推選爲新master節點

    ·此後發生網絡問題形成網絡分區使得大多數節點沒法訪問節點A

    ·通過從新選舉節點B成爲新master

    ·此後發生網絡問題形成網絡分區使得大多數節點沒法訪問節點B

    ·第一次的網絡分區恢復,A節點從新可達

 

  此刻B節點仍處於大多數節點不可達狀態,節點A在恢復可達後會與節點C競選嘗試得到選票對節點B進行故障轉移。

  這兩個有一樣的哈希槽的slave節點最終都會請求被提高,然而因爲它們發佈的configEpoch是不同的,並且節點C的epoch比較大,因此全部的節點都會把它們的配置更新爲節點C的。

  節點 A 會歷來源於節點 C(負責一樣哈希槽的節點)的ping包中檢測出節點C的epoch是更大的,因此它會從新設置本身爲節點C的一個slave節點。

 

  哈希槽配置傳播

  Redis集羣的一個重要組成部分是用於傳播關於集羣節點負責哪些哈希槽的信息的機制。這對於新集羣的啓動和提高slave節點來負責處理哈希槽(master節點負責的哈希槽)的能力來講是相當重要的。

  此機制也容許節點在任什麼時候間脫離集羣,而後再次以合理的方式加入集羣。

 

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

    ·心跳消息(Heartbeat messages)。PING或PONG數據包發送者總會附帶它(或者它的master節點)服務的哈希槽範圍信息。

  ·更新消息(UPDATE messages)。在每一個心跳數據包中都包含configEpoch信息,以及哈希槽所服務的集合信息。若心跳包的接收者發現發送者發送的信息是陳舊的,則會返回一個更新過的數據包,強制發送者更新信息。

 

  心跳消息和UPDATE消息的接收者使用某些簡單規則將哈希槽列表更新到節點。當新建立一個redis集羣節點,它本地哈希槽槽列表(哈希槽和給定節點ID 的映射關係表)被初始化,每一個哈希槽被置爲NULL,也就是說此時每一個哈希槽還未綁定集合或未連接到任何節點。這與下例狀況類似:

    0 -> NULL

    1 -> NULL

    2 -> NULL

    ...

    16383 -> NULL

 

  一個節點爲更新其哈希槽而要遵循的第一條規則以下:

  RULE1:若一個哈希槽未被分配(仍是NULL),而後一個可達節點來認領它,那麼我將修改個人哈希槽列表,並將這個哈希槽關聯到這個節點。

  所以,若咱們接收到來自節點A(服務哈希槽1和哈希槽二、configEpoch爲3)的心跳信息,那該表將被修改成:

    0 -> NULL

    1 -> A [3]

    2 -> A [3]

    ...

    16383 -> NULL

 

  當建立新集羣時,管理員須要手動將每一個master節點所服務的哈希槽分配給master節點自己(使用命令CLUSTER ADDSLOTS,經過redis-trib工具)此後信息將迅速在羣集中傳播。

 

  然而只有這條規則是不夠的。咱們知道哈希槽映射表會在兩種狀況下發生變化:

    ·一個slave節點在故障轉移中替換它的master

    ·一個哈希槽從一個節點從新分片到不一樣節點

 

  如今再讓咱們看看故障轉移。當一個slave節點故障轉移替換它的master後,會得到一個保證比master要大的configEpoch(一般大於先前生成的任何configEpoch)。如節點B,是A的一個salve節點,能夠在configEpoch爲4狀況下故障轉移B爲新master。以後節點B將發送心跳包(第一次時,在集羣中大規模廣播),且因爲第二條規則,接收者將更新其哈希槽列表:

  RULE2:若一個哈希槽已被分配,有個節點它的configEpoch比哈希槽當前擁有者的值更大,而且該節點宣稱正在負責該哈希槽,那麼我會把這個哈希槽從新綁定到這個新節點上。

  所以在接收到來自B節點通告configEpoch爲4,哈希槽1和2的消息後,接收者將按照如下方式更新列表:

    0 -> NULL

    1 -> B [4]

    2 -> B [4]

    ...

    16383 -> NULL

 

  因爲第二規則,因此集羣全部節點最終都會贊成哈希槽的擁有者是全部聲稱擁有它的節點中configEpoch值最大的那個。

  此種機制在Redis集羣中被稱做最終故障轉移獲勝(last failover wins)。

 

  這也會發生在從新分片的狀況中,當一個節點導入哈希槽後完成導入操做,它的configEpoch會增長來保證此更改在集羣中最終傳播到全部可達節點。

 

  更新消息(UPDATE messages)

  結合上述,能更加明瞭的看到更新消息的工做方式。節點A可能在一段時間後從新加入集羣,它將發送心跳數據包,並通告它正服務於configEpoch爲3的哈希槽1和2。全部接收者將轉而看到相同的哈希插槽關聯着具備較高configEpoch的節點B,將發送心跳包。所以,它們會發送帶有最新哈希槽配置的更新消息到A。 A將更新其配置,鑑於上面第二規則。

 

  節點如何從新加入集羣

  節點從新加入集羣使用相同的基本機制。繼續上面的例子,節點A將被通告哈希槽1和2如今由B節點服務。假設這兩個是由A服務的惟一哈希槽,那麼由A服務的哈希槽將降低到0。所以A將重配置爲新master的slave節點。

  事實上後面的規則要複雜一點。一般A可能會過很長時間後再加入集羣,在此期間可能最初由節點A服務的哈希槽變成由多個其餘master節點服務,如哈希槽1由B服務,2則由C服務。

  所以,實際上Redis集羣節電角色切換規則是:master節點將改變其配置,以複製(做爲一個slave節點的身份)最終所須要服務的哈希槽。

  在重配置期間,最終負責的哈希槽數會降爲0,而後節點會相應的從新配置。但注意通常狀況下,這意味着舊master將成爲故障轉移它的slave節點的slave節點。然而基本上這些規則覆蓋了全部狀況。

  全部的Slave節點在故障轉移它舊master時都會作相同的事情:重配置以複製舊master最終所須要服務的哈希槽。

 

  備份遷移(Replica migration)

  Redis集羣實現一個概念爲備份遷移(Replica migration)功能,以此提高系統可用性。集羣中有master/slave兩種角色,若master和slave間映射關係是固定的,那麼長此以往,當發生多個單一節點獨立故障時,系統可用性會變得頗有限。

  如在一個集羣中,每一個master都有一個slave節點,當master或slave節點故障時集羣仍是能正常操做(但master和slave節點同時故障集羣就不可了),然而這樣長期會積累不少由硬件或軟件問題引發的單一節點獨立故障。如:

    ·master A有一個slave A1

    ·master A故障,A1被提高爲新master

    ·三小時後A1由於自身故障掛掉了。因爲沒有其餘slave節點能夠提高爲master節點(A仍未恢復正常),集羣沒法繼續正常操做。

 

  若master和slave間的映射關係固定,讓集羣面對以上問題能輕鬆解決的惟一方法是爲而每一個master添加多個slave節點,然而此也會付出更多成本,如多一些實例,更多的內存等。

  一個候選方案是在集羣中建立非對稱性master-slave節點數量,讓集羣架構隨時間自動變化。如集羣中有三個master節點A、B、C,A和B各自有一個slave節點,A1和B1。節點C有兩個slave節點C1和C2。

 

  備份遷移是slave節點自動從新配置的過程,主要是爲了將「多餘」的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節點是指從給定節點的角度看,該slave節點不處於FAIL狀態。

  每一個slave節點若檢測出存在至少一個沒有好的slave節點的單一master節點,那麼就會觸發這個算法的執行。然而在全部檢測出這種狀況的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,而後但願在同一時間不出現衝突。這兩個事件都由管理員手動觸發:

    ·在大多數master節點不可達時,具備TAKEOVER選項的CLUSTER FAILOVER命令可以手動將slave節點升級爲master節點。在多數據中心環境下這頗有用。

    ·在沒有沒有協議性能問題的狀況下,集羣遷移哈希槽會在本地節點生成新的configEpoch(Migration of slots for cluster rebalancing also generates new configuration epochs inside the local node without agreement for performance reasons.)

  具體來講,在手動從新分片中,當一個哈希槽從節點A遷移到節點B時,從新分片程序會迫使B升級他的configEpoch到集羣中已發現的最大的configEpoch再加1(若configEpoch已經是集羣中最大,則再也不升級),此行爲不須要其餘節點贊成。實際從新分片中一般涉及到幾百個哈希槽,每一個哈希槽遷移都須要一個協議在從新分片過程當中來產生新的配置epoch,很是低效。另外它須要集羣節點實時同步來存儲最新的配置,由於此種運行方式,當第一個槽移動只須要一個新的configEpoch,使得在生產環境更加高效。

  然而因爲以上兩種狀況,有可能(雖然不太可能發生)最終會在多個節點生成相同的configEpoch。若管理員執行從新分片操做的同時發生故障轉移事件,頗有可能會致使currentEpoch衝突。

  此外,軟件bug和系統錯誤也可能致使多個節點持有相同的configEpoch。

  服務不一樣哈希槽的master擁有相同的configEpoch,這是ok的,重要的是slave故障轉移它的master必須擁有一個更大、惟一的configEpoch。

  也就是說人工干預或從新分片會以不一樣的方式改變集羣的配置。Redis集羣特性要求哈希槽配置常常彙總,所以在任何狀況下,咱們都但願全部master節點擁有不一樣的configEpoch。

  爲了實現這一點,使用一個衝突解決算法,以此保證兩個節點最終擁有相同的configEpoch。

    ·若一個master發現其它master節點正在通告跟它相同的configEpoch

    ·且若與通告相同的節點相比,節點具備字典上較小的節點ID configEpoch

    ·而後它把currentEpoch加1做爲新的configEpoch

  如有一些節點有相同的configEpoch,除了最大節點 ID的節點其餘節點都將自增configEpoch,保證不管發生什麼最終有惟一configEpoch。

  該機制還保證在新羣集建立後,全部節點以不一樣的configEpoch啓動(即便這實際上未被使用),由於redis-trib確保在啓動時使用CONFIG SET-CONFIG-EPOCH。然而,若因爲某種緣由,一個節點被配置錯誤,它將自動更新它的配置到一個不一樣的configEpoch。

 

  節點重置

  節點能夠軟重置(無需重啓)以便以不一樣角色或在不一樣集羣中重用。這在正常操做、測試和雲環境很是有用,其中給定節點能夠被從新配置,加入不一樣的節點集以放大或建立新的集羣。

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

    ·CLUSTER RESET SOFT

    ·CLUSTER RESET HARD

 

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

 

  下面是重置執行的操做列表:

    ·軟重置和硬重置:如果slave節點,則將其轉換爲master,並丟棄其數據集。如果master節點且包含鍵,則停止重置操做

    ·軟重置和硬重置:全部哈希槽被釋放,手工故障轉移狀態也被重置

    ·軟重置和硬重置:節點表中的全部其餘節點都被刪除,所以節點再也不知道任何其餘節點

    ·僅硬重置:currentEpoch,configEpoch和lastVoteEpoch都被設置成 0

    ·僅硬重置:節點ID被更新爲新的隨機ID

 

  非空數據集的master不能重置(一般先從新分佈數據到其它節點)。然而特殊狀況下,想銷燬當前集羣而後從新建立新的集羣時,先執行FLUSHALL清空後數據,再進行重置。

 

  從集羣刪除節點

  將全部數據從新分配給其餘節點並關閉它,可實現將節點從現有集羣刪除。但其餘節點仍然會保存已刪除節點的ID和鏈接信息,並嘗試與之鏈接。

  所以,刪除一個節點後,還要從各節點保存的其餘節點列表中刪除信息,經過CLUSTER FORGET <node-id>命令完成。

  該命令執行後會完成兩件事:

    ·從節點列表刪除具備指定ID的節點

    ·60s內禁止具備相同ID的節點被從新添加

  第二項有必要是由於Redis集羣使用gossip協議來自動發現節點,假設從節點A移除節點X,會致使B 把節點X又告訴A。因爲60秒禁止具備相同ID的節點被從新添加,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;

}
相關文章
相關標籤/搜索