《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的版本。
最後更新:2017-05-22 10:03:06