鍍金池/ 教程/ 大數(shù)據(jù)/ 集群(中)
使用 Redis 實現(xiàn) Twitter(上)
集群(下)
使用 Redis 實現(xiàn) Twitter(下)
使用 Redis 作為 LRU 緩存
高可用(上)
高可用客戶端指引
集群(中)
高可用(下)
持久化
Redis 介紹
集中插入
集群(上)
從入門到精通(上)
從入門到精通(下)
從入門到精通(中)
分片
數(shù)據(jù)類型初探
復(fù)制

集群(中)

使用 redis-rb-cluster 寫一個示例應(yīng)用

在后面介紹如何操作 Redis 集群之前,像故障轉(zhuǎn)移或者重新分片這樣的事情,我們需要創(chuàng)建一個示例應(yīng)用,或者至少要了解簡單的 Redis 集群客戶端的交互語義。

我們采用運行一個示例,同時嘗試使節(jié)點失效,或者開始重新分片這樣的方式,來看看在真實世界條件下 Redis 集群如何表現(xiàn)。如果沒有人往集群寫的話,觀察集群發(fā)生了什么也沒有什么實際用處。

這一小節(jié)通過兩個例子來解釋 redis-rb-cluster 的基本用法。第一個例子在 redis-rb-cluster 發(fā)行版本的 exemple.rb 文件中,如下:

     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)
              puts rc.get("foo#{x}")
              rc.set("__last__",x)
          rescue => e
              puts "error #{e.to_s}"
          end
          sleep 0.1
      }

這個程序做了一件很簡單的事情,一個一個地設(shè)置形式為 foo<number> 的鍵的值為一個數(shù)字。所以如果你運行這個程序,結(jié)果就是下面的命令流:

SET foo0 0  
SET foo1 1  
SET foo2 2  
And so forth...  

這個程序看起來要比通??雌饋砀鼜?fù)雜,因為這個是設(shè)計用來在屏幕上展示錯誤,而不是由于異常退出,所以每一個對集群執(zhí)行的操作都被 begin rescue 代碼塊包圍起來。

第 7 行是程序中第一個有意思的地方。創(chuàng)建了 Redis 集群對象,使用啟動節(jié)點(startup nodes)的列表,對象允許的最大連接數(shù),以及指定操作被認(rèn)為失效的超時時間作為參數(shù)。 啟動節(jié)點不需要是全部的集群節(jié)點。重要的是至少有一個節(jié)點可達(dá)。也要注意,redis-rb-cluster 一旦連接上了第一個節(jié)點就會更新啟動節(jié)點的列表。你可以從任何真實的客戶端中看到這樣的行為。

現(xiàn)在,我們將 Redis 集群對象實例保存在 rc 變量中,我們準(zhǔn)備像一個正常的 Redis 對象實例一樣來使用這個對象。

第 11 至 19 行說的是:當(dāng)我們重啟示例的時候,我們不想又從 foo0 開始,所以我們保存計數(shù)到 Redis 里面。上面的代碼被設(shè)計為讀取這個計數(shù)值,或者,如果這個計數(shù)器不存在,就賦值為 0。

但是,注意這里為什么是個 while 循環(huán),因為我們想即使集群下線并返回錯誤也要不斷地重試。一般的程序不必這么小心謹(jǐn)慎。

第 21 到 30 行開始了主循環(huán),鍵被設(shè)置賦值或者展示錯誤。

注意循環(huán)最后 sleep 調(diào)用。在你的測試中,如果你想盡可能快地往集群寫入,你可以移除這個 sleep(相對來說,這是一個繁忙的循環(huán)而不是真實的并發(fā),所以在最好的條件下通??梢缘玫矫棵?10k 次操作)。

正常情況下,寫被放慢了速度,讓人可以更容易地跟蹤程序的輸出。

運行程序產(chǎn)生了如下輸出:

ruby ./example.rb
1
2
3
4
5
6
7
8
9
^C (I stopped the program here)  

這不是一個很有趣的程序,稍后我們會使用一個更有意思的例子,看看在程序運行時進(jìn)行重新分片會發(fā)生什么事情。

重新分片集群(Resharding the cluster)

現(xiàn)在,我們準(zhǔn)備嘗試集群重分片。要做這個請保持 example.rb 程序在運行中,這樣你可以看到是否對運行中的程序有一些影響。你也可能想注釋掉 sleep 調(diào)用,這樣在重分片期間就有一些真實的寫負(fù)載。

重分片基本上就是從部分節(jié)點移動哈希槽到另外一部分節(jié)點上去,像創(chuàng)建集群一樣也是通過使用 redis-trib 工具來完成。

開啟重分片只需要輸入:

./redis-trib.rb reshard 127.0.0.1:7000  

你只需要指定單個節(jié)點,redis-trib 會自動找到其它節(jié)點。

當(dāng)前 redis-trib 只能在管理員的支持下進(jìn)行重分片,你不能只是說從這個節(jié)點移動 5%的哈希槽到另一個節(jié)點(但是這也很容易實現(xiàn))。那么問題就隨之而來了。第一個問題就是你想要重分片多少:

你想移動多少哈希槽(從 1 到 16384)?

我們嘗試重新分片 1000 個哈希槽,如果沒有 sleep 調(diào)用的那個例子程序還在運行的話,這些槽里面應(yīng)該已經(jīng)包含了不少的鍵了。

然后,redis-trib 需要知道重分片的目標(biāo)了,也就是將接收這些哈希槽的節(jié)點。我將使用第一個主服務(wù)器節(jié)點,也就是 127.0.0.1:7000,但是我得指定這個實例的節(jié)點 ID。這已經(jīng)被 redis-trib 打印在一個列表中了,但是我總是可以在需要時使用下面的命令找到節(jié)點的 ID:

$ redis-cli -p 7000 cluster nodes | grep myself  
97a3a64667477371c4479320d683e4c8db5858b1 :0 myself,master - 0 0 0 connected 0-5460  

好了,我的目標(biāo)節(jié)點是 97a3a64667477371c4479320d683e4c8db5858b1。

現(xiàn)在,你會被詢問想從哪些節(jié)點獲取這些鍵。我會輸入 all,這樣就會從所有其它的主服務(wù)器節(jié)點獲取一些哈希槽。

在最后的確認(rèn)后,你會看到每一個被 redis-trib 準(zhǔn)備從一個節(jié)點移動到另一個節(jié)點的槽的消息,并且會為每一個被從一側(cè)移動到另一側(cè)的真實的鍵打印一個圓點。

在重分片進(jìn)行的過程中,你應(yīng)該能夠看到你的示例程序運行沒有受到影響。如果你愿意的話,你可以在重分片期間多次停止和重啟它。

在重分片的最后,你可以使用下面的命令來測試一下集群的健康情況:

./redis-trib.rb check 127.0.0.1:7000  

像平時一樣,所有的槽都會被覆蓋到,但是這次在 127.0.0.1:7000 的主服務(wù)器會擁有更多的哈希槽,大約 6461 個左右。

一個更有意思的示例程序

到目前為止一切挺好,但是我們使用的示例程序卻不夠好。不顧后果地(acritically)往集群里面寫,而不檢查寫入的東西是否是正確的。

從我們的觀點看,接收寫請求的集群可能一直將每個操作都作為設(shè)置鍵 foo 值為 42,我們卻根本沒有察覺到。

所以在 redis-rb-cluster 倉庫中,有一個叫做 consistency-test.rb 的更有趣的程序。這個程序有意思得多,因為它使用一組計數(shù)器,默認(rèn) 1000 個,發(fā)送 INCR 命令來增加這些計數(shù)器。

但是,除了寫入,程序還做另外兩件事情:

  • 當(dāng)計數(shù)器使用 INCR 被更新后,程序記住了寫操作。
  • 在每次寫之前讀取一個隨機計數(shù)器,檢查這個值是否是期待的值,與其在內(nèi)存中的值比較。

這個的意思就是,這個程序就是一個一致性檢查器,可以告訴你集群是否丟失了一些寫操作,或者是否接受了一個我們沒有收到確認(rèn)(acknowledgement)的寫操作。在第一種情況下,我們會看到計數(shù)器的值小于我們記錄的值,而在第二種情況下,這個值會大于。

運行 consistency-test 程序每秒鐘產(chǎn)生一行輸出:

$ 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) |

每一行展示了執(zhí)行的讀操作和寫操作的次數(shù),以及錯誤數(shù)(錯誤導(dǎo)致的未被接受的查詢是因為系統(tǒng)不可用)。

如果發(fā)現(xiàn)了不一致性,輸出將增加一些新行。例如,當(dāng)我在程序運行期間手工重置計數(shù)器,就會發(fā)生:

$ 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 |

當(dāng)我把計數(shù)器設(shè)置為 0 時,真實值是 144,所以程序報告了 144 個寫操作丟失(集群沒有記住的 INCR 命令執(zhí)行的次數(shù))。

這個程序作為測試用例很有意思,所以我們會使用它來測試 Redis 集群的故障轉(zhuǎn)移。

測試故障轉(zhuǎn)移(Testing the failover)

注意:在測試期間,你應(yīng)該打開一個標(biāo)簽窗口,一致性檢查的程序在其中運行。

為了觸發(fā)故障轉(zhuǎn)移,我們可以做的最簡單的事情(這也是能發(fā)生在分布式系統(tǒng)中語義上最簡單的失?。┚褪亲屢粋€進(jìn)程崩潰,在我們的例子中就是一個主服務(wù)器。

我們可以使用下面的命令來識別一個集群并讓其崩潰:

$ 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 都是主服務(wù)器。我們使用 DEBUG SEGFAULT 命令來使節(jié)點 7002 崩潰:

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

現(xiàn)在,我們可以看看一致性測試的輸出報告了些什么內(nèi)容。

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) | 

你可以看到,在故障轉(zhuǎn)移期間,系統(tǒng)不能接受 578 個讀請求和 577 個寫請求,但是數(shù)據(jù)庫中沒有產(chǎn)生不一致性。這聽起來好像和我們在這篇教程的第一部分中陳述的不一樣,我們說道,Redis 集群在故障轉(zhuǎn)移期間會丟失寫操作,因為它使用異步復(fù)制。但是我們沒有說過的是,這并不是經(jīng)常發(fā)生,因為 Redis 發(fā)送回復(fù)給客戶端,和發(fā)送復(fù)制命令給從服務(wù)器差不多是同時,所以只有一個很小的丟失數(shù)據(jù)窗口。但是,很難觸發(fā)并不意味著不可能發(fā)生,所以這并沒有改變 Redis 集群提供的一致性保證(即非強一致性,譯者注)。

我們現(xiàn)在可以看看故障轉(zhuǎn)移后的集群布局(注意,與此同時,我重啟了崩潰的實例,所以它以從服務(wù)器的身份重新加入了集群):

$ 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  

現(xiàn)在,主服務(wù)器運行在 7000,7001 和 7005 端口。之前運行在 7002 端口的主服務(wù)器現(xiàn)在是 7005 的從服務(wù)器了。

CLUSTER NODES 命令的輸出看起來挺可怕的,但是實際上相當(dāng)?shù)暮唵危梢韵虏糠纸M成:

  • 節(jié)點 ID
  • ip:port
  • flags: master, slave, myself, fail, ...
  • 如果是從服務(wù)器的話,就是其主服務(wù)器的節(jié)點 ID
  • 最近一次發(fā)送 PING 后等待回復(fù)的時間
  • 最近一次發(fā)送 PONG 的時間
  • 節(jié)點的配置紀(jì)元(請看集群規(guī)范)
  • 節(jié)點的連接狀態(tài)
  • 服務(wù)的哈希槽