《Redis官方文件》Redis叢集教程
這篇教程是Redis叢集的簡要介紹,而非講解分散式系統的複雜概念。它主要從一個使用者的角度介紹如何搭建、測試和使用Redis叢集,至於Redis叢集的詳細設計將在“Redis叢集規範”中進行描述。
本教程以redis使用者的角度,用簡單易懂的方式介紹Redis叢集的可用性和一致性。
注意: 本教程要求redis3.0或以上的版本。
如果你打算部署redis叢集,你可以讀一些關於叢集的詳細設計,當然,這不是必須的。由這篇教程入門,先大概使用一下Redis的叢集,然後再讀Redis叢集的詳細設計,也是不錯的選擇。
Redis叢集 101
redis叢集在啟動的時候就自動在多個節點間分好片。同時提供了分片之間的可用性:當一部分redis節點故障或網路中斷,叢集也能繼續工作。但是,當大面積的節點故障或網路中斷(比如大部分的主節點都不可用了),叢集就不能使用。
所以,從實用性的角度,Redis叢集提供以下功能:
- 自動把資料切分到多個redis節點中
- 當一部分節點掛了或不可達,叢集依然能繼續工作
Redis叢集的TCP埠
redis叢集中的每個節點都需要建立2個tcp連線,監聽這2個埠:一個埠稱之為“客戶端埠”,用於接受客戶端指令,與客戶端互動,比如6379;另一個埠稱之為“叢集匯流排埠”,是在客戶端埠號上加10000,比如16379,用於節點之間通過二進位制協議通訊。各節點通過叢集匯流排檢測宕機節點、更新配置、故障轉移驗證等。客戶端只能使用客戶端埠,不能使用叢集匯流排埠。請確保你的防火牆允許開啟這兩個埠,否則redis叢集沒法工作。客戶端埠和叢集匯流排埠之間的差值是固定的,叢集匯流排埠比客戶端埠高10000。
注意,關於叢集的2個埠:
- 客戶端埠(一般是6379)需要對所有客戶端和叢集節點開放,因為叢集節點需要通過該埠轉移資料。
- 叢集匯流排埠(一般是16379)只需對叢集中的所有節點開放
這2個埠必須開啟,否則叢集沒法正常工作。
叢集節點之間通過叢集匯流排埠互動資料,使用的協議不同於客戶端的協議,是二進位制協議,這可以減少頻寬和處理時間。
Redis叢集資料的分片
Redis叢集不是使用一致性雜湊,而是使用雜湊槽。整個redis叢集有16384個雜湊槽,決定一個key應該分配到那個槽的演算法是:計算該key的CRC16結果再模16834。
叢集中的每個節點負責一部分雜湊槽,比如叢集中有3個節點,則:
- 節點A儲存的雜湊槽範圍是:0 – 5500
- 節點B儲存的雜湊槽範圍是:5501 – 11000
- 節點C儲存的雜湊槽範圍是:11001 – 16384
這樣的分佈方式方便節點的新增和刪除。比如,需要新增一個節點D,只需要把A、B、C中的部分雜湊槽資料移到D節點。同樣,如果希望在叢集中刪除A節點,只需要把A節點的雜湊槽的資料移到B和C節點,當A節點的資料全部被移走後,A節點就可以完全從叢集中刪除。
因為把雜湊槽從一個節點移到另一個節點是不需要停機的,所以,增加或刪除節點,或更改節點上的雜湊槽,也是不需要停機的。
如果多個key都屬於一個雜湊槽,叢集支援通過一個命令(或事務, 或lua指令碼)同時操作這些key。通過“雜湊標籤”的概念,使用者可以讓多個key分配到同一個雜湊槽。雜湊標籤在叢集詳細文件中有描述,這裡做個簡單介紹:如果key含有大括號”{}”,則只有大括號中的字串會參與雜湊,比如”this{foo}”和”another{foo}”這2個key會分配到同一個雜湊槽,所以可以在一個命令中同時操作他們。
Redis叢集的主從模式
為了保證在部分節點故障或網路不通時叢集依然能正常工作,叢集使用了主從模型,每個雜湊槽有一(主節點)到N個副本(N-1個從節點)。在我們剛才的叢集例子中,有A,B,C三個節點,如果B節點故障叢集就不能正常工作了,因為B節點中的雜湊槽資料沒法操作。但是,如果我們給每一個節點都增加一個從節點,就變成了:A,B,C三個節點是主節點,A1, B1, C1 分別是他們的從節點,當B節點宕機時,我們的叢集也能正常運作。B1節點是B節點的副本,如果B節點故障,叢集會提升B1為主節點,從而讓叢集繼續正常工作。但是,如果B和B1同時故障,叢集就不能繼續工作了。
Redis叢集的一致性保證
Redis叢集不能保證強一致性。一些已經向客戶端確認寫成功的操作,會在某些不確定的情況下丟失。
產生寫操作丟失的第一個原因,是因為主從節點之間使用了非同步的方式來同步資料。
一個寫操作是這樣一個流程:
- 1)客戶端向主節點B發起寫的操作
- 2)主節點B迴應客戶端寫操作成功
- 3)主節點B向它的從節點B1,B2,B3同步該寫操作
從上面的流程可以看出來,主節點B並沒有等從節點B1,B2,B3寫完之後再回復客戶端這次操作的結果。所以,如果主節點B在通知客戶端寫操作成功之後,但同步給從節點之前,主節點B故障了,其中一個沒有收到該寫操作的從節點會晉升成主節點,該寫操作就這樣永遠丟失了。
就像傳統的資料庫,在不涉及到分散式的情況下,它每秒寫回磁碟。為了提高一致性,可以在寫盤完成之後再回復客戶端,但這樣就要損失效能。這種方式就等於Redis叢集使用同步複製的方式。
基本上,在效能和一致性之間,需要一個權衡。
如果真的需要,Redis叢集支援同步複製的方式,通過WAIT指令來實現,這可以讓丟失寫操作的可能性降到很低。但就算使用了同步複製的方式,Redis叢集依然不是強一致性的,在某些複雜的情況下,比如從節點在與主節點失去連線之後被選為主節點,不一致性還是會發生。
這種不一致性發生的情況是這樣的,當客戶端與少數的節點(至少含有一個主節點)網路聯通,但他們與其他大多數節點網路不通。比如6個節點,A,B,C是主節點,A1,B1,C1分別是他們的從節點,一個客戶端稱之為Z1。
當網路出問題時,他們被分成2組網路,組內網路聯通,但2組之間的網路不通,假設A,C,A1,B1,C1彼此之間是聯通的,另一邊,B和Z1的網路是聯通的。Z1可以繼續往B發起寫操作,B也接受Z1的寫操作。當網路恢復時,如果這個時間間隔足夠短,叢集仍然能繼續正常工作。如果時間比較長,以致B1在大多數的這邊被選為主節點,那剛才Z1發給B的寫操作都將丟失。
注意,Z1給B傳送寫操作是有一個限制的,如果時間長度達到了大多數節點那邊可以選出一個新的主節點時,少數這邊的所有主節點都不接受寫操作。
這個時間的配置,稱之為節點超時(node timeout),對叢集來說非常重要,當達到了這個節點超時的時間之後,主節點被認為已經宕機,可以用它的一個從節點來代替。同樣,在節點超時時,如果主節點依然不能聯絡到其他主節點,它將進入錯誤狀態,不再接受寫操作。
Redis叢集引數配置
我們後面會部署一個Redis叢集作為例子,在那之前,先介紹一下叢集在redis.conf中的引數。
- cluster-enabled
<yes/no>
: 如果配置”yes”則開啟叢集功能,此redis例項作為叢集的一個節點,否則,它是一個普通的單一的redis例項。 - cluster-config-file
<filename>
: 注意:雖然此配置的名字叫“叢集配置檔案”,但是此配置檔案不能人工編輯,它是叢集節點自動維護的檔案,主要用於記錄叢集中有哪些節點、他們的狀態以及一些持久化引數等,方便在重啟時恢復這些狀態。通常是在收到請求之後這個檔案就會被更新。 - cluster-node-timeout
<milliseconds>
: 這是叢集中的節點能夠失聯的最大時間,超過這個時間,該節點就會被認為故障。如果主節點超過這個時間還是不可達,則用它的從節點將啟動故障遷移,升級成主節點。注意,任何一個節點在這個時間之內如果還是沒有連上大部分的主節點,則此節點將停止接收任何請求。 - cluster-slave-validity-factor
<factor>
: 如果設定成0,則無論從節點與主節點失聯多久,從節點都會嘗試升級成主節點。如果設定成正數,則cluster-node-timeout乘以cluster-slave-validity-factor得到的時間,是從節點與主節點失聯後,此從節點資料有效的最長時間,超過這個時間,從節點不會啟動故障遷移。假設cluster-node-timeout=5,cluster-slave-validity-factor=10,則如果從節點跟主節點失聯超過50秒,此從節點不能成為主節點。注意,如果此引數配置為非0,將可能出現由於某主節點失聯卻沒有從節點能頂上的情況,從而導致叢集不能正常工作,在這種情況下,只有等到原來的主節點重新迴歸到叢集,叢集才恢復運作。 - cluster-migration-barrier
<count>
:主節點需要的最小從節點數,只有達到這個數,主節點失敗時,它從節點才會進行遷移。更詳細介紹可以看本教程後面關於副本遷移到部分。 - cluster-require-full-coverage <yes/no>:在部分key所在的節點不可用時,如果此引數設定為”yes”(預設值), 則整個叢集停止接受操作;如果此引數設定為”no”,則叢集依然為可達節點上的key提供讀操作。
建立和使用Redis叢集
注意:手動部署Redis叢集能夠很好的瞭解它是如何運作的,但如果你希望儘快的讓叢集執行起來,可以跳過本節和下一節,直接到”使用create-cluster指令碼建立Redis叢集”章節。
要建立叢集,首先需要以叢集模式執行的空redis例項。也就說,以普通模式啟動的redis是不能作為叢集的節點的,需要以叢集模式啟動的redis例項才能有叢集節點的特性、支援叢集的指令,成為叢集的節點。
下面是最小的redis叢集的配置檔案:
port 7000
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
開啟叢集模式只需開啟cluster-enabled配置項即可。每一個redis例項都包含一個配置檔案,預設是nodes.conf,用於儲存此節點的一些配置資訊。這個配置檔案由redis叢集的節點自行建立和更新,不能由人手動地去修改。
一個最小的叢集需要最少3個主節點。第一次測試,強烈建議你配置6個節點:3個主節點和3個從節點。
開始測試,步驟如下:先進入新的目錄,以redis例項的埠為目錄名,建立目錄,我們將在這些目錄裡執行我們的例項。
類似這樣:
mkdir cluster-test
cd cluster-test
mkdir 7000 7001 7002 7003 7004 7005
在7000-7005的每個目錄中建立配置檔案redis.conf,內容就用上面的最簡配置做模板,注意修改埠號,改為跟目錄一致的埠。
把你的redis伺服器(用GitHub中的不穩定分支的最新的程式碼編譯來)拷貝到cluster-test目錄,然後開啟6個終端頁準備測試。
在每個終端啟動一個redis例項,指令類似這樣:
cd 7000
../redis-server ./redis.conf
在日誌中我們可以看到,由於沒有nodes.conf檔案不存在,每個節點都給自己一個新的ID。
[82462] 26 Nov 11:56:55.329 * No cluster configuration found, I'm 97a3a64667477371c4479320d683e4c8db5858b1
這個ID將一直被此節點使用,作為此節點在整個叢集中的唯一標識。節點區分其他節點也是通過此ID來標識,而非IP或埠。IP可以改,埠可以改,但此ID不能改,直到這個節點離開叢集。這個ID稱之為節點ID(Node ID)。
建立叢集
現在6個例項已經執行起來了,我們需要給節點寫一些有意義的配置來建立叢集。redis叢集的命令工具redis-trib可以讓我們建立叢集變得非常簡單。redis-trib是一個用ruby寫的指令碼,用於給各節點發指令建立叢集、檢查叢集狀態或給叢集重新分片等。redis-trib在Redis原始碼的src目錄下,需要gem redis來執行redis-trib。
gem install redis
建立叢集只需輸入指令:
./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
這裡用的命令是create,因為我們需要建立一個新的叢集。選項”–replicas 1”表示每個主節點需要一個從節點。其他引數就是需要加入這個叢集的redis例項的地址。
我們建立的叢集有3個主節點和3個從節點。
redis-trib會給你一些配置建議,輸入yes表示接受。叢集會被配置並彼此連線好,意思是各節點例項被引導彼此通話並最終形成叢集。最後,如果一切順利,會看到類似下面的資訊:
[OK] All 16384 slots covered
這表示,16384個雜湊槽都被主節點正常服務著。
使用create-cluster指令碼建立redis叢集
如果你不想像上面那樣,單獨的手工配置各節點的方式來建立叢集,還有一個更簡單的系統(當然也沒法瞭解到叢集運作的一些細節)。
在utils/create-cluster目錄下,有一個名為create-cluster的bash指令碼。如果需要啟動一個有3個主節點和3個從節點的叢集,只需要輸入以下指令
1. create-cluster start
2. create-cluster create
在步驟2,當redis-trib要你接受叢集的佈局時,輸入”yes”。
現在你可以跟叢集互動,第一個節點的起始埠預設是30001。當你完成後,停止叢集用如下指令:
1. create-cluster stop.
請檢視目錄下的README,它有詳細的介紹如何使用此指令碼。
試用一下叢集
在這個階段叢集的其中一個問題就是客戶端庫比較少。
我知道的一些客戶端庫有:
- redis-rb-cluster 我用ruby寫的,作為其他語言實現的一個參考。它是原來的redis-rb的簡單封裝,實現了叢集互動的最基礎功能。
- redis-py-cluster redis-rb-cluster匯出的python介面。支援大部分redis-py的功能,它處於活躍開發狀態。
- Predis 在最近的更新中已經支援Redis叢集,並且在活躍開發狀態。
- java最常用的客戶端 Jedis 最近加入對叢集的支援,具體請檢視Jedis README中關於叢集章節。
- StackExchange.Redis 支援C#,對於大部分.NET語言,VB,F#等應該都支援。
- thunk-redid 支援Node.js和io.js。它是支援pipelining和叢集的 thunk/promise-based redis 客戶端
- redis-cli 在不穩定版分支中,對叢集提供了最基礎的支援,使用-c指令開啟。
可以用上面提供的客戶端或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可能監聽在不同的埠,預設是從30001開始。
redis-cli利用叢集的任意節點會告知客戶端正確節點的特性,實現了叢集客戶端的最基礎功能。實現得比較嚴謹的客戶端可以快取雜湊槽到節點的對映關係,讓客戶端直接連線到正確的節點,只有叢集的節點配置有更新時才重新整理快取,比如發生了故障遷移,或者管理員增加或減少了節點等。
用redis-rb-cluster寫一個應用例子
在進一步學習如何操作Redis叢集之前,比如瞭解故障遷移、重新分片等,我們先理解客戶端是如何與叢集互動的。
我們通過一個例子,讓部分節點故障或重新分片等,來了解這實際運作中,redis叢集是如何處理的。如果這期間沒有客戶端對叢集發起寫操作,將不益於我們瞭解情況。
這節通過2個例子來演示redis-rb-cluster的基礎用法,下面是第一個例子,原始碼在redis-rb-cluster目錄下的example.rb檔案中。
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>”的數值為number,等效于于執行下面的指令:
- * SET foo0 0
- * SET foo1 1
- * SET foo2 2
- * 以此類推…
這指令碼看起來比平普通的指令碼複雜,因為在遇到錯誤的時候,它需要把錯誤顯示出來,而不是因為一個異常就停止執行,所以每個操作都用”begin” “rescure”包裹起來。
第7行,建立了一個Redis Cluster物件,使用的3個引數分別是:第一個引數startup_nodes,客戶端需要連線的叢集的部分或全部節點列表;第二個引數是此物件連線叢集內的各節點時允許建立的最大連線數;第三個引數是一個操作多久得不到響應則被判為失敗的超時設定。
第一個引數startup_nodes不要求包含叢集的所有節點,但要求至少有一個節點是正常運作的。注意,redis-rb-cluster在它連上第一個節點後,會自動更新startup_nodes,實現得比較嚴謹的其他客戶端也應該是這樣的。
現在,我們可以像使用普通的Redis物件例項一樣,來使用變數名為rc的Redis叢集物件例項了。
第11到19行:每次我們重新執行此指令碼的時候,我們不希望每次都從”foo0″開始執行,所以我們在redis中儲存了一個計數器,記錄我們執行到哪了。這幾行程式碼就是讀這個計數器,如果這個計數器不存在,就給他個初始值:0。
注意一下while迴圈,我們希望這個指令碼可以一直執行,哪怕在叢集宕機的時候,列印一個error提示然後要繼續執行。普通的應用程式可以不用這樣。
第21到30行:寫key的主迴圈。
注意最後一行的sleep呼叫,如果希望往叢集中寫的速度快點,可以把這行sleep呼叫刪除(在最好的情況下,每秒可以執行1萬個請求)。
為了方便我們跟蹤指令碼的情況,一般情況下我們會讓它執行得慢點。
下面是我執行指令碼的輸出
ruby ./example.rb
1
2
3
4
5
6
7
8
9
^C (我停止了此指令碼)
這個指令碼很無趣(後面我們再寫個有趣點的),但它已經足夠幫助我們瞭解重新分片的情況(需要讓它一直執行著)。
重新分片
現在,我們開始重新分片。在重新分片時,請讓剛才的example.rb指令碼繼續執行,這樣可以看到重新分片對此指令碼的影響,同時,你可以把sleep呼叫註釋掉,以便在重新分片過程中的增加寫入的壓力。
重新分片簡單的說就是把雜湊槽從一些節點移動到另外一些節點。重新分片可以像建立叢集一樣,使用redis-trib來完成。
開始重新分片,輸入以下指令:
./redis-trib.rb reshard 127.0.0.1:7000
你只需要指定叢集中的一個節點,redis-trib會自動找到叢集中的其他節點。
目前,redis-trib只支援管理員操作,不能夠說:移動50%的雜湊槽從這個節點到那個節點。它以問問題的方式開始。第一個問題是,你需要重新分片多少個雜湊槽:
How many slots do you want to move (from 1 to 16384)?
由於我們之前的指令碼一直在執行,而且沒有用sleep呼叫,這時候應該已經插入了比較多的key了。我們可以嘗試給1000個雜湊槽重新分片。
然後,redis-trib需要知道我們要把這1000個雜湊槽移動到哪個節點去,也就是接受這1000個雜湊槽的節點。我想用127.0.0.1:7000這個節點。需要用節點ID來告知redis-trib是哪個節點。redis-trib已經在螢幕上列出了所有的節點和他們的ID。也可以通過以下命令找到指定節點的ID:
$ redis-cli -p 7000 cluster nodes | grep myself
97a3a64667477371c4479320d683e4c8db5858b1 :0 myself,master - 0 0 0 connected 0-5460
好了,我的目標節點是97a3a64667477371c4479320d683e4c8db5858b1。
現在redis-trib會問:你想從哪些節點中挪走這些雜湊槽呢?我輸入all,會從其他的主節點中挪走雜湊槽。
在輸入最後確認之後,redis-trib在螢幕上會輸出每一個雜湊槽將從哪個節點轉移到哪個節點。每實際移動一個key螢幕就會列印一個點。
在重新分片的過程中,你可以看到,你剛才執行的指令碼不受影響,你甚至可以在重新分片的過程中,反覆的重新執行該例子指令碼。
在重新分片結束後,你可以檢查叢集的當前狀態是否正常,執行下面的命令:
./redis-trib.rb check 127.0.0.1:7000
所有的雜湊槽都存在,這時候127.0.0.1:7000的主節點有多一點的雜湊槽,有大概6461個。
指令碼化重新分片
重新分片可以不用以互動的方式進行,使用下面的指令可以自動執行:
./redis-trib.rb reshard --from <node-id> --to <node-id> --slots <number of slots> --yes <host>:<port>
如果你想要經常的重新分片,可以使用上面的指令自動分片,但是目前redis-trib指令碼不會根據節點上的key的分佈來做負載均衡、智慧地遷移雜湊槽。這個特性在將來我們會新增的。
一個更有趣的例子
我們之前寫的例子不太好,因為它只是簡單的向叢集寫資料,卻不檢查寫的資料是不是正確的。假設它一直往叢集中寫的都是把”set foo 42”, 我們也不會發現。
所以在redis-rb-cluster中有一個更有趣的例子,叫consistency-test.rb,它使用一組計數器,通過INCR指令來增加這些計數器。
除了只是INCR的寫之外,它還做了2件其他事情:
*當一個計數器使用INCR指令的時候,該應用程式記錄著它的返回值
*每次寫之前,先隨機地讀一個計數器,比較一下它的結果是否跟快取的結果一致
這個程式是一個簡單的一致性檢查器(consistency checker),如果計數器在redis中的數值小於快取中的數值,則認為丟失了部分寫;如果是大於,則認為多了一些不屬於此應用程式加進去的資料。
執行此測試指令碼,每秒會在螢幕顯示類似以下的資料:
$ 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) |
每行顯示一共執行了多少次讀和寫,以及相關的錯誤(讀錯誤,是因為系統不能正常工作了)。
如果一些不一致性被檢測到,螢幕也會有不一樣的顯示。下面就是一個例子,在該應用程式在執行的過程中,我手動的重置了一個計數器:
$ 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 |
當我把一個本來是114的計數器設定成0的時候,此程式就報告有114個寫丟失了。
這個例子作為一個測試用例非常有趣,下面我們用它來測試叢集的故障遷移。
測試故障遷移
注意:測試過程中,請讓上面的一致性測試的應用程式一直執行中。
為了觸發故障遷移,最簡單的辦法是讓一個程序宕機,在我們的用例中,就是讓其中一個主節點程序宕機。
我們可以用下面的指令區分叢集節點:
$ 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
現在,我們看看剛才那個一致性檢測器的例子輸出了什麼:
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-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
現在,主節點的埠變成了:7000,7001,7005(之前主節點是7002的,現在變成主節點是7005了)。
“cluster nodes”指令的輸出看起來蠻嚇人的,其實很簡單,它的每列意義如下:
*節點ID
*IP:埠
*標記位:主節點,從節點,自己,失敗。。。
*如果是從節點,則接下來是它的主節點的ID
*最後一次傳送ping依然等待響應的時間
*最後一次收到pong的時間
*上次更新此配置的時間
*節點的連線狀態
*儲存的雜湊槽
手動故障轉移
有時候,手動故障轉移是非常有用的,它不會給主節點帶來任何問題,比如,需要升級某個主節點的redis程序,可以先通過手動故障轉移使之成為從節點,讓升級對叢集可用性的影響達到最低。
redis叢集支援通過指令”CLUSTER FAILOVER”產生故障轉移,但需要在被失效的主節點的一個從節點上執行該命令。
相對於真的主節點宕機,手動故障轉移是比較安全的,它可以避免資料丟失,當新的主節點複製完所有資料之後,會讓客戶端從原來的主節點重定向到新的主節點。
下面是在其中一個從節點上執行了cluster failover指令之後看到的一些日誌:
# Manual failover user request accepted.
# Received replication offset for paused master manual failover: 347540
# All master replication stream processed, manual failover can start.
# Start of election delayed for 0 milliseconds (rank #0, offset 347540).
# Starting a failover election for epoch 7545.
# Failover election won: I'm the new master.
簡單的說:客戶端停止連線被故障轉移的原主節點;同時該原主節點把還沒同步的複製集同步給從節點;當從節點收到所有複製集之後,故障轉移開始,原來主節點被通知配置更新,主節點更換了;客戶端被重定向到新的主節點。
新增新的節點
新增一個節點,就增加一個空的節點到叢集。有兩種情況:如果新增的是主節點,則是從叢集的其他節點中轉移部分資料給它;如果新增的是從節點,則告訴它從一個已知的節點中同步複製集。
我們2種情況都試試。首先是新增一個新的主節點到叢集中。
兩種情況,都是需要先加入一個空的節點到叢集中
鑑於我們前面已經啟動了6個節點,埠號7000-7005已經用了,新增節點的埠號就用7006吧。新增一個新的空節點,就跟上面啟動前面6個節點的步驟一樣(記得改配置檔案的埠號):
- *在終端開啟一個新的頁面
- *進入到cluster-test目錄
- *建立名為“7006”的目錄
- *在該目錄下建立redis.conf檔案,內容跟其他節點的內容一致,只是埠號改成7006.
- *最後啟動它:../redis-server ./redis.conf
現在該節點應該執行起來了。
現在,我們使用redis-trib來增加一個新節點到叢集中:
./redis-trib.rb add-node 127.0.0.1:7006 127.0.0.1:7000
使用add-node指令來新增節點,第一個地址是需要新增的節點地址,第二個地址是叢集中任意一個節點地址。
redis-trib指令碼只是給傳送CLUSTER MEET訊息給節點,這也可以手動地通過客戶端傳送,但redis-trib在傳送之前會檢查叢集的狀態,所以,還是用redis-trib指令碼來操作叢集會比較好。
現在我們可以連上新的節點,看看它是不是已經加入叢集了:
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指令碼,增–slave選項,類似這樣:
./redis-trib.rb add-node --slave 127.0.0.1:7006 127.0.0.1:7000
注意到上面的命令列跟我們加主節點的命令列類似,所以沒有沒有指定新增的從節點的主節點是哪個,這時候redis-trib會在擁有最少從節點的主節點中隨機選一個作為新增節點的主節點。
當然也可以通過如下的命令指定新增從節點的主節點:
./redis-trib.rb add-node --slave --master-id 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 127.0.0.1:7006 127.0.0.1:7000
用上面的指令,我們可以指定新的從節點是那個主節點的副本集。
另一個方法,先把新節點以主節點的形式加入到叢集,然後再用“CLUSTER REPLICATE”指令把它變為從節點。這個方式也適用於給從節點更換主節點。
比如,已有主節點127.0.0.1:7005,它儲存的雜湊槽範圍是11423-16383,節點ID為3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e,我們希望給它新增從節點。首先用之前的方法新增一個空的主節點,然後連上該新節點,傳送如下指令:
redis 127.0.0.1:7006> cluster replicate 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…有2個從節點,分別是原來的執行在7002埠的節點和剛剛新增的7006埠的節點。
刪除節點
使用redis-trib的指令”del-node”可以刪除節點:
./redis-trib del-node 127.0.0.1:7000 `node-id`
第一個引數是叢集的任意一個節點,第二個引數是需要刪除的節點的ID。
同樣的方法可以刪除主節點,但是在刪除之前,需要通過重新分片把資料都移走。
另一個刪除主節點的方式是通過手動故障轉移,讓它的其中一個從節點升級成主節點後再把此節點刪除。但這樣並不會減少叢集的主節點數,如果需要減少主節點數,重新分片在所難免。
複製集遷移
在redis叢集中,可以通過如下指令,在任意時間給從節點更換主節點:
CLUSTER REPLICATE <master-node-id>
有一種特殊的場景:系統自動地更改複製集的主節點,而不是要管理員手動處理。這種自動重新配置從節點的情景叫副本集遷移(replicas migration),它可以增加redis叢集的健壯性。
注意:你可以通過《Redis Cluster Specification》瞭解更多詳細的內容,這裡只是對它的簡單介紹以及它的用處。
假如一個每個主節點只有一個從節點的叢集,在一個主節點和從節點同時故障的情況下,叢集將不能繼續工作,因為已經故障的節點中儲存的雜湊槽資料已經沒法讀寫。雖然網路斷開很可能會讓一大批節點同時被隔離,但是還有很多其他情況會導致節點故障,比如硬體或者軟體的故障導致一個節點宕機,也是非常重要的導致節點故障的原因,這種情況一般不會所有節點同時故障。比如,叢集中每個主節點都有一個從節點,在4點的時候一個從節點被kill,該主節點在6點被kill。這樣依然會導致叢集不能工作。
為了增強系統的可用性,可以給每個主節點再增加一個從節點,但這樣做是比較昂貴的。複製集遷移可以讓我們只給部分主節點增加多些從節點。比如有10個主節點,每個主節點有1個從節點,總共20個節點,然後可以再增加一些從節點(比如3個從節點)到一些主節點,這樣就有部分主節點的從節點數超過1。
當一個主節點沒有從節點時,如果叢集中存在一個主節點有多個從節點時,複製集遷移機制在這些多個從節點中找一個節點,給沒有從節點的主節點做複製集。所以當4點鐘一個從節點宕機,另一個從節點將會代替它成為該主節點的從節點;然後在5點鐘主節點宕機時還有一個從節點可以升級成主節點,這樣叢集可以繼續執行。
所以,簡單的說副本集遷移就是:
*叢集會找到擁有最多從節點的主節點,在它的從節點中挑選一個,進行復制集遷移
*為了讓複製集遷移生效,只需要在叢集中多加幾個從節點,隨便加到哪個主節點都可以
*關於複製集遷移,有一個配置引數叫“cluster-migration-barrier”,在叢集的樣板配置檔案中有詳細說明,需要了解清楚
升級叢集中的節點
升級從節點非常簡單,只要停止它再重啟更新過的版本即可。如果客戶端連到了從節點,在該節點不可用時,客戶端需要重連到另外可用的從節點上。
升級主點則相對複雜,下面是推薦的流程:
1. 使用CLUSTER FAILOVER指令觸發手動故障轉移,讓主節點變成從節點
2. 等到主節點成為從節點
3.升級該從節點
4. 如果你想讓升級過的節點重新變成主節點,則再次觸發手動故障轉移,讓它變成新的主節點。
用這樣的步驟,一個個的升級所有節點。
遷移到redis叢集
使用者需要把redis的資料遷移到redis叢集,原來的資料可能是隻有一個主節點,也可能是用已有的方式分片過,key被儲存在N個幾節點中。
上面2中情況都很容易遷移,特別重要的細節是是否使用了多個key以及是如何使用多個key的。下面是3種不同的情況:
1. 沒有操作多個key(包括操作多個key的指令、事務、lua指令碼)。所有key都是獨立操作的.
2. 操作了多個key(包括操作多個key的指令、事務、lua指令碼),但這些key都有相同的雜湊標籤,比如這些被同時操作的key:SUNION{user:1000}.foo {user:1000}.bar
3. 操作了多個key(包括操作多個key的指令、事務、lua指令碼),這些key沒有特別處理,也沒有相同標籤。
第三種情況redis叢集沒法處理,需要修改應用程式,不要使用多個key,或者給這些key加上相同的雜湊標籤。
第一和第二種情況可以處理,而且他們的處理方式一樣。
假設你已有的資料被分成N個主節點儲存(當N=1時,就是沒有分片的情況),要把資料遷移到redis叢集,需要執行下面幾個步驟:
1. 停止你的客戶端。目前沒有自動線上遷移到redis叢集的方法。你可以自己策劃如何讓你的應用程式支援線上遷移。
2. 使用BGREWRITEAOF指令讓所有主節點產生AOF檔案,並且等待這些檔案建立完成。
3. 把這些AOF檔案儲存下來,分別命名為aof-1, aof-2, ..aof-N,如果需要,可以停止原來的redis例項(對於非虛擬化部署,需要重用這臺電腦來說,把舊程序停掉很有幫助)。
4. 建立N個主節點+0個從節點的redis叢集。晚些時候再新增從節點。請確認所有節點都開啟了appendonly的配置。
5. 停止叢集的所有節點,然後用剛才儲存的AOF檔案,代替每個節點的AOF檔案,aof-1給第一個節點,aof-2給第二個節點,以此類推。
6. 重啟所有節點,這些節點可能會提示說根據配置有些key不應該儲存在這個節點。
7. 使用redis-trib fix指令,讓叢集自動根據雜湊槽遷移資料
8. 使用redis-trib check指令確保你的叢集是正常的
9. 讓你的客戶端使用redis叢集客戶端庫,並重啟它。
還有一個方法可以從已有的redis例項中匯入資料到redis叢集,使用redis-trib import指令。該指令會把源例項中的資料都刪除,並把資料寫入事先部署好的叢集中。需要注意的是,如果你的源例項使用的是redis2.8版本,這個匯入過程可能會比較長,因為2.8版本沒有實現資料遷移的連線快取,所以最好把源例項的redis版本先升級到3.x的版本。