Redis 3.0中文官方文件翻譯計劃(17) ——叢集(中)
阿新 • • 發佈:2019-02-11
使用redis-rb-cluster寫一個示例應用
在後面介紹如何操作Redis叢集之前,像故障轉移或者重新分片這樣的事情,我們需要建立一個示例應用,或者至少要了解簡單的Redis叢集客戶端的互動語義。
我們採用執行一個示例,同時嘗試使節點失效,或者開始重新分片這樣的方式,來看看在真實世界條件下Redis叢集如何表現。如果沒有人往叢集寫的話,觀察叢集發生了什麼也沒有什麼實際用處。
這一小節通過兩個例子來解釋redis-rb-cluster的基本用法。第一個例子在redis-rb-cluster發行版本的exemple.rb檔案中,如下:
Ruby程式碼
這個程式做了一件很簡單的事情,一個一個地設定形式為foo<number>的鍵的值為一個數字。所以如果你執行這個程式,結果就是下面的命令流:
Java程式碼
這個程式看起來要比通常看起來更復雜,因為這個是設計用來在螢幕上展示錯誤,而不是由於異常退出,所以每一個對叢集執行的操作都被begin rescue程式碼塊包圍起來。
第7行是程式中第一個有意思的地方。建立了Redis叢集物件,使用啟動節點(startup nodes)的列表,物件允許的最大連線數,以及指定操作被認為失效的超時時間作為引數。
啟動節點不需要是全部的叢集節點。重要的是至少有一個節點可達。也要注意,redis-rb-cluster一旦連線上了第一個節點就會更新啟動節點的列表。你可以從任何真實的客戶端中看到這樣的行為。
現在,我們將Redis叢集物件例項儲存在rc變數中,我們準備像一個正常的Redis物件例項一樣來使用這個物件。
第11至19行說的是:當我們重啟示例的時候,我們不想又從foo0開始,所以我們儲存計數到Redis裡面。上面的程式碼被設計為讀取這個計數值,或者,如果這個計數器不存在,就賦值為0。
但是,注意這裡為什麼是個while迴圈,因為我們想即使叢集下線並返回錯誤也要不斷地重試。一般的程式不必這麼小心謹慎。
第21到30行開始了主迴圈,鍵被設定賦值或者展示錯誤。
注意迴圈最後sleep呼叫。在你的測試中,如果你想盡可能快地往叢集寫入,你可以移除這個sleep(相對來說,這是一個繁忙的迴圈而不是真實的併發,所以在最好的條件下通常可以得到每秒10k次操作)。
正常情況下,寫被放慢了速度,讓人可以更容易地跟蹤程式的輸出。
執行程式產生了如下輸出:
Java程式碼
這不是一個很有趣的程式,稍後我們會使用一個更有意思的例子,看看在程式執行時進行重新分片會發生什麼事情。
重新分片叢集(Resharding the cluster)
現在,我們準備嘗試叢集重分片。要做這個請保持example.rb程式在執行中,這樣你可以看到是否對執行中的程式有一些影響。你也可能想註釋掉sleep呼叫,這樣在重分片期間就有一些真實的寫負載。
重分片基本上就是從部分節點移動雜湊槽到另外一部分節點上去,像建立叢集一樣也是通過使用redis-trib工具來完成。
開啟重分片只需要輸入:
Java程式碼
你只需要指定單個節點,redis-trib會自動找到其它節點。
當前redis-trib只能在管理員的支援下進行重分片,你不能只是說從這個節點移動5%的雜湊槽到另一個節點(但是這也很容易實現)。那麼問題就隨之而來了。第一個問題就是你想要重分片多少:
你想移動多少雜湊槽(從1到16384)?
我們嘗試重新分片1000個雜湊槽,如果沒有sleep呼叫的那個例子程式還在執行的話,這些槽裡面應該已經包含了不少的鍵了。
然後,redis-trib需要知道重分片的目標了,也就是將接收這些雜湊槽的節點。我將使用第一個主伺服器節點,也就是127.0.0.1:7000,但是我得指定這個例項的節點ID。這已經被redis-trib列印在一個列表中了,但是我總是可以在需要時使用下面的命令找到節點的ID:
Java程式碼
好了,我的目標節點是97a3a64667477371c4479320d683e4c8db5858b1。
現在,你會被詢問想從哪些節點獲取這些鍵。我會輸入all,這樣就會從所有其它的主伺服器節點獲取一些雜湊槽。
在最後的確認後,你會看到每一個被redis-trib準備從一個節點移動到另一個節點的槽的訊息,並且會為每一個被從一側移動到另一側的真實的鍵列印一個圓點。
在重分片進行的過程中,你應該能夠看到你的示例程式執行沒有受到影響。如果你願意的話,你可以在重分片期間多次停止和重啟它。
在重分片的最後,你可以使用下面的命令來測試一下叢集的健康情況:
Java程式碼
像平時一樣,所有的槽都會被覆蓋到,但是這次在127.0.0.1:7000的主伺服器會擁有更多的雜湊槽,大約6461個左右。
一個更有意思的示例程式
到目前為止一切挺好,但是我們使用的示例程式卻不夠好。不顧後果地(acritically)往叢集裡面寫,而不檢查寫入的東西是否是正確的。
從我們的觀點看,接收寫請求的叢集可能一直將每個操作都作為設定鍵foo值為42,我們卻根本沒有察覺到。
所以在redis-rb-cluster倉庫中,有一個叫做consistency-test.rb的更有趣的程式。這個程式有意思得多,因為它使用一組計數器,預設1000個,傳送INCR命令來增加這些計數器。
但是,除了寫入,程式還做另外兩件事情:
這個的意思就是,這個程式就是一個一致性檢查器,可以告訴你叢集是否丟失了一些寫操作,或者是否接受了一個我們沒有收到確認(acknowledgement)的寫操作。在第一種情況下,我們會看到計數器的值小於我們記錄的值,而在第二種情況下,這個值會大於。
執行consistency-test程式每秒鐘產生一行輸出:
Java程式碼
每一行展示了執行的讀操作和寫操作的次數,以及錯誤數(錯誤導致的未被接受的查詢是因為系統不可用)。
如果發現了不一致性,輸出將增加一些新行。例如,當我在程式執行期間手工重置計數器,就會發生:
Java程式碼
當我把計數器設定為0時,真實值是144,所以程式報告了144個寫操作丟失(叢集沒有記住的INCR命令執行的次數)。
這個程式作為測試用例很有意思,所以我們會使用它來測試Redis叢集的故障轉移。
測試故障轉移(Testing the failover)
注意:在測試期間,你應該開啟一個標籤視窗,一致性檢查的程式在其中執行。
為了觸發故障轉移,我們可以做的最簡單的事情(這也是能發生在分散式系統中語義上最簡單的失敗)就是讓一個程序崩潰,在我們的例子中就是一個主伺服器。
我們可以使用下面的命令來識別一個叢集並讓其崩潰:
Java程式碼
好了,7000,7001,7002都是主伺服器。我們使用DEBUG SEGFAULT命令來使節點7002崩潰:
Java程式碼
現在,我們可以看看一致性測試的輸出報告了些什麼內容。
Java程式碼
你可以看到,在故障轉移期間,系統不能接受578個讀請求和577個寫請求,但是資料庫中沒有產生不一致性。這聽起來好像和我們在這篇教程的第一部分中陳述的不一樣,我們說道,Redis叢集在故障轉移期間會丟失寫操作,因為它使用非同步複製。但是我們沒有說過的是,這並不是經常發生,因為Redis傳送回覆給客戶端,和傳送複製命令給從伺服器差不多是同時,所以只有一個很小的丟失資料視窗。但是,很難觸發並不意味著不可能發生,所以這並沒有改變Redis叢集提供的一致性保證(即非強一致性,譯者注)。
我們現在可以看看故障轉移後的叢集佈局(注意,與此同時,我重啟了崩潰的例項,所以它以從伺服器的身份重新加入了叢集):
Java程式碼
現在,主伺服器執行在7000,7001和7005埠。之前執行在7002埠的主伺服器現在是7005的從伺服器了。
CLUSTER NODES命令的輸出看起來挺可怕的,但是實際上相當的簡單,由以下部分組成:
在後面介紹如何操作Redis叢集之前,像故障轉移或者重新分片這樣的事情,我們需要建立一個示例應用,或者至少要了解簡單的Redis叢集客戶端的互動語義。
我們採用執行一個示例,同時嘗試使節點失效,或者開始重新分片這樣的方式,來看看在真實世界條件下Redis叢集如何表現。如果沒有人往叢集寫的話,觀察叢集發生了什麼也沒有什麼實際用處。
這一小節通過兩個例子來解釋redis-rb-cluster的基本用法。第一個例子在redis-rb-cluster發行版本的exemple.rb檔案中,如下:
Ruby程式碼
-
1 require './cluster'
- 2
- 3 startup_nodes = [
- 4 {:host => "127.0.0.1", :port => 7000},
- 5 {:host => "127.0.0.1", :port => 7001}
- 6 ]
- 7 rc = RedisCluster.new(startup_nodes,32,:timeout => 0.1)
- 8
- 9 last = false
- 10
- 11 while not last
- 12 begin
-
13 last = rc.get("__last__"
- 14 last = 0 if !last
- 15 rescue => e
- 16 puts "error #{e.to_s}"
- 17 sleep 1
- 18 end
- 19 end
- 20
- 21 ((last.to_i+1)..1000000000).each{|x|
- 22 begin
- 23 rc.set("foo#{x}",x)
- 24 puts rc.get("foo#{x}")
-
25 rc.set("__last__",x)
- 26 rescue => e
- 27 puts "error #{e.to_s}"
- 28 end
- 29 sleep 0.1
- 30 }
這個程式做了一件很簡單的事情,一個一個地設定形式為foo<number>的鍵的值為一個數字。所以如果你執行這個程式,結果就是下面的命令流:
Java程式碼
- SET foo0 0
- SET foo1 1
- SET foo2 2
- And so forth...
這個程式看起來要比通常看起來更復雜,因為這個是設計用來在螢幕上展示錯誤,而不是由於異常退出,所以每一個對叢集執行的操作都被begin rescue程式碼塊包圍起來。
第7行是程式中第一個有意思的地方。建立了Redis叢集物件,使用啟動節點(startup nodes)的列表,物件允許的最大連線數,以及指定操作被認為失效的超時時間作為引數。
啟動節點不需要是全部的叢集節點。重要的是至少有一個節點可達。也要注意,redis-rb-cluster一旦連線上了第一個節點就會更新啟動節點的列表。你可以從任何真實的客戶端中看到這樣的行為。
現在,我們將Redis叢集物件例項儲存在rc變數中,我們準備像一個正常的Redis物件例項一樣來使用這個物件。
第11至19行說的是:當我們重啟示例的時候,我們不想又從foo0開始,所以我們儲存計數到Redis裡面。上面的程式碼被設計為讀取這個計數值,或者,如果這個計數器不存在,就賦值為0。
但是,注意這裡為什麼是個while迴圈,因為我們想即使叢集下線並返回錯誤也要不斷地重試。一般的程式不必這麼小心謹慎。
第21到30行開始了主迴圈,鍵被設定賦值或者展示錯誤。
注意迴圈最後sleep呼叫。在你的測試中,如果你想盡可能快地往叢集寫入,你可以移除這個sleep(相對來說,這是一個繁忙的迴圈而不是真實的併發,所以在最好的條件下通常可以得到每秒10k次操作)。
正常情況下,寫被放慢了速度,讓人可以更容易地跟蹤程式的輸出。
執行程式產生了如下輸出:
Java程式碼
- ruby ./example.rb
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- ^C (I stopped the program here)
這不是一個很有趣的程式,稍後我們會使用一個更有意思的例子,看看在程式執行時進行重新分片會發生什麼事情。
重新分片叢集(Resharding the cluster)
現在,我們準備嘗試叢集重分片。要做這個請保持example.rb程式在執行中,這樣你可以看到是否對執行中的程式有一些影響。你也可能想註釋掉sleep呼叫,這樣在重分片期間就有一些真實的寫負載。
重分片基本上就是從部分節點移動雜湊槽到另外一部分節點上去,像建立叢集一樣也是通過使用redis-trib工具來完成。
開啟重分片只需要輸入:
Java程式碼
- ./redis-trib.rb reshard 127.0.0.1:7000
你只需要指定單個節點,redis-trib會自動找到其它節點。
當前redis-trib只能在管理員的支援下進行重分片,你不能只是說從這個節點移動5%的雜湊槽到另一個節點(但是這也很容易實現)。那麼問題就隨之而來了。第一個問題就是你想要重分片多少:
你想移動多少雜湊槽(從1到16384)?
我們嘗試重新分片1000個雜湊槽,如果沒有sleep呼叫的那個例子程式還在執行的話,這些槽裡面應該已經包含了不少的鍵了。
然後,redis-trib需要知道重分片的目標了,也就是將接收這些雜湊槽的節點。我將使用第一個主伺服器節點,也就是127.0.0.1:7000,但是我得指定這個例項的節點ID。這已經被redis-trib列印在一個列表中了,但是我總是可以在需要時使用下面的命令找到節點的ID:
Java程式碼
- $ redis-cli -p 7000 cluster nodes | grep myself
- 97a3a64667477371c4479320d683e4c8db5858b1 :0 myself,master - 0 0 0 connected 0-5460
好了,我的目標節點是97a3a64667477371c4479320d683e4c8db5858b1。
現在,你會被詢問想從哪些節點獲取這些鍵。我會輸入all,這樣就會從所有其它的主伺服器節點獲取一些雜湊槽。
在最後的確認後,你會看到每一個被redis-trib準備從一個節點移動到另一個節點的槽的訊息,並且會為每一個被從一側移動到另一側的真實的鍵列印一個圓點。
在重分片進行的過程中,你應該能夠看到你的示例程式執行沒有受到影響。如果你願意的話,你可以在重分片期間多次停止和重啟它。
在重分片的最後,你可以使用下面的命令來測試一下叢集的健康情況:
Java程式碼
- ./redis-trib.rb check 127.0.0.1:7000
像平時一樣,所有的槽都會被覆蓋到,但是這次在127.0.0.1:7000的主伺服器會擁有更多的雜湊槽,大約6461個左右。
一個更有意思的示例程式
到目前為止一切挺好,但是我們使用的示例程式卻不夠好。不顧後果地(acritically)往叢集裡面寫,而不檢查寫入的東西是否是正確的。
從我們的觀點看,接收寫請求的叢集可能一直將每個操作都作為設定鍵foo值為42,我們卻根本沒有察覺到。
所以在redis-rb-cluster倉庫中,有一個叫做consistency-test.rb的更有趣的程式。這個程式有意思得多,因為它使用一組計數器,預設1000個,傳送INCR命令來增加這些計數器。
但是,除了寫入,程式還做另外兩件事情:
- 當計數器使用INCR被更新後,程式記住了寫操作。
- 在每次寫之前讀取一個隨機計數器,檢查這個值是否是期待的值,與其在記憶體中的值比較。
這個的意思就是,這個程式就是一個一致性檢查器,可以告訴你叢集是否丟失了一些寫操作,或者是否接受了一個我們沒有收到確認(acknowledgement)的寫操作。在第一種情況下,我們會看到計數器的值小於我們記錄的值,而在第二種情況下,這個值會大於。
執行consistency-test程式每秒鐘產生一行輸出:
Java程式碼
- $ 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) |
每一行展示了執行的讀操作和寫操作的次數,以及錯誤數(錯誤導致的未被接受的查詢是因為系統不可用)。
如果發現了不一致性,輸出將增加一些新行。例如,當我在程式執行期間手工重置計數器,就會發生:
Java程式碼
- $ redis 127.0.0.1:7000> set key_217 0
- OK
- (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 |
當我把計數器設定為0時,真實值是144,所以程式報告了144個寫操作丟失(叢集沒有記住的INCR命令執行的次數)。
這個程式作為測試用例很有意思,所以我們會使用它來測試Redis叢集的故障轉移。
測試故障轉移(Testing the failover)
注意:在測試期間,你應該開啟一個標籤視窗,一致性檢查的程式在其中執行。
為了觸發故障轉移,我們可以做的最簡單的事情(這也是能發生在分散式系統中語義上最簡單的失敗)就是讓一個程序崩潰,在我們的例子中就是一個主伺服器。
我們可以使用下面的命令來識別一個叢集並讓其崩潰:
Java程式碼
- $ 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都是主伺服器。我們使用DEBUG SEGFAULT命令來使節點7002崩潰:
Java程式碼
- $ redis-cli -p 7002 debug segfault
- Error: Server closed the connection
現在,我們可以看看一致性測試的輸出報告了些什麼內容。
Java程式碼
- 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) |
你可以看到,在故障轉移期間,系統不能接受578個讀請求和577個寫請求,但是資料庫中沒有產生不一致性。這聽起來好像和我們在這篇教程的第一部分中陳述的不一樣,我們說道,Redis叢集在故障轉移期間會丟失寫操作,因為它使用非同步複製。但是我們沒有說過的是,這並不是經常發生,因為Redis傳送回覆給客戶端,和傳送複製命令給從伺服器差不多是同時,所以只有一個很小的丟失資料視窗。但是,很難觸發並不意味著不可能發生,所以這並沒有改變Redis叢集提供的一致性保證(即非強一致性,譯者注)。
我們現在可以看看故障轉移後的叢集佈局(注意,與此同時,我重啟了崩潰的例項,所以它以從伺服器的身份重新加入了叢集):
Java程式碼
- $ 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 connect
現在,主伺服器執行在7000,7001和7005埠。之前執行在7002埠的主伺服器現在是7005的從伺服器了。
CLUSTER NODES命令的輸出看起來挺可怕的,但是實際上相當的簡單,由以下部分組成:
- 節點ID
- ip:port
- flags: master, slave, myself, fail, ...
- 如果是從伺服器的話,就是其主伺服器的節點ID
- 最近一次傳送PING後等待回覆的時間
- 最近一次傳送PONG的時間
- 節點的配置紀元(請看叢集規範).
- 節點的連線狀態
- 服務的雜湊槽