1 集群的作用
集群,即Redis Cluster,是Redis 3.0開始引入的分布式存儲方案。
集群由多個節(jié)點(Node)組成,Redis的數(shù)據(jù)分布在這些節(jié)點中。集群中的節(jié)點分為主節(jié)點和從節(jié)點:只有主節(jié)點負責(zé)讀寫請求和集群信息的維護;從節(jié)點只進行主節(jié)點數(shù)據(jù)和狀態(tài)信息的復(fù)制。
集群的作用,可以歸納為兩點:
1.數(shù)據(jù)分區(qū):數(shù)據(jù)分區(qū)(或稱數(shù)據(jù)分片)是集群最核心的功能。
集群將數(shù)據(jù)分散到多個節(jié)點,一方面突破了Redis單機內(nèi)存大小的限制,存儲容量大大增加;另一方面每個主節(jié)點都可以對外提供讀服務(wù)和寫服務(wù),大大提高了集群的響應(yīng)能力。
Redis單機內(nèi)存大小受限問題,在介紹持久化和主從復(fù)制時都有提及;例如,如果單機內(nèi)存太大,bgsave和bgrewriteaof的fork操作可能導(dǎo)致主進程阻塞,主從環(huán)境下主機切換時可能導(dǎo)致從節(jié)點長時間無法提供服務(wù),全量復(fù)制階段主節(jié)點的復(fù)制緩沖區(qū)可能溢出……
2、高可用:集群支持主從復(fù)制和主節(jié)點的自動故障轉(zhuǎn)移(與哨兵類似);當(dāng)任一節(jié)點發(fā)生故障時,集群仍然可以對外提供服務(wù)。
本文內(nèi)容基于Redis 3.0.6。
2 集群的搭建
這一部分我們將搭建一個簡單的集群:共6個節(jié)點,3主3從。方便起見:所有節(jié)點在同一臺服務(wù)器上,以端口號進行區(qū)分;配置從簡。3個主節(jié)點端口號:7000/7001/7002,對應(yīng)的從節(jié)點端口號:8000/8001/8002。
集群的搭建有兩種方式:(1)手動執(zhí)行Redis命令,一步步完成搭建;(2)使用Ruby腳本搭建。二者搭建的原理是一樣的,只是Ruby腳本將Redis命令進行了打包封裝;在實際應(yīng)用中推薦使用腳本方式,簡單快捷不容易出錯。下面分別介紹這兩種方式。
2.1 執(zhí)行Redis命令搭建集群
集群的搭建可以分為四步:
(1)啟動節(jié)點:將節(jié)點以集群模式啟動,此時節(jié)點是獨立的,并沒有建立聯(lián)系;
(2)節(jié)點握手:讓獨立的節(jié)點連成一個網(wǎng)絡(luò);
(3)分配槽:將16384個槽分配給主節(jié)點;
(4)指定主從關(guān)系:為從節(jié)點指定主節(jié)點。
實際上,前三步完成后集群便可以對外提供服務(wù);但指定從節(jié)點后,集群才能夠提供真正高可用的服務(wù)。
(1)啟動節(jié)點
集群節(jié)點的啟動仍然是使用redis-server命令,但需要使用集群模式啟動。下面是7000節(jié)點的配置文件(只列出了節(jié)點正常工作關(guān)鍵配置,其他配置(如開啟AOF)可以參照單機節(jié)點進行):
#redis-7000.conf
port 7000
cluster-enabled yes
cluster-config-file "node-7000.conf"
logfile "log-7000.log"
dbfilename "dump-7000.rdb"
daemonize yes
其中的cluster-enabled和cluster-config-file是與集群相關(guān)的配置。
cluster-enabled yes:Redis實例可以分為單機模式(standalone)和集群模式(cluster);cluster-enabled yes可以啟動集群模式。
在單機模式下啟動的Redis實例,如果執(zhí)行info server命令,可以發(fā)現(xiàn)redis_mode一項為standalone,如下圖所示:

集群模式下的節(jié)點,其redis_mode為cluster,如下圖所示:

cluster-config-file:該參數(shù)指定了集群配置文件的位置。
每個節(jié)點在運行過程中,會維護一份集群配置文件;
每當(dāng)集群信息發(fā)生變化時(如增減節(jié)點),集群內(nèi)所有節(jié)點會將最新信息更新到該配置文件;
當(dāng)節(jié)點重啟后,會重新讀取該配置文件,獲取集群信息,可以方便的重新加入到集群中。
也就是說,當(dāng)Redis節(jié)點以集群模式啟動時,會首先尋找是否有集群配置文件,如果有則使用文件中的配置啟動,如果沒有,則初始化配置并將配置保存到文件中。
集群配置文件由Redis節(jié)點維護,不需要人工修改。
編輯好配置文件后,使用redis-server命令啟動該節(jié)點:
redis-server redis-7000.conf
節(jié)點啟動以后,通過cluster nodes命令可以查看節(jié)點的情況,如下圖所示。

其中返回值第一項表示節(jié)點id,由40個16進制字符串組成,節(jié)點id與主從復(fù)制一文中提到的runId不同:Redis每次啟動runId都會重新創(chuàng)建,但是節(jié)點id只在集群初始化時創(chuàng)建一次,然后保存到集群配置文件中,以后節(jié)點重新啟動時會直接在集群配置文件中讀取。
其他節(jié)點使用相同辦法啟動,不再贅述。需要特別注意,在啟動節(jié)點階段,節(jié)點是沒有主從關(guān)系的,因此從節(jié)點不需要加slaveof配置。
(2)節(jié)點握手
節(jié)點啟動以后是相互獨立的,并不知道其他節(jié)點存在;需要進行節(jié)點握手,將獨立的節(jié)點組成一個網(wǎng)絡(luò)。
節(jié)點握手使用cluster meet {ip} {port}命令實現(xiàn),例如在7000節(jié)點中執(zhí)行cluster meet 192.168.72.128 7001,可以完成7000節(jié)點和7001節(jié)點的握手;注意ip使用的是局域網(wǎng)ip而不是localhost或127.0.0.1,是為了其他機器上的節(jié)點或客戶端也可以訪問。此時再使用cluster nodes查看:

在7001節(jié)點下也可以類似查看:

同理,在7000節(jié)點中使用cluster meet命令,可以將所有節(jié)點加入到集群,完成節(jié)點握手:
cluster meet 192.168.72.128 7002
cluster meet 192.168.72.128 8000
cluster meet 192.168.72.128 8001
cluster meet 192.168.72.128 8002
執(zhí)行完上述命令后,可以看到7000節(jié)點已經(jīng)感知到了所有其他節(jié)點:

通過節(jié)點之間的通信,每個節(jié)點都可以感知到所有其他節(jié)點,以8000節(jié)點為例:

(3)分配槽
在Redis集群中,借助槽實現(xiàn)數(shù)據(jù)分區(qū),具體原理后文會介紹。集群有16384個槽,槽是數(shù)據(jù)管理和遷移的基本單位。當(dāng)數(shù)據(jù)庫中的16384個槽都分配了節(jié)點時,集群處于上線狀態(tài)(ok);如果有任意一個槽沒有分配節(jié)點,則集群處于下線狀態(tài)(fail)。
cluster info命令可以查看集群狀態(tài),分配槽之前狀態(tài)為fail:

分配槽使用cluster addslots命令,執(zhí)行下面的命令將槽(編號0-16383)全部分配完畢:
redis-cli -p 7000 cluster addslots {0..5461}
redis-cli -p 7001 cluster addslots {5462..10922}
redis-cli -p 7002 cluster addslots {10923..16383}
此時查看集群狀態(tài),顯示所有槽分配完畢,集群進入上線狀態(tài):

(4)指定主從關(guān)系
集群中指定主從關(guān)系不再使用slaveof命令,而是使用cluster replicate命令;參數(shù)使用節(jié)點id。
通過cluster nodes獲得幾個主節(jié)點的節(jié)點id后,執(zhí)行下面的命令為每個從節(jié)點指定主節(jié)點:
redis-cli -p 8000 cluster replicate be816eba968bc16c884b963d768c945e86ac51ae
redis-cli -p 8001 cluster replicate 788b361563acb175ce8232569347812a12f1fdb4
redis-cli -p 8002 cluster replicate a26f1624a3da3e5197dde267de683d61bb2dcbf1
此時執(zhí)行cluster nodes查看各個節(jié)點的狀態(tài),可以看到主從關(guān)系已經(jīng)建立。

至此,集群搭建完畢。
2.2 使用Ruby腳本搭建集群
在{REDIS_HOME}/src目錄下可以看到redis-trib.rb文件,這是一個Ruby腳本,可以實現(xiàn)自動化的集群搭建。
(1)安裝Ruby環(huán)境
以Ubuntu為例,如下操作即可安裝Ruby環(huán)境:
apt-get install ruby #安裝ruby環(huán)境
gem install redis #gem是ruby的包管理工具,該命令可以安裝ruby-redis依賴
(2)啟動節(jié)點
與第一種方法中的“啟動節(jié)點”完全相同。
(3)搭建集群
redis-trib.rb腳本提供了眾多命令,其中create用于搭建集群,使用方法如下:
./redis-trib.rb create --replicas 1 192.168.72.128:7000 192.168.72.128:7001 192.168.72.128:7002 192.168.72.128:8000 192.168.72.128:8001 192.168.72.128:8002
其中:--replicas=1表示每個主節(jié)點有1個從節(jié)點;后面的多個{ip:port}表示節(jié)點地址,前面的做主節(jié)點,后面的做從節(jié)點。使用redis-trib.rb搭建集群時,要求節(jié)點不能包含任何槽和數(shù)據(jù)。
執(zhí)行創(chuàng)建命令后,腳本會給出創(chuàng)建集群的計劃,如下圖所示;計劃包括哪些是主節(jié)點,哪些是從節(jié)點,以及如何分配槽。

輸入yes確認執(zhí)行計劃,腳本便開始按照計劃執(zhí)行,如下圖所示。

至此,集群搭建完畢。
2.3 集群方案設(shè)計
設(shè)計集群方案時,至少要考慮以下因素:
(1)高可用要求:根據(jù)故障轉(zhuǎn)移的原理,至少需要3個主節(jié)點才能完成故障轉(zhuǎn)移,且3個主節(jié)點不應(yīng)在同一臺物理機上;每個主節(jié)點至少需要1個從節(jié)點,且主從節(jié)點不應(yīng)在一臺物理機上;因此高可用集群至少包含6個節(jié)點。
(2)數(shù)據(jù)量和訪問量:估算應(yīng)用需要的數(shù)據(jù)量和總訪問量(考慮業(yè)務(wù)發(fā)展,留有冗余),結(jié)合每個主節(jié)點的容量和能承受的訪問量(可以通過benchmark得到較準(zhǔn)確估計),計算需要的主節(jié)點數(shù)量。
(3)節(jié)點數(shù)量限制:Redis官方給出的節(jié)點數(shù)量限制為1000,主要是考慮節(jié)點間通信帶來的消耗。在實際應(yīng)用中應(yīng)盡量避免大集群;如果節(jié)點數(shù)量不足以滿足應(yīng)用對Redis數(shù)據(jù)量和訪問量的要求,可以考慮:
(1)業(yè)務(wù)分割,大集群分為多個小集群;
(2)減少不必要的數(shù)據(jù);
(3)調(diào)整數(shù)據(jù)過期策略等。
(4)適度冗余:Redis可以在不影響集群服務(wù)的情況下增加節(jié)點,因此節(jié)點數(shù)量適當(dāng)冗余即可,不用太大。
3 集群管理
上一章介紹了集群的搭建方法和設(shè)計方案,下面將進一步深入,介紹集群的原理。
集群最核心的功能是數(shù)據(jù)分區(qū),因此首先介紹數(shù)據(jù)的分區(qū)規(guī)則;然后介紹集群實現(xiàn)的細節(jié):通信機制和數(shù)據(jù)結(jié)構(gòu);最后以cluster meet(節(jié)點握手)、cluster addslots(槽分配)為例,說明節(jié)點是如何利用上述數(shù)據(jù)結(jié)構(gòu)和通信機制實現(xiàn)集群命令的。
3.1 數(shù)據(jù)分區(qū)方案
數(shù)據(jù)分區(qū)有順序分區(qū)、哈希分區(qū)等,其中哈希分區(qū)由于其天然的隨機性,使用廣泛;集群的分區(qū)方案便是哈希分區(qū)的一種。
哈希分區(qū)的基本思路是:對數(shù)據(jù)的特征值(如key)進行哈希,然后根據(jù)哈希值決定數(shù)據(jù)落在哪個節(jié)點。常見的哈希分區(qū)包括:哈希取余分區(qū)、一致性哈希分區(qū)、帶虛擬節(jié)點的一致性哈希分區(qū)等。
衡量數(shù)據(jù)分區(qū)方法好壞的標(biāo)準(zhǔn)有很多,其中比較重要的兩個因素是(1)數(shù)據(jù)分布是否均勻(2)增加或刪減節(jié)點對數(shù)據(jù)分布的影響。由于哈希的隨機性,哈希分區(qū)基本可以保證數(shù)據(jù)分布均勻;因此在比較哈希分區(qū)方案時,重點要看增減節(jié)點對數(shù)據(jù)分布的影響。
(1)哈希取余分區(qū)
哈希取余分區(qū)思路非常簡單:計算key的hash值,然后對節(jié)點數(shù)量進行取余,從而決定數(shù)據(jù)映射到哪個節(jié)點上。該方案最大的問題是,當(dāng)新增或刪減節(jié)點時,節(jié)點數(shù)量發(fā)生變化,系統(tǒng)中所有的數(shù)據(jù)都需要重新計算映射關(guān)系,引發(fā)大規(guī)模數(shù)據(jù)遷移。
(2)一致性哈希分區(qū)
一致性哈希算法將整個哈希值空間組織成一個虛擬的圓環(huán),如下圖所示,范圍為0-2^32-1;對于每個數(shù)據(jù),根據(jù)key計算hash值,確定數(shù)據(jù)在環(huán)上的位置,然后從此位置沿環(huán)順時針行走,找到的第一臺服務(wù)器就是其應(yīng)該映射到的服務(wù)器。

與哈希取余分區(qū)相比,一致性哈希分區(qū)將增減節(jié)點的影響限制在相鄰節(jié)點。以上圖為例,如果在node1和node2之間增加node5,則只有node2中的一部分數(shù)據(jù)會遷移到node5;如果去掉node2,則原node2中的數(shù)據(jù)只會遷移到node4中,只有node4會受影響。
一致性哈希分區(qū)的主要問題在于,當(dāng)節(jié)點數(shù)量較少時,增加或刪減節(jié)點,對單個節(jié)點的影響可能很大,造成數(shù)據(jù)的嚴(yán)重不平衡。還是以上圖為例,如果去掉node2,node4中的數(shù)據(jù)由總數(shù)據(jù)的1/4左右變?yōu)?/2左右,與其他節(jié)點相比負載過高。
(3)帶虛擬節(jié)點的一致性哈希分區(qū)
該方案在一致性哈希分區(qū)的基礎(chǔ)上,引入了虛擬節(jié)點的概念。Redis集群使用的便是該方案,其中的虛擬節(jié)點稱為槽(slot)。槽是介于數(shù)據(jù)和實際節(jié)點之間的虛擬概念;每個實際節(jié)點包含一定數(shù)量的槽,每個槽包含哈希值在一定范圍內(nèi)的數(shù)據(jù)。引入槽以后,數(shù)據(jù)的映射關(guān)系由數(shù)據(jù)hash?實際節(jié)點,變成了數(shù)據(jù)hash?槽?實際節(jié)點。
在使用了槽的一致性哈希分區(qū)中,槽是數(shù)據(jù)管理和遷移的基本單位。槽解耦了數(shù)據(jù)和實際節(jié)點之間的關(guān)系,增加或刪除節(jié)點對系統(tǒng)的影響很小。仍以上圖為例,系統(tǒng)中有4個實際節(jié)點,假設(shè)為其分配16個槽(0-15); 槽0-3位于node1,4-7位于node2,以此類推。如果此時刪除node2,只需要將槽4-7重新分配即可,例如槽4-5分配給node1,槽6分配給node3,槽7分配給node4;可以看出刪除node2后,數(shù)據(jù)在其他節(jié)點的分布仍然較為均衡。
槽的數(shù)量一般遠小于2^32,遠大于實際節(jié)點的數(shù)量;在Redis集群中,槽的數(shù)量為16384。
下面這張圖很好的總結(jié)了Redis集群將數(shù)據(jù)映射到實際節(jié)點的過程:

(1)Redis對數(shù)據(jù)的特征值(一般是key)計算哈希值,使用的算法是CRC16。
(2)根據(jù)哈希值,計算數(shù)據(jù)屬于哪個槽。
(3)根據(jù)槽與節(jié)點的映射關(guān)系,計算數(shù)據(jù)屬于哪個節(jié)點。
3.2 節(jié)點通信機制
集群要作為一個整體工作,離不開節(jié)點之間的通信。
3.2.1 兩個端口
在哨兵系統(tǒng)中,節(jié)點分為數(shù)據(jù)節(jié)點和哨兵節(jié)點:前者存儲數(shù)據(jù),后者實現(xiàn)額外的控制功能。在集群中,沒有數(shù)據(jù)節(jié)點與非數(shù)據(jù)節(jié)點之分:所有的節(jié)點都存儲數(shù)據(jù),也都參與集群狀態(tài)的維護。為此,集群中的每個節(jié)點,都提供了兩個TCP端口:
普通端口:即我們在前面指定的端口(7000等)。普通端口主要用于為客戶端提供服務(wù)(與單機節(jié)點類似);但在節(jié)點間數(shù)據(jù)遷移時也會使用。
集群端口:端口號是普通端口+10000(10000是固定值,無法改變),如7000節(jié)點的集群端口為17000。集群端口只用于節(jié)點之間的通信,如搭建集群、增減節(jié)點、故障轉(zhuǎn)移等操作時節(jié)點間的通信;不要使用客戶端連接集群接口。為了保證集群可以正常工作,在配置防火墻時,要同時開啟普通端口和集群端口。
3.2.2 Gossip協(xié)議
節(jié)點間通信,按照通信協(xié)議可以分為幾種類型:單對單、廣播、Gossip協(xié)議等。重點是廣播和Gossip的對比。
廣播是指向集群內(nèi)所有節(jié)點發(fā)送消息;
優(yōu)點是集群的收斂速度快(集群收斂是指集群內(nèi)所有節(jié)點獲得的集群信息是一致的);
缺點是每條消息都要發(fā)送給所有節(jié)點,CPU、帶寬等消耗較大。
Gossip協(xié)議的特點是:在節(jié)點數(shù)量有限的網(wǎng)絡(luò)中,每個節(jié)點都“隨機”的與部分節(jié)點通信(并不是真正的隨機,而是根據(jù)特定的規(guī)則選擇通信的節(jié)點),經(jīng)過一番雜亂無章的通信,每個節(jié)點的狀態(tài)很快會達到一致。
Gossip協(xié)議的優(yōu)點有負載(比廣播)低、去中心化、容錯性高(因為通信有冗余)等;
缺點主要是集群的收斂速度慢。
3.2.3 消息類型
集群中的節(jié)點采用固定頻率(每秒10次)的定時任務(wù)進行通信相關(guān)的工作:判斷是否需要發(fā)送消息及消息類型、確定接收節(jié)點、發(fā)送消息等。如果集群狀態(tài)發(fā)生了變化,如增減節(jié)點、槽狀態(tài)變更,通過節(jié)點間的通信,所有節(jié)點會很快得知整個集群的狀態(tài),使集群收斂。
節(jié)點間發(fā)送的消息主要分為5種:meet消息、ping消息、pong消息、fail消息、publish消息。不同的消息類型,通信協(xié)議、發(fā)送的頻率和時機、接收節(jié)點的選擇等是不同的。
MEET消息:在節(jié)點握手階段,當(dāng)節(jié)點收到客戶端的CLUSTER MEET命令時,會向新加入的節(jié)點發(fā)送MEET消息,請求新節(jié)點加入到當(dāng)前集群;新節(jié)點收到MEET消息后會回復(fù)一個PONG消息。
PING消息:集群里每個節(jié)點每秒鐘會選擇部分節(jié)點發(fā)送PING消息,接收者收到消息后會回復(fù)一個PONG消息。PING消息的內(nèi)容是自身節(jié)點和部分其他節(jié)點的狀態(tài)信息;作用是彼此交換信息,以及檢測節(jié)點是否在線。PING消息使用Gossip協(xié)議發(fā)送,接收節(jié)點的選擇兼顧了收斂速度和帶寬成本,具體規(guī)則如下:(1)隨機找5個節(jié)點,在其中選擇最久沒有通信的1個節(jié)點(2)掃描節(jié)點列表,選擇最近一次收到PONG消息時間大于cluster_node_timeout/2的所有節(jié)點,防止這些節(jié)點長時間未更新。
PONG消息:PONG消息封裝了自身狀態(tài)數(shù)據(jù)??梢苑譃閮煞N:第一種是在接到MEET/PING消息后回復(fù)的PONG消息;第二種是指節(jié)點向集群廣播PONG消息,這樣其他節(jié)點可以獲知該節(jié)點的最新信息,例如故障恢復(fù)后新的主節(jié)點會廣播PONG消息。
FAIL消息:當(dāng)一個主節(jié)點判斷另一個主節(jié)點進入FAIL狀態(tài)時,會向集群廣播這一FAIL消息;接收節(jié)點會將這一FAIL消息保存起來,便于后續(xù)的判斷。
PUBLISH消息:節(jié)點收到PUBLISH命令后,會先執(zhí)行該命令,然后向集群廣播這一消息,接收節(jié)點也會執(zhí)行該PUBLISH命令。
3.3 數(shù)據(jù)結(jié)構(gòu)
節(jié)點需要專門的數(shù)據(jù)結(jié)構(gòu)來存儲集群的狀態(tài)。所謂集群的狀態(tài),是一個比較大的概念,包括:集群是否處于上線狀態(tài)、集群中有哪些節(jié)點、節(jié)點是否可達、節(jié)點的主從狀態(tài)、槽的分布……
節(jié)點為了存儲集群狀態(tài)而提供的數(shù)據(jù)結(jié)構(gòu)中,最關(guān)鍵的是clusterNode和clusterState結(jié)構(gòu):前者記錄了一個節(jié)點的狀態(tài),后者記錄了集群作為一個整體的狀態(tài)。
clusterNode
clusterNode結(jié)構(gòu)保存了一個節(jié)點的當(dāng)前狀態(tài),包括創(chuàng)建時間、節(jié)點id、ip和端口號等。每個節(jié)點都會用一個clusterNode結(jié)構(gòu)記錄自己的狀態(tài),并為集群內(nèi)所有其他節(jié)點都創(chuàng)建一個clusterNode結(jié)構(gòu)來記錄節(jié)點狀態(tài)。
下面列舉了clusterNode的部分字段,并說明了字段的含義和作用:
typedef struct clusterNode {
//節(jié)點創(chuàng)建時間
mstime_t ctime;
//節(jié)點id
char name[REDIS_CLUSTER_NAMELEN];
//節(jié)點的ip和端口號
char ip[REDIS_IP_STR_LEN];
int port;
//節(jié)點標(biāo)識:整型,每個bit都代表了不同狀態(tài),如節(jié)點的主從狀態(tài)、是否在線、是否在握手等
int flags;
//配置紀(jì)元:故障轉(zhuǎn)移時起作用,類似于哨兵的配置紀(jì)元
uint64_t configEpoch;
//槽在該節(jié)點中的分布:占用16384/8個字節(jié),16384個比特;每個比特對應(yīng)一個槽:比特值為1,則該比特對應(yīng)的槽在節(jié)點中;比特值為0,則該比特對應(yīng)的槽不在節(jié)點中
unsigned char slots[16384/8];
//節(jié)點中槽的數(shù)量
int numslots;
…………
} clusterNode;
除了上述字段,clusterNode還包含節(jié)點連接、主從復(fù)制、故障發(fā)現(xiàn)和轉(zhuǎn)移需要的信息等。
clusterState
clusterState結(jié)構(gòu)保存了在當(dāng)前節(jié)點視角下,集群所處的狀態(tài)。主要字段包括:
typedef struct clusterState {
//自身節(jié)點
clusterNode *myself;
//配置紀(jì)元
uint64_t currentEpoch;
//集群狀態(tài):在線還是下線
int state;
//集群中至少包含一個槽的節(jié)點數(shù)量
int size;
//哈希表,節(jié)點名稱->clusterNode節(jié)點指針
dict *nodes;
//槽分布信息:數(shù)組的每個元素都是一個指向clusterNode結(jié)構(gòu)的指針;如果槽還沒有分配給任何節(jié)點,則為NULL
clusterNode *slots[16384];
…………
} clusterState;
除此之外,clusterState還包括故障轉(zhuǎn)移、槽遷移等需要的信息。
3.4 集群命令的實現(xiàn)
這一部分將以cluster meet(節(jié)點握手)、cluster addslots(槽分配)為例,說明節(jié)點是如何利用上述數(shù)據(jù)結(jié)構(gòu)和通信機制實現(xiàn)集群命令的。
cluster meet
假設(shè)要向A節(jié)點發(fā)送cluster meet命令,將B節(jié)點加入到A所在的集群,則A節(jié)點收到命令后,執(zhí)行的操作如下:
① A為B創(chuàng)建一個clusterNode結(jié)構(gòu),并將其添加到clusterState的nodes字典中
② A向B發(fā)送MEET消息
③ B收到MEET消息后,會為A創(chuàng)建一個clusterNode結(jié)構(gòu),并將其添加到clusterState的nodes字典中
④ B回復(fù)A一個PONG消息
⑤ A收到B的PONG消息后,便知道B已經(jīng)成功接收自己的MEET消息
⑥ 然后,A向B返回一個PING消息
⑦ B收到A的PING消息后,便知道A已經(jīng)成功接收自己的PONG消息,握手完成
⑧ 之后,A通過Gossip協(xié)議將B的信息廣播給集群內(nèi)其他節(jié)點,其他節(jié)點也會與B握手;一段時間后,集群收斂,B成為集群內(nèi)的一個普通節(jié)點
通過上述過程可以發(fā)現(xiàn),集群中兩個節(jié)點的握手過程與TCP類似,都是三次握手:A向B發(fā)送MEET;B向A發(fā)送PONG;A向B發(fā)送PING。
cluster addslots
集群中槽的分配信息,存儲在clusterNode的slots數(shù)組和clusterState的slots數(shù)組中,兩個數(shù)組的結(jié)構(gòu)前面已做介紹;二者的區(qū)別在于:前者存儲的是該節(jié)點中分配了哪些槽,后者存儲的是集群中所有槽分別分布在哪個節(jié)點。
cluster addslots命令接收一個槽或多個槽作為參數(shù),例如在A節(jié)點上執(zhí)行cluster addslots {0..10}命令,是將編號為0-10的槽分配給A節(jié)點,具體執(zhí)行過程如下:
① 遍歷輸入槽,檢查它們是否都沒有分配,如果有一個槽已分配,命令執(zhí)行失??;方法是檢查輸入槽在clusterState.slots[]中對應(yīng)的值是否為NULL。
② 遍歷輸入槽,將其分配給節(jié)點A;方法是修改clusterNode.slots[]中對應(yīng)的比特為1,以及clusterState.slots[]中對應(yīng)的指針指向A節(jié)點
③ A節(jié)點執(zhí)行完成后,通過節(jié)點通信機制通知其他節(jié)點,所有節(jié)點都會知道0-10的槽分配給了A節(jié)點
4 客戶端訪問集群
在集群中,數(shù)據(jù)分布在不同的節(jié)點中,客戶端通過某節(jié)點訪問數(shù)據(jù)時,數(shù)據(jù)可能不在該節(jié)點中;下面介紹集群是如何處理這個問題的。
4.1 redis-cli
當(dāng)節(jié)點收到redis-cli發(fā)來的命令(如set/get)時,過程如下:
(1)計算key屬于哪個槽:CRC16(key) & 16383
集群提供的cluster keyslot命令也是使用上述公式實現(xiàn),如:

(2)判斷key所在的槽是否在當(dāng)前節(jié):
假設(shè)key位于第i個槽,clusterState.slots[i]則指向了槽所在的節(jié)點,如果clusterState.slots[i]==clusterState.myself,說明槽在當(dāng)前節(jié)點,可以直接在當(dāng)前節(jié)點執(zhí)行命令;否則,說明槽不在當(dāng)前節(jié)點,則查詢槽所在節(jié)點的地址(clusterState.slots[i].ip/port),并將其包裝到MOVED錯誤中返回給redis-cli。
(3)redis-cli收到MOVED錯誤后,根據(jù)返回的ip和port重新發(fā)送請求。
下面的例子展示了redis-cli和集群的互動過程:在7000節(jié)點中操作key1,但key1所在的槽9189在節(jié)點7001中,因此節(jié)點返回MOVED錯誤(包含7001節(jié)點的ip和port)給redis-cli,redis-cli重新向7001發(fā)起請求。

上例中,redis-cli通過-c指定了集群模式,如果沒有指定,redis-cli無法處理MOVED錯誤:

4.2 Smart客戶端
redis-cli這一類客戶端稱為Dummy客戶端,因為它們在執(zhí)行命令前不知道數(shù)據(jù)在哪個節(jié)點,需要借助MOVED錯誤重新定向。與Dummy客戶端相對應(yīng)的是Smart客戶端。
Smart客戶端(以Java的JedisCluster為例)的基本原理:
(1)JedisCluster初始化時,在內(nèi)部維護slot?node的緩存,方法是連接任一節(jié)點,執(zhí)行cluster slots命令,該命令返回如下所示:

(2)此外,JedisCluster為每個節(jié)點創(chuàng)建連接池(即JedisPool)。
(3)當(dāng)執(zhí)行命令時,JedisCluster根據(jù)key?slot?node選擇需要連接的節(jié)點,發(fā)送命令。如果成功,則命令執(zhí)行完畢。如果執(zhí)行失敗,則會隨機選擇其他節(jié)點進行重試,并在出現(xiàn)MOVED錯誤時,使用cluster slots重新同步slot?node的映射關(guān)系。
下面代碼演示了如何使用JedisCluster訪問集群(未考慮資源釋放、異常處理等):
public static void test() {
Set<HostAndPort> nodes = new HashSet<>();
nodes.add(new HostAndPort("192.168.72.128", 7000));
nodes.add(new HostAndPort("192.168.72.128", 7001));
nodes.add(new HostAndPort("192.168.72.128", 7002));
nodes.add(new HostAndPort("192.168.72.128", 8000));
nodes.add(new HostAndPort("192.168.72.128", 8001));
nodes.add(new HostAndPort("192.168.72.128", 8002));
JedisCluster cluster = new JedisCluster(nodes);
System.out.println(cluster.get("key1"));
cluster.close();
}
注意事項如下:
(1)JedisCluster中已經(jīng)包含所有節(jié)點的連接池,因此JedisCluster要使用單例。
(2)客戶端維護了slot?node映射關(guān)系以及為每個節(jié)點創(chuàng)建了連接池,當(dāng)節(jié)點數(shù)量較多時,應(yīng)注意客戶端內(nèi)存資源和連接資源的消耗。
(3)Jedis較新版本針對JedisCluster做了一些性能方面的優(yōu)化,如cluster slots緩存更新和鎖阻塞等方面的優(yōu)化,應(yīng)盡量使用2.8.2及以上版本的Jedis。
5 實踐須知
前面介紹了集群正常運行和訪問的方法和原理,下面是一些重要的補充內(nèi)容。
5.1 集群伸縮
實踐中常常需要對集群進行伸縮,如訪問量增大時的擴容操作。Redis集群可以在不影響對外服務(wù)的情況下實現(xiàn)伸縮。伸縮的核心是槽遷移:修改槽與節(jié)點的對應(yīng)關(guān)系,實現(xiàn)槽(即數(shù)據(jù))在節(jié)點之間的移動。例如,如果槽均勻分布在集群的3個節(jié)點中,此時增加一個節(jié)點,則需要從3個節(jié)點中分別拿出一部分槽給新節(jié)點,從而實現(xiàn)槽在4個節(jié)點中的均勻分布。
增加節(jié)點
假設(shè)要增加7003和8003節(jié)點,其中8003是7003的從節(jié)點;
步驟如下:
(1)啟動節(jié)點:方法參見集群搭建
(2)節(jié)點握手:可以使用cluster meet命令,但在生產(chǎn)環(huán)境中建議使用redis-trib.rb的add-node工具,其原理也是cluster meet,但它會先檢查新節(jié)點是否已加入其它集群或者存在數(shù)據(jù),避免加入到集群后帶來混亂。
redis-trib.rb add-node 192.168.72.128:7003 192.168.72.128 7000
redis-trib.rb add-node 192.168.72.128:8003 192.168.72.128 7000
(3)遷移槽:推薦使用redis-trib.rb的reshard工具實現(xiàn)。reshard自動化程度很高,只需要輸入redis-trib.rb reshard ip:port (ip和port可以是集群中的任一節(jié)點),然后按照提示輸入以下信息,槽遷移會自動完成:
①待遷移的槽數(shù)量:16384個槽均分給4個節(jié)點,每個節(jié)點4096個槽,因此待遷移槽數(shù)量為4096
②目標(biāo)節(jié)點id:7003節(jié)點的id
③源節(jié)點的id:7000/7001/7002節(jié)點的id
(4)指定主從關(guān)系:方法參見集群搭建
減少節(jié)點
假設(shè)要下線7000/8000節(jié)點,可以分為兩步:
(1)遷移槽:使用reshard將7000節(jié)點中的槽均勻遷移到7001/7002/7003節(jié)點
(2)下線節(jié)點:使用redis-trib.rb del-node工具;應(yīng)先下線從節(jié)點再下線主節(jié)點,因為若主節(jié)點先下線,從節(jié)點會被指向其他主節(jié)點,造成不必要的全量復(fù)制。
redis-trib.rb del-node 192.168.72.128:7001 {節(jié)點8000的id}
redis-trib.rb del-node 192.168.72.128:7001 {節(jié)點7000的id}
ASK錯誤
集群伸縮的核心是槽遷移,在槽遷移過程中,如果客戶端向源節(jié)點發(fā)送命令,源節(jié)點執(zhí)行流程如下:

客戶端收到ASK錯誤后,從中讀取目標(biāo)節(jié)點的地址信息,并向目標(biāo)節(jié)點重新發(fā)送請求,就像收到MOVED錯誤時一樣。但是二者有很大區(qū)別:ASK錯誤說明數(shù)據(jù)正在遷移,不知道何時遷移完成,因此重定向是臨時的,SMART客戶端不會刷新slots緩存;MOVED錯誤重定向則是(相對)永久的,SMART客戶端會刷新slots緩存。
5.2 故障轉(zhuǎn)移
在哨兵一文中,介紹了哨兵實現(xiàn)故障發(fā)現(xiàn)和故障轉(zhuǎn)移的原理。雖然細節(jié)上有很大不同,但集群的實現(xiàn)與哨兵思路類似:通過定時任務(wù)發(fā)送PING消息檢測其他節(jié)點狀態(tài);節(jié)點下線分為主觀下線和客觀下線;客觀下線后選取從節(jié)點進行故障轉(zhuǎn)移。
與哨兵一樣,集群只實現(xiàn)了主節(jié)點的故障轉(zhuǎn)移;從節(jié)點故障時只會被下線,不會進行故障轉(zhuǎn)移。因此,使用集群時,應(yīng)謹慎使用讀寫分離技術(shù),因為從節(jié)點故障會導(dǎo)致讀服務(wù)不可用,可用性變差。
這里不再詳細介紹故障轉(zhuǎn)移的細節(jié),只對重要事項進行說明:
節(jié)點數(shù)量:在故障轉(zhuǎn)移階段,需要由主節(jié)點投票選出哪個從節(jié)點成為新的主節(jié)點;從節(jié)點選舉勝出需要的票數(shù)為N/2+1;其中N為主節(jié)點數(shù)量(包括故障主節(jié)點),但故障主節(jié)點實際上不能投票。因此為了能夠在故障發(fā)生時順利選出從節(jié)點,集群中至少需要3個主節(jié)點(且部署在不同的物理機上)。
故障轉(zhuǎn)移時間:從主節(jié)點故障發(fā)生到完成轉(zhuǎn)移,所需要的時間主要消耗在主觀下線識別、主觀下線傳播、選舉延遲等幾個環(huán)節(jié);具體時間與參數(shù)cluster-node-timeout有關(guān),一般來說:故障轉(zhuǎn)移時間(毫秒) ≤ 1.5 * cluster-node-timeout + 1000
cluster-node-timeout的默認值為15000ms(15s),因此故障轉(zhuǎn)移時間會在20s量級。
5.3 集群的限制及應(yīng)對方法
由于集群中的數(shù)據(jù)分布在不同節(jié)點中,導(dǎo)致一些功能受限,包括:
(1)key批量操作受限:例如mget、mset操作,只有當(dāng)操作的key都位于一個槽時,才能進行。針對該問題,一種思路是在客戶端記錄槽與key的信息,每次針對特定槽執(zhí)行mget/mset;另外一種思路是使用Hash Tag,將在下一小節(jié)介紹。
(2)keys/flushall等操作:keys/flushall等操作可以在任一節(jié)點執(zhí)行,但是結(jié)果只針對當(dāng)前節(jié)點,例如keys操作只返回當(dāng)前節(jié)點的所有鍵。針對該問題,可以在客戶端使用cluster nodes獲取所有節(jié)點信息,并對其中的所有主節(jié)點執(zhí)行keys/flushall等操作。
(3)事務(wù)/Lua腳本:集群支持事務(wù)及Lua腳本,但前提條件是所涉及的key必須在同一個節(jié)點。Hash Tag可以解決該問題。
(4)數(shù)據(jù)庫:單機Redis節(jié)點可以支持16個數(shù)據(jù)庫,集群模式下只支持一個,即db0。
(5)復(fù)制結(jié)構(gòu):只支持一層復(fù)制結(jié)構(gòu),不支持嵌套。
5.4 Hash Tag
Hash Tag原理是:當(dāng)一個key包含 {} 的時候,不對整個key做hash,而僅對 {} 包括的字符串做hash。
Hash Tag可以讓不同的key擁有相同的hash值,從而分配在同一個槽里;這樣針對不同key的批量操作(mget/mset等),以及事務(wù)、Lua腳本等都可以支持。不過Hash Tag可能會帶來數(shù)據(jù)分配不均的問題,這時需要:(1)調(diào)整不同節(jié)點中槽的數(shù)量,使數(shù)據(jù)分布盡量均勻;(2)避免對熱點數(shù)據(jù)使用Hash Tag,導(dǎo)致請求分布不均。
下面是使用Hash Tag的一個例子;通過對product加Hash Tag,可以將所有產(chǎn)品信息放到同一個槽中,便于操作。

5.5 參數(shù)優(yōu)化
cluster_node_timeout
cluster_node_timeout參數(shù)在前面已經(jīng)初步介紹;它的默認值是15s,影響包括:
(1)影響PING消息接收節(jié)點的選擇:值越大對延遲容忍度越高,選擇的接收節(jié)點越少,可以降低帶寬,但會降低收斂速度;應(yīng)根據(jù)帶寬情況和應(yīng)用要求進行調(diào)整。
(2)影響故障轉(zhuǎn)移的判定和時間:值越大,越不容易誤判,但完成轉(zhuǎn)移消耗時間越長;應(yīng)根據(jù)網(wǎng)絡(luò)狀況和應(yīng)用要求進行調(diào)整。
cluster-require-full-coverage
前面提到,只有當(dāng)16384個槽全部分配完畢時,集群才能上線。這樣做是為了保證集群的完整性,但同時也帶來了新的問題:當(dāng)主節(jié)點發(fā)生故障而故障轉(zhuǎn)移尚未完成,原主節(jié)點中的槽不在任何節(jié)點中,此時會集群處于下線狀態(tài),無法響應(yīng)客戶端的請求。
cluster-require-full-coverage參數(shù)可以改變這一設(shè)定:如果設(shè)置為no,則當(dāng)槽沒有完全分配時,集群仍可以上線。參數(shù)默認值為yes,如果應(yīng)用對可用性要求較高,可以修改為no,但需要自己保證槽全部分配。
5.6 redis-trib.rb
redis-trib.rb提供了眾多實用工具:創(chuàng)建集群、增減節(jié)點、槽遷移、檢查完整性、數(shù)據(jù)重新平衡等;通過help命令可以查看詳細信息。在實踐中如果能使用redis-trib.rb工具則盡量使用,不但方便快捷,還可以大大降低出錯概率。