全面剖析Redis Cluster原理和應用 (good)


redis

redis cluster注意的問題 :

1、‘cluster-require-full-coverage’參數的設置。該參數是redis配置文件中cluster模式的一個參數,從字面上基本就能看出它的作用:需要全部覆蓋!
具體點是redis cluster需要16384個slot都正常的時候才能對外提供服務,換句話說,只要任何一個slot異常那么整個cluster不對外提供服務。
redis默認是‘yes’,即需要全覆蓋!建議設置成‘no’。

2、阻塞命令產生failover。由於一些阻塞命令(flushall, del key1 key2 …)會造成redis在‘cluster-node-timeout’時間內無法響應其他節點的ping請求,
從而導致其他節點都把該redis標記出了pfail狀態,進而產生failover。redis作者計划使用lazy redis解決。

3、連接建立。當redis cluster的節點數多了以后,client對每個節點建立一個tcp連接需要花比較多的時間。如果是長連接,用戶只需忍受一次連接建立的過程,
如果是短連接,那么頻繁建立連接將會極大的降低效率。但即便是短連接,只要每次請求只涉及到一個key,有些客戶端可能只需要與一個節點建立連接。

4、Jedis。Jedis是redis最流行的Java客戶端,支持redis cluster。
‘MaxRedirectionsException’異常,出現該異常說明剛剛執行的那條命令經過多次重試,沒有執行成功,需要用戶再次執行。
在cluster擴容或者slot遷移的時候比較容易出現。建議捕獲該異常並采取相應重試工作。
Pipeline,Jedis目前不支持cluster模式的pipeline,建議采用多並發代替pipeline。

5、Multi-key。Redis cluster對多key操作有限,要求命令中所有的key都屬於一個slot,才可以被執行。客戶端可以對multi-key命令進行拆分,再發給redis。
另外一個局限是,在slot遷移過程中,multi-key命令特別容易報錯(CROSSSLOT Keys in request don’t hash to the same slot)。建議不用multi-key命令。

6、擴容速度慢。redis官方提供了redis cluster管理腳本redis-trib.rb。使用該腳本進行擴容cluster的時候,是串行的遷移slot中的每個key,這樣導致了
擴容的速度非常慢,百G的數據要數小時。擴容時間越長,越容易出現異常。

http://www.ithao123.cn/content-10677390.html

先有雞還是先有蛋?

最近有朋友問了一個問題,說畢業后去大城市還是小城市?去大公司還是小公司?我的回答都是大城市!大公司!
為什么這么說呢,你想一下,無論女孩男孩找朋友都喜歡找個子高胸大的。同樣的道理嘛,「大」總有大的好。
當然,如果你要有能力找一個胸大個子高就更完美了。

Redis 集群簡介

Redis 是一個開源的 key-value 存儲系統,由於出眾的性能,大部分互聯網企業都用來做服務器端緩存。Redis 在3.0版本前只支持單實例模式,雖然支持主從模式、哨兵模式部署來解決單點故障,但是現在互聯網企業動輒大幾百G的數據,可完全是沒法滿足業務的需求,所以,Redis 在 3.0 版本以后就推出了集群模式。

Redis 集群采用了P2P的模式,完全去中心化。Redis 把所有的 Key 分成了 16384 個 slot,每個 Redis 實例負責其中一部分 slot 。集群中的所有信息(節點、端口、slot等),都通過節點之間定期的數據交換而更新。
Redis 客戶端可以在任意一個 Redis 實例發出請求,如果所需數據不在該實例中,通過重定向命令引導客戶端訪問所需的實例。

隨隨便便搭建一個集群

安裝部署任何一個應用其實都很簡單,只要安裝步驟一步一步來就行了。下面說一下 Redis 集群搭建規划,由於集群至少需要6個節點(3主3從模式),所以,沒有這么多機器給我玩,我本地也起不了那么多虛擬機(電腦太爛),現在計划是在一台機器上模擬一個集群,當然,這和生產環境的集群搭建沒本質區別。

我現在就要在已經有安裝了 Redis 的一個 CentOS 下開始進行集群搭建,如果你還不是很清楚 Linux 下如何安裝 Redis ,可以去看這一篇文章《了解一下 Redis 並在 CentOS 下進行安裝配置》。請注意,下面所有集群搭建環境都基於已安裝好的 Redis 做的。

1.創建文件夾
我們計划集群中 Redis 節點的端口號為 9001-9006 ,端口號即集群下各實例文件夾。數據存放在 端口號/data 文件夾中。

mkdir_9001-9006

mkdir /usr/local/redis-cluster
cd redis-cluster/
mkdir -p 9001/data 9002/data 9003/data 9004/data 9005/data 9006/data

 

2.復制執行腳本
在 /usr/local/redis-cluster 下創建 bin 文件夾,用來存放集群運行腳本,並把安裝好的 Redis 的 src 路徑下的運行腳本拷貝過來。看命令:

mkdir redis-cluster/bin
cd /usr/local/redis/src
cp mkreleasehdr.sh redis-benchmark redis-check-aof redis-check-dump redis-cli redis-server redis-trib.rb /usr/local/redis-cluster/bin

3.復制一個新 Redis 實例
我們現在從已安裝好的 Redis 中復制一個新的實例到 9001 文件夾,並修改 redis.conf 配置。

mv9001

cp /usr/local/redis/* /usr/local/redis-cluster/9001

 

注意,修改 redis.conf 配置和單點唯一區別是下圖部分,其余還是常規的這幾項:
port 9001(每個節點的端口號)
daemonize yes
bind 192.168.119.131(綁定當前機器 IP)
dir /usr/local/redis-cluster/9001/data/(數據文件存放位置)
pidfile /var/run/redis_9001.pid(pid 9001和port要對應)
cluster-enabled yes(啟動集群模式)
cluster-config-file nodes9001.conf(9001和port要對應)
cluster-node-timeout 15000
appendonly yes
 
        

集群搭建配置重點就是取消下圖中的這三個配置:
cluster_conf

4.再復制出五個新 Redis 實例
我們已經完成了一個節點了,其實接下來就是機械化的再完成另外五個節點,其實可以這么做:把 9001 實例 復制到另外五個文件夾中,唯一要修改的就是 redis.conf 中的所有和端口的相關的信息即可,其實就那么四個位置。開始操作,看圖:

cp9001-9006

\cp -rf /usr/local/redis-cluster/9001/* /usr/local/redis-cluster/9002
\cp -rf /usr/local/redis-cluster/9001/* /usr/local/redis-cluster/9003
\cp -rf /usr/local/redis-cluster/9001/* /usr/local/redis-cluster/9004
\cp -rf /usr/local/redis-cluster/9001/* /usr/local/redis-cluster/9005
\cp -rf /usr/local/redis-cluster/9001/* /usr/local/redis-cluster/9006

\cp -rf 命令是不使用別名來復制,因為 cp 其實是別名 cp -i,操作時會有交互式確認,比較煩人。

5.修改 9002-9006 的 redis.conf 文件
其實非常簡單了,你通過搜索會發現其實只有四個點需要修改,我們全局替換下吧,進入相應的節點文件夾,做替換就好了。命令非常簡單,看圖:

%s-9001-9002g

vim redis.conf
:%s/9001/9002g

回車后,就會有替換幾個地方成功的提示,不放心可以手工檢查下:

%s-success

其實我們也就是替換了下面這四行:

port 9002
dir /usr/local/redis-cluster/9002/data/
cluster-config-file nodes-9002.conf
pidfile /var/run/redis_9002.pid

到這里,我們已經把最基本的環境搞定了,接下來就是啟動了。

其實我們已經幾乎搭建好了

1.啟動 9001-9006 六個節點
少廢話,直接看圖:
redis-server_start

/usr/local/bin/redis-server /usr/local/redis-cluster/9001/redis.conf 
/usr/local/bin/redis-server /usr/local/redis-cluster/9002/redis.conf 
/usr/local/bin/redis-server /usr/local/redis-cluster/9003/redis.conf 
/usr/local/bin/redis-server /usr/local/redis-cluster/9004/redis.conf 
/usr/local/bin/redis-server /usr/local/redis-cluster/9005/redis.conf 
/usr/local/bin/redis-server /usr/local/redis-cluster/9006/redis.conf

 

可以檢查一下是否啟動成功:ps -el | grep redis
看的出來,六個節點已經全部啟動成功了。

2.隨便找一個節點測試試

/usr/local/redis-cluster/bin/redis-cli -h 192.168.119.131 -p 9001

set name mafly

 

redis-server_start_test

連接成功了,但好像報錯了阿???
(error) CLUSTERDOWN Hash slot not served(不提供集群的散列槽),這是什么鬼?
這是因為雖然我們配置並啟動了 Redis 集群服務,但是他們暫時還並不在一個集群中,互相直接發現不了,而且還沒有可存儲的位置,就是所謂的slot(槽)

3.安裝集群所需軟件
由於 Redis 集群需要使用 ruby 命令,所以我們需要安裝 ruby 和相關接口。

yum install ruby
yum install rubygems
gem install redis 

 

yum_ruby

這才是真正的創建集群

先不廢話,直接敲命令:

/usr/local/redis-cluster/bin/redis-trib.rb create --replicas 1 192.168.119.131:9001 192.168.119.131:9002 192.168.119.131:9003 192.168.119.131:9004 192.168.119.131:9005 192.168.119.131:9006

cluster_create

簡單解釋一下這個命令:調用 ruby 命令來進行創建集群,--replicas 1 表示主從復制比例為 1:1,即一個主節點對應一個從節點;然后,默認給我們分配好了每個主節點和對應從節點服務,以及 solt 的大小,因為在 Redis 集群中有且僅有 16383 個 solt ,默認情況會給我們平均分配,當然你可以指定,后續的增減節點也可以重新分配。

M: 10222dee93f6a1700ede9f5424fccd6be0b2fb73 為主節點Id

S: 9ce697e49f47fec47b3dc290042f3cc141ce5aeb 192.168.119.131:9004 replicates 10222dee93f6a1700ede9f5424fccd6be0b2fb73 從節點下對應主節點Id

目前來看,9001-9003 為主節點,9004-9006 為從節點,並向你確認是否同意這么配置。輸入 yes 后,會開始集群創建。

cluster_create_success

上圖則代表集群搭建成功啦!!!

驗證一下:
依然是通過客戶端命令連接上,通過集群命令看一下狀態和節點信息等。

/usr/local/redis-cluster/bin/redis-cli -c -h 192.168.119.131 -p 9001 cluster info cluster nodes

cluster_info

通過命令,可以詳細的看出集群信息和各個節點狀態,主從信息以及連接數、槽信息等。這么看到,我們已經真的把 Redis 集群搭建部署成功啦!

設置一個 mafly:
你會發現,當我們 set name mafly 時,出現了 Redirected to slot 信息並自動連接到了9002節點。這也是集群的一個數據分配特性,這里不詳細說了。

redirected_9002

總結一下

這一篇 Redis 集群部署搭建的文章真的是一步一步的走下來的,只要你安裝我的步驟來,就保證你能成功搭建一個 Redis 集群玩玩,也可以這么說,除了步驟繁瑣外,幾乎不存在技術含量,估計能看完的人都感覺累(說真的,寫這種文章真的很累人)。

接下來可能就是動態擴容、增加節點和減少節點,重新分配槽大小等,當然,還有最重要的就是怎么和我們程序結合起來,以及如何更好的把 Redis 緩存集群發揮出應有的效果,這些才是最重要的。

http://www.cnblogs.com/mafly/p/redis_cluster.html

 

 

redis3.0 cluster功能介紹

redis從3.0開始支持集群功能。redis集群采用無中心節點方式實現,無需proxy代理,客戶端直接與redis集群的每個節點連接,根據同樣的hash算法計算出key對應的slot,然后直接在slot對應的redis上執行命令。在redis看來,響應時間是最苛刻的條件,增加一層帶來的開銷是redis不原因接受的。因此,redis實現了客戶端對節點的直接訪問,為了去中心化,節點之間通過gossip協議交換互相的狀態,以及探測新加入的節點信息。redis集群支持動態加入節點,動態遷移slot,以及自動故障轉移。

集群架構
每個節點都會跟其他節點保持連接,用來交換彼此的信息。節點組成集群的方式使用cluster meet命令,meet命令可以讓兩個節點相互握手,然后通過gossip協議交換信息。如果一個節點r1在集群中,新節點r2加入的時候與r1節點握手,r1節點會把集群內的其他節點信息通過gossip協議發送給r2,r2會一一與這些節點完成握手,從而加入到集群中。
節點在啟動的時候會生成一個全局的標識符,並持久化到配置文件,在節點與其他節點握手后,這些信息也都持久化下來。節點與其他節點通信,標識符是它唯一的標識,而不是IP、PORT地址。如果一個節點移動位置導致IP、PORT地址發生變更,集群內的其他節點能把該節點的IP、PORT地址糾正過來。

集群數據分布
集群數據以數據分布表的方式保存在各個slot上。默認的數據分布表默認含有16384個slot。
key與slot映射使用的CRC16算法,即:slot = CRC16(key) mod 16384。
集群只有在16384個slot都有對應的節點才能正常工作。slot可以動態的分配、刪除和遷移。
每個節點會保存一份數據分布表,節點會將自己的slot信息發送給其他節點,發送的方式使用一個unsigned char的數組,數組長度為16384/8。每個bit標識為0或者1來標識某個slot是否是它負責的。

使用這樣的方式傳輸,就能大大減少數據分布表的字節。這種方式使用的字節為2048,如果純粹的傳遞數據分布表,那邊一個slot至少需要2字節的slot值+2字節port+4字節ip,共8*16384=131072字節,或者以ip、port為鍵,把節點對應的slot放在后面,那么也至少需要2*16384加上節點的數量,也遠遠大於2048字節。由於節點間不停的在傳遞數據分布表,所以為了節省帶寬,redis選擇了只傳遞自己的分布數據。但這樣的方式也會帶來管理方面的麻煩,如果一個節點刪除了自己負責的某個slot,這樣該節點傳遞給其他節點數據分布表的slot標識為0,而redis采用了bitmapTestBit方法,只處理slot為1的節點,而並未把每個slot與收到的數據分布表對比,從而產生了節點間數據分布表視圖的不一致。這種問題目前只能通過使用者來避免。

redis目前還支持slot的遷移,可以把一個slot從一個節點遷移到另一個節點,節點上的數據需要使用者通過cluster getkeysinslot去除遷移slot上的key,然后執行migrate命令一個個遷移到新節點。具體細節會在下面的slot遷移章節介紹。

集群訪問
客戶端在初始化的時候只需要知道一個節點的地址即可,客戶端會先嘗試向這個節點執行命令,比如“get key”,如果key所在的slot剛好在該節點上,則能夠直接執行成功。如果slot不在該節點,則節點會返回MOVED錯誤,同時把該slot對應的節點告訴客戶端。客戶端可以去該節點執行命令。目前客戶端有兩種做法獲取數據分布表,一種就是客戶端每次根據返回的MOVED信息緩存一個slot對應的節點,但是這種做法在初期會經常造成訪問兩次集群。還有一種做法是在節點返回MOVED信息后,通過cluster nodes命令獲取整個數據分布表,這樣就能每次請求到正確的節點,一旦數據分布表發生變化,請求到錯誤的節點,返回MOVED信息后,重新執行cluster nodes命令更新數據分布表。

在訪問集群的時候,節點可能會返回ASK錯誤。這種錯誤是在key對應的slot正在進行數據遷移時產生的,這時候向slot的原節點訪問,如果key在遷移源節點上,則該次命令能直接執行。如果key不在遷移源節點上,則會返回ASK錯誤,描述信息會附上遷移目的節點的地址。客戶端這時候要先向遷移目的節點發送ASKING命令,然后執行之前的命令。

這些細節一般都會被客戶端sdk封裝起來,使用者完全感受不到訪問的是集群還是單節點。

集群支持hash tags功能,即可以把一類key定位到同一個節點,tag的標識目前支持配置,只能使用{},redis處理hash tag的邏輯也很簡單,redis只計算從第一次出現{,到第一次出現}的substring的hash值,substring為空,則仍然計算整個key的值,這樣對於foo{}{bar}、{foo}{bar}、foo這些沖突的{},也能取出tag值。使用者需遵循redis的hash tag規范。

127.0.0.1:6379> CLUSTER KEYSLOT foo{hash_tag}
(integer) 2515
127.0.0.1:6379> CLUSTER KEYSLOT fooadfasdf{hash_tag}
(integer) 2515
127.0.0.1:6379> CLUSTER KEYSLOT fooadfasdfasdfadfasdf{hash_tag}
(integer) 2515
集群版本的redis能夠支持全部的單機版命令。不過還是有些命令會有些限制。

訂閱在使用上與單機版沒有任何區別。訂閱功能通過集群間共享publish消息實現的,客戶端可以向任意一個節點(包括slave節點)訂閱消息,然后在一個節點上執行的publish命令,該節點會把該命令傳播給集群內的每個節點,這樣訂閱該消息的客戶端就能收到該命令。

redis集群版只使用db0,select命令雖然能夠支持select 0。其他的db都會返回錯誤。
127.0.0.1:6379> select 0
OK
127.0.0.1:6379> select 1
(error) ERR SELECT is not allowed in cluster mode
redis集群版對多key命令的支持,只能支持多key都在同一個slot上,即使多個slot在一個節點上也不行。

127.0.0.1:6379> mget key7 key28
(error) CROSSSLOT Keys in request don't hash to the same slot
事務的支持只能在也一個slot上完成。MULTI命令之后的命令的key必須都在同一個slot上,如果某條命令的key對應不在相同的slot上,則事務直接回滾。遷移的時候,在遷移源節點執行命令的key必須在移原節點上存在,否則事務就會回滾。在遷移目的節點執行的時候需要先執行ASKING命令再執行MULTI命令,這樣接下來該slot的命令都能被執行。可以看出,對於單key和相同hash tags的事務,集群還是能很好的支持。

在遷移的時候有個地方需要注意,對於多key命令在遷移目的節點執行時,如果多個key全在該節點上,則命令無法執行。如下所示,key和key14939對應的slot為12539,執行命令的節點是遷移目的節點:

127.0.0.1:6379> asking
OK
127.0.0.1:6379> mget key key14939
(error) TRYAGAIN Multiple keys request during rehashing of slot

集群消息
集群間互相發送消息,使用另外的端口,所有的消息在該端口上完成,可以成為消息總線,這樣可以做到不影響客戶端訪問redis,可見redis對於性能的追求。目前集群有如下幾種消息:

CLUSTERMSG_TYPE_PING:gossip協議的ping消息。
CLUSTERMSG_TYPE_PONG:gossip協議的pong消息。
CLUSTERMSG_TYPE_MEET:握手消息。
CLUSTERMSG_TYPE_FAIL:master節點檢測到,超過半數master認為某master離線,則發送fail消息。
CLUSTERMSG_TYPE_PUBLISH:publish消息,向其他節點推送消息。
CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST:故障轉移時,slave發送向其他master投票請求。
CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK:故障轉移時,其他master回應slave的請求。
CLUSTERMSG_TYPE_UPDATE:通知某節點,它負責的某些slot被另一個節點替換。
CLUSTERMSG_TYPE_MFSTART:手動故障轉移時,slave請求master停止訪問,從而對比兩者的數據偏移量,可以達到一致。
集群節點間相互通信使用了gossip協議的push/pull方式,ping和pong消息,節點會把自己的詳細信息和已經和自己完成握手的3個節點地址發送給對方,詳細信息包括消息類型,集群當前的epoch,節點自己的epoch,節點復制偏移量,節點名稱,節點數據分布表,節點master的名稱,節點地址,節點flag為,節點所處的集群狀態。節點根據自己的epoch和對方的epoch來決定哪些數據需要更新,哪些數據需要告訴對方更新。然后根據對方發送的其他地址信息,來發現新節點的加入,從而和新節點完成握手。

節點默認每秒在集群中的其他節點選擇一個節點,發送ping消息。選擇節點步驟是:

隨機 5 個節點。
跳過斷開連接和已經在ping還沒收到pong響應的節點。
從篩選的節點中選擇最近一次接收pong回復距離現在最舊的節點。
除了常規的選擇節點外,對於那些一直未隨機到節點,redis也有所支持。當有節點距離上一次接收到pong消息超過節點超時配置的一半,節點就會給這些節點發送ping消息。

ping消息會帶上其他節點的信息,選擇其他節點步驟是:

最多選擇3個節點。
最多隨機遍歷6個節點,如果因為一些條件不能被選出,可能會不滿3個。
忽略自己。
忽略正在握手的節點。
忽略帶有 NOADDR 標識的節點。
忽略連接斷開而且沒有負責任何slot的節點。
ping消息會把發送節點的ping_sent改成當前時間,直到接收到pong消息,才更新ping_sent為0。當ping消息發送后,超過節點超時配置的一半,就會把發送節點的連接斷開。超過節點超時配置,就會認為該節點已經下線。

接收到某個節點發來的ping或者pong消息,節點會更新對接收節點的認識。比如該節點主從角色是否變化,該節點負責的slot是否變化,然后獲取消息帶上的節點信息,處理新節點的加入。

slot更新這里要細說下。假設這里是節點A接收到節點B的消息。A節點會取出保存的B節點的分布表與消息的分布表進行對比,B節點如果是slave,則比較的是A節點保存的B的master的分布表和消息中的分布表。比較只是使用memcmp簡單的比較兩份數據是否一樣,一樣則無需處理,不一樣就處理不一致的slot。更新的時候,這里只是處理消息中分布表為1的slot。如果和A節點保持一致或者該slot正准備遷移到A節點,則繼續處理。如果slot產生了沖突,則以epoch大的為准。如果沖突的slot中有A自己負責的節點,而且B比A的epoch大導致需要更新slot為B負責,此時A負責的slot為0的時候,可以認為B是A的slave。這種情況經常發生在A由原來的master變成slave,B提升為master的場景下。

前面說到slot更新的時候,如果B比A的epoch大,則A更新對slot的認識。如果A比B的epoch大, 在redis接下來的邏輯會再次處理,A會給B發送update消息,B收到A發送的update消息,執行slot更新方法。這種情況也經常發生在主從切換的時候。第一種情況發生在新master把數據分布表推給舊master。第二種情況發生在舊master給新master發消息的時候,新master給舊master發送update消息。

slot遷移
redis cluster支持slot的動態遷移,遷移需要按照指定步驟進行,不然可能破壞當前的集群數據分布表。cluster setslot <slot> IMPORTING <node ID>命令在遷移目的節點執行,表示需要把該slot遷移到本節點。redis的cluster setslot命令提供了對遷移的支持。cluster setslot <slot> MIGRATING <node ID>命令在遷移源節點執行,表示需要把該slot遷出。cluster setslot <slot> NODE <node ID>在遷移完成后在遷移源節點和遷移目的節點執行后,表示遷移完成,數據分布表恢復穩定。如果需要取消遷移操作,在遷移源節點和遷移目的節點上執行cluster setslot <slot> STABLE。

下面先來看一下完整的遷移流程:

在遷移目的節點執行cluster setslot <slot> IMPORTING <node ID>命令,指明需要遷移的slot和遷移源節點。
在遷移源節點執行cluster setslot <slot> MIGRATING <node ID>命令,指明需要遷移的slot和遷移目的節點。
在遷移源節點執行cluster getkeysinslot獲取該slot的key列表。
在遷移源節點執行對每個key執行migrate命令,該命令會同步把該key遷移到目的節點。
在遷移源節點反復執行cluster getkeysinslot命令,直到該slot的列表為空。
在遷移源節點和目的節點執行cluster setslot <slot> NODE <node ID>,完成遷移操作。

遷移過程中該slot允許繼續有流量進來,redis保證了遷移過程中slot的正常訪問。在遷移過程中,對於該slot的請求,如果key在源節點,則表示該key還沒有遷移到目的節點。源節點會返回ASK錯誤,告訴客戶端去遷移目的節點請求。這樣新的key就直接寫入到遷移目的節點了。客戶端寫入目的節點前需要發送ASKING命令,告訴遷移目的節點我是寫入增量數據,沒有ASKING命令,遷移目的節點會不認這次請求,返回MOVED錯誤,告訴客戶端去遷移源節點請求。

憑借ASK機制和migrate命令,redis能保證slot的全量數據和增量數據都能導入目的節點。因為對於源節點返回了ASK錯誤,就能保證該key不在源節點上,那么它只會出現在目的節點或者不存在。所以客戶端獲取ASK錯誤到向目的節點無需保證原子性。然后migrate命令是個原子操作,它會等待目的節點寫入成功才在源節點返回,保證了遷移期間不接受其他請求,從一個外部客戶端的視角來看,在某個時間點上,遷移的鍵要么存在於源節點,要么存在於目的節點,但不會同時存在於源節點和目的節點。

遷移過程中幾乎不影響用戶使用,除了在多key的命令在遷移目的節點無法執行,這個在集群訪問已經說明。

不過由於migrate命令是同步阻塞執行的,所以如果key對應的value很大,會增加阻塞時間,特別對於list、set、zset等結構如果value很大的話,redis並不關心這些結構的長度,而是直接以key為單位一次性遷移。同時遷移過程中大量執行migrate命令,會增加客戶端的響應時間。

遷移的時候在master出現異常的時候,遷移工作需要做些處理。
如果在遷移過程中,源節點宕機,此時需做如下調整:
目的節點執行cluster setslot <slot> IMPORTING <node ID>命令,node ID為源節點group的新master
源節點group內的新master執行cluster setslot <slot> MIGRATING <node ID>命令,遷移該slot到目的節點。
這樣可以繼續完成遷移操作。

如果在遷移過程中,目的節點宕機,此時需做如下調整:
目的節點group內的新master執行cluster setslot <slot> IMPORTING <node ID>命令,node ID為源節點。
源節點執行cluster setslot <slot> MIGRATING <node ID>命令,遷移該slot到目的節點group內的新master。
這樣也可以繼續完成遷移操作。

主從復制
集群間節點支持主從關系,復制的邏輯基本復用了單機版的實現。不過還是有些地方需要注意。
首先集群間節點建立主從關系不再使用原有的SLAVEOF命令和SLAVEOF配置,而是通過cluster replicate命令,這保證了主從節點需要先完成握手,才能建立主從關系。
集群是不能組成鏈式主從關系的,也就是說從節點不能有自己的從節點。不過對於集群外的沒開啟集群功能的節點,redis並不干預這些節點去復制集群內的節點,但是在集群故障轉移時,這些集群外的節點,集群不會處理。
集群內節點想要復制另一個節點,需要保證本節點不再負責任何slot,不然redis也是不允許的。
集群內的從節點在與其他節點通信的時候,傳遞的消息中數據分布表和epoch是master的值。

故障轉移

集群主節點出現故障,發生故障轉移時,其他主節點會把故障主節點的從節點自動提為主節點,原來的主節點恢復后,自動成為新主節點的從節點。

這里先說明,把一個master和它的全部slave描述為一個group,故障轉移是以group為單位的,集群故障轉移的方式跟sentinel的實現很類似。某個節點一段時間沒收到心跳響應,則集群內的master會把該節點標記為pfail,類似sentinel的sdown。集群間的節點會交換相互的認識,超過一半master認為該異常master宕機,則這些master把異常master標記為fail,類似sentinel的odown。fail消息會被master廣播出來。group的slave收到fail消息后開始競選成為master。競選的方式跟sentinel選主的方式類似,都是使用了raft協議,slave會從其他的master拉取選票,票數最多的slave被選為新的master,新master會馬上給集群內的其他節點發送pong消息,告知自己角色的提升。其他slave接着開始復制新master。等舊master上線后,發現新master的epoch高於自己,通過gossip消息交互,把自己變成了slave。大致就是這么個流程。自動故障轉移的方式跟sentinel很像,

redis還支持手動的故障轉移,即通過在slave上執行cluster failover命令,可以讓slave提升為master。failover命令支持傳入FORCE和TAKEOVER參數。

FORCE:使用FORCE參數與sentinel的手動故障轉移流程基本類似,強制開始一次故障轉移。
不傳入額外參數:如果主節點異常,則不能進行failover,主節點正常的情況下需要先比較從節點和主節點的偏移量,此時會讓主節點停止客戶端請求,直到超時或者故障轉移完成。主從偏移量相同后開始手動故障轉移流程。
TAKEOVER:這種手動故障轉移的方式比較暴力,slave直接提升自己的epoch為最大的epoch。並把自己變成master。這樣在消息交互過程中,舊master能發現自己的epoch小於該slave,同時兩者負責的slot一致,它會把自己降級為slave。

均衡集群的slave(Replica migration)

在集群運行過程中,有的master的slave宕機,導致了該master成為孤兒master(orphaned masters),而有的master有很多slave。此處孤兒master的定義是那些本來有slave,但是全部離線的master,對於那些原來就沒有slave的master不能認為是孤兒master。redis集群支持均衡slave功能,官方稱為Replica migration,而我覺得均衡集群的slave更好理解該概念。集群能把某個slave較多的group上的slave遷移到那些孤兒master上,該功能通過cluster-migration-barrier參數配置,默認為1。slave在每次定時任務都會檢查是否需要遷移slave,即把自己變成孤兒master的slave。 滿足以下條件,slave就會成為孤兒master的slave:

自己所在的group是slave最多的group。
目前存在孤兒master。
自己所在的group的slave數目至少超過2個,只有自己一個的話遷移到其他group,自己原來的group的master又成了孤兒master。
自己所在的group的slave數量大於cluster-migration-barrier配置。
與group內的其他slave基於memcmp比較node id,自己的node id最小。這個可以防止多個slave並發復制孤兒master,從而原來的group失去過多的slave。

網絡分區說明

redis的集群模式下,客戶端需要和全部的節點保持連接,這樣可能出現網絡分區問題,客戶端和一些節點在一個網絡分區,另一部分節點在另一個網絡分區。在分區期間,客戶端仍然能執行命令,直到集群經過cluster-node-timeout發現分區情況,節點探測到有slot無法提供服務,才開始禁止客戶端執行命令。

這時候會出現一種現象,假設客戶端和一個master在小分區,其他節點在大分區。超時后,其他節點共同投票把group內的一個slave提為master,等分區恢復。舊的master會成為新master的slave。這樣在cluster-node-timeout期間對舊master的寫入數據都會丟失。
這個問題可以通過設置cluster-node-timeout來減少不一致。如果對一致性要求高的應用還可以通過min-slaves-to-write配置來提高寫入的要求。

slot保存key列表

redis提供了cluster countkeysinslot和cluster getkeysinslot命令,可以獲得某個slot的全部key列表。通過該列表,可以實現slot的遷移。該功能是通過skiplist實現的,skiplist是redis內部用來實現zset的數據結構,在slot保持key的時候也派上了用場。redis所有在db層對hash表的操作,也會在skiplist上執行相應的操作。比如往hash表增加數據,redis也會往skiplist也寫一份數據,該skiplist的score就是slot的值,value對應了具體的key。這等於是redis在數據分布表上冗余了所有的key。不過相比skiplist所帶來遷移的方便,冗余的結果是可以接受的,這也期望客戶端,不要使用過長的key,從而增加內存的消耗。

附錄1:集群相關命令

cluster meet ip:port
集群間相互握手,加入彼此所在的集群。(將指定節點加入到集群)

cluster nodes
獲取集群間節點信息的列表,如下所示,格式為<node ID> <node IP:PORT> <node role> [master node ID|-] <node ping_sent> <node pong_received> <node epoch> <node status>。

127.0.0.1:6379> cluster nodes
a15705fdb7cac60e07ff699bf4c514e80f245a2c 10.180.157.205:6379 slave 2b5603326d0fca28031467727fae4558115a99d8 0 1450854214289 11 connected
6477541e4594e60e095c8f440882636236545936 10.180.157.202:6379 slave 9b35a393fa6623887215023b761d531dde452d3c 0 1450854211276 12 connected
ecf9ae60e87ea3358d9c5f1f269e0ed9a387ea40 10.180.157.201:6379 master - 0 1450854214788 5 connected 10923-16383
2b5603326d0fca28031467727fae4558115a99d8 10.180.157.200:6379 master - 0 1450854213283 11 connected 5461-10922
f31f6ce49b3a2f3a246b2d97349c8f8614cf3a2c 10.180.157.208:6379 slave ecf9ae60e87ea3358d9c5f1f269e0ed9a387ea40 0 1450854212286 9 connected
9b35a393fa6623887215023b761d531dde452d3c 10.180.157.199:6379 myself,master - 0 0 12 connected 0-5460

cluster myid
返回節點的id。
127.0.0.1:6379> cluster myid
"9b35a393fa6623887215023b761d531dde452d3c"

cluster slots
返回集群間節點負責的數據分布表。
127.0.0.1:6379> cluster slots
1) 1) (integer) 10923
2) (integer) 16383
3) 1) "10.180.157.201"
2) (integer) 6379
4) 1) "10.180.157.208"
2) (integer) 6379
2) 1) (integer) 5461
2) (integer) 10922
3) 1) "10.180.157.200"
2) (integer) 6379
4) 1) "10.180.157.205"
2) (integer) 6379
3) 1) (integer) 0
2) (integer) 5460
3) 1) "10.180.157.199"
2) (integer) 6379
4) 1) "10.180.157.202"
2) (integer) 6379

cluster flushslots
清空該節點負責slots,必須在節點負責的這些slot都沒有數據的情況下才能執行,該命令需要謹慎使用,由於之前說的bitmapTestBit方法,redis只比較負責的節點,清空的slots信息無法被其他節點同步。

cluster addslots [slot]
在當前節點上增加slot。(將指定的一個或多個slot 指派給當前節點)

cluster delslots [slot]
在節點上取消slot的負責。這也會導致前面說的slot信息無法同步,而且一旦集群有slot不負責,配置cluster-require-full-coverage為yes的話,該節點就無法提供服務了,所以使用也需謹慎。

cluster setslot <slot> MIGRATING <nodeid>
把本節點負責的某個slot設置為遷移到目的節點。(即將本節點的slot指派給或叫遷移到指定的節點)

cluster setslot <slot> IMPORTING <nodeid>
設置某個slot為從遷移源節點遷移標志。(即將指定節點的slot指派給或叫遷移到本節點)

cluster setslot <slot> STABLE
設置某個slot為從遷移狀態恢復為正常狀態。(取消slot的導入(importing)或遷移(migrating))

cluster setslot <slot> NODE <nodeid>
設置某個slot為某節點負責。該命令使用也需要注意,cluster setslot的四個命令需要配置遷移工具使用,單獨使用容易引起集群混亂。該命令在集群出現異常時,需要指定某個slot為某個節點負責時,最好在每個節點上都執行一遍,至少要在遷移的節點和最高epoch的節點上執行成功。(將指定的slot指派給指定的節點,如果該slot已經指派給另一個節點,則要另一個節點先刪除該slot)

cluster info
集群的一些info信息。
127.0.0.1:6379> cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:12
cluster_my_epoch:12
cluster_stats_messages_sent:1449982
cluster_stats_messages_received:1182698

cluster saveconfig
保存集群的配置文件,集群默認在配置修改的時候會自動保存配置文件,該方法也能手動執行命令保存。

cluster keyslot
可以查詢某個key對應的slot地址。
127.0.0.1:6379> cluster keyslot key
(integer) 12539

cluster countkeysinslot
可以查詢該節點負責的某個slot內部key的數量。
127.0.0.1:6379> cluster countkeysinslot 13252
(integer) 2

cluster getkeysinslot <slot> <count>
可以查詢該節點負責的某個slot內部指定數量的key列表。
127.0.0.1:6379> cluster getkeysinslot 13252 10
1) "key0"
2) "key2298"

cluster forget
把某個節點加入黑名單,這樣就無法完成握手。黑名單的過期時為60s,60s后兩節點又會繼續完成握手。

cluster replicate <nodeid>
負責某個節點,成為它的slave。(將當前節點設置為指定nodeid節點的從節點)

cluster slaves
列出某個節點slave列表。
127.0.0.1:6379> cluster slaves 2b5603326d0fca28031467727fae4558115a99d8
1) "a15705fdb7cac60e07ff699bf4c514e80f245a2c 10.180.157.205:6379 slave 2b5603326d0fca28031467727fae4558115a99d8 0 1450854932667 11 connected"

cluster count-failure-reports
列出某個節點的故障轉移記錄的長度。

cluster failover [FORCE|TAKEOVER]
手動執行故障轉移。

cluster set-config-epoch
設置節點epoch,只有在節點加入集群前才能設置。

cluster reset [SOFT|HARD]
重置集群信息,soft是清空其他節點的信息,但不修改自己的id。hard還會修改自己的id。不傳該參數則使用soft方式。

readonly
在slave上執行,執行該命令后,可以在slave上執行只讀命令。

readwrite
在slave上執行,執行該命令后,取消在slave上執行命令。

附錄2:集群相關配置
cluster-enabled
說明:集群開關,默認是不開啟集群模式。
默認值:no。
是否可以動態修改:no。
值的范圍:yes|no。

cluster-config-file
說明:集群配置文件的名稱,每個節點都有一個集群相關的配置文件,持久化保存集群的信息。
默認值:”nodes.conf”。
是否可以動態修改:no。
值的范圍:文件路徑。

cluster-node-timeout
說明:節點的超時時間,單位是毫秒。
默認值:15000。
是否可以動態修改:yes。
值的范圍:大於0。

cluster-slave-validity-factor
說明:在進行故障轉移的時候,group的全部slave都會請求申請為master,但是有些slave可能與master斷開連接一段時間了,導致數據過於陳舊,這樣的slave不應該被提升為master。該參數就是用來判斷slave節點與master斷線的時間是否過長。判斷方法是比較slave斷開連接的時間和(node-timeout * slave-validity-factor) + repl-ping-slave-period。
默認值:10。
是否可以動態修改:yes。
值的范圍:大於等於0。

cluster-migration-barrier
說明:master的slave數量大於該值,slave才能遷移到其他孤兒master上,具體說明見均衡集群的slave章節。
默認值:1。
是否可以動態修改:yes。
值的范圍:大於等於0。

cluster-require-full-coverage
說明:默認情況下,集群全部的slot有節點負責,集群狀態才為ok,才能提供服務。設置為no,可以在slot沒有全部分配的時候提供服務。不建議打開該配置,這樣會造成分區的時候,小分區的master一直在接受寫請求,而造成很長時間數據不一致。
默認值:yes。
是否可以動態修改:yes。
值的范圍:yes|no。

 

 

 

 

1.Redis Cluster總覽

1.1 設計原則和初衷

官方文檔Cluster Spec中,作者詳細介紹了Redis集群為什么要設計成現在的樣子。最核心的目標有三個:

  1. 性能:這是Redis賴以生存的看家本領,增加集群功能后當然不能對性能產生太大影響,所以Redis采取了P2P而非Proxy方式、異步復制、客戶端重定向等設計,而犧牲了部分的一致性、使用性。
  2. 水平擴展:集群的最重要能力當然是擴展,文檔中稱可以線性擴展到1000結點。
  3. 可用性:在Cluster推出之前,可用性要靠Sentinel保證。有了集群之后也自動具有了Sentinel的監控和自動Failover能力。

1.2 架構變化與CAP理論

Redis Cluster集群功能推出已經有一段時間了。在單機版的Redis中,每個Master之間是沒有任何通信的,所以我們一般在Jedis客戶端或者Codis這樣的代理中做Pre-sharding。按照CAP理論來說,單機版的Redis屬於保證CP(Consistency & Partition-Tolerancy)而犧牲A(Availability),也就說Redis能夠保證所有用戶看到相同的數據(一致性,因為Redis不自動冗余數據)和網絡通信出問題時,暫時隔離開的子系統能繼續運行(分區容忍性,因為Master之間沒有直接關系,不需要通信),但是不保證某些結點故障時,所有請求都能被響應(可用性,某個Master結點掛了的話,那么它上面分片的數據就無法訪問了)。

有了Cluster功能后,Redis從一個單純的NoSQL內存數據庫變成了分布式NoSQL數據庫,CAP模型也從CP變成了AP。也就是說,通過自動分片和冗余數據,Redis具有了真正的分布式能力,某個結點掛了的話,因為數據在其他結點上有備份,所以其他結點頂上來就可以繼續提供服務,保證了Availability。然而,也正因為這一點,Redis無法保證曾經的強一致性了。這也是CAP理論要求的,三者只能取其二。

關於CAP理論的通俗講解,請參考我的譯文《可能是CAP理論的最好解釋 》。簡單分析了Redis在架構上的變化后,咱們就一起來體驗一下Redis Cluster功能吧!


2.Redis集群初探

Redis的安裝很簡單,以前已經介紹過,就不詳細說了。關於Redis Cluster的基礎知識之前也有過整理,請參考《Redis集群功能預覽》。如果需要全面的了解,那一定要看官方文檔Cluster Tutorial,只看這一個就夠了!

2.1 集群配置

要想開啟Redis Cluster模式,有幾項配置是必須的。此外為了方便使用和后續的測試,我還額外做了一些配置:

  • 綁定地址:bind 192.168.XXX.XXX。不能綁定到127.0.0.1或localhost,否則指導客戶端重定向時會報”Connection refused”的錯誤。
  • 開啟Cluster:cluster-enabled yes
  • 集群配置文件:cluster-config-file nodes-7000.conf。這個配置文件不是要我們去配的,而是Redis運行時保存配置的文件,所以我們也不可以修改這個文件。
  • 集群超時時間:cluster-node-timeout 15000。結點超時多久則認為它宕機了。
  • 槽是否全覆蓋:cluster-require-full-coverage no。默認是yes,只要有結點宕機導致16384個槽沒全被覆蓋,整個集群就全部停止服務,所以一定要改為no
  • 后台運行:daemonize yes
  • 輸出日志:logfile “./redis.log”
  • 監聽端口:port 7000

配置好后,根據我們的集群規模,拷貝出來幾份同樣的配置文件,唯一不同的就是監聽端口,可以依次改為7001、7002… 因為Redis Cluster如果數據冗余是1的話,至少要3個Master和3個Slave,所以我們拷貝出6個實例的配置文件。為了避免相互影響,為6個實例的配置文件建立獨立的文件夾。

[root@8gVm redis-3.0.4]# pwd /root/Software/redis-3.0.4 [root@8gVm redis-3.0.4]# tree -I "*log|nodes*" cfg-cluster/ cfg-cluster/ ├── 7000 │ └── redis.conf.7000 ├── 7001 │ └── redis.conf.7001 ├── 7002 │ └── redis.conf.7002 ├── 7003 │ └── redis.conf.7003 ├── 7004 │ └── redis.conf.7004 └── 7005 └── redis.conf.7005 6 directories, 6 files

2.2 redis-trib管理器

Redis作者應該是個Ruby愛好者,Ruby客戶端就是他開發的。這次集群的管理功能沒有嵌入到Redis代碼中,於是作者又順手寫了個叫做redis-trib的管理腳本。redis-trib依賴Ruby和RubyGems,以及redis擴展。可以先用which命令查看是否已安裝ruby和rubygems,用gem list –local查看本地是否已安裝redis擴展。

最簡便的方法就是用apt或yum包管理器安裝RubyGems后執行gem install redis。如果網絡或環境受限的話,可以手動安裝RubyGems和redis擴展(國外鏈接可能無法下載,可以從CSDN下載):

[root@8gVm Software]# wget https://github.com/rubygems/rubygems/releases/download/v2.2.3/rubygems-2.2.3.tgz [root@8gVm Software]# tar xzvf rubygems-2.2.3.tgz [root@8gVm Software]# cd rubygems-2.2.3 [root@8gVm rubygems-2.2.3]# ruby setup.rb --no-rdoc --no-ri [root@8gVm Software]# wget https://rubygems.org/downloads/redis-3.2.1.gem [root@8gVm Software]# gem install redis-3.2.1.gem --local --no-rdoc --no-ri Successfully installed redis-3.2.1 1 gem installed

2.3 集群建立

首先,啟動我們配置好的6個Redis實例。

[root@8gVm redis-3.0.4]# for ((i=0; i<6; ++i)) > do > cd cfg-cluster/700$i && ../../src/redis-server redis.conf.700$i && cd - > done

此時6個實例還沒有形成集群,現在用redis-trb.rb管理腳本建立起集群。可以看到,redis-trib默認用前3個實例作為Master,后3個作為Slave。因為Redis基於Master-Slave做數據備份,而非像Cassandra或Hazelcast一樣不區分結點角色,自動復制並分配Slot的位置到各個結點

[root@8gVm redis-3.0.4]# src/redis-trib.rb create --replicas 1 192.168.1.100:7000 192.168.1.100:7001 192.168.1.100:7002 192.168.1.100:7003 192.168.1.100:7004 192.168.1.100:7005 >>> Creating cluster Connecting to node 192.168.1.100:7000: OK Connecting to node 192.168.1.100:7001: OK Connecting to node 192.168.1.100:7002: OK Connecting to node 192.168.1.100:7003: OK Connecting to node 192.168.1.100:7004: OK Connecting to node 192.168.1.100:7005: OK >>> Performing hash slots allocation on 6 nodes... Using 3 masters: 192.168.1.100:7000 192.168.1.100:7001 192.168.1.100:7002 Adding replica 192.168.1.100:7003 to 192.168.1.100:7000 Adding replica 192.168.1.100:7004 to 192.168.1.100:7001 Adding replica 192.168.1.100:7005 to 192.168.1.100:7002 ... Can I set the above configuration? (type 'yes' to accept): yes >>> Nodes configuration updated >>> Assign a different config epoch to each node >>> Sending CLUSTER MEET messages to join the cluster Waiting for the cluster to join.... >>> Performing Cluster Check (using node 192.168.1.100:7000) ... [OK] All nodes agree about slots configuration. >>> Check for open slots... >>> Check slots coverage... [OK] All 16384 slots covered.

至此,集群就已經建立成功了!“貼心”的Redis還在utils/create-cluster下提供了一個create-cluster腳本,能夠創建出一個集群,類似我們上面建立起的3主3從的集群。

2.4 簡單測試

我們連接到集群中的任意一個結點,啟動redis-cli時要加-c選項,存取兩個Key-Value感受一下Redis久違的集群功能。

[root@8gVm redis-3.0.4]# src/redis-cli -c -h 192.168.1.100 -p 7000 192.168.1.100:7000> set foo bar -> Redirected to slot [12182] located at 192.168.1.100:7002 OK 192.168.1.100:7002> set hello world -> Redirected to slot [866] located at 192.168.1.100:7000 OK 192.168.1.100:7000> get foo -> Redirected to slot [12182] located at 192.168.1.100:7002 "bar" 192.168.1.100:7002> get hello -> Redirected to slot [866] located at 192.168.1.100:7000 "world"

仔細觀察能夠注意到,redis-cli根據指示,不斷在7000和7002結點之前重定向跳轉。如果啟動時不加-c選項的話,就能看到以錯誤形式顯示出的MOVED重定向消息。

[root@8gVm redis-3.0.4]# src/redis-cli -h 192.168.1.100 -p 7000 192.168.1.100:7000> get foo (error) MOVED 12182 192.168.1.100:7002

2.5 集群重啟

目前redis-trib的功能還比較弱,需要重啟集群的話先手動kill掉各個進程,然后重新啟動就可以了。這也有點太… 網上有人重啟后會碰到問題,我還比較幸運,這種“土鱉”的方式重啟試了兩次還沒發現問題。

[root@8gVm redis-3.0.4]# ps -ef | grep redis | awk '{print $2}' | xargs kill

 


3.高級功能嘗鮮

說是“高級功能”,其實在其他分布式系統中早就都有實現了,只不過在Redis世界里是比較新鮮的。本部分主要試驗一下Redis Cluster中的數據遷移(Resharding)和故障轉移功能。

3.1 數據遷移

本小節我們體驗一下Redis集群的Resharding功能!

3.1.1 創建測試數據

首先保存foo1~10共10個Key-Value作為測試數據。

[root@8gVm redis-3.0.4]# for ((i=0; i<10; ++i)) > do > src/redis-cli -c -h 192.168.1.100 -p 7000 set foo$i bar > done [root@8gVm redis-3.0.4]# src/redis-cli -c -h 192.168.1.100 -p 7000 192.168.1.100:7000> keys * 1) "foo6" 2) "foo7" 3) "foo3" 4) "foo2" 192.168.1.100:7000> get foo4 -> Redirected to slot [9426] located at 192.168.1.100:7001 "bar" 192.168.1.100:7001> keys * 1) "foo4" 2) "foo8" 192.168.1.100:7001> get foo5 -> Redirected to slot [13555] located at 192.168.1.100:7002 "bar" 192.168.1.100:7002> keys * 1) "foo5" 2) "foo1" 3) "foo10" 4) "foo9"

3.1.2 啟動新結點

參照之前的方法新拷貝出兩份redis.conf配置文件redis.conf.7010和7011,與之前結點的配置文件做一下區分。啟動新的兩個Redis實例之后,通過redis-trib.rb腳本添加新的Master和Slave到集群中。

[root@8gVm redis-3.0.4]# cd cfg-cluster/7010 && ../../src/redis-server redis.conf.7010 && cd - [root@8gVm redis-3.0.4]# cd cfg-cluster/7011 && ../../src/redis-server redis.conf.7011 && cd -

3.1.3 添加到集群

使用redis-trib.rb add-node分別將兩個新結點添加到集群中,一個作為Master,一個作為其Slave。

[root@8gVm redis-3.0.4]# src/redis-trib.rb add-node 192.168.1.100:7010 192.168.1.100:7000 >>> Adding node 192.168.1.100:7010 to cluster 192.168.1.100:7000 Connecting to node 192.168.1.100:7000: OK Connecting to node 192.168.1.100:7001: OK Connecting to node 192.168.1.100:7002: OK Connecting to node 192.168.1.100:7005: OK Connecting to node 192.168.1.100:7003: OK Connecting to node 192.168.1.100:7004: OK >>> Performing Cluster Check (using node 192.168.1.100:7000) ... [OK] All nodes agree about slots configuration. >>> Check for open slots... >>> Check slots coverage... [OK] All 16384 slots covered. Connecting to node 192.168.1.100:7010: OK >>> Send CLUSTER MEET to node 192.168.1.100:7010 to make it join the cluster. [OK] New node added correctly. [root@8gVm redis-3.0.4]# src/redis-cli -c -h 192.168.1.100 -p 7000 cluster nodes 0d1f9c979684e0bffc8230c7bb6c7c0d37d8a5a9 192.168.1.100:7010 master - 0 1442452249525 0 connected ... [root@8gVm redis-3.0.4]# src/redis-trib.rb add-node --slave --master-id 0d1f9c979684e0bffc8230c7bb6c7c0d37d8a5a9 192.168.1.100:7011 192.168.1.100:7000 >>> Adding node 192.168.1.100:7011 to cluster 192.168.1.100:7000 Connecting to node 192.168.1.100:7000: OK Connecting to node 192.168.1.100:7010: OK Connecting to node 192.168.1.100:7001: OK Connecting to node 192.168.1.100:7002: OK Connecting to node 192.168.1.100:7005: OK Connecting to node 192.168.1.100:7003: OK Connecting to node 192.168.1.100:7004: OK >>> Performing Cluster Check (using node 192.168.1.100:7000) ... [OK] All nodes agree about slots configuration. >>> Check for open slots... >>> Check slots coverage... [OK] All 16384 slots covered. Connecting to node 192.168.1.100:7011: OK >>> Send CLUSTER MEET to node 192.168.1.100:7011 to make it join the cluster. Waiting for the cluster to join. >>> Configure node as replica of 192.168.1.100:7010. [OK] New node added correctly.

3.1.4 Resharding

通過redis-trib.rb reshard可以交互式地遷移Slot。下面的例子將5000個Slot從7000~7002遷移到7010上。也可以通過./redis-trib.rb reshard <host>:<port> --from <node-id> --to <node-id> --slots --yes在程序中自動完成遷移。

[root@8gVm redis-3.0.4]# src/redis-trib.rb reshard 192.168.1.100:7000 Connecting to node 192.168.1.100:7000: OK Connecting to node 192.168.1.100:7010: OK Connecting to node 192.168.1.100:7001: OK Connecting to node 192.168.1.100:7002: OK Connecting to node 192.168.1.100:7005: OK Connecting to node 192.168.1.100:7011: OK Connecting to node 192.168.1.100:7003: OK Connecting to node 192.168.1.100:7004: OK >>> Performing Cluster Check (using node 192.168.1.100:7000) M: b2036adda128b2eeffa36c3a2056444d23b548a8 192.168.1.100:7000 slots:0-5460 (4128 slots) master 1 additional replica(s) M: 0d1f9c979684e0bffc8230c7bb6c7c0d37d8a5a9 192.168.1.100:7010 slots:0 (4000 slots) master 1 additional replica(s) ... [OK] All nodes agree about slots configuration. >>> Check for open slots... >>> Check slots coverage... [OK] All 16384 slots covered. How many slots do you want to move (from 1 to 16384)? 5000 What is the receiving node ID? 0d1f9c979684e0bffc8230c7bb6c7c0d37d8a5a9 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 [root@8gVm redis-3.0.4]# src/redis-cli -c -h 192.168.1.100 -p 7000 cluster nodes 0d1f9c979684e0bffc8230c7bb6c7c0d37d8a5a9 192.168.1.100:7010 master - 0 1442455872019 7 connected 0-1332 5461-6794 10923-12255 b2036adda128b2eeffa36c3a2056444d23b548a8 192.168.1.100:7000 myself,master - 0 0 1 connected 1333-5460 b5ab302f5c2395e3c8194c354a85d02f89bace62 192.168.1.100:7001 master - 0 1442455875022 2 connected 6795-10922 0c565e207ce3118470fd5ed3c806eb78f1fdfc01 192.168.1.100:7002 master - 0 1442455874521 3 connected 12256-16383 ...

 

遷移完成后,查看之前保存的foo1~10的分布情況,可以看到部分Key已經遷移到了新的結點7010上。

[root@8gVm redis-3.0.4]# src/redis-cli -c -h 192.168.1.100 -p 7000 keys "*" 1) "foo3" 2) "foo7" [root@8gVm redis-3.0.4]# src/redis-cli -c -h 192.168.1.100 -p 7001 keys "*" 1) "foo4" 2) "foo8" 3) "foo0" [root@8gVm redis-3.0.4]# src/redis-cli -c -h 192.168.1.100 -p 7002 keys "*" 1) "foo1" 2) "foo9" 3) "foo5" [root@8gVm redis-3.0.4]# src/redis-cli -c -h 192.168.1.100 -p 7010 keys "*" 1) "foo6" 2) "foo2"

3.2 故障轉移

在高可用性方面,Redis可算是能夠”Auto”一把了!Redis Cluster重用了Sentinel的代碼邏輯,不需要單獨啟動一個Sentinel集群,Redis Cluster本身就能自動進行Master選舉和Failover切換

下面我們故意kill掉7010結點,之后可以看到結點狀態變成了fail,而Slave 7011被選舉為新的Master。

[root@8gVm redis-3.0.4]# kill 43637 [root@8gVm redis-3.0.4]# src/redis-cli -c -h 192.168.1.100 -p 7000 cluster nodes 0d1f9c979684e0bffc8230c7bb6c7c0d37d8a5a9 192.168.1.100:7010 master,fail - 1442456829380 1442456825674 7 disconnected b2036adda128b2eeffa36c3a2056444d23b548a8 192.168.1.100:7000 myself,master - 0 0 1 connected 1333-5460 b5ab302f5c2395e3c8194c354a85d02f89bace62 192.168.1.100:7001 master - 0 1442456848722 2 connected 6795-10922 0c565e207ce3118470fd5ed3c806eb78f1fdfc01 192.168.1.100:7002 master - 0 1442456846717 3 connected 12256-16383 5a3c67248b1df554fbf2c93112ba429f31b1d3d1 192.168.1.100:7005 slave 0c565e207ce3118470fd5ed3c806eb78f1fdfc01 0 1442456847720 6 connected 99bff22b97119cf158d225c2b450732a1c0d3c44 192.168.1.100:7011 master - 0 1442456849725 8 connected 0-1332 5461-6794 10923-12255 cd305d509c34842a8047e19239b64df94c13cb96 192.168.1.100:7003 slave b2036adda128b2eeffa36c3a2056444d23b548a8 0 1442456848220 4 connected 64b544cdd75c1ce395fb9d0af024b7f2b77213a3 192.168.1.100:7004 slave b5ab302f5c2395e3c8194c354a85d02f89bace62 0 1442456845715 5 connected

 

嘗試查詢之前保存在7010上的Key,可以看到7011頂替上來繼續提供服務,整個集群沒有受到影響。

[root@8gVm redis-3.0.4]# src/redis-cli -c -h 192.168.1.100 -p 7000 get foo6 "bar" [root@8gVm redis-3.0.4]# [root@8gVm redis-3.0.4]# src/redis-cli -c -h 192.168.1.100 -p 7000 get foo2 "bar"

4.內部原理剖析

前面我們已經學習過,用Redis提供的redis-trib或create-cluster腳本能幾步甚至一步就建立起一個Redis集群。這一部分我們為了深入學習,所以要暫時拋開這些方便的工具,完全手動建立一遍上面的3主3從集群。

4.1 集群發現:MEET

最開始時,每個Redis實例自己是一個集群,我們通過cluster meet讓各個結點互相“握手”。這也是Redis Cluster目前的一個欠缺之處:缺少結點的自動發現功能

[root@8gVm redis-3.0.4]# src/redis-cli -c -h 192.168.1.100 -p 7000 cluster nodes 33c0bd93d7c7403ef0239ff01eb79bfa15d2a32c :7000 myself,master - 0 0 0 connected [root@8gVm redis-3.0.4]# src/redis-cli -c -h 192.168.1.100 -p 7000 cluster meet 192.168.1.100 7001 OK ... [root@8gVm redis-3.0.4]# src/redis-cli -c -h 192.168.1.100 -p 7000 cluster meet 192.168.1.100 7005 OK [root@8gVm redis-3.0.4]# src/redis-cli -c -h 192.168.1.100 -p 7000 cluster nodes 7b953ec26bbdbf67179e5d37e3cf91626774e96f 192.168.1.100:7003 master - 0 1442466369259 4 connected 5d9f14cec1f731b6477c1e1055cecd6eff3812d4 192.168.1.100:7005 master - 0 1442466368659 4 connected 33c0bd93d7c7403ef0239ff01eb79bfa15d2a32c 192.168.1.100:7000 myself,master - 0 0 1 connected 63162ed000db9d5309e622ec319a1dcb29a3304e 192.168.1.100:7001 master - 0 1442466371262 3 connected 45baa2cb45435398ba5d559cdb574cfae4083893 192.168.1.100:7002 master - 0 1442466372264 2 connected cdd5b3a244761023f653e08cb14721f70c399b82 192.168.1.100:7004 master - 0 1442466370261 0 connecte

4.2 角色設置:REPLICATE

結點全部“握手”成功后,就可以用cluster replicate命令為結點指定角色了,默認每個結點都是Master。

[root@8gVm redis-3.0.4]# src/redis-cli -c -h 192.168.1.100 -p 7003 cluster replicate 33c0bd93d7c7403ef0239ff01eb79bfa15d2a32c OK [root@8gVm redis-3.0.4]# src/redis-cli -c -h 192.168.1.100 -p 7004 cluster replicate 63162ed000db9d5309e622ec319a1dcb29a3304e OK [root@8gVm redis-3.0.4]# src/redis-cli -c -h 192.168.1.100 -p 7005 cluster replicate 45baa2cb45435398ba5d559cdb574cfae4083893 OK [root@8gVm redis-3.0.4]# src/redis-cli -c -h 192.168.1.100 -p 7000 cluster nodes 7b953ec26bbdbf67179e5d37e3cf91626774e96f 192.168.1.100:7003 slave 33c0bd93d7c7403ef0239ff01eb79bfa15d2a32c 0 1442466812984 4 connected 5d9f14cec1f731b6477c1e1055cecd6eff3812d4 192.168.1.100:7005 slave 45baa2cb45435398ba5d559cdb574cfae4083893 0 1442466813986 5 connected 33c0bd93d7c7403ef0239ff01eb79bfa15d2a32c 192.168.1.100:7000 myself,master - 0 0 1 connected 63162ed000db9d5309e622ec319a1dcb29a3304e 192.168.1.100:7001 master - 0 1442466814987 3 connected 45baa2cb45435398ba5d559cdb574cfae4083893 192.168.1.100:7002 master - 0 1442466811982 2 connected cdd5b3a244761023f653e08cb14721f70c399b82 192.168.1.100:7004 slave 63162ed000db9d5309e622ec319a1dcb29a3304e 0 1442466812483 3 connected

 

4.3 槽指派:ADDSLOTS

設置好主從關系之后,就可以用cluster addslots命令指派16384個槽的位置了。有點惡心的是,ADDSLOTS命令需要在參數中一個個指明槽的ID,而不能指定范圍。這里用Bash 3.0的特性簡化了,不然就得用Bash的循環來完成了:

[root@8gVm redis-3.0.4]# src/redis-cli -c -h 192.168.1.100 -p 7000 cluster addslots {0..5000} OK [root@8gVm redis-3.0.4]# src/redis-cli -c -h 192.168.1.100 -p 7001 cluster addslots {5001..10000} OK [root@8gVm redis-3.0.4]# src/redis-cli -c -h 192.168.1.100 -p 7001 cluster addslots {10001..16383} OK [root@8gVm redis-3.0.4]# src/redis-trib.rb check 192.168.1.100:7000 Connecting to node 192.168.1.100:7000: OK ... >>> Performing Cluster Check (using node 192.168.1.100:7000) ... [OK] All nodes agree about slots configuration. >>> Check for open slots... >>> Check slots coverage... [OK] All 16384 slots covered.

 

這樣我們就通過手動執行命令得到了與之前一樣的集群。

4.4 數據遷移:MIGRATE

真正開始Resharding之前,redis-trib會先在源結點和目的結點上執行cluster setslot <slot> importingcluster setslot <slot> migrating命令,將要遷移的槽分別標記為遷出中和導入中的狀態。然后,執行cluster getkeysinslot獲得Slot中的所有Key。最后就可以對每個Key執行migrate命令進行遷移了。槽遷移完成后,執行cluster setslot命令通知整個集群槽的指派已經發生變化。

關於遷移過程中的數據訪問,客戶端訪問源結點時,如果Key還在源結點上就直接操作。如果已經不在源結點了,就向客戶端返回一個ASK錯誤,將客戶端重定向到目的結點

4.5 內部數據結構

Redis Cluster功能涉及三個核心的數據結構clusterState、clusterNode、clusterLink都在cluster.h中定義。這三個數據結構中最重要的屬性就是:clusterState.slots、clusterState.slots_to_keys和clusterNode.slots了,它們保存了三種映射關系

  • clusterState:集群狀態
    • nodes:所有結點
    • migrating_slots_to:遷出中的槽
    • importing_slots_from:導入中的槽
    • slots_to_keys:槽中包含的所有Key,用於遷移Slot時獲得其包含的Key
    • slots:Slot所屬的結點,用於處理請求時判斷Key所在Slot是否自己負責
  • clusterNode:結點信息
    • slots:結點負責的所有Slot,用於發送Gossip消息通知其他結點自己負責的Slot。通過位圖方式保存節省空間,16384/8恰好是2048字節,所以槽總數16384不是隨意定的
  • clusterLink:與其他結點通信的連接
// 集群狀態,每個節點都保存着一個這樣的狀態,記錄了它們眼中的集群的樣子。 // 另外,雖然這個結構主要用於記錄集群的屬性,但是為了節約資源, // 有些與節點有關的屬性,比如 slots_to_keys 、 failover_auth_count // 也被放到了這個結構里面。 typedef struct clusterState { ... // 指向當前節點的指針 clusterNode *myself; /* This node */ // 集群當前的狀態:是在線還是下線 int state; /* REDIS_CLUSTER_OK, REDIS_CLUSTER_FAIL, ... */ // 集群節點名單(包括 myself 節點) // 字典的鍵為節點的名字,字典的值為 clusterNode 結構 dict *nodes; /* Hash table of name -> clusterNode structures */ // 記錄要從當前節點遷移到目標節點的槽,以及遷移的目標節點 // migrating_slots_to[i] = NULL 表示槽 i 未被遷移 // migrating_slots_to[i] = clusterNode_A 表示槽 i 要從本節點遷移至節點 A clusterNode *migrating_slots_to[REDIS_CLUSTER_SLOTS]; // 記錄要從源節點遷移到本節點的槽,以及進行遷移的源節點 // importing_slots_from[i] = NULL 表示槽 i 未進行導入 // importing_slots_from[i] = clusterNode_A 表示正從節點 A 中導入槽 i clusterNode *importing_slots_from[REDIS_CLUSTER_SLOTS]; // 負責處理各個槽的節點 // 例如 slots[i] = clusterNode_A 表示槽 i 由節點 A 處理 clusterNode *slots[REDIS_CLUSTER_SLOTS]; // 跳躍表,表中以槽作為分值,鍵作為成員,對槽進行有序排序 // 當需要對某些槽進行區間(range)操作時,這個跳躍表可以提供方便 // 具體操作定義在 db.c 里面 zskiplist *slots_to_keys; ... } clusterState; // 節點狀態 struct clusterNode { ... // 節點標識 // 使用各種不同的標識值記錄節點的角色(比如主節點或者從節點), // 以及節點目前所處的狀態(比如在線或者下線)。 int flags; /* REDIS_NODE_... */ // 由這個節點負責處理的槽 // 一共有 REDIS_CLUSTER_SLOTS / 8 個字節長 // 每個字節的每個位記錄了一個槽的保存狀態 // 位的值為 1 表示槽正由本節點處理,值為 0 則表示槽並非本節點處理 // 比如 slots[0] 的第一個位保存了槽 0 的保存情況 // slots[0] 的第二個位保存了槽 1 的保存情況,以此類推 unsigned char slots[REDIS_CLUSTER_SLOTS/8]; /* slots handled by this node */ // 指針數組,指向各個從節點 struct clusterNode **slaves; /* pointers to slave nodes */ // 如果這是一個從節點,那么指向主節點 struct clusterNode *slaveof; /* pointer to the master node */ ... }; /* clusterLink encapsulates everything needed to talk with a remote node. */ // clusterLink 包含了與其他節點進行通訊所需的全部信息 typedef struct clusterLink { ... // TCP 套接字描述符 int fd; /* TCP socket file descriptor */ // 與這個連接相關聯的節點,如果沒有的話就為 NULL struct clusterNode *node; /* Node related to this link if any, or NULL */ ... } clusterLink;

 

4.6 處理流程全梳理

在單機模式下,Redis對請求的處理很簡單。Key存在的話,就執行請求中的操作;Key不存在的話,就告訴客戶端Key不存在。然而在集群模式下,因為涉及到請求重定向和Slot遷移,所以對請求的處理變得很復雜,流程如下:

  1. 檢查Key所在Slot是否屬於當前Node?
    2.1 計算crc16(key) % 16384得到Slot
    2.2 查詢clusterState.slots負責Slot的結點指針
    2.3 與myself指針比較
  2. 若不屬於,則響應MOVED錯誤重定向客戶端
  3. 若屬於且Key存在,則直接操作,返回結果給客戶端
  4. 若Key不存在,檢查該Slot是否遷出中?(clusterState.migrating_slots_to)
  5. 若Slot遷出中,返回ASK錯誤重定向客戶端到遷移的目的服務器上
  6. 若Slot未遷出,檢查Slot是否導入中?(clusterState.importing_slots_from)
  7. 若Slot導入中且有ASKING標記,則直接操作
  8. 否則響應MOVED錯誤重定向客戶端

5.應用案例收集

5.1 有道:Redis Cluster使用經驗

詳情請參見原文,關鍵內容摘錄如下:

5.1.1 兩個缺點

“redis cluster的設計在這塊有點奇葩,跟集群相關的操作需要一個外部的ruby腳本來協助(當然可能是為了讓主程序的代碼足夠簡潔?),然后那個腳本還只支持填實例的ip不支持host,還不告訴你不支持讓你用host之后各種莫名其妙。”

“第一個缺點就是嚴格依賴客戶端driver的成熟度。如果把redis cluster設計成類似Cassandra,請求集群中任何一個節點都可以負責轉發請求,client會好寫一些。”

“第二個缺點完全是設計問題了,就是一個redis進程既負責讀寫數據又負責集群交互,雖然設計者已經盡可能簡化了代碼和邏輯,但還是讓redis從一個內存NoSQL變成了一個分布式NoSQL。分布式系統很容易有坑,一旦有坑必須升級redis。”

5.1.2 去中心化 vs. Proxy

“關於redis cluster的設計,Gossip/P2P的去中心化架構本身不是問題,但一旦有了中心節點,能做的事情就多了,比如sharding不均勻是很容易自動rebalance的,而無中心的只能靠外界來搞。然后redis cluster又是slot的形式而非C*式的一致性哈希,新節點分slot又不自動,依賴外界(ruby腳本)來分配顯得不方便更不優美和諧。而且因為是master-slave的系統而非W+R>N的那種,master掛掉之后盡快發現是比較重要的,gossip對於節點掛掉的發現終究沒有中心節點/zookeeper方便快速。”

“基於proxy做轉發意味着屏蔽了下層存儲,完全可以根據前綴/tag/冷熱程度,來把部分甚至大多數數據放在磁盤從而節約成本又保證一致性,這都是有中心節點所帶來的好處。”

5.2 奇虎360:Redis Cluster淺析和Bada對比

詳情請參見原文,關鍵內容摘錄如下:

5.2.1 負載均衡問題

“redis cluster的主備是以節點為單位,而bada則是以partition為單位,這樣,同樣是3個節點,1024個partition的情況下,redis cluster的主節點負責整個1024個partition的服務,而兩個從節點則只負責異步備份,導致集群負載不均,再看bada,將1024個partition的主均分到3個節點中,每個節點各有主備,主對外提供服務,這樣均分了訪問壓力,有效的利用了資源。”

5.2.2 一致性的保證

redis cluster與bada一樣,最終一致性,讀寫都只請求主節點,當一條寫請求在對應的主節點寫成功后,會立刻返回給客戶端成功,然后主節點通過異步的方式將新的數據同步到對應的從節點,這樣的方式減少了客戶端多個節點寫成功等待的時間,不過在某些情況下會造成寫丟失:

1)當主節點接受一條寫請求,寫入並返回給客戶端成功后不幸宕掉,此時剛才的寫還未同步給其對應的從節點,而從節點在發現主節點掛掉並重新選主后,新的主節點則永久丟失了之前老的主節點向用戶確認的寫

2)當網絡發生割裂,將集群分裂成少數派與多數派,這樣在客戶端不知情的情況下,會將寫繼續寫入到少數派中的某些主節點中,而當割裂超過一定時長后,集群感知到異常,此時少數派中的所有主節點會停止響應所有的寫請求,多數派的其對應的從節點則會發起選舉成為新的主節點,假設過了一會后割裂恢復,老的主節點發現有更新的主存在,自動變成其從節點,而新的主節點中則會永久丟失掉網絡割裂至集群感知異常進行切主這個階段老主節點確認的所有寫

相對於redis cluster的永久丟失,bada通過binlog merge有效的解決了這一問題。所有partition的主節點在響應客戶端的寫請求時,都會在本地記錄binlog,binlog實質就是帶有時間戳的KV對。當老主以從節點的身份重新加入集群時,會觸發binlog merge操作,新主會比較並且合並二者的binlog,這樣就可以將之前丟失掉得寫再補回來。”

5.2.3 請求重定向問題

“bada服務端節點在收到本不該由自己負責的Partition請求后,不會向客戶端返回重定向信息,而是通過代理的方式,直接在集群內部向正確節點轉發客戶端的請求,並將結果同meta信息再轉發回客戶端。”

“再看multi key操作,redis cluster為了追求高性能,支持multi key的前提是所有的key必須在同一個節點中, 不過這樣的處理需要交給用戶,對需要進行multi key操作的所有key,在寫入前人為的加上hash tags。當redis cluster進行resharding的時候,也就是將某些slot從一個節點遷移到另一個節點時,此時的multi key操作可能會失敗,因為在遷移的slot中的key此時存在於兩個節點。

bada怎么做呢?用戶如果對multi key操作性能很在乎時,可以采用與redis cluster同樣的方式,給這些key加上hash tags來讓它們落在同一個節點,如果可以接受性能的稍微損耗而解放用戶的處理邏輯,則可以像single key操作一樣,請求任一bada節點,它會代理所有的key請求並將結果返回給用戶。並且在multi key操作在任何時候都可以,即使在進行partition的遷移,bada也會提前進行切主,保證服務的正常提供。”

5.3 芒果TV:Redis服務解決方案

詳情請參見原文,關鍵內容摘錄如下:

芒果TV在Redis Cluster基礎上進行開發,主要增加了兩個組件:

  • 監控管理:以Python為主要開發框架的Web應用程序Redis-ctl
  • 請求代理:以C++11為開發語言的輕量數據代理程序cerberus。其作用和優點為:
    • 集群代理程序的自動請求分發/重試機制使得應用不必修改自身代碼或更新Redis庫
    • 代理節點為所有Redis節點加上統一管理和狀態監測, 可以查閱歷史數據, 或在發生任何問題之后快速響應修復
    • 代理進程的無狀態性使之可在故障后快速恢復, 不影響后端集群數據完整性

這兩個組件都已開源到GitHub上,大家可以關注一下!


6.Pros & Cons總結

關於Redis Cluster帶來的種種優勢就不說了,在這里主要是“雞蛋里挑骨頭”,總結一下現階段集群功能的欠缺之處和可能的“坑”。

6.1 無中心化架構

6.1.1 Gossip消息

Gossip消息的網絡開銷和時延是決定Redis Cluster能夠線性擴展的因素之一。關於這個問題,在《redis cluster百萬QPS的挑戰》一文中有所提及。

6.1.2 結點粒度備份

此外,Redis Cluster也許是為了簡化設計采用了Master-Slave復制的數據備份方案,並沒有采取如Cassandra或IMDG等對等分布式系統中常見的Slot粒度(或叫Partition/Bucket等)的自動冗余和指派。

這種設計雖然避免比較復雜的分布式技術,但也帶來了一些問題:

  • Slave完全閑置:即便是讀請求也不會被重定向到Slave結點上,Slave屬於“冷備”
  • 寫壓力無法分攤:Slave閑置導致的另一個問題就是寫壓力也都在Master上

6.2 客戶端的挑戰

由於Redis Cluster的設計,客戶端要擔負起一部分責任:

  • Cluster協議支持:不管Dummy還是Smart模式,都要具備解析Cluster協議的能力
  • 網絡開銷:Dummy客戶端不斷重定向的網絡開銷
  • 連接維護:Smart客戶端對連接到集群中每個結點Socket的維護
  • 緩存路由表:Smart客戶端Slot路由表的緩存和更新
  • 內存消耗:Smart客戶端上述維護的信息都是有內存消耗的
  • MultiOp有限支持:對於MultiOp,由客戶端通過KeyTag保證所有Key都在同一Slot。而即便如此,遷移時也會導致MultiOp失敗。同理,對Pipeline和Transaction的支持也受限於必須操作同一Slot內的Key。

6.3 Redis實現問題

盡管屬於無中心化架構一類的分布式系統,但不同產品的細節實現和代碼質量還是有不少差異的,就比如Redis Cluster有些地方的設計看起來就有一些“奇葩”和簡陋:

  • 不能自動發現:無Auto Discovery功能。集群建立時以及運行中新增結點時,都要通過手動執行MEET命令或redis-trib.rb腳本添加到集群中
  • 不能自動Resharding:不僅不自動,連Resharding算法都沒有,要自己計算從哪些結點上遷移多少Slot,然后還是得通過redis-trib.rb操作
  • 嚴重依賴外部redis-trib:如上所述,像集群健康狀況檢查、結點加入、Resharding等等功能全都抽離到一個Ruby腳本中了。還不清楚上面提到的缺失功能未來是要繼續加到這個腳本里還是會集成到集群結點中?redis-trib也許要變成Codis中Dashboard的角色
  • 無監控管理UI:即便未來加了UI,像遷移進度這種信息在無中心化設計中很難得到
  • 只保證最終一致性:寫Master成功后立即返回,如需強一致性,自行通過WAIT命令實現。但對於“腦裂”問題,目前Redis沒提供網絡恢復后的Merge功能,“腦裂”期間的更新可能丟失

6.4 性能損耗

由於之前手頭沒有空閑的物理機資源,所以只在虛擬機上做了簡單的單機測試,在單獨的一台壓力機使用YCSB測試框架向虛擬機產生讀寫負載。虛擬機的配置為8核Intel Xeon CPU X5650@2.67GHz,16GB內存,分別搭建了4結點的單機版Redis和集群版Redis,測試一下Redis Cluster的性能損耗。由於不是最近做的測試,所以Jedis用的2.6.2版本。注:當然Redis Cluster可以通過多機部署獲得水平擴展帶來的性能提升,這里只是由於環境有限所以做的簡單單機測試。

由於YCSB本身僅支持Redis單機版,所以需要我們自己增加擴展插件,具體方法請參照《YCSB性能測試工具使用》。通過YCSB產生2000w隨機數據,Value大約100Byte左右。然后通過YCSB測試Read-Mostly(90% Read)和Read-Write-Mixed(50% Read)兩種情況:

  • 數據加載:吞吐量上有約18%的下降。
  • Read-Mostly:吞吐量上有約3.5%~7.9%的下降。
  • Read-Write-Mixed:吞吐量上有約3.3%~5.5%下降。
  • 內存占用:Jedis客戶端多占用380MB內存。

6.5 最后的總結

從現階段看來,相比Sentinel或Codis等方案,Redis Cluster的優勢還真是有限,個人覺得最大的優點有兩個:

  1. 官方提供的Slot實現而不用像Codis那樣去改源碼了;
  2. 不用額外的Sentinel集群或類似的代碼實現了。

同其他分布式系統,如Cassandra,或內存型的IMDG如Hazelcast和GridGain,除了性能方面外,從功能上Redis Cluster簡直被爆得體無完膚… 看看我之前總結過的GridGain介紹《開源IMDG之GridGain》

  • 結點自動發現和Rebalance
  • 分區粒度的備份
  • 故障時分區角色自動調整
  • 結果聚合(不會重定向客戶端)
  • “腦裂”恢復后的Merge(Hazelcast支持多種合並策略)
  • 多Primary分區寫操作(見Replicated模式)

這些都是Redis Cluster沒有或者要手動完成的。當然這也不足為奇,因為這與Redis的設計初衷有關,畢竟作者都已經說了,最核心的設計目標就是性能、水平伸縮和可用性。

從Redis Cluster的環境搭建使用到高級功能和內部原理剖析,再到應用案例收集和優缺點的分析羅列,講了這么多,關於Redis集群到底如何,相信大家根據自己切身和項目的具體情況一定有了自己的結論。不管是評估測試也好,二次開發也好,還是直接上線使用也好,相信隨着官方的不斷迭代更新和大家的力量,Redis Cluster一定會逐漸完善成熟的!

 

http://www.cnblogs.com/lixigang/articles/4847110.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM