全面剖析Redis Cluster原理和應用

全面剖析Redis Cluster原理和應用 更多關注:MAYOU18技術redis專欄html

問題導讀:node

1.怎樣配置Redis集羣?git

2.Redis Cluster怎樣作數據遷移?github

3.Redis Cluster怎樣實現故障轉移?redis

4.Redis Cluster與Bada有什麼區別?算法


1.Redis Cluster總覽

1.1 設計原則和初衷數據庫

在官方文檔Cluster Spec中,做者詳細介紹了Redis集羣爲何要設計成如今的樣子。最核心的目標有三個:後端

 

  • 性能:這是Redis賴以生存的看家本領,增長集羣功能後固然不能對性能產生太大影響,因此Redis採起了P2P而非Proxy方式、異步複製、客戶端重定向等設計,而犧牲了部分的一致性、使用性。數組

  • 水平擴展:集羣的最重要能力固然是擴展,文檔中稱能夠線性擴展到1000結點。緩存

  • 可用性:在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

 

配置好後,根據咱們的集羣規模,拷貝出來幾份一樣的配置文件,惟一不一樣的就是監聽端口,能夠依次改成700一、7002… 由於Redis Cluster若是數據冗餘是1的話,至少要3個Master和3個Slave,因此咱們拷貝出6個實例的配置文件。爲了不相互影響,爲6個實例的配置文件創建獨立的文件夾。

 

 

 

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
[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下載):

 

 

 

1
2
3
4
5
6
7
8
9
[root@8gVm Software] # wget [url=https://github.com/rubygems/ruby]https://github.com/rubygems/ruby[/url] ... /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 [url=https://rubygems.org/downloads/redis-3.2.1.gem]https://rubygems.org/downloads/redis-3.2.1.gem[/url]
[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實例。

 

 

 

1
2
3
4
[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的位置到各個結點。

 

 

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[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久違的集羣功能。

 

01
02
03
04
05
06
07
08
09
10
11
12
13
[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重定向消息。

 

 

 

1
2
3
[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掉各個進程,而後從新啓動就能夠了。這也有點太… 網上有人重啓後會碰到問題,我還比較幸運,這種「土鱉」的方式重啓試了兩次還沒發現問題。

 

 

 

1
[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做爲測試數據。

 

 

 

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[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到集羣中。

 

 

 

1
2
[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。

 

 

 

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
[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在程序中自動完成遷移。

 

 

 

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
[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上。

 

 

 

01
02
03
04
05
06
07
08
09
10
11
12
13
14
[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。

 

 

 

01
02
03
04
05
06
07
08
09
10
11
[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頂替上來繼續提供服務,整個集羣沒有受到影響。

 

 

 

1
2
3
4
5
[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目前的一個欠缺之處:缺乏結點的自動發現功能。

 

 

 

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
[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。

 

 

 

01
02
03
04
05
06
07
08
09
10
11
12
13
14
[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的循環來完成了:

 

 

 

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
[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> importing和cluster 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:與其餘結點通訊的鏈接

 

 

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
//  集羣狀態,每一個節點都保存着一個這樣的狀態,記錄了它們眼中的集羣的樣子。
//  另外,雖然這個結構主要用於記錄集羣的屬性,可是爲了節約資源,
//  有些與節點有關的屬性,好比 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 = NULL 表示槽 i 未被遷移
[i]     //  migrating_slots_to = clusterNode_A 表示槽 i 要從本節點遷移至節點 A
     clusterNode *migrating_slots_to[REDIS_CLUSTER_SLOTS];
 
     //  記錄要從源節點遷移到本節點的槽,以及進行遷移的源節點
     //  importing_slots_from = NULL 表示槽 i 未進行導入
     //  importing_slots_from = clusterNode_A 表示正從節點 A 中導入槽 i
     clusterNode *importing_slots_from[REDIS_CLUSTER_SLOTS];
 
     //  負責處理各個槽的節點
     //  例如 slots = 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遷移,因此對請求的處理變得很複雜,流程以下:

 

  • 檢查Key所在Slot是否屬於當前Node? 
    2.1 計算crc16(key) % 16384獲得Slot 
    2.2 查詢clusterState.slots負責Slot的結點指針 
    2.3 與myself指針比較

  • 若不屬於,則響應MOVED錯誤重定向客戶端

  • 若屬於且Key存在,則直接操做,返回結果給客戶端

  • 若Key不存在,檢查該Slot是否遷出中?(clusterState.migrating_slots_to)

  • 若Slot遷出中,返回ASK錯誤重定向客戶端到遷移的目的服務器上

  • 若Slot未遷出,檢查Slot是否導入中?(clusterState.importing_slots_from)

  • 若Slot導入中且有ASKING標記,則直接操做

  • 不然響應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的優點還真是有限,我的以爲最大的優勢有兩個:

 

  • 官方提供的Slot實現而不用像Codis那樣去改源碼了;

  • 不用額外的Sentinel集羣或相似的代碼實現了。

 

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

 

  • 結點自動發現和Rebalance

  • 分區粒度的備份

  • 故障時分區角色自動調整

  • 結果聚合(不會重定向客戶端)

  • 「腦裂」恢復後的Merge(Hazelcast支持多種合併策略)

  • 多Primary分區寫操做(見Replicated模式)

 

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

 

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

相關文章
相關標籤/搜索