1. 程式人生 > >Redis 3.0中文官方文件翻譯計劃(17) ——叢集(中)

Redis 3.0中文官方文件翻譯計劃(17) ——叢集(中)

使用redis-rb-cluster寫一個示例應用
    在後面介紹如何操作Redis叢集之前,像故障轉移或者重新分片這樣的事情,我們需要建立一個示例應用,或者至少要了解簡單的Redis叢集客戶端的互動語義。 
    我們採用執行一個示例,同時嘗試使節點失效,或者開始重新分片這樣的方式,來看看在真實世界條件下Redis叢集如何表現。如果沒有人往叢集寫的話,觀察叢集發生了什麼也沒有什麼實際用處。 
    這一小節通過兩個例子來解釋redis-rb-cluster的基本用法。第一個例子在redis-rb-cluster發行版本的exemple.rb檔案中,如下: 
Ruby程式碼  收藏程式碼
  1.  1  require './cluster'
      
  2.  2  
  3.  3  startup_nodes = [  
  4.  4      {:host => "127.0.0.1":port => 7000},  
  5.  5      {:host => "127.0.0.1":port => 7001}  
  6.  6  ]  
  7.  7  rc = RedisCluster.new(startup_nodes,32,:timeout => 0.1)  
  8.  8  
  9.  9  last = false  
  10. 10  
  11. 11  while not last  
  12. 12      begin  
  13. 13          last = rc.get("__last__"
    )  
  14. 14          last = 0 if !last  
  15. 15      rescue => e  
  16. 16          puts "error #{e.to_s}"  
  17. 17          sleep 1  
  18. 18      end  
  19. 19  end  
  20. 20  
  21. 21  ((last.to_i+1)..1000000000).each{|x|  
  22. 22      begin  
  23. 23          rc.set("foo#{x}",x)  
  24. 24          puts rc.get("foo#{x}")  
  25. 25          rc.set("__last__",x)  
  26. 26      rescue => e  
  27. 27          puts "error #{e.to_s}"  
  28. 28      end  
  29. 29      sleep 0.1  
  30. 30  }  

    這個程式做了一件很簡單的事情,一個一個地設定形式為foo<number>的鍵的值為一個數字。所以如果你執行這個程式,結果就是下面的命令流: 
Java程式碼  收藏程式碼
  1. SET foo0 0  
  2. SET foo1 1  
  3. SET foo2 2  
  4. 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程式碼  收藏程式碼
  1. ruby ./example.rb  
  2. 1  
  3. 2  
  4. 3  
  5. 4  
  6. 5  
  7. 6  
  8. 7  
  9. 8  
  10. 9  
  11. ^C (I stopped the program here)  

    這不是一個很有趣的程式,稍後我們會使用一個更有意思的例子,看看在程式執行時進行重新分片會發生什麼事情。 

重新分片叢集(Resharding the cluster)
    現在,我們準備嘗試叢集重分片。要做這個請保持example.rb程式在執行中,這樣你可以看到是否對執行中的程式有一些影響。你也可能想註釋掉sleep呼叫,這樣在重分片期間就有一些真實的寫負載。 
    重分片基本上就是從部分節點移動雜湊槽到另外一部分節點上去,像建立叢集一樣也是通過使用redis-trib工具來完成。 
    開啟重分片只需要輸入: 
Java程式碼  收藏程式碼
  1. ./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程式碼  收藏程式碼
  1. $ redis-cli -p 7000 cluster nodes | grep myself  
  2. 97a3a64667477371c4479320d683e4c8db5858b1 :0 myself,master - 0 0 0 connected 0-5460  

    好了,我的目標節點是97a3a64667477371c4479320d683e4c8db5858b1。 
    現在,你會被詢問想從哪些節點獲取這些鍵。我會輸入all,這樣就會從所有其它的主伺服器節點獲取一些雜湊槽。 
    在最後的確認後,你會看到每一個被redis-trib準備從一個節點移動到另一個節點的槽的訊息,並且會為每一個被從一側移動到另一側的真實的鍵列印一個圓點。 
    在重分片進行的過程中,你應該能夠看到你的示例程式執行沒有受到影響。如果你願意的話,你可以在重分片期間多次停止和重啟它。 
    在重分片的最後,你可以使用下面的命令來測試一下叢集的健康情況: 
Java程式碼  收藏程式碼
  1. ./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程式碼  收藏程式碼
  1. $ ruby consistency-test.rb  
  2. 925 R (0 err) | 925 W (0 err) |  
  3. 5030 R (0 err) | 5030 W (0 err) |  
  4. 9261 R (0 err) | 9261 W (0 err) |  
  5. 13517 R (0 err) | 13517 W (0 err) |  
  6. 17780 R (0 err) | 17780 W (0 err) |  
  7. 22025 R (0 err) | 22025 W (0 err) |  
  8. 25818 R (0 err) | 25818 W (0 err) |  

    每一行展示了執行的讀操作和寫操作的次數,以及錯誤數(錯誤導致的未被接受的查詢是因為系統不可用)。 
    如果發現了不一致性,輸出將增加一些新行。例如,當我在程式執行期間手工重置計數器,就會發生: 
Java程式碼  收藏程式碼
  1. $ redis 127.0.0.1:7000> set key_217 0  
  2. OK  
  3. (in the other tab I see...)  
  4. 94774 R (0 err) | 94774 W (0 err) |  
  5. 98821 R (0 err) | 98821 W (0 err) |  
  6. 102886 R (0 err) | 102886 W (0 err) | 114 lost |  
  7. 107046 R (0 err) | 107046 W (0 err) | 114 lost |  

    當我把計數器設定為0時,真實值是144,所以程式報告了144個寫操作丟失(叢集沒有記住的INCR命令執行的次數)。 
    這個程式作為測試用例很有意思,所以我們會使用它來測試Redis叢集的故障轉移。 

測試故障轉移(Testing the failover)
    注意:在測試期間,你應該開啟一個標籤視窗,一致性檢查的程式在其中執行。 
    為了觸發故障轉移,我們可以做的最簡單的事情(這也是能發生在分散式系統中語義上最簡單的失敗)就是讓一個程序崩潰,在我們的例子中就是一個主伺服器。 
    我們可以使用下面的命令來識別一個叢集並讓其崩潰: 
Java程式碼  收藏程式碼
  1. $ redis-cli -p 7000 cluster nodes | grep master  
  2. 3e3a6cb0d9a9a87168e266b0a0b24026c0aae3f0 127.0.0.1:7001 master - 0 1385482984082 0 connected 5960-10921  
  3. 2938205e12de373867bf38f1ca29d31d0ddb3e46 127.0.0.1:7002 master - 0 1385482983582 0 connected 11423-16383  
  4. 97a3a64667477371c4479320d683e4c8db5858b1 :0 myself,master - 0 0 0 connected 0-5959 10922-11422  

    好了,7000,7001,7002都是主伺服器。我們使用DEBUG SEGFAULT命令來使節點7002崩潰: 
Java程式碼  收藏程式碼
  1. $ redis-cli -p 7002 debug segfault  
  2. Error: Server closed the connection  

    現在,我們可以看看一致性測試的輸出報告了些什麼內容。 
Java程式碼  收藏程式碼
  1. 18849 R (0 err) | 18849 W (0 err) |  
  2. 23151 R (0 err) | 23151 W (0 err) |  
  3. 27302 R (0 err) | 27302 W (0 err) |  
  4. ... many error warnings here ...  
  5. 29659 R (578 err) | 29660 W (577 err) |  
  6. 33749 R (578 err) | 33750 W (577 err) |  
  7. 37918 R (578 err) | 37919 W (577 err) |  
  8. 42077 R (578 err) | 42078 W (577 err) |  

    你可以看到,在故障轉移期間,系統不能接受578個讀請求和577個寫請求,但是資料庫中沒有產生不一致性。這聽起來好像和我們在這篇教程的第一部分中陳述的不一樣,我們說道,Redis叢集在故障轉移期間會丟失寫操作,因為它使用非同步複製。但是我們沒有說過的是,這並不是經常發生,因為Redis傳送回覆給客戶端,和傳送複製命令給從伺服器差不多是同時,所以只有一個很小的丟失資料視窗。但是,很難觸發並不意味著不可能發生,所以這並沒有改變Redis叢集提供的一致性保證(即非強一致性,譯者注)。 
    我們現在可以看看故障轉移後的叢集佈局(注意,與此同時,我重啟了崩潰的例項,所以它以從伺服器的身份重新加入了叢集): 
Java程式碼  收藏程式碼
  1. $ redis-cli -p 7000 cluster nodes  
  2. 3fc783611028b1707fd65345e763befb36454d73 127.0.0.1:7004 slave 3e3a6cb0d9a9a87168e266b0a0b24026c0aae3f0 0 1385503418521 0 connected  
  3. a211e242fc6b22a9427fed61285e85892fa04e08 127.0.0.1:7003 slave 97a3a64667477371c4479320d683e4c8db5858b1 0 1385503419023 0 connected  
  4. 97a3a64667477371c4479320d683e4c8db5858b1 :0 myself,master - 0 0 0 connected 0-5959 10922-11422  
  5. 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 127.0.0.1:7005 master - 0 1385503419023 3 connected 11423-16383  
  6. 3e3a6cb0d9a9a87168e266b0a0b24026c0aae3f0 127.0.0.1:7001 master - 0 1385503417005 0 connected 5960-10921  
  7. 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的時間
  • 節點的配置紀元(請看叢集規範).
  • 節點的連線狀態
  • 服務的雜湊槽