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