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

集群(中)

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

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

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

這一小節(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ù)字。所以如果你運(yùn)行這個程序,結(jié)果就是下面的命令流:

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

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

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

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

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

但是,注意這里為什么是個 while 循環(huán),因?yàn)槲覀兿爰词辜合戮€并返回錯誤也要不斷地重試。一般的程序不必這么小心謹(jǐn)慎。

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

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

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

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

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

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

重新分片集群(Resharding the cluster)

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

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

開啟重分片只需要輸入:

./redis-trib.rb reshard 127.0.0.1:7000  

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

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

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

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

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

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

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

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

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

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

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

./redis-trib.rb check 127.0.0.1:7000  

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

一個更有意思的示例程序

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

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

所以在 redis-rb-cluster 倉庫中,有一個叫做 consistency-test.rb 的更有趣的程序。這個程序有意思得多,因?yàn)樗褂靡唤M計(jì)數(shù)器,默認(rèn) 1000 個,發(fā)送 INCR 命令來增加這些計(jì)數(shù)器。

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

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

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

運(yùn)行 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)致的未被接受的查詢是因?yàn)橄到y(tǒng)不可用)。

如果發(fā)現(xiàn)了不一致性,輸出將增加一些新行。例如,當(dāng)我在程序運(yùn)行期間手工重置計(jì)數(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)我把計(jì)數(shù)器設(shè)置為 0 時,真實(shí)值是 144,所以程序報(bào)告了 144 個寫操作丟失(集群沒有記住的 INCR 命令執(zhí)行的次數(shù))。

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

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

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

為了觸發(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é)點(diǎn) 7002 崩潰:

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

現(xiàn)在,我們可以看看一致性測試的輸出報(bào)告了些什么內(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)移期間會丟失寫操作,因?yàn)樗褂卯惒綇?fù)制。但是我們沒有說過的是,這并不是經(jīng)常發(fā)生,因?yàn)?Redis 發(fā)送回復(fù)給客戶端,和發(fā)送復(fù)制命令給從服務(wù)器差不多是同時,所以只有一個很小的丟失數(shù)據(jù)窗口。但是,很難觸發(fā)并不意味著不可能發(fā)生,所以這并沒有改變 Redis 集群提供的一致性保證(即非強(qiáng)一致性,譯者注)。

我們現(xiàn)在可以看看故障轉(zhuǎn)移后的集群布局(注意,與此同時,我重啟了崩潰的實(shí)例,所以它以從服務(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ù)器運(yùn)行在 7000,7001 和 7005 端口。之前運(yùn)行在 7002 端口的主服務(wù)器現(xiàn)在是 7005 的從服務(wù)器了。

CLUSTER NODES 命令的輸出看起來挺可怕的,但是實(shí)際上相當(dāng)?shù)暮唵?,由以下部分組成:

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