1. 程式人生 > >redis 叢集(3.0版本開始支援叢集功能)

redis 叢集(3.0版本開始支援叢集功能)

叢集簡介

Redis 叢集是一個可以在多個 Redis 節點之間進行資料共享的設施(installation)。

Redis 叢集不支援那些需要同時處理多個鍵的 Redis 命令, 因為執行這些命令需要在多個 Redis 節點之間移動資料, 並且在高負載的情況下, 這些命令將降低 Redis 叢集的效能, 並導致不可預測的行為。

Redis 叢集通過分割槽(partition)來提供一定程度的可用性(availability): 即使叢集中有一部分節點失效或者無法進行通訊, 叢集也可以繼續處理命令請求。

Redis 叢集提供了以下兩個好處:

  • 將資料自動切分(split)到多個節點的能力。
  • 當叢集中的一部分節點失效或者無法進行通訊時, 仍然可以繼續處理命令請求的能力。

Redis 叢集資料共享

Redis 叢集使用資料分片(sharding)而非一致性雜湊(consistency hashing)來實現: 一個 Redis 叢集包含16384 個雜湊槽(hash slot), 資料庫中的每個鍵都屬於這 16384 個雜湊槽的其中一個, 叢集使用公式CRC16(key) % 16384 來計算鍵 key 屬於哪個槽, 其中 CRC16(key) 語句用於計算鍵 key 的 CRC16 校驗和 。

叢集中的每個節點負責處理一部分雜湊槽。 舉個例子, 一個叢集可以有三個雜湊槽, 其中:

  • 節點 A 負責處理 0 號至 5500 號雜湊槽。
  • 節點 B 負責處理 5501 號至 11000 號雜湊槽。
  • 節點 C 負責處理 11001 號至 16384 號雜湊槽。

這種將雜湊槽分佈到不同節點的做法使得使用者可以很容易地向叢集中新增或者刪除節點。 比如說:

  • 如果使用者將新節點 D 新增到叢集中, 那麼叢集只需要將節點 A 、B 、 C 中的某些槽移動到節點 D 就可以了。
  • 與此類似, 如果使用者要從叢集中移除節點 A , 那麼叢集只需要將節點 A 中的所有雜湊槽移動到節點 B 和節點 C , 然後再移除空白(不包含任何雜湊槽)的節點 A 就可以了。

因為將一個雜湊槽從一個節點移動到另一個節點不會造成節點阻塞, 所以無論是新增新節點還是移除已存在節點, 又或者改變某個節點包含的雜湊槽數量, 都不會造成叢集下線。

Redis 叢集中的主從複製

為了使得叢集在一部分節點下線或者無法與叢集的大多數(majority)節點進行通訊的情況下, 仍然可以正常運作, Redis 叢集對節點使用了主從複製功能: 叢集中的每個節點都有 1 個至 N 個複製品(replica), 其中一個複製品為主節點(master), 而其餘的 N-1 個複製品為從節點(slave)。

在之前列舉的節點 A 、B 、C 的例子中, 如果節點 B 下線了, 那麼叢集將無法正常執行, 因為叢集找不到節點來處理 5501 號至 11000 號的雜湊槽。

另一方面, 假如在建立叢集的時候(或者至少在節點 B 下線之前), 我們為主節點 B 添加了從節點 B1 , 那麼當主節點 B 下線的時候, 叢集就會將 B1 設定為新的主節點, 並讓它代替下線的主節點 B , 繼續處理 5501 號至 11000 號的雜湊槽, 這樣叢集就不會因為主節點 B 的下線而無法正常運作了。

不過如果節點 B 和 B1 都下線的話, Redis 叢集還是會停止運作。

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

Redis 叢集不保證資料的強一致性(strong consistency): 在特定條件下, Redis 叢集可能會丟失已經被執行過的寫命令。

使用非同步複製(asynchronous replication)是 Redis 叢集可能會丟失寫命令的其中一個原因。 考慮以下這個寫命令的例子:

  • 客戶端向主節點 B 傳送一條寫命令。
  • 主節點 B 執行寫命令,並向客戶端返回命令回覆。
  • 主節點 B 將剛剛執行的寫命令複製給它的從節點 B1 、 B2 和 B3 。

如你所見, 主節點對命令的複製工作發生在返回命令回覆之後, 因為如果每次處理命令請求都需要等待複製操作完成的話, 那麼主節點處理命令請求的速度將極大地降低 —— 我們必須在效能和一致性之間做出權衡。

如果真的有必要的話, Redis 叢集可能會在將來提供同步地(synchronou)執行寫命令的方法。

Redis 叢集另外一種可能會丟失命令的情況是, 叢集出現網路分裂(network partition), 並且一個客戶端與至少包括一個主節點在內的少數(minority)例項被孤立。

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

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

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

  • 如果網路分裂出現的時間很短, 那麼叢集會繼續正常執行;
  • 但是, 如果網路分裂出現的時間足夠長, 使得大多數一方將從節點 B1 設定為新的主節點, 並使用 B1 來代替原來的主節點 B , 那麼 Z1 傳送給主節點 B 的寫命令將丟失。

注意, 在網路分裂出現期間, 客戶端 Z1 可以向主節點 B 傳送寫命令的最大時間是有限制的, 這一時間限制稱為節點超時時間(node timeout), 是 Redis 叢集的一個重要的配置選項:

  • 對於大多數一方來說, 如果一個主節點未能在節點超時時間所設定的時限內重新聯絡上叢集, 那麼叢集會將這個主節點視為下線, 並使用從節點來代替這個主節點繼續工作。
  • 對於少數一方, 如果一個主節點未能在節點超時時間所設定的時限內重新聯絡上叢集, 那麼它將停止處理寫命令, 並向客戶端報告錯誤。

建立並使用 Redis 叢集

Redis 叢集由多個執行在叢集模式(cluster mode)下的 Redis 例項組成, 例項的叢集模式需要通過配置來開啟, 開啟叢集模式的例項將可以使用叢集特有的功能和命令。

以下是一個包含了最少選項的叢集配置檔案示例:

port 7000 cluster-enabled yes cluster-config-file nodes.conf cluster-node-timeout 5000 appendonly yes

檔案中的 cluster-enabled 選項用於開例項的叢集模式, 而 cluster-conf-file 選項則設定了儲存節點配置檔案的路徑, 預設值為 nodes.conf 。

節點配置檔案無須人為修改, 它由 Redis 叢集在啟動時建立, 並在有需要時自動進行更新。

要讓叢集正常運作至少需要三個主節點, 不過在剛開始試用叢集功能時, 強烈建議使用六個節點: 其中三個為主節點, 而其餘三個則是各個主節點的從節點。

首先, 讓我們進入一個新目錄, 並建立六個以埠號為名字的子目錄, 稍後我們在將每個目錄中執行一個 Redis 例項:

mkdir cluster-test cd cluster-test mkdir 7000 7001 7002 7003 7004 7005

在資料夾 7000 至 7005 中, 各建立一個 redis.conf 檔案, 檔案的內容可以使用上面的示例配置檔案, 但記得將配置中的埠號從 7000 改為與資料夾名字相同的號碼。

現在, 從 Redis Github 頁面 的 unstable 分支中取出最新的 Redis 原始碼, 編譯出可執行檔案 redis-server , 並將檔案複製到 cluster-test 資料夾, 然後使用類似以下命令, 在每個標籤頁中開啟一個例項:

cd 7000 ../redis-server ./redis.conf

例項列印的日誌顯示, 因為 nodes.conf 檔案不存在, 所以每個節點都為它自身指定了一個新的 ID :

[82462] 26 Nov 11:56:55.329 * No cluster configuration found, I'm 97a3a64667477371c4479320d683e4c8db5858b1

例項會一直使用同一個 ID , 從而在叢集中保持一個獨一無二(unique)的名字。

每個節點都使用 ID 而不是 IP 或者埠號來記錄其他節點, 因為 IP 地址和埠號都可能會改變, 而這個獨一無二的識別符號(identifier)則會在節點的整個生命週期中一直保持不變。

我們將這個識別符號稱為節點 ID

建立叢集

現在我們已經有了六個正在執行中的 Redis 例項, 接下來我們需要使用這些例項來建立叢集, 併為每個節點編寫配置檔案。

通過使用 Redis 叢集命令列工具 redis-trib , 編寫節點配置檔案的工作可以非常容易地完成: redis-trib 位於 Redis 原始碼的 src 資料夾中, 它是一個 Ruby 程式, 這個程式通過向例項傳送特殊命令來完成建立新叢集, 檢查叢集, 或者對叢集進行重新分片(reshared)等工作。

我們需要執行以下命令來建立叢集:

./redis-trib.rb create --replicas 1 127.0.0.1:7000 127.0.0.1:7001 \ 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005

命令的意義如下:

  • 給定 redis-trib.rb 程式的命令是 create , 這表示我們希望建立一個新的叢集。
  • 選項 --replicas 1 表示我們希望為叢集中的每個主節點建立一個從節點。
  • 之後跟著的其他引數則是例項的地址列表, 我們希望程式使用這些地址所指示的例項來建立新叢集。

簡單來說, 以上命令的意思就是讓 redis-trib 程式建立一個包含三個主節點和三個從節點的叢集。

接著, redis-trib 會打印出一份預想中的配置給你看, 如果你覺得沒問題的話, 就可以輸入 yes , redis-trib 就會將這份配置應用到叢集當中:

>>> Creating cluster Connecting to node 127.0.0.1:7000: OK Connecting to node 127.0.0.1:7001: OKConnecting to node 127.0.0.1:7002: OK Connecting to node 127.0.0.1:7003: OK Connecting to node 127.0.0.1:7004: OK Connecting to node 127.0.0.1:7005: OK >>> Performing hash slots allocation on 6nodes... Using 3 masters: 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7000 replica #1 is 127.0.0.1:7003 127.0.0.1:7001 replica #1 is 127.0.0.1:7004 127.0.0.1:7002 replica #1 is 127.0.0.1:7005 M: 9991306f0e50640a5684f1958fd754b38fa034c9 127.0.0.1:7000 slots:0-5460 (5461 slots) master M: e68e52cee0550f558b03b342f2f0354d2b8a083b 127.0.0.1:7001 slots:5461-10921 (5461 slots) master M: 393c6df5eb4b4cec323f0e4ca961c8b256e3460a 127.0.0.1:7002 slots:10922-16383 (5462 slots) master S: 48b728dbcedff6bf056231eb44990b7d1c35c3e0 127.0.0.1:7003 S: 345ede084ac784a5c030a0387f8aaa9edfc59af3 127.0.0.1:7004 S: 3375be2ccc321932e8853234ffa87ee9fde973ff 127.0.0.1:7005 Can I set the above configuration? (type 'yes' to accept): yes

輸入 yes 並按下回車確認之後, 叢集就會將配置應用到各個節點, 並連線起(join)各個節點 —— 也即是, 讓各個節點開始互相通訊:

>>> Nodes configuration updated >>> Sending CLUSTER MEET messages to join the cluster Waiting for the cluster to join... >>> Performing Cluster Check (using node 127.0.0.1:7000) M: 9991306f0e50640a5684f1958fd754b38fa034c9 127.0.0.1:7000 slots:0-5460 (5461 slots) master M: e68e52cee0550f558b03b342f2f0354d2b8a083b 127.0.0.1:7001 slots:5461-10921 (5461 slots) master M: 393c6df5eb4b4cec323f0e4ca961c8b256e3460a 127.0.0.1:7002 slots:10922-16383 (5462 slots) master M: 48b728dbcedff6bf056231eb44990b7d1c35c3e0 127.0.0.1:7003 slots: (0 slots) master M: 345ede084ac784a5c030a0387f8aaa9edfc59af3 127.0.0.1:7004 slots: (0 slots) master M: 3375be2ccc321932e8853234ffa87ee9fde973ff 127.0.0.1:7005 slots: (0 slots) master [OK] All nodes agree about slots configuration.

如果一切正常的話, redis-trib 將輸出以下資訊:

>>> Check for open slots... >>> Check slots coverage... [OK] All 16384 slots covered.

這表示叢集中的 16384 個槽都有至少一個主節點在處理, 叢集運作正常。

叢集的客戶端

Redis 叢集現階段的一個問題是客戶端實現很少。 以下是一些我知道的實現:

  • redis-rb-cluster 是我(@antirez)編寫的 Ruby 實現, 用於作為其他實現的參考。 該實現是對 redis-rb 的一個簡單包裝, 高效地實現了與叢集進行通訊所需的最少語義(semantic)。
  • redis-py-cluster 看上去是 redis-rb-cluster 的一個 Python 版本, 這個專案有一段時間沒有更新了(最後一次提交是在六個月之前), 不過可以將這個專案用作學習叢集的起點。
  • 流行的 Predis 曾經對早期的 Redis 叢集有過一定的支援, 但我不確定它對叢集的支援是否完整, 也不清楚它是否和最新版本的 Redis 叢集相容 (因為新版的 Redis 叢集將槽的數量從 4k 改為 16k 了)。
  • Redis unstable 分支中的 redis-cli 程式實現了非常基本的叢集支援, 可以使用命令 redis-cli -c 來啟動。

測試 Redis 叢集比較簡單的辦法就是使用 redis-rb-cluster 或者 redis-cli , 接下來我們將使用 redis-cli 為例來進行演示:

$ redis-cli -c -p 7000 redis 127.0.0.1:7000> set foo bar -> Redirected to slot [12182] located at 127.0.0.1:7002 OK redis 127.0.0.1:7002> set hello world -> Redirected to slot [866] located at 127.0.0.1:7000 OK redis 127.0.0.1:7000> get foo -> Redirected to slot [12182] located at 127.0.0.1:7002 "bar" redis 127.0.0.1:7000> get hello -> Redirected to slot [866] located at 127.0.0.1:7000 "world"

redis-cli 對叢集的支援是非常基本的, 所以它總是依靠 Redis 叢集節點來將它轉向(redirect)至正確的節點。

一個真正的(serious)叢集客戶端應該做得比這更好: 它應該用快取記錄起雜湊槽與節點地址之間的對映(map), 從而直接將命令傳送到正確的節點上面。

這種對映只會在叢集的配置出現某些修改時變化, 比如說, 在一次故障轉移(failover)之後, 或者系統管理員通過新增節點或移除節點來修改了叢集的佈局(layout)之後, 諸如此類。

使用 redis-rb-cluster 編寫一個示例應用

在展示如何使用叢集進行故障轉移、重新分片等操作之前, 我們需要建立一個示例應用, 瞭解一些與 Redis 叢集客戶端進行互動的基本方法。

在執行示例應用的過程中, 我們會嘗試讓節點進入失效狀態, 又或者開始一次重新分片, 以此來觀察 Redis 叢集在真實世界執行時的表現, 並且為了讓這個示例儘可能地有用, 我們會讓這個應用向叢集進行寫操作。

本節將通過兩個示例應用來展示 redis-rb-cluster 的基本用法, 以下是本節的第一個示例應用, 它是一個名為example.rb 的檔案, 包含在redis-rb-cluster 專案裡面

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
require './cluster' startup_nodes = [ {:host => "127.0.0.1", :port => 7000}, {:host =>"127.0.0.1", :port => 7001} ] rc = RedisCluster.new(startup_nodes,32,:timeout => 0.1) last =false while not last begin last = rc.get("__last__") last = 0 if !last rescue => e puts "error #{e.to_s}" sleep 1 end end ((last.to_i+1)..1000000000).each{|x| begin rc.set("foo#{x}",x) putsrc.get("foo#{x}") rc.set("__last__",x) rescue => e puts "error #{e.to_s}" end sleep 0.1 }

這個應用所做的工作非常簡單: 它不斷地以 foo 為鍵, number 為值, 使用 SET 命令向資料庫設定鍵值對。

如果我們執行這個應用的話, 應用將按順序執行以下命令:

  • SET foo0 0
  • SET foo1 1
  • SET foo2 2
  • 諸如此類。。。

程式碼中的每個叢集操作都使用一個 begin 和 rescue 程式碼塊(block)包裹著, 因為我們希望在程式碼出錯時, 將錯誤列印到終端上面, 而不希望應用因為異常(exception)而退出。

程式碼的第七行是程式碼中第一個有趣的地方, 它建立了一個 Redis 叢集物件, 其中建立物件所使用的引數及其意義如下:

  • 第一個引數是記錄了啟動節點的 startup_nodes 列表, 列表中包含了兩個叢集節點的地址。
  • 第二個引數指定了對於叢集中的各個不同的節點, Redis 叢集物件可以獲得(take)的最大連線數 (maximum number of connections this object is allowed to take)。
  • 第三個引數 timeout 指定了一個命令在執行多久之後, 才會被看作是執行失敗。

記住, 啟動列表中並不需要包含所有叢集節點的地址, 但這些地址中至少要有一個是有效的(reachable): 一旦 redis-rb-cluster 成功連線上叢集中的某個節點時, 叢集節點列表就會被自動更新, 任何真正的(serious)的叢集客戶端都應該這樣做。

現在, 程式建立的 Redis 叢集物件例項被儲存到 rc 變數裡面, 我們可以將這個物件當作普通 Redis 物件例項來使用。

十一至十九行, 我們先嚐試閱讀計數器中的值, 如果計數器不存在的話, 我們才將計數器初始化為 0 : 通過將計數值儲存到 Redis 的計數器裡面, 我們可以在示例重啟之後, 仍然繼續之前的執行過程, 而不必每次重啟之後都從 foo0 開始重新設定鍵值對。

為了讓程式在叢集下線的情況下, 仍然不斷地嘗試讀取計數器的值, 我們將讀取操作包含在了一個 while 迴圈裡面, 一般的應用程式並不需要如此小心。

二十一至三十行是程式的主迴圈, 這個迴圈負責設定鍵值對, 並在設定出錯時列印錯誤資訊。

程式在主迴圈的末尾添加了一個 sleep 呼叫, 讓寫操作的執行速度變慢, 幫助執行示例的人更容易看清程式的輸出。

執行 example.rb 程式將產生以下輸出:

ruby ./example.rb 1 2 3 4 5 6 7 8 9 ...

這個程式並不是十分有趣, 稍後我們就會看到一個更有趣的叢集應用示例, 不過在此之前, 讓我們先使用這個示例來演示叢集的重新分片操作。

對叢集進行重新分片

現在, 讓我們來試試對叢集進行重新分片操作。

在執行重新分片的過程中, 請讓你的 example.rb 程式處於執行狀態, 這樣你就會看到, 重新分片並不會對正在執行的叢集程式產生任何影響, 你也可以考慮將 example.rb 中的 sleep 呼叫刪掉, 從而讓重新分片操作在近乎真實的寫負載下執行。

重新分片操作基本上就是將某些節點上的雜湊槽移動到另外一些節點上面, 和建立叢集一樣, 重新分片也可以使用 redis-trib 程式來執行。

執行以下命令可以開始一次重新分片操作:

$ ./redis-trib.rb reshard 127.0.0.1:7000

你只需要指定叢集中其中一個節點的地址, redis-trib 就會自動找到叢集中的其他節點。

目前 redis-trib 只能在管理員的協助下完成重新分片的工作, 要讓 redis-trib 自動將雜湊槽從一個節點移動到另一個節點, 目前來說還做不到 (不過實現這個功能並不難)。

執行 redis-trib 的第一步就是設定你打算移動的雜湊槽的數量:

$ ./redis-trib.rb reshard 127.0.0.1:7000 Connecting to node 127.0.0.1:7000: OK Connecting to node 127.0.0.1:7002: OK Connecting to node 127.0.0.1:7005: OK Connecting to node 127.0.0.1:7001: OK Connecting to node 127.0.0.1:7003: OK Connecting to node 127.0.0.1:7004: OK >>> Performing Cluster Check (using node 127.0.0.1:7000) M: 9991306f0e50640a5684f1958fd754b38fa034c9 127.0.0.1:7000 slots:0-5460 (5461 slots) master M: 393c6df5eb4b4cec323f0e4ca961c8b256e3460a 127.0.0.1:7002 slots:10922-16383 (5462 slots) master S: 3375be2ccc321932e8853234ffa87ee9fde973ff 127.0.0.1:7005 slots: (0 slots) slave M: e68e52cee0550f558b03b342f2f0354d2b8a083b 127.0.0.1:7001 slots:5461-10921 (5461 slots) master S: 48b728dbcedff6bf056231eb44990b7d1c35c3e0 127.0.0.1:7003 slots: (0 slots) slave S: 345ede084ac784a5c030a0387f8aaa9edfc59af3 127.0.0.1:7004 slots: (0 slots) slave [OK] All nodes agree about slots configuration. >>> Check for open slots... >>> Check slots coverage... [OK] All 16384 slots covered. How many slots do you want to move (from 1 to 16384)? 1000

我們將打算移動的槽數量設定為 1000 個, 如果 example.rb 程式一直執行著的話, 現在 1000 個槽裡面應該有不少鍵了。

除了移動的雜湊槽數量之外, redis-trib 還需要知道重新分片的目標(target node), 也即是, 負責接收這1000 個雜湊槽的節點。

指定目標需要使用節點的 ID , 而不是 IP 地址和埠。 比如說, 我們打算使用叢集的第一個主節點來作為目標, 它的 IP 地址和埠是 127.0.0.1:7000 , 而節點 ID 則是 9991306f0e50640a5684f1958fd754b38fa034c9 , 那麼我們應該向 redis-trib 提供節點的 ID :

$ ./redis-trib.rb reshard 127.0.0.1:7000 ... What is the receiving node ID? 9991306f0e50640a5684f1958fd754b38fa034c9

redis-trib 會打印出叢集中所有節點的 ID , 並且我們也可以通過執行以下命令來獲得節點的執行 ID :

$ ./redis-cli -p 7000 cluster nodes | grep myself 9991306f0e50640a5684f1958fd754b38fa034c9 :0 myself,master - 0 0 0 connected 0-5460

接著, redis-trib 會向你詢問重新分片的源節點(source node), 也即是, 要從哪個節點中取出 1000 個雜湊槽, 並將這些槽移動到目標節點上面。

如果我們不打算從特定的節點上取出指定數量的雜湊槽, 那麼可以向 redis-trib 輸入 all , 這樣的話, 叢集中的所有主節點都會成為源節點, redis-trib 將從各個源節點中各取出一部分雜湊槽, 湊夠 1000 個, 然後移動到目標節點上面:

$ ./redis-trib.rb reshard 127.0.0.1:7000 ... Please enter all the source node IDs. Type 'all' to use all the nodes as source nodes for the hash slots. Type 'done' once you entered all the source nodes IDs. Source node #1:all

輸入 all 並按下回車之後, redis-trib 將打印出雜湊槽的移動計劃, 如果你覺得沒問題的話, 就可以輸入 yes並再次按下回車:

$ ./redis-trib.rb reshard 127.0.0.1:7000 ... Moving slot 11421 from 393c6df5eb4b4cec323f0e4ca961c8b256e3460a Moving slot 11422 from 393c6df5eb4b4cec323f0e4ca961c8b256e3460a Moving slot 5461 from e68e52cee0550f558b03b342f2f0354d2b8a083b Moving slot 5469 from e68e52cee0550f558b03b342f2f0354d2b8a083b ... Moving slot 5959 from e68e52cee0550f558b03b342f2f0354d2b8a083b Do you want to proceed with the proposed reshard plan (yes/no)? yes

輸入 yes 並使用按下回車之後, redis-trib 就會正式開始執行重新分片操作, 將指定的雜湊槽從源節點一個個地移動到目標節點上面:

$ ./redis-trib.rb reshard 127.0.0.1:7000 ... Moving slot 5934 from 127.0.0.1:7001 to 127.0.0.1:7000: Moving slot 5935 from 127.0.0.1:7001 to 127.0.0.1:7000: Moving slot 5936 from 127.0.0.1:7001 to 127.0.0.1:7000: Moving slot 5937 from 127.0.0.1:7001 to 127.0.0.1:7000: ... Moving slot 5959 from 127.0.0.1:7001 to 127.0.0.1:7000:

在重新分片的過程中, example.rb 應該可以繼續正常執行, 不會出現任何問題。

在重新分片操作執行完畢之後, 可以使用以下命令來檢查叢集是否正常:

$ ./redis-trib.rb check 127.0.0.1:7000 Connecting to node 127.0.0.1:7000: OK Connecting to node 127.0.0.1:7002: OK Connecting to node 127.0.0.1:7005: OK Connecting to node 127.0.0.1:7001: OK Connecting to node 127.0.0.1:7003: OK Connecting to node 127.0.0.1:7004: OK >>> Performing Cluster Check (using node 127.0.0.1:7000) M: 9991306f0e50640a5684f1958fd754b38fa034c9 127.0.0.1:7000 slots:0-5959,10922-11422 (6461 slots) master M: 393c6df5eb4b4cec323f0e4ca961c8b256e3460a 127.0.0.1:7002 slots:11423-16383 (4961 slots) master S: 3375be2ccc321932e8853234ffa87ee9fde973ff 127.0.0.1:7005 slots: (0 slots) slave M: e68e52cee0550f558b03b342f2f0354d2b8a083b 127.0.0.1:7001 slots:5960-10921 (4962 slots) master S: 48b728dbcedff6bf056231eb44990b7d1c35c3e0 127.0.0.1:7003 slots: (0 slots) slave S: 345ede084ac784a5c030a0387f8aaa9edfc59af3 127.0.0.1:7004 slots: (0 slots) slave [OK] All nodes agree about slots configuration. >>> Check for open slots... >>> Check slots coverage... [OK] All 16384 slots covered.

根據檢查結果顯示, 叢集運作正常。

需要注意的就是, 在三個主節點中, 節點 127.0.0.1:7000 包含了 6461 個雜湊槽, 而節點 127.0.0.1:7001 和節點 127.0.0.1:7002 都只包含了 4961 個雜湊槽, 因為後兩者都將自己的 500 個雜湊槽移動到了節點127.0.0.1:7000 。

一個更有趣的示例應用

我們在前面使用的示例程式 example.rb 並不是十分有趣, 因為它只是不斷地對叢集進行寫入, 但並不檢查寫入結果是否正確。 比如說, 叢集可能會錯誤地將 example.rb 傳送的所有 SET 命令都改成了 SET foo 42 , 但因為 example.rb 並不檢查寫入後的值, 所以它不會意識到叢集實際上寫入的值是錯誤的。

因為這個原因, redis-rb-cluster 專案包含了一個名為 consistency-test.rb 的示例應用, 這個應用比起example.rb 有趣得多: 它建立了多個計數器(預設為 1000 個), 並通過傳送 INCR 命令來增加這些計數器的值。

在增加計數器值的同時, consistency-test.rb 還執行以下操作:

  • 每次使用 INCR 命令更新一個計數器時, 應用會記錄下計數器執行 INCR 命令之後應該有的值。 舉個例子, 如果計數器的起始值為 0 , 而這次是程式第 50 次向它傳送 INCR 命令, 那麼計數器的值應該是 50
  • 在每次傳送 INCR 命令之前, 程式會隨機從叢集中讀取一個計數器的值, 並將它與自己記錄的值進行對比, 看兩個值是否相同。

換句話說, 這個程式是一個一致性檢查器(consistency checker): 如果叢集在執行 INCR 命令的過程中, 丟失了某條 INCR 命令, 又或者多執行了某條客戶端沒有確認到的 INCR 命令, 那麼檢查器將察覺到這一點 —— 在前一種情況中, consistency-test.rb 記錄的計數器值將比叢集記錄的計數器值要大; 而在後一種情況中, consistency-test.rb 記錄的計數器值將比叢集記錄的計數器值要小。

執行 consistency-test 程式將產生類似以下的輸出:

$ ruby consistency-test.rb 925 R (0 err) | 925 W (0 err) | 5030 R (0 err) | 5030 W (0 err) | 9261 R (0 err) | 9261 W (0 err) | 13517 R (0 err) | 13517 W (0 err) | 17780 R (0 err) | 17780 W (0 err) | 22025 R (0 err) | 22025 W (0 err) | 25818 R (0 err) | 25818 W (0 err) |

每行輸出都列印了程式執行的讀取次數和寫入次數, 以及執行操作的過程中因為叢集不可用而產生的錯誤數。

如果程式察覺了不一致的情況出現, 它將在輸出行的末尾顯式不一致的詳細情況。

比如說, 如果我們在 consistency-test.rb 執行的過程中, 手動修改某個計數器的值:

$ redis 127.0.0.1:7000> set key_217 0 OK

那麼 consistency-test.rb 將向我們報告不一致情況:

(in the other tab I see...) 94774 R (0 err) | 94774 W (0 err) | 98821 R (0 err) | 98821 W (0 err) | 102886 R (0 err) | 102886 W (0 err) | 114 lost | 107046 R (0 err) | 107046 W (0 err) | 114 lost |

在我們修改計數器值的時候, 計數器的正確值是 114 (執行了 114 次 INCR 命令), 因為我們將計數器的值設成了 0 , 所以 consistency-test.rb 會向我們報告說丟失了 114 個 INCR 命令。

因為這個示例程式具有一致性檢查功能, 所以我們用它來測試 Redis 叢集的故障轉移操作。

故障轉移測試

在執行本節操作的過程中, 請一直執行 consistency-test 程式。

要觸發一次故障轉移, 最簡單的辦法就是令叢集中的某個主節點進入下線狀態。

首先用以下命令列出叢集中的所有主節點:

$ redis-cli -p 7000 cluster nodes | grep master 3e3a6cb0d9a9a87168e266b0a0b24026c0aae3f0 127.0.0.1:7001 master - 0 1385482984082 0 connected 5960-10921 2938205e12de373867bf38f1ca29d31d0ddb3e46 127.0.0.1:7002 master - 0 1385482983582 0 connected 11423-16383 97a3a64667477371c4479320d683e4c8db5858b1 :0 myself,master - 0 0 0 connected 0-5959 10922-11422

通過命令輸出, 我們知道埠號為 7000 、 7001 和 7002 的節點都是主節點, 然後我們可以通過向埠號為7002 的主節點發送 DEBUG SEGFAULT 命令, 讓這個主節點崩潰:

$ redis-cli -p 7002 debug segfault Error: Server closed the connection

現在, 切換到執行著 consistency-test 的標籤頁, 可以看到, consistency-test 在 7002 下線之後的一段時間裡將產生大量的錯誤警告資訊:

18849 R (0 err) | 18849 W (0 err) | 23151 R (0 err) | 23151 W (0 err) | 27302 R (0 err) | 27302 W (0 err) | ... many error warnings here ... 29659 R (578 err) | 29660 W (577 err) | 33749 R (578 err) | 33750 W (577 err) | 37918 R (578 err) | 37919 W (577 err) | 42077 R (578 err) | 42078 W (577 err) |

從 consistency-test 的這段輸出可以看到, 叢集在執行故障轉移期間, 總共丟失了 578 個讀命令和 577 個寫命令, 但是並沒有產生任何資料不一致。

這聽上去可能有點奇怪, 因為在教程的開頭我們提到過, Redis 使用的是非同步複製, 在執行故障轉移期間, 叢集可能會丟失寫命令。

但是在實際上, 丟失命令的情況並不常見, 因為 Redis 幾乎是同時執行將命令回覆傳送給客戶端, 以及將命令複製給從節點這兩個操作, 所以實際上造成命令丟失的時間視窗是非常小的。

不過, 儘管出現的機率不高, 但丟失命令的情況還是有可能會出現的, 所以我們對 Redis 叢集不能提供強一致性的這一描述仍然是正確的。

現在, 讓我們使用 cluster nodes 命令, 檢視叢集在執行故障轉移操作之後, 主從節點的佈局情況:

$ redis-cli -p 7000 cluster nodes 3fc783611028b1707fd65345e763befb36454d73 127.0.0.1:7004 slave 3e3a6cb0d9a9a87168e266b0a0b24026c0aae3f0 0 1385503418521 0 connected a211e242fc6b22a9427fed61285e85892fa04e08 127.0.0.1:7003 slave 97a3a64667477371c4479320d683e4c8db5858b1 0 1385503419023 0 connected 97a3a64667477371c4479320d683e4c8db5858b1 :0 myself,master - 0 0 0 connected 0-5959 10922-11422 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 127.0.0.1:7005 master - 0 1385503419023 3 connected 11423-16383 3e3a6cb0d9a9a87168e266b0a0b24026c0aae3f0 127.0.0.1:7001 master - 0 1385503417005 0 connected 5960-10921 2938205e12de373867bf38f1ca29d31d0ddb3e46 127.0.0.1:7002 slave 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 0 1385503418016 3 connected

我重啟了之前下線的 127.0.0.1:7002 節點, 該節點已經從原來的主節點變成了從節點, 而現在叢集中的三個主節點分別是 127.0.0.1:7000 、 127.0.0.1:7001 和 127.0.0.1:7005 , 其中 127.0.0.1:7005 就是因為127.0.0.1:7002 下線而變成主節點的。

cluster nodes 命令的輸出有點兒複雜, 它的每一行都是由以下資訊組成的:

  • 節點 ID :例如 3fc783611028b1707fd65345e763befb36454d73 。
  • ip:port :節點的 IP 地址和埠號, 例如 127.0.0.1:7000 , 其中 :0 表示的是客戶端當前連線的 IP 地址和埠號。
  • flags :節點的角色(例如 master 、 slave 、 myself )以及狀態(例如 fail ,等等)。
  • 如果節點是一個從節點的話, 那麼跟在 flags 之後的將是主節點的節點 ID : 例如 127.0.0.1:7002 的主節點的節點 ID 就是 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 。
  • 叢集最近一次向節點發送 PING 命令之後, 過去了多長時間還沒接到回覆。
  • 節點最近一次返回 PONG 回覆的時間。
  • 節點的配置紀元(configuration epoch):詳細資訊請參考 Redis 叢集規範 。
  • 本節點的網路連線情況:例如 connected 。
  • 節點目前包含的槽:例如 127.0.0.1:7001 目前包含號碼為 5960 至 10921 的雜湊槽。

新增新節點到叢集

根據新新增節點的種類, 我們需要用兩種方法來將新節點新增到叢集裡面:

  • 如果要新增的新節點是一個主節點, 那麼我們需要建立一個空節點(empty node), 然後將某些雜湊桶移動到這個空節點裡面。
  • 另一方面, 如果要新增的新節點是一個從節點, 那麼我們需要將這個新節點設定為叢集中某個節點的複製品(replica)。

本節將對以上兩種情況進行介紹, 首先介紹主節點的新增方法, 然後再介紹從節點的新增方法。

無論新增的是那種節點, 第一步要做的總是新增一個空節點。

我們可以繼續使用之前啟動 127.0.0.1:7000 、 127.0.0.1:7001 等節點的方法, 建立一個埠號為 7006 的新節點, 使用的配置檔案也和之前一樣, 只是記得要將配置中的埠號改為 7000 。

以下是啟動埠號為 7006 的新節點的詳細步驟:

  1. 在終端裡建立一個新的標籤頁。
  2. 進入 cluster-test 資料夾。
  3. 建立並進入 7006 資料夾。
  4. 將 redis.conf 檔案複製到 7006 資料夾裡面,然後將配置中的埠號選項改為 7006 。
  5. 使用命令 ../../redis-server redis.conf 啟動節點。

如果一切正常, 那麼節點應該會正確地啟動。

接下來, 執行以下命令, 將這個新節點新增到叢集裡面:

./redis-trib.rb addnode 127.0.0.1:7006 127.0.0.1:7000

命令中的 addnode 表示我們要讓 redis-trib 將一個節點新增到叢集裡面, addnode 之後跟著的是新節點的 IP 地址和埠號, 再之後跟著的是叢集中任意一個已存在節點的 IP 地址和埠號, 這裡我們使用的是127.0.0.1:7000 。

通過 cluster nodes 命令, 我們可以確認新節點 127.0.0.1:7006 已經被新增到叢集裡面了:

redis 127.0.0.1:7006> cluster nodes 3e3a6cb0d9a9a87168e266b0a0b24026c0aae3f0 127.0.0.1:7001 master - 0 1385543178575 0 connected 5960-10921 3fc783611028b1707fd65345e763befb36454d73 127.0.0.1:7004 slave 3e3a6cb0d9a9a87168e266b0a0b24026c0aae3f0 0 1385543179583 0 connected f093c80dde814da99c5cf72a7dd01590792b783b :0 myself,master - 0 0 0 connected 2938205e12de373867bf38f1ca29d31d0ddb3e46 127.0.0.1:7002 slave 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 0 1385543178072 3 connected a211e242fc6b22a9427fed61285e85892fa04e08 127.0.0.1:7003 slave 97a3a64667477371c4479320d683e4c8db5858b1 0 1385543178575 0 connected 97a3a64667477371c4479320d683e4c8db5858b1 127.0.0.1:7000 master - 0 1385543179080 0 connected 0-5959 10922-11422 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 127.0.0.1:7005 master - 0 1385543177568 3 connected 11423-16383

新節點現在已經連線上了叢集, 成為叢集的一份子, 並且可以對客戶端的命令請求進行轉向了, 但是和其他主節點相比, 新節點還有兩點區別:

  • 新節點沒有包含任何資料, 因為它沒有包含任何雜湊桶。
  • 儘管新節點沒有包含任何雜湊桶, 但它仍然是一個主節點, 所以在叢集需要將某個從節點升級為新的主節點時, 這個新節點不會被選中。

接下來, 只要使用 redis-trib 程式, 將叢集中的某些雜湊桶移動到新節點裡面, 新節點就會成為真正的主節點了。

因為使用 redis-trib 移動雜湊桶的方法在前面已經介紹過, 所以這裡就不再重複介紹了。

現在, 讓我們來看看, 將一個新節點轉變為某個主節點的複製品(也即是從節點)的方法。

舉個例子, 如果我們打算讓新節點成為 127.0.0.1:7005 的從節點, 那麼我們只要用客戶端連線上新節點, 然後執行以下命令就可以了:

redis 127.0.0.1:7006> cluster replicate 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e

其中命令提供的 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 就是主節點 127.0.0.1:7005 的節點 ID 。

執行 cluster replicate 命令之後, 我們可以使用以下命令來確認 127.0.0.1:7006 已經成為了 ID 為3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 的節點的從節點:

$ redis-cli -p 7000 cluster nodes | grep slave | grep 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e f093c80dde814da99c5cf72a7dd01590792b783b 127.0.0.1:7006 slave 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 0 1385543617702 3 connected 2938205e12de373867bf38f1ca29d31d0ddb3e46 127.0.0.1:7002 slave 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 0 1385543617198 3 connected
3c3a0c... 現在有兩個從節點, 一個從節點的埠號為 7002 , 而另一個從節點的埠號為 7006