Redis入門指南之集群
配置集群
只需要將每個數據庫節(jié)點的 cluster-enabled打開即可,每個集群至少需要3個主數據庫才能正常運行。
cluster-enabled?yes
集群會將當前節(jié)點記錄的集群狀態(tài)持久化地存儲在指定文件,每個節(jié)點對應的文件必須不同。
cluster-config-file?nodes-6381.conf
每個節(jié)點啟動后都會輸出類似下面的內容:?
No?cluster?configuration?found,?I'm?4b7cbeb343a2ba14d5b1b14457600624c076c157
4b7cbeb343a2…表示該節(jié)點的運行ID,運行ID是節(jié)點在集群中的唯一標識;同一個運行ID,可能地址和端口是不同的。
啟動后,可連接任意一個節(jié)點使用 INFO 命令來判斷集群是否正常啟用了:
127.0.0.1:6381>?info?cluster #?Cluster cluster_enabled:1
##1表示集群正常啟用
?
現在每個節(jié)點都是完全獨立的,下面將它們加入同一個集群里。
Redis源代碼中提供了一個輔助工具redis-trib.rb可以非常方便地完成這一任務。因為redis-trib.rb是用Ruby語言編寫的,所以運行前需要在服務器上安裝Ruby程序。redis-trib.rb 依賴于 gem 包 redis,可以執(zhí)行 gem install redis來安裝。
使用redis-trib.rb來初始化集群,只需要執(zhí)行:
$ /path/to/redis-trib.rb create --replicas 1 127.0.0.1:6380 127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384 127.0.0.1:6385
其中 create參數表示要初始化集群,--replicas 1表示每個主數據庫擁有的從數據庫個數為1 。
執(zhí)行完后,redis-trib.rb會輸出如下內容:
?>>> Creating cluster?
Connecting to node 127.0.0.1:6380: OK?
Connecting to node 127.0.0.1:6381: OK?
Connecting to node 127.0.0.1:6382: OK
?Connecting to node 127.0.0.1:6383: OK
?Connecting to node 127.0.0.1:6384: OK?
Connecting to node 127.0.0.1:6385: OK?
>>> Performing hash slots allocation on 6 nodes...?
Using 3 masters:?
127.0.0.1:6380?
127.0.0.1:6381?
127.0.0.1:6382?
Adding replica?127.0.0.1:6383 to 127.0.0.1:6380?
Adding replica 127.0.0.1:6384 to 127.0.0.1:6381
?Adding replica 127.0.0.1:6385 to 127.0.0.1:6382
?M:?
d4f906940d68714db787a60837f57fa496de5d12 127.0.0.1:6380 slots:0-5460 (5461 slots) master?
.....
內容包括集群具體的分配方案,如果覺得沒問題則輸入yes來開始創(chuàng)建。
首先redis-trib.rb會以客戶端的形式嘗試連接所有的節(jié)點,并發(fā)送PING命令以確定節(jié)點能夠正常服務。如果有任何節(jié)點無法連接,則創(chuàng)建失敗。同時發(fā)送 INFO 命令獲取每個節(jié)點的運行ID以及是否開啟了集群功能(即cluster_enabled為1)。
準備就緒后集群會向每個節(jié)點發(fā)送 CLUSTER MEET命令,格式為 CLUSTER MEET ip port,這個命令用來告訴當前節(jié)點指定ip和port上在運行的節(jié)點也是集群的一部分,從而使得6個節(jié)點最終可以歸入一個集群。
然后redis-trib.rb會分配主從數據庫節(jié)點,分配的原則是盡量保證每個主數據庫運行在不同的IP地址上,同時每個從數據庫和主數據庫均不運行在同一IP地址上,以保證系統(tǒng)的容災能力。分配結果如下:
Using 3 masters:?
127.0.0.1:6380
?127.0.0.1:6381
?127.0.0.1:6382
Adding replica 127.0.0.1:6383 to 127.0.0.1:6380?
Adding replica 127.0.0.1:6384 to 127.0.0.1:6381
?Adding replica 127.0.0.1:6385 to 127.0.0.1:6382
其中主數據庫是 6380、6381 和 6382 端口上的節(jié)點,6383是6380的從數據庫,6384是6381的從數據庫,6385是6382的從數據庫。
分配完成后,會為每個主數據庫分配插槽(分配哪些鍵歸哪些節(jié)點負責)。
對每個要成為子數據庫的節(jié)點發(fā)送 CLUSTER REPLICATE主數據庫的運行 ID來將當前節(jié)點轉換成從數據庫并復制指定運行 ID 的節(jié)點(主數據庫)。?
此時整個集群的過程即創(chuàng)建完成,使用 Redis 命令行客戶端連接任意一個節(jié)點執(zhí)行CLUSTER NODES可以獲得集群中的所有節(jié)點信息,如在6380執(zhí)行:
redis 6380> CLUSTER NODES
551e5094789035affc489db267c8519c3a29f35d 127.0.0.1:6385 slave 887fe91bf218f203194403807e0aee941e985286 0 1424677377448 6 connected?
.......
從上面的輸出中可以看到所有節(jié)點的運行ID、地址和端口、角色、狀態(tài)以及負責的插槽等信息.
節(jié)點的增加?
向新節(jié)點A發(fā)送如下命令即可:
?CLUSTER MEET ip port?
ip和port是集群中任意一個節(jié)點的地址和端口號,
A接收到客戶端發(fā)來的命令后,會與該地址和端口號的節(jié)點B進行握手,使B將A認作當前集群中的一員。當B與A握手成功后,B會使用Gossip協(xié)議將節(jié)點A的信息通知給集群中的每一個節(jié)點。通過這一方式,即使集群中有多個節(jié)點,也只需要選擇 MEET 其中任意一個節(jié)點,即可使新節(jié)點最終加入整個集群中。
插槽的分配?
新的節(jié)點加入集群后有兩種選擇,要么使用 CLUSTER REPLICATE命令復制每個主數據庫來以從數據庫的形式運行,要么向集群申請分配插槽(slot)來以主數據庫的形式運行。 在一個集群中,所有的鍵會被分配給16384個插槽,而每個主數據庫會負責處理其中的一部分插槽。
redis-trib.rb初始化集群時分配給每個節(jié)點的插槽都是連續(xù)的,但是實際上Redis并沒有此限制,可以將任意的幾個插槽分配給任意的節(jié)點負責。
鍵與插槽的對應關系
Redis 將每個鍵的鍵名的有效部分使用CRC16算法計算出散列值,然后取對16384的余數。這樣使得每個鍵都可以分配到16384個插槽中,進而分配的指定的一個節(jié)點中處理。這里鍵名的有效部分是指:
(1)如果鍵名包含{符號,且在{符號后面存在}符號,并且{和}之間有至少一個字符,則有效部分是指{和}之間的內容;?
(2)如果不滿足上一條規(guī)則,那么整個鍵名為有效部分。
如果命令涉及多個鍵(如MGET),只有當所有鍵都位于同一個節(jié)點時 Redis 才能正常支持。利用鍵的分配規(guī)則,可以將所有相關的鍵的有效部分設置成同樣的值使得相關鍵都能分配到同一個節(jié)點以支持多鍵操作。
將插槽分配給指定節(jié)點
插槽的分配分為如下幾種情況。?
(1)插槽之前沒有被分配過,現在想分配給指定節(jié)點。
(2)插槽之前被分配過,現在想移動到指定節(jié)點。
其中第一種情況使用 CLUSTER ADD SLOT S命令來實現,redis-trib.rb 也是通過該命令在創(chuàng)建集群時為新節(jié)點分配插槽的。CLUSTER ADDSLOTS命令的用法為:
CLUSTER ADDSLOTS slot1 [slot2] ... [slotN]
如想將 100 和 101 兩個插槽分配給某個節(jié)點,只需要在該節(jié)點執(zhí)行:
CLUSTER ADDSLOTS 100 101即可。
如果指定插槽已經分配過了,則會提示:
(error) ERR Slot 100 is already busy
可以通過命令 CLUSTER SLOTS來查看插槽的分配情況,如:
?redis 6380> CLUSTER SLOTS?
1) 1) (integer) 5461?
2) (integer) 10922?
3) 1) "127.0.0.1"?
2) (integer) 6381
?4) 1) "127.0.0.1"?
2) (integer) 6384
2) 1) (integer) 0?
2) (integer) 5460?
3) 1) "127.0.0.1"?
2) (integer) 6380?
4) 1) "127.0.0.1"?
2) (integer) 6383
一共3條記錄,每條記錄的前兩個值表示插槽的開始號碼和結束號碼,
后面的值則為負責該插槽的節(jié)點,包括主數據庫和所有的從數據庫,主數據庫始終在第一位。
使用redis-trib.rb將一個插槽從6380遷移到6381
$ /path/to/redis-trib.rb reshard 127.0.0.1:6380
其中reshard表示告訴redis-trib.rb要重新分片,127.0.0.1:6380是集群中的任意一個節(jié)點的地址和端口,redis-trib.rb會自動獲取集群信息。接下來,redis-trib.rb將會詢問具體如何進行重新分片,首先會詢問想要遷移多少個插槽:
How many slots do you want to move (from 1 to 16384)?
我們只需要遷移一個,所以輸入1后回車。接下來redis-trib.rb會詢問要把插槽遷移到哪個節(jié)點:
What is the receiving node ID?
可以通過 CLUSTER NODES命令獲取6381的運行ID,這里是 b547d05c9d0e188993befec 4ae5ccb430343fb4b,輸入并回車。接著最后一步是詢問從哪個節(jié)點移出插槽:
Please enter all the source node IDs. Type 'all' to use all the nodes as source nodes for the hash slots. Type 'done' once you entered all the source nodes IDs. Source node #1:all
我們輸入6380對應的運行ID按回車然后輸入done再按回車確認即可。
接下來輸入 yes來確認重新分片方案,重新分片即告成功。
如何不借助redis-trib.rb手工進行重新分片呢?
使用如下命令即可:
?CLUSTER SETSLOT 插槽號 NODE 新節(jié)點的運行 ID
然而這樣遷移插槽的前提是插槽中并沒有任何鍵,因為使用 CLUSTER SETSLOT命令遷移插槽時并不會連同相應的鍵一起遷移,這就造成了客戶端在指定節(jié)點無法找到未遷移的鍵,造成這些鍵對客戶端來說“丟失了”
手工獲取某個插槽存在哪些鍵的方法是:
?CLUSTER GETKEYSINSLOT 插槽號要返回的鍵的數量
之后對每個鍵,使用MIGRATE命令將其遷移到目標節(jié)點:?
MIGRATE 目標節(jié)點地址目標節(jié)點端口鍵名數據庫號碼超時時間 [COPY] [REPLACE]?
其中COPY選項表示不將鍵從當前數據庫中刪除,而是復制一份副本。
REPLACE表示如果目標節(jié)點存在同名鍵,則覆蓋。
因為集群模式只能使用0號數據庫,所以數據庫號碼始終為0。
如要把鍵abc從當前節(jié)點(如6381)遷移到6380:
redis 6381> MIGRATE 127.0.0.1 6380 abc 0 15999 REPLACE
Redis提供了如下兩個命令用來實現在集群不下線的情況下遷移數據:?
CLUSTER SETSLOT 插槽號 MIGRATING 新節(jié)點的運行 ID
CLUSTER SETSLOT 插槽號 IMPORTING 原節(jié)點的運行 ID
進行遷移時,假設要把0號插槽從A遷移到B,此時redis-trib.rb會依次執(zhí)行如下操作
(1)在B執(zhí)行 CLUSTER SETSLOT 0 IMPORTING A。
?(2)在A 執(zhí)行 CLUSTER SETSLOT 0 MIGRATING B。?
(3)執(zhí)行 CLUSTER GETKEYSINSLOT 0獲取0號插槽的鍵列表。?
(4)對第3步獲取的每個鍵執(zhí)行MIGRATE命令,將其從A遷移到B。 (5)執(zhí)行 CLUSTER SETSLOT 0 NODE B來完成遷移。
從上面的步驟來看 redis-trib.rb多了 1和 2兩個步驟,這兩個步驟就是為了解決遷移過程中鍵的臨時“丟失”問題。首先執(zhí)行完前兩步后,當客戶端向 A 請求插槽 0 中的鍵時,如果鍵存在(即尚未被遷移),則正常處理,如果不存在,則返回一個 ASK跳轉請求,告訴客戶端這個鍵在 B里,如圖 8-6所示??蛻舳私邮盏?ASK跳轉請求后,首先向 B發(fā)送 ASKING命令,然后再重新發(fā)送之前的命令。相反,當客戶端向 B請求插槽 0 中的鍵時,如果前面執(zhí)行了 ASKING 命令,則返回鍵值內容,否則返回 MOVED跳轉請求(會在8.3.4節(jié)介紹),如圖8-7所示。這樣一來客戶端只有能夠處理ASK跳轉,則可以在數據庫遷移時自動從正確的節(jié)點獲取到相應的鍵值,避免了鍵在遷移過程中臨時“丟失”的問題。
獲取與插槽對應的節(jié)點
當客戶端向集群中的任意一個節(jié)點發(fā)送命令后,該節(jié)點會判斷相應的鍵是否在當前節(jié)點中,如果鍵在該節(jié)點中,則會像單機實例一樣正常處理該命令;如果鍵不在該節(jié)點中,就會返回一個 MOVE 重定向請求,告訴客戶端這個鍵目前由哪個節(jié)點負責,然后客戶端再將同樣的請求向目標節(jié)點重新發(fā)送一次以獲得結果。
鍵foo實際應該由6382節(jié)點負責,如果嘗試在6380節(jié)點執(zhí)行與鍵foo相關的命令,就會有如下輸出:
redis 6380> SET foo bar?
(error) MOVED 12182 127.0.0.1:6382
返回的是一個MOVE重定向請求,12182表示foo所屬的插槽號,127.0.0.1:6382則是負責該插槽的節(jié)點地址和端口,客戶端收到重定向請求后,應該將命令重新向 6382節(jié)點發(fā)送一次:
redis 6382> SET foo bar OK
Redis命令行客戶端提供了集群模式來支持自動重定向,使用-c參數來啟用:
$ redis-cli -c -p 6380 reds 6380> SET foo bar -> Redirected to slot [12182] located at 127.0.0.1:6382 OK
可見加入了-c參數后,如果當前節(jié)點并不負責要處理的鍵,Redis命令行客戶端會進行自動命令重定向。而這一過程正是每個支持集群的客戶端應該實現的。
然而相比單機實例,集群的命令重定向也增加了命令的請求次數,原先只需要執(zhí)行一次的命令現在有可能需要依次發(fā)向兩個節(jié)點,算上往返時延,可以說請求重定向對性能的還是有些影響的。 為了解決這一問題,當發(fā)現新的重定向請求時,客戶端應該在重新向正確節(jié)點發(fā)送命令的同時,緩存插槽的路由信息,即記錄下當前插槽是由哪個節(jié)點負責的。這樣每次發(fā)起命令時,客戶端首先計算相關鍵是屬于哪個插槽的,然后根據緩存的路由判斷插槽由哪個節(jié)點負責??紤]到插槽總數相對較少(16384個),緩存所有插槽的路由信息后,每次命令將均只發(fā)向正確的節(jié)點,從而達到和單機實例同樣的性能。
故障恢復
在一個集群中,每個節(jié)點都會定期向其他節(jié)點發(fā)送 PING 命令,并通過有沒有收到回復來判斷目標節(jié)點是否已經下線了。具體來說,集群中的每個節(jié)點每隔1秒鐘就會隨機選擇5個節(jié)點,然后選擇其中最久沒有響應的節(jié)點發(fā)送PING命令。
?如果一定時間內目標節(jié)點沒有響應回復,則發(fā)起 PING 命令的節(jié)點會認為目標節(jié)點疑似下線(PFAIL)。疑似下線可以與哨兵的主觀下線類比,兩者都表示某一節(jié)點從自身的角度認為目標節(jié)點是下線的狀態(tài)。與哨兵的模式類似,如果要使在整個集群中的所有節(jié)點都認為某一節(jié)點已經下線,需要一定數量的節(jié)點都認為該節(jié)點疑似下線才可以,這一過程具體為:?
(1)一旦節(jié)點A認為節(jié)點B是疑似下線狀態(tài),就會在集群中傳播該消息,所有其他節(jié)點收到消息后都會記錄下這一信息;?
(2)當集群中的某一節(jié)點C收集到半數以上的節(jié)點認為B是疑似下線的狀態(tài)時,就會將B標記為下線(FAIL),并且向集群中的其他節(jié)點傳播該消息,從而使得B在整個集群中下線。?
<p style="clear:both;font-family:'Helvetica Neue', Hel





