【19-Redis集羣規範】

【博文總目錄>>>】|【工程下載>>>】

Redis 集羣的目標

Redis 集羣是 Redis 的一個分佈式實現,主要是爲了實現以下這些目標(按在設計中的重要性排序):

  • 在1000個節點的時候仍能表現得很好並且可擴展性(scalability)是線性的。

  • 沒有合併操作,這樣在 Redis 的數據模型中最典型的大數據值中也能有很好的表現。

  • 寫入安全(Write safety):那些與大多數節點相連的客戶端所做的寫入操作,系統嘗試全部都保存下來。不過公認的,還是會有小部分(small windows?)寫入會丟失。

  • 可用性(Availability):在絕大多數的主節點(master node)是可達的,並且對於每一個不可達的主節點都至少有一個它的從節點(slave)可達的情況下,Redis 集羣仍能進行分區(partitions)操作。

這篇文檔要講的是,在 Redis 倉庫(放在Github上)中的 unstable 分支中實現的功能。

實現的功能子集

Redis 集羣實現了所有在非分佈式 Redis 版本中出現的處理單一鍵值(key)的命令。那些使用多個鍵值的複雜操作, 比如 set 裏的並集(unions)和交集(intersections)操作,就沒有實現。通常來說,那些處理命令的節點獲取不到鍵值的所有操作都不會被實現。 在將來,用戶或許可以通過使用 MIGRATE COPY 命令,在集羣上用 計算節點(Computation Nodes) 來執行多鍵值的只讀操作, 但 Redis 集羣本身不會執行復雜的多鍵值操作來把鍵值在節點間移來移去。 Redis 集羣不像單機版本的 Redis 那樣支持多個數據庫,集羣只有數據庫 0,而且也不支持 SELECT 命令。

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

在 Redis 集羣中,節點負責存儲數據、記錄集羣的狀態(包括鍵值到正確節點的映射)。集羣節點同樣能自動發現其他節點,檢測出沒正常工作的節點, 並且在需要的時候在從節點中推選出主節點。

爲了執行這些任務,所有的集羣節點都通過TCP連接(TCP bus?)和一個二進制協議(集羣連接,cluster bus)建立通信。 每一個節點都通過集羣連接(cluster bus)與集羣上的其餘每個節點連接起來。節點們使用一個 gossip 協議來傳播集羣的信息,這樣可以:發現新的節點、 發送ping包(用來確保所有節點都在正常工作中)、在特定情況發生時發送集羣消息。集羣連接也用於在集羣中發佈或訂閱消息。

由於集羣節點不能代理(proxy)請求,所以客戶端在接收到重定向錯誤(redirections errors) -MOVED 和 -ASK 的時候, 將命令重定向到其他節點。理論上來說,客戶端是可以自由地向集羣中的所有節點發送請求,在需要的時候把請求重定向到其他節點,所以客戶端是不需要保存集羣狀態。 不過客戶端可以緩存鍵值和節點之間的映射關係,這樣能明顯提高命令執行的效率。

安全寫入

Redis 集羣節點間使用異步冗餘備份(asynchronous replication),所以在分區過程中總是存在一些時間段(windows?),在這些時間段裏容易丟失寫入數據。 但是一個連接到絕大部分主節點的客戶端的時間段,與一個連接到極小部分主節點的客戶端的時間段是相當不同的。 Redis 集羣會努力嘗試保存所有與大多數主節點連接的客戶端執行的寫入,但以下兩種情況除外: 1) 一個寫入操作能到達一個主節點,但當主節點要回復客戶端的時候,這個寫入有可能沒有通過主從節點間的異步冗餘備份傳播到從節點那裏。 如果在某個寫入操作沒有到達從節點的時候主節點已經宕機了,那麼該寫入會永遠地丟失掉,以防主節點長時間不可達而它的一個從節點已經被提升爲主節點。 2) 另一個理論上可能會丟失寫入操作的模式是:

  • 因爲分區使一個主節點變得不可達。

  • 故障轉移(fail over)到主節點的一個從節點。(即從節點被提升爲主節點)

  • 過一段時間之後主節點再次變得可達。

  • 一個沒有更新路由表(routing table)的客戶端或許會在集羣把這個主節點變成一個從節點(新主節點的從節點)之前對它進行寫入操作。

實際上這是極小概率事件,這是因爲,那些由於長時間無法被大多數主節點訪問到的節點會被故障轉移掉,不再接受任何寫入操作,當其分區修復好以後仍然會在一小段時間內拒絕寫入操作好讓其他節點有時間被告知配置信息的變更。通常所有節點都會嘗試通過非阻塞連接嘗試(non-blocking connection attempt)儘快去訪問一個再次加入到集羣裏的節點,一旦跟該節點建立一個新的連接就會發送一個ping包過去(這足夠升級節點配置信息)。這就使得一個節點很難在恢復可寫入狀態之前沒被告知配置信息更改。 Redis 集羣在擁有少數主節點和至少一個客戶端的分區上容易丟失爲數不少的寫入操作,這是因爲如果主節點被故障轉移到集羣中多數節點那邊的節點上, 那麼所有發送到這些主節點的寫入操作都會永久性丟失。

一個主節點要被故障轉移,必須是大多數主節點在至少 NODE_TIMEOUT 這麼長時間裏無法訪問該節點,所以如果分區在這段時間之前修復好了,就沒有寫入操作會丟失。當分區故障持續超過 NODE_TIMEOUT,集羣的多數節點這邊會在一超過 NODE_TIMEOUT 這個時間段後開始拒絕往受損分區進行寫入,所以在少數節點這邊(指分區)變得不再可用後,會有一個寫入操作最大損失範圍(因爲在指定時間段後將不會再有寫入操作被接收或丟失)。

可用性

Redis 集羣在分區的少數節點那邊不可用。集羣假設在分區的多數節點這邊至少有大多數可達的主節點,並且對於每個不可達主節點都至少有一個從節點可達,在經過了差不多 NODE_TIMEOUT 這麼長時間後,有個從節點被推選出來並故障轉移掉它的主節點,這時集羣又再恢復可用。

這意味着 Redis 集羣的設計是能容忍集羣中少數節點的出錯,但對於要求大量網絡分塊(large net splits)的可用性的應用來說,這並不是一個合適的解決方案。

舉個例子,一個由 N 個主節點組成的集羣,每個主節點都只有一個從節點。當有一個節點(因爲故障)被分割出去後,集羣的多數節點這邊仍然是可訪問的。當有兩個節點(因故障)被分割出去後集羣仍可用的概率是 1-(1/(N2-1))(在第一個節點故障出錯後總共剩下 N2-1 個節點,那麼失去冗餘備份(即失去從節點)的那個主節點也故障出錯的概率是 1/(N*2-1)))。

比如一個擁有5個節點的集羣,每個節點都只有一個從節點,那麼在兩個節點從多數節點這邊分割出去後集羣不再可用的概率是 1/(5*2-1) = 0.1111,即有大約 11% 的概率。

表現

在 Redis 集羣中節點並不是把命令轉發到管理所給出的鍵值的正確節點上,而是把客戶端重定向到服務一定範圍內的鍵值的節點上。 最終客戶端獲得一份最新的集羣表示,裏面有寫着哪些節點服務哪些鍵值子集,所以在正常操作中客戶端是直接聯繫到對應的節點並把給定的命令發過去。

由於使用了異步冗餘備份,節點不會等待其他節點對寫入操作的承認。(目前正在開發可選同步冗餘備份,極有可能會添加入將來的代碼發佈中)

同樣,由於一些命令不支持操作多個鍵值,如果不是碎片重整(resharding),那麼數據是永遠不會在節點間移動的。

所以普通操作是可以被處理得跟在單一 Redis 上一樣的。這意味着,在一個擁有 N 個主節點的 Redis 集羣中,由於 Redis 的設計是支持線性擴展的,所以你可以認爲同樣的操作在集羣上的表現會跟在單一 Redis 上的表現乘以 N 一樣。同時,詢問(query)通常在一次循環中被執行,客戶端會保持跟節點持續不斷的連接,所以延遲數據跟在單一 Reids 上是一樣的。

爲什麼要避免使用合併操作

Redis 集羣的設計是避免在多個節點中存在同個鍵值對的衝突版本,這是因爲 Redis 數據模型並不提倡這麼做:Redis 中的值通常都是比較大的,經常可以看到列表或者排序好的集合中有數以百萬計的元素。數據類型也是語義複雜的。傳輸和合並這樣的值將會變成一個主要的性能瓶頸。

Redis集羣主要組件概述

鍵分佈模型

鍵空間被分割爲 16384 槽(slot),事實上集羣的最大節點數量是 16384 個。(然而建議最大節點數量設置在1000這個數量級上)

所有的主節點都負責 16384 個哈希槽中的一部分。當集羣處於穩定狀態時,集羣中沒有在執行重配置(reconfiguration)操作,每個哈希槽都只由一個節點進行處理(不過主節點可以有一個或多個從節點,可以在網絡斷線或節點失效時替換掉主節點)。

以下是用來把鍵映射到哈希槽的算法(下一段哈希標籤例外就是按照這個規則):

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 tags),但這有一個例外。哈希標籤是確保兩個鍵都在同一個哈希槽裏的一種方式。將來也許會使用到哈希標籤,例如爲了在集羣穩定的情況下(沒有在做碎片重組操作)允許某些多鍵操作。

爲了實現哈希標籤,哈希槽是用另一種不同的方式計算的。基本來說,如果一個鍵包含一個 “{…}” 這樣的模式,只有 { 和 } 之間的字符串會被用來做哈希以獲取哈希槽。但是由於可能出現多個 { 或 },計算的算法如下:

  • 如果鍵包含一個 { 字符。

  • 那麼在 { 的右邊就會有一個 }。

  • 在 { 和 } 之間會有一個或多個字符,第一個 } 一定是出現在第一個 { 之後。

然後不是直接計算鍵的哈希,只有在第一個 { 和它右邊第一個 } 之間的內容會被用來計算哈希值。

例子:

  • 比如這兩個鍵 {user1000}.following 和 {user1000}.followers 會被哈希到同一個哈希槽裏,因爲只有 user1000 這個子串會被用來計算哈希值。

  • 對於 foo{}{bar} 這個鍵,整個鍵都會被用來計算哈希值,因爲第一個出現的 { 和它右邊第一個出現的 } 之間沒有任何字符。

  • 對於 foozap 這個鍵,用來計算哈希值的是 {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;
}

集羣節點屬性

在集羣中,每個節點都有一個唯一的名字。節點名字是一個十六進制表示的160 bit 隨機數,這個隨機數是節點第一次啓動時獲得的(通常是用 /dev/urandom)。 節點會把它的ID保存在配置文件裏,以後永遠使用這個ID,只要這個節點配置文件沒有被系統管理員刪除掉。

節點ID是用於在整個集羣中標識每個節點。一個給定的節點可以在不改變節點ID的情況下改變 IP 和地址。集羣能檢測到 IP 或端口的變化,然後使用在集羣連接(cluster bus)上的 gossip 協議來發布廣播消息,通知配置變更。

每個節點都有其他相關信息是所有節點都知道的:

  • 節點的 IP 地址和 TCP 端口號。

  • 各種標識。

  • 節點使用的哈希槽。

  • 最近一次用集羣連接發送 ping 包的時間。

  • 最近一次在回覆中收到一個 pong 包的時間。

  • 最近一次標識節點失效的時間。

  • 該節點的從節點個數。

  • 如果該節點是從節點,會有主節點ID信息。(如果它是個主節點則該信息置爲0000000…)

使用 CLUSTER NODES 命令可以獲得以上的一些信息,這個命令可以發送到集羣中的所有節點,無論主節點還是從節點。 下面的例子是在一個只有三個節點的小集羣中發送 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

在上面羅列出來的信息中,各個域依次表示的是:節點ID,IP地址:端口號,標識,上一次發送 ping 包的時間,上一次收到 pong 包的時間,連接狀態,節點使用的哈希槽。

集羣拓撲結構

Redis 集羣是一個網狀結構,每個節點都通過 TCP 連接跟其他每個節點連接。

在一個有 N 個節點的集羣中,每個節點都有 N-1 個流出的 TCP 連接,和 N-1 個流入的連接。 這些 TCP 連接會永久保持,並不是按需創建的。

節點握手

節點總是在集羣連接端口接受連接,甚至會回覆接收到的 ping 包,即使發送 ping 包的節點是不可信的。 然而如果某個節點不被認爲是在集羣中,那麼所有它發出的數據包都會被丟棄掉。

只有在兩種方式下,一個節點纔會認爲另一個節點是集羣中的一部分:

  • 當一個節點使用 MEET 消息介紹自己。一個 meet 消息跟一個 PING 消息完全一樣,但它會強制讓接收者接受發送者爲集羣中的一部分。 只有在系統管理員使用以下命令要求的時候,節點纔會發送 MEET 消息給其他節點:CLUSTER MEET ip port

  • 一個已被信任的節點能通過傳播gossip消息讓另一個節點被註冊爲集羣中的一部分。也就是說,如果 A 知道 B,B 知道 C,那麼 B 會向 A 發送 C 的gossip消息。A 收到後就會把 C 當作是網絡中的一部分,並且嘗試連接 C。 這意味着,只要我們往任何連接圖中加入節點,它們最終會自動形成一個完全連接圖。從根本上來說,這表示集羣能自動發現其他節點,但前提是有一個由系統管理員強制創建的信任關係。 這個機制能防止不同的 Redis 集羣因爲 IP 地址變更或者其他網絡事件而意外混合起來,從而使集羣更具健壯性。 當節點的網絡連接斷掉時,它會積極嘗試連接所有其他已知節點。

MOVED 重定向

一個 Redis 客戶端可以自由地向集羣中的任意節點(包括從節點)發送查詢。接收的節點會分析查詢,如果這個命令是集羣可以執行的(就是查詢中只涉及一個鍵),那麼節點會找這個鍵所屬的哈希槽對應的節點。

如果剛好這個節點就是對應這個哈希槽,那麼這個查詢就直接被節點處理掉。否則這個節點會查看它內部的 哈希槽 -> 節點ID 映射,然後給客戶端返回一個 MOVED 錯誤。

一個 MOVED 錯誤如下:

GET x
-MOVED 3999 127.0.0.1:6381

這個錯誤包括鍵(3999)的哈希槽和能處理這個查詢的節點的 ip:端口號(127.0.0.1:6381)。客戶端需要重新發送查詢到給定 ip 地址和端口號的節點。 注意,即使客戶端在重發查詢之前等待了很長一段時間,與此同時集羣的配置信息發生改變,如果哈希槽 3999 現在是爲其他節點服務,那麼目標節點會再向客戶端回覆一個 MOVED 錯誤。

從集羣的角度看,節點是以 ID 來標識的。我們嘗試簡化接口,所以只向客戶端暴露哈希槽和用“ip:端口號”標識的 Redis 節點之間的映射。

雖然並沒有要求,但是客戶端應該嘗試記住哈希槽 3999 是服務於 127.0.0.1:6381。這樣的話一旦有一個新的命令需要發送,它能計算出目標鍵的哈希槽,提高找到正確節點的機率。

注意,當集羣是穩定的時候,所有客戶端最終都會得到一份哈希槽 -> 節點的映射表,這樣能使得集羣效率非常高:客戶端直接定位目標節點,不用重定向、或代理或發生其他單點故障(single point of failure entities)。

一個客戶端也應該能處理本文後面將提到的 -ASK 重定向錯誤。

集羣在線重配置(live reconfiguration)

Redis 集羣支持在集羣運行過程中添加或移除節點。實際上,添加或移除節點都被抽象爲同一個操作,那就是把哈希槽從一個節點移到另一個節點。

  • 向集羣添加一個新節點,就是把一個空節點加入到集羣中並把某些哈希槽從已存在的節點移到新節點上。

  • 從集羣中移除一個節點,就是把該節點上的哈希槽移到其他已存在的節點上。

  • 所以實現這個的核心是能把哈希槽移來移去。從實際角度看,哈希槽就只是一堆鍵,所以 Redis 集羣在重組碎片(reshard)時做的就是把鍵從一個節點移到另一個節點。

爲了理解這是怎麼工作的,我們需要介紹 CLUSTER 的子命令,這些命令是用來操作 Redis 集羣節點上的哈希槽轉換表(slots translation table)。

以下是可用的子命令:

  • 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 節點指派(assign)或移除哈希槽。 在哈希槽被指派後,節點會將這個消息通過 gossip 協議向整個集羣傳播。ADDSLOTS 命令通常是用於在一個集羣剛建立的時候快速給所有節點指派哈希槽。

當 SETSLOT 子命令使用 NODE 形式的時候,用來給指定 ID 的節點指派哈希槽。 除此之外哈希槽能通過兩個特殊的狀態來設定,MIGRATING 和 IMPORTING:

  • 當一個槽被設置爲 MIGRATING,原來持有該哈希槽的節點仍會接受所有跟這個哈希槽有關的請求,但只有當查詢的鍵還存在原節點時,原節點會處理該請求,否則這個查詢會通過一個 -ASK 重定向(-ASK redirection)轉發到遷移的目標節點。

  • 當一個槽被設置爲 IMPORTING,只有在接受到 ASKING 命令之後節點纔會接受所有查詢這個哈希槽的請求。如果客戶端一直沒有發送 ASKING 命令,那麼查詢都會通過 -MOVED 重定向錯誤轉發到真正處理這個哈希槽的節點那裏。

這麼講可能顯得有點奇怪,現在我們用實例讓它更清晰些。假設我們有兩個 Redis 節點,稱爲 A 和 B。我們想要把哈希槽 8 從 節點A 移到 節點B,所以我們發送了這樣的命令:

  • 我們向 節點B 發送:CLUSTER SETSLOT 8 IMPORTING A

  • 我們向 節點A 發送:CLUSTER SETSLOT 8 MIGRATING B

其他所有節點在每次被詢問到的一個鍵是屬於哈希槽 8 的時候,都會把客戶端引向節點”A”。具體如下:

  • 所有關於已存在的鍵的查詢都由節點”A”處理。

  • 所有關於不存在於節點 A 的鍵都由節點”B”處理。

這種方式讓我們可以不用在節點 A 中創建新的鍵。同時,一個叫做 redis-trib 的特殊客戶端,它也是 Redis 集羣的配置程序(configuration utility),會確保把已存在的鍵從節點 A 移到節點 B。這通過以下命令實現:

CLUSTER GETKEYSINSLOT slot count

上面這個命令會返回指定的哈希槽中 count 個鍵。對於每個返回的鍵,redis-trib 向節點 A 發送一個 MIGRATE 命令,這樣會以原子性的方式(在移動鍵的過程中兩個節點都被鎖住,以免出現競爭狀況)把指定的鍵從節點 A 移到節點 B。以下是 MIGRATE 的工作原理:

MIGRATE target_host target_port key target_database id timeout

執行 MIGRATE 命令的節點會連接到目標節點,把序列化後的 key 發送過去,一旦收到 OK 回覆就會從它自己的數據集中刪除老的 key。所以從一個外部客戶端看來,在某個時間點,一個 key 要不就存在於節點 A 中要不就存在於節點 B 中。

在 Redis 集羣中,不需要指定一個除了 0 號之外的數據庫,但 MIGRATE 命令能用於其他跟 Redis 集羣無關的的任務,所以它是一個足夠通用的命令。MIGRATE 命令被優化了,使得即使在移動像長列表這樣的複雜鍵仍然能做到快速。 不過當在重配置一個擁有很多鍵且鍵的數據量都很大的集羣的時候,這個過程就並不那麼好了,對於使用數據庫的應用程序來說就會有延時這個限制。

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:端口號 上。 注意,即使客戶端出現bug,過早地執行這個映射更新,也是沒有問題的,因爲它不會在查詢前發送 ASKING 命令,節點 B 會用 MOVED 重定向錯誤把客戶端重定向到節點 A 上。

失效檢測(Failure detection)

Redis 集羣失效檢測是用來識別出大多數節點何時無法訪問某一個主節點或從節點。當這個事件發生時,就提升一個從節點來做主節點;若如果無法提升從節點來做主節點的話,那麼整個集羣就置爲錯誤狀態並停止接收客戶端的查詢。

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

PFAIL 標識:

當一個節點在超過 NODE_TIMEOUT 時間後仍無法訪問某個節點,那麼它會用 PFAIL 來標識這個不可達的節點。無論節點類型是什麼,主節點和從節點都能標識其他的節點爲 PFAIL。

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

FAIL 標識:

單獨一個 PFAIL 標識只是每個節點的一些關於其他節點的本地信息,它不是爲了起作用而使用的,也不足夠觸發從節點的提升。要讓一個節點真正被認爲失效了,那需要讓 PFAIL 狀態上升爲 FAIL 狀態。 在本文的節點心跳章節有提到的,每個節點向其他每個節點發送的 gossip 消息中有包含一些隨機的已知節點的狀態。最終每個節點都能收到一份其他每個節點的節點標識。使用這種方法,每個節點都有一套機制去標記他們檢查到的關於其他節點的失效狀態。

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

  • 某個節點,我們稱爲節點 A,標記另一個節點 B 爲 PFAIL。

  • 節點 A 通過 gossip 字段收集到集羣中大部分主節點標識的節點 B 的狀態信息。

  • 大部分主節點標記節點 B 爲 PFAIL 狀態,或者在 NODE_TIMEOUT * FAIL_REPORT_VALIDITY_MULT 這個時間內是處於 PFAIL 狀態。

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

  • 標記節點 B 爲 FAIL。

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

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

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

  • 節點已經恢復可達的,並且它是一個從節點。在這種情況下,FAIL 標識可以清除掉,因爲從節點並沒有被故障轉移。

  • 節點已經恢復可達的,而且它是一個主節點,但經過了很長時間(N * NODE_TIMEOUT)後也沒有檢測到任何從節點被提升了。

PFAIL -> FAIL 的轉變使用一種弱協議(agreement):

1) 節點是在一段時間內收集其他節點的信息,所以即使大多數主節點要去”同意”標記某節點爲 FAIL,實際上這只是表明說我們在不同時間裏從不同節點收集了信息,得出當前的狀態不一定是穩定的結論。

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

然而 Redis 集羣的失效檢測有一個要求:最終所有節點都應該同意給定節點的狀態是 FAIL,哪怕它處於分區。有兩種情況是來源於腦裂情況(?),或者是小部分節點相信該節點處於 FAIL 狀態,或者是相信節點不處於 FAIL 狀態。在這兩種情況中,最後集羣都會認爲給定的節點只有一個狀態:

第 1 種情況: 如果大多數節點都標記了某個節點爲 FAIL,由於鏈條反應,這個主節點最終會被標記爲 FAIL。

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

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

集羣階段(Cluster epoch)

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

Redis 集羣中的每個節點,包括主節點和從節點,都在創建的時候設置了 currentEpoch 爲0。

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

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

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

目前這個只發生在從節點的提升過程,這個將在下一節中詳述。本質上說,epoch 是一個集羣裏的邏輯時鐘,並決定一個給定的消息贏了另一個帶着更小 epoch 的消息。

配置階段(Configuration epoch)

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

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

從節點由於故障轉移事件被提升爲主節點時,爲了取代它那失效的主節點,會把 configEpoch 設置爲它贏得選舉的時候的 configEpoch 值。

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

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

每次由於一些已知節點的值比自己的值大而更新 configEpoch 值,它都會永久性地存儲在 nodes.conf 文件中。

當一個節點重啓,它的 configEpoch 值被設爲所有已知節點中最大的那個 configEpoch 值。

叢節點的選舉和提升

從節點的選舉和提升都是由從節點處理的,主節點會投票要提升哪個從節點。一個從節點的選舉是在主節點被至少一個具有成爲主節點必備條件的從節點標記爲 FAIL 的狀態的時候發生的。

當以下條件滿足時,一個從節點可以發起選舉:

  • 該從節點的主節點處於 FAIL 狀態。

  • 這個主節點負責的哈希槽數目不爲零。

  • 從節點和主節點之間的重複連接(replication link)斷線不超過一段給定的時間,這是爲了確保從節點的數據是可靠的。

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

從節點通過廣播一個 FAILOVER_AUTH_REQUEST 數據包給集羣裏的每個主節點來請求選票。然後等待回覆(最多等 NODE_TIMEOUT 這麼長時間)。一旦一個主節點給這個從節點投票,會回覆一個 FAILOVER_AUTH_ACK,並且在 NODE_TIMEOUT * 2 這段時間內不能再給同個主節點的其他從節點投票。在這段時間內它完全不能回覆其他授權請求。

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

一旦某個從節點收到了大多數主節點的迴應,那麼它就贏得了選舉。否則,如果無法在 NODE_TIMEOUT 時間內訪問到大多數主節點,那麼當前選舉會被中斷並在 NODE_TIMEOUT * 4 這段時間後由另一個從節點嘗試發起選舉。

從節點並不是在主節點一進入 FAIL 狀態就馬上嘗試發起選舉,而是有一點點延遲,這段延遲是這麼計算的:

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

固定延時(fixed delay)確保我們會等到 FAIL 狀態在集羣內廣播後,否則若從節點嘗試發起選舉,主節點們仍然不知道那個主節點已經 FAIL,就會拒絕投票。

data_age / 10 參數是用來讓從節點有時間去獲得新鮮數據(在與主節點斷線的這一小段時間內)。 隨機延時(random delay)是用來添加一些不確定因素以減少多個從節點在同一時間發起選舉的可能性,因爲若同時多個從節點發起選舉或許會導致沒有任何節點贏得選舉,要再次發起另一個選舉的話會使集羣在當時變得不可用。

一旦有從節點贏得選舉,它就會開始用 ping 和 pong 數據包向其他節點宣佈自己已經是主節點,並提供它負責的哈希槽,設置 configEpoch 爲 currentEpoch(選舉開始時生成的)。

爲了加速其他節點的重新配置,該節點會廣播一個 pong 包 給集羣裏的所有節點(那些現在訪問不到的節點最終也會收到一個 ping 包或 pong 包,並且進行重新配置)。

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

主節點回復從節點的投票請求

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

主節點接收到來自於從節點、要求以 FAILOVER_AUTH_REQUEST 請求的形式投票的請求。 要授予一個投票,必須要滿足以下條件:

  • 1) 在一個給定的時段(epoch)裏,一個主節點只能投一次票,並且拒絕給以前時段投票:每個主節點都有一個 lastVoteEpoch 域,一旦認證請求數據包(auth request packet)裏的 currentEpoch 小於 lastVoteEpoch,那麼主節點就會拒絕再次投票。當一個主節點積極響應一個投票請求,那麼 lastVoteEpoch 會相應地進行更新。

  • 2) 一個主節點投票給某個從節點當且僅當該從節點的主節點被標記爲 FAIL。

  • 3) 如果認證請求裏的 currentEpoch 小於主節點裏的 currentEpoch 的話,那麼該請求會被忽視掉。因此,主節點的迴應總是帶着和認證請求一致的 currentEpoch。如果同一個從節點在增加 currentEpoch 後再次請求投票,那麼保證一個來自於主節點的、舊的延遲迴復不會被新一輪選舉接受。

下面的例子是沒有依據這個規則引發的事件:

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

  • 從節點的 currentEpoch 是 3。

  • 從節點嘗試用 epoch 值爲 4(3+1)來贏得選票,主節點回復 ok,裏面的 currentEpoch 是 5,可是這個回覆延遲了。

  • 從節點嘗試用 epoch 值爲 5(4+1)來再次贏得選票,收到的是帶着 currentEpoch 值爲 5 的延遲迴復,這個回覆會被當作有效的來接收。

  • 4) 主節點若已經爲某個失效主節點的一個從節點投票後,在經過 NODE_TIMEOUT * 2 時間之前不會爲同個失效主節點的另一個從節點投票。這並不是嚴格要求的,因爲兩個從節點用同個 epoch 來贏得選舉的可能性很低,不過在實際中,系統確保正常情況當一個從節點被選舉上,那麼它有足夠的時間來通知其他從節點,以避免另一個從節點發起另一個新的選舉。

  • 5) 主節點不會用任何方式來嘗試選出最好的從節點,只要從節點的主節點處於 FAIL 狀態並且投票主節點在這一輪中還沒投票,主節點就能進行積極投票。

  • 6) 若一個主節點拒絕爲給定從節點投票,它不會給任何負面的迴應,只是單純忽略掉這個投票請求。

  • 7) 主節點不會授予投票給那些 configEpoch 值比主節點哈希槽表裏的 configEpoch 更小的從節點。記住,從節點發送了它的主節點的 configEpoch 值,還有它的主節點負責的哈希槽對應的位圖。本質上來說,這意味着,請求投票的從節點必須擁有它想要進行故障轉移的哈希槽的配置信息,而且信息應該比它請求投票的主節點的配置信息更新或者一致。

從節點選舉的競爭情況

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

  • 主節點不是無限期地可達。它擁有三個從節點 A,B,C。

  • 從節點 A 贏得了選舉並且被推選爲主節點。

  • 一個分區操作使得集羣中的大多數節點無法訪問節點 A。

  • 節點 B 贏得了選舉並且被推選爲主節點。

  • 一個分區操作使得集羣中大多數節點無法訪問節點 B。

  • 之前分區操作的問題被修復了,節點 A 又恢復可訪問狀態。

此刻,節點 B 仍然失效,節點 A 恢復可訪問,會與節點 C 競選去獲得選票對節點 B 進行故障轉移。

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

節點 A 會從來源於節點 C(負責同樣哈希槽的節點)的 ping 包中檢測出節點 C 的 epoch 是更大的,所以它會重新設置自己爲節點 C 的一個從節點。

服務器哈希槽信息的傳播規則

Redis 集羣很重要的一個部分是用來傳播關於集羣節點負責哪些哈希槽的信息的機制。這對於新集羣的啓動和提升從節點來負責處理哈希槽(它那失效的主節點本該處理的槽)的能力來說是必不可少的。

個體持續交流使用的 ping 包和 pong 包都包含着一個頭部,這個頭部是給發送者使用的,爲了向別的節點宣傳它負責的哈希槽。這是主要用來傳播變更的機制,不過集羣管理員手動進行重新配置是例外(比如爲了在主節點間移動哈希槽,通過 redis-trib 來進行手動碎片整理)。

當一個新的 Redis 集羣節點創建的時候,它的本地哈希槽表(表示給定哈希槽和給定節點 ID 的映射關係表)被初始化,每個哈希槽被置爲 nil,也就是,每個哈希槽都是沒賦值的。

一個節點要更新它的哈希槽表所要遵守的第一個規則如下:

規則 1:如果一個哈希槽是沒有賦值的,然後有個已知節點認領它,那麼我就會修改我的哈希槽表,把這個哈希槽和這個節點關聯起來。

由於這個規則,當一個新集羣被創建的時候,只需要手動給哈希槽賦值上(通常是通過 redis-trib 命令行工具使用 CLUSTER 命令來實現)負責它的主節點,然後這些信息就會迅速在集羣中傳播開來。

然而,當一個配置更新的發生是因爲一個從節點在其主節點失效後被提升爲主節點的時候,這個規則顯然還不足夠。新的主節點會宣傳之前它做從節點的時候負責的哈希槽,但從其他節點看來這些哈希槽並沒有被重新賦值,所以如果它們只遵守第一個規則的話就不會升級配置信息。

由於這個原因就有第二個規則,是用來把一個已賦值給以前節點的哈希槽重新綁定到一個新的認領它的節點上。規則如下:

規則 2:如果一個哈希槽已經被賦值了,有個節點它的 configEpoch 比哈希槽當前擁有者的值更大,並且該節點宣稱正在負責該哈希槽,那麼我們會把這個哈希槽重新綁定到這個新節點上。

因爲有這第二個規則,所以集羣中的所有節點最終都會同意哈希槽的擁有者是所有聲稱擁有它的節點中 configEpoch 值最大的那個。

UPDATE 消息

上面描述的傳播哈希槽配置信息的系統只使用節點間交換信息的普通 ping 包和 pong 包。 這要求存在一個節點(可以是負責給定哈希槽的主節點或從節點)擁有更新後的配置信息,因爲節點是在 ping 包和 pong 包頭部中發送它們自己的配置信息。

然而也存在例外。當有一個節點,它是唯一一個負責處理給定哈希槽的節點,有可能在分區操作後它恢復正常,但擁有的配置信息是過時的。

例子:一個給定的哈希槽是由節點 A 和 B 負責的。節點 A 是一個主節點,然後它在某個時刻失效了,所以節點 B 被提升爲主節點。過了一段時間節點 B 也失效了,集羣沒有其他備份節點可以來處理這個哈希槽,所以只能開始修復操作。

在一段時間過後節點 A 恢復正常了,並且作爲一個可寫入的主節點重新加入集羣,但它的配置信息是過時的。此時沒有任何備份節點能更新它的配置信息。這就是 UPDATE 消息存在的目的:當一個節點檢測到其他節點在宣傳它的哈希槽的時候是用一份過時的配置信息,那麼它就會向這個節點發送一個 UPDATE 消息,這個消息包含新節點的 ID 和它負責的哈希槽(以 bitmap 形式發送)。

注意:目前更新配置信息可以用 ping 包/ pong 包,也可以用 UPDATE 消息,這兩種方法是共享同一個代碼路徑(code path)。這兩者在更新一個帶有老舊信息的節點的配置信息時會有功能上的重複。然而這兩種機制都是非常有用的,因爲 ping / pong 包在一段時間後能填充(populate)新節點的哈希槽路由表,而 UPDATE 消息只是在一個過時配置信息被檢測出來時才被髮送出去,並且只覆蓋那些需要修復的錯誤配置信息。

備份遷移

Redis 集羣實現了一個叫做備份遷移(replica migration)的概念,以提高系統的可用性。在集羣中有主節點-從節點的設定,如果主從節點間的映射關係是固定的,那麼久而久之,當發生多個單一節點獨立故障的時候,系統可用性會變得很有限。

例如有一個每個主節點都只有一個從節點的集羣,當主節點或者從節點故障失效的時候集羣能讓操作繼續執行下去,但如果主從節點都失效的話就沒法讓操作繼續執行下去。然而這樣長期會積累很多由硬件或軟件問題引起的單一節點獨立故障。例如:

  • 主節點 A 有且只有一個從節點 A1。

  • 主節點 A 失效了。A1 被提升爲新的主節點。

  • 三個小時後,A1 因爲一個獨立事件(跟節點 A 的失效無關)失效了。由於沒有其他從節點可以提升爲主節點(因爲節點 A 仍未恢復正常),集羣沒法繼續進行正常操作。

如果主從節點間的映射關係是固定的,那要讓集羣更有抵抗力地面對上面的情況的唯一方法就是爲每個主節點添加從節點。然而這要付出的代價也更昂貴,因爲要求 Redis 執行更多的實例、更多的內存等等。

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

備份遷移是從節點自動重構的過程,爲了遷移到一個沒有可工作從節點的主節點上。在上面提到的例子中,備份遷移過程如下:

  • 主節點 A 失效。A1 被提升爲主節點。

  • 節點 C2 遷移成爲節點 A1 的從節點,要不然 A1 就沒有任何從節點。

  • 三個小時後節點 A1 也失效了。

  • 節點 C2 被提升爲取代 A1 的新主節點。

  • 集羣仍然能繼續正常工作。

備份遷移算法

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

接下來講這個算法是如何工作的。在開始之前我們需要定義清楚在這個上下文中什麼纔算是一個好的從節點:一個好的從節點是指從給定節點的角度看,該從節點不處於 FAIL 狀態。

每個從節點若檢測出存在至少一個沒有好的從節點的單一主節點,那麼就會觸發這個算法的執行。然而在所有檢測出這種情況的從節點中,只有一部分從節點會採取行動。 通常這“一部分從節點”都只有一個,除非有不同的從節點在給定時間間隔裏對其他節點的失效狀態有稍微不同的視角。

採取行動的從節點是屬於那些擁有最多從節點的主節點,並且不處於 FAIL 狀態及擁有最小的節點 ID。

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

最終每個主節點都會至少有一個從節點作爲備份節點。通常表現出來的行爲是,一個從節點從一個擁有多個從節點的主節點遷移到一個孤立的主節點。

這個算法能通過一個用戶可配置的參數 cluster-migration-barrier 進行控制。這個參數表示的是,一個主節點在擁有多少個好的從節點的時候就要割讓一個從節點出來。例如這個參數若被設爲 2,那麼只有當一個主節點擁有 2 個可工作的從節點時,它的一個從節點會嘗試遷移。

發佈/訂閱(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;
}
發佈了534 篇原創文章 · 獲贊 1128 · 訪問量 162萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章