0%

Redis (六) - 主從複製、哨兵與叢集模式

在 Redis 系列文章的第一篇 Redis (一) - 基本概念 我們有介紹過 Redis 是一個 in-memory 的資料庫,所以資料都會被儲存在記憶體中,這樣的好處是可以提升資料的存取速度。

以往想要將資料放在記憶體來提升存取速度大多是使用 Session 來達成,但是 Session 會因為電腦或伺服器被關閉而被清除。更麻煩的問題是當一個應用要部署到多台伺服器上來分散流量的話 Session 就會不同步,這樣會導致資料和結果不正確。

所以另外一種作法就是搭配 Cookie 存放 Session Id 然後把 Session 資料存到資料庫來解決資料不同步的問題,但是這樣就要每次都去資料庫讀資料反而會增加存取的時間。

因此 Redis 提供了一些非常實用的功能來實現多機的 in-memory 資料庫,如下:

  • 主從複製模式 (Master-Slave Replication)
  • 哨兵模式 (Sentinel)
  • 叢集模式 (Cluster)

主從複製模式

主從複製模式提供讓 Master(主伺服器) 向任意數量的 Slave(附屬伺服器) 進行資料同步,以解決單點資料庫的問題。

主從複製模式最主要的目的是要實現讀寫分離和資料備份。Redis 讀寫分離的作法是 Master 可以進行讀取和寫入,其他的 Slave 只能讀取。透過 Slave 幫忙處理讀取的工作來減輕 Master 的負擔。

master-slave replication

工作流程

  1. Slave 啟動之後會主動向 Master 發送 Sync 命令要求進行同步。
  2. Master 收到之後會執行 Bgsave 命令建立 rdb 快照檔案儲存到硬碟。同時,Master 會把新收到的寫入和修改資料庫的命令存到緩衝區。
  3. Master 會將剛建立好的快照檔案傳給 Slave。
  4. Slave 收到快照檔案後會先把記憶體清空,接著載入收到的快照檔案。
  5. Master 會再把存在緩衝區的命令傳給 Slave,Slave 再執行這些命令以達成和 Master 同步。
  6. 以上便完成了 Slave 的資料初始化,此後只要 Master 每執行一道寫入或修改資料庫的命令都會傳送給 Slave 以達到資料的同步。
  7. Master 和 Slave 會互相發送 heartbeat 的訊息,也就是傳送 Ping 指令。以告知對方我還正常運作也檢查對方是否還正常運作。

範例

這裡我們使用 Docker-Compose 來實作範例,因為 Docker-Compose 可以一次快速啟動多個 Container。

範例 github 連結

若你不使用 Docker-Compose 可以直接在 Redis 裡下 Slaveof 指令來設定,指令用法請參考 Redis (五) - 管理 Redis Server

首先先建立一個名為 master-slave 的資料夾,並在資料夾內建立 docker-compose.yml,我們建立一個 Master 和 兩個 Slave,且兩個 Slave 指定要作為 Master 的 Slave。

此外在建立 Container 時,也用 Volume 掛載了本機的資料夾到 Container 的 data 資料夾,在 Redis (五) - 管理 Redis Server 中有提到資料持久化的 AOF 檔和 rdb 檔會儲存在 Redis 安裝目錄下,也就是 data 資料夾。所以為了避免 Container 故障停止造成資料遺失,必須要將資料同步到主機上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
version: '3'
services:
master:
image: redis
container_name: redis-master
volumes:
- ./data/redis-master/data:/data
ports:
- 6379:6379
slave1:
image: redis
container_name: redis-slave1
volumes:
- ./data/redis-slave1/data:/data
ports:
- 6380:6379
command: redis-server --slaveof redis-master 6379
depends_on:
- master
slave2:
image: redis
container_name: redis-slave2
volumes:
- ./data/redis-slave2/data:/data
ports:
- 6381:6379
command: redis-server --slaveof redis-master 6379
depends_on:
- master
- slave1

設定好 docker-compose.yml 後就可以啟動 docker-compose。接下來我們來看一下每個 Redis Server 的角色。這裡為了一起顯示出三個 Redis Server 的結果,就不進到每個 Redis Server 的 Container 裡面操作,直接用 docker exec 操作。

1
2
3
4
5
6
$ docker exec -it redis-master redis-cli info replication | grep role
role:master
$ docker exec -it redis-slave1 redis-cli info replication | grep role
role:slave
$ docker exec -it redis-slave2 redis-cli info replication | grep role
role:slave

接著再向 Master 寫入一筆資料,可以看到馬上就同步到兩個 Slave 了。

1
2
3
4
5
6
$ docker exec -it redis-master redis-cli set k1 v1
OK
$ docker exec -it redis-slave1 redis-cli get k1
"v1"
$ docker exec -it redis-slave2 redis-cli get k1
"v1"

哨兵模式

哨兵模式用於監控 Redis 系統,哨兵會監控 Master 是否正常運行。當 Master 出現故障或下線時,哨兵會將其所屬的其中一個 Slave 升格為 Master,並將其他的 Slave 指向新的 Master。

Sentinel

監控

哨兵會和要監控的 Master 建立兩條連接,CmdPub/Sub

  • Cmd 是哨兵用來定期向 Master 發送 Info 命令以取得 Master 的訊息,訊息中也會包含 Master 有哪些 Slave。當獲得了 Slave 的訊息時哨兵也同樣會和 Slave 建立兩條連結。
  • 此外哨兵也會定期透過 Cmd 向 Master、Slave和其他哨兵發送 Ping 指令來作為心跳檢測,確認節點的狀態。
  • Pub/Sub 讓哨兵可以透過訂閱 Master 和 Slave 的 __sentinel__:hello 這個頻道來和其他哨兵定期的進行資訊交換。

主觀下線

主觀下線指的是單個哨兵認為 Master 已經停止服務了,有可能是網路不通或是接收不到訂閱等等。而哨兵判斷的依據是在傳送 Ping 指令之後一定的時間內沒有回覆或是回傳錯誤,則哨兵就會主觀的認為這個 Master 已經下線停止服務了。

客觀下線

客觀下線指的是由多個哨兵對同一個 Master 各自進行主觀下線的判斷,再綜合所有哨兵的判斷。若認為主觀下線的哨兵到達一定的數量(配置文件中的 quorum),則即為客觀下線。

客觀下線會由起初認定 Master 主觀下線的哨兵發起,該哨兵會向其他的哨兵發送訊息詢問他們是否認為這個 Master 主觀下線。

故障轉移 (Failover)

當 Master 已經被標記為客觀下線時,起初發現 Master 下線的哨兵會發起一個選舉(採用 Raft 演算法),並要求其他哨兵選他做為領頭哨兵,領頭哨兵會負責進行故障的恢復。當選的標準是要有超過一半的哨兵同意,所以哨兵的數量建議是設定奇數個。

此時若有多個哨兵同時參選領頭哨兵,則有可能會發生一輪之內沒有產生勝選者,則所有的哨兵會再等待一個隨機的時間再次發起參選的請求,進行下一輪的選舉,一直到選出領頭為止。所以若哨兵為偶數個就很有可能一直無法產生領頭哨兵。

選出領頭哨兵之後,領頭哨兵會開始從下線的 Master 所屬的 Slave 中挑選一個出來升格為新的 Master,挑選的依據如下 :

  1. 所有在線的 Slave 擁有最高優先權的,優先權可以透過 slave-priority 設定。
  2. 如果有多個同為最高優先權的 Slave,則選取複製偏移量最大的(複製最完整的)。
  3. 若還是有多個 Slave 皆符合上述條件,則選取 id 最小的。

接著領頭哨兵會向選出來的 Slave 發送命令將其升格為 Master,然後再向其他 Slave 發送命令指向新的 Master 並進行數據的複製。

最後領頭哨兵會將舊的 Master 更新為新的 Master 的 Slave,讓其恢復服務後以 Slave 的身分繼續運作。

範例

前面我們已經有建立一組 Master-Slave 的 Redis Server,現在我們就建立一組哨兵來監控這組 Redis Server。

範例 github 連結

檔案結構

1
2
3
4
5
├─sentinel
| ├─docker-compose.yml
| ├─sentinel1.conf
| ├─sentinel2.conf
| └sentinel3.conf

首先要先下載範例的 sentinel.conf,接著修改以下部分。redis-master-ip 可以透過 docker inspect redis-master | grep "IPAddress" 取得。

1
2
3
4
5
6
7
8
9
10
port 26379

# 設定要監控的 Master,最後的 2 代表判定客觀下線所需的哨兵數
sentinel monitor mymaster <redis-master-ip> 6379 2

# 哨兵 Ping 不到 Master 超過此毫秒數會認定主觀下線,預設30秒,因測試改5秒
sentinel down-after-milliseconds mymaster 5000

# failover 超過次毫秒數即代表 failover 失敗,預設3分鐘
sentinel failover-timeout mymaster 180000

修改完後將 sentinel.conf 複製成 3 份,並重新命名為 sentinel1.conf、sentinel2.conf、sentinel3.conf。接著將 sentinel2.conf 和 sentinel3.conf 的 port 分別改成 26380 和 26381。

接下來我們要建立 docker-compose.yml,並將剛剛建好的 sentinel.conf 使用 Volume 掛載進哨兵的 Redis Container,如下。如果不用 docker-compose 或是 Volume,也可以用 Dockerfile 將 sentinel.conf 直接複製進去。使用 Dockerfile 會更輕鬆,想了解請參考 我的 github

1
2
3
4
5
6
7
8
9
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
version: '3'
services:
sentinel1:
container_name: redis-sentinel1
build:
context: .
args:
- REDIS_PORT=26379
volumes:
- ./data/redis-sentinel1/data:/data
ports:
- 26379:26379
sentinel2:
container_name: redis-sentinel2
build:
context: .
args:
- REDIS_PORT=26380
volumes:
- ./data/redis-sentinel2/data:/data
ports:
- 26380:26379
sentinel3:
container_name: redis-sentinel3
build:
context: .
args:
- REDIS_PORT=26381
volumes:
- ./data/redis-sentinel3/data:/data
ports:
- 26381:26379
networks:
default:
external:
name: master-slave_default

這個 docker-compose.yml 最後還有指定網路的環境,因為 Docker Compose 預設啟動時會為啟動的 Container 建立網路環境,而這些 Container 就可以互相溝通。但是現在哨兵和 Master-Slave 並不是一起被建立起來的,所以必須要指定 Master-Slave 使用的網路環境給哨兵,這樣哨兵才能和 Master-Slave 互動。不過如果網路模式選擇 host 則不需要再特別設定連結到 master-slave 的網路。

上述步驟都完成後就可以啟動 docker-compose,並且可以看到會輸出下面這些資訊,這些資訊代表哨兵已經開始監視 Master 而且也獲得了 Slave 的資訊。

1
2
3
4
5
6
7
8
9
10
11
12
...
redis-sentinel1 | 1:X 23 Jul 2020 08:56:03.301 # +monitor master mymaster 172.30.0.2 6379 quorum 2
redis-sentinel1 | 1:X 23 Jul 2020 08:56:03.302 * +slave slave 172.30.0.3:6379 172.30.0.3 6379 @ mymaster 172.30.0.2 6379
redis-sentinel1 | 1:X 23 Jul 2020 08:56:03.483 * +slave slave 172.30.0.4:6379 172.30.0.4 6379 @ mymaster 172.30.0.2 6379
...
redis-sentinel2 | 1:X 23 Jul 2020 08:56:03.301 # +monitor master mymaster 172.30.0.2 6379 quorum 2
redis-sentinel2 | 1:X 23 Jul 2020 08:56:03.302 * +slave slave 172.30.0.3:6379 172.30.0.3 6379 @ mymaster 172.30.0.2 6379
redis-sentinel2 | 1:X 23 Jul 2020 08:56:03.456 * +slave slave 172.30.0.4:6379 172.30.0.4 6379 @ mymaster 172.30.0.2 6379
...
redis-sentinel3 | 1:X 23 Jul 2020 08:56:03.261 # +monitor master mymaster 172.30.0.2 6379 quorum 2
redis-sentinel3 | 1:X 23 Jul 2020 08:56:03.262 * +slave slave 172.30.0.3:6379 172.30.0.3 6379 @ mymaster 172.30.0.2 6379
redis-sentinel3 | 1:X 23 Jul 2020 08:56:03.456 * +slave slave 172.30.0.4:6379 172.30.0.4 6379 @ mymaster 172.30.0.2 6379

也可以使用下面這個指令來查看哨兵的資訊。

1
2
3
4
5
6
7
8
$ docker exec -it redis-sentinel1 redis-cli -p 26379 info sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=172.30.0.2:6379,slaves=2,sentinels=3

故障轉移

接著我們來嘗試一下故障轉移,先停止 redis-master 這個 Container,這時哨兵就會開始啟動故障轉移,以下是進行故障轉移時的輸出。

首先可以看到三個哨兵都認定 master 為 sdown(主觀下線),這時 Sentinel3 便認定為 odown(客觀下線),並打算發起投票要求成為領頭哨兵。

1
2
3
4
5
6
redis-sentinel1 | 1:X 23 Jul 2020 09:28:35.907 # +sdown master mymaster 172.30.0.2 6379
redis-sentinel2 | 1:X 23 Jul 2020 09:28:35.907 # +sdown master mymaster 172.30.0.2 6379
redis-sentinel3 | 1:X 23 Jul 2020 09:28:35.919 # +sdown master mymaster 172.30.0.2 6379
redis-sentinel3 | 1:X 23 Jul 2020 09:28:35.990 # +odown master mymaster 172.30.0.2 6379 #quorum 3/2
redis-sentinel3 | 1:X 23 Jul 2020 09:28:35.990 # +new-epoch 1
redis-sentinel3 | 1:X 23 Jul 2020 09:28:35.990 # +try-failover master mymaster 172.30.0.2 6379

與此同時 Sentinel2 也認定為客觀下線,雖然看起來是 Sentinel3 先認定客觀下線打算發起投票了,但是最後還是由 Sentinel2 發起投票要求成為領頭哨兵。Sentinel2 和 Sentinel3 都加入參選且各自投給自己,Sentinel1 則沒有投票所以這一輪流局。

1
2
3
4
5
6
7
redis-sentinel2 | 1:X 23 Jul 2020 09:28:35.990 # +odown master mymaster 172.30.0.2 6379 #quorum 2/2
redis-sentinel2 | 1:X 23 Jul 2020 09:28:35.990 # +new-epoch 1
redis-sentinel2 | 1:X 23 Jul 2020 09:28:35.990 # +try-failover master mymaster 172.30.0.2 6379
redis-sentinel2 | 1:X 23 Jul 2020 09:28:36.066 # +vote-for-leader 8ce97092515e4e1f53f5bdfa276676c664dc100b 1
redis-sentinel2 | 1:X 23 Jul 2020 09:28:36.092 # 8fc467a6fa4a1d798be4a054d78db4c5db10fb97 voted for 8fc467a6fa4a1d798be4a054d78db4c5db10fb97 1
redis-sentinel3 | 1:X 23 Jul 2020 09:28:36.092 # +vote-for-leader 8fc467a6fa4a1d798be4a054d78db4c5db10fb97 1
redis-sentinel3 | 1:X 23 Jul 2020 09:28:36.092 # 8ce97092515e4e1f53f5bdfa276676c664dc100b voted for 8ce97092515e4e1f53f5bdfa276676c664dc100b 1

Sentinel1 再次發起了一輪投票要推舉 Sentinel2 為領頭哨兵,Sentinel2 和 Sentinel3 都投給 Sentinel2,所以最後 Sentinel2 當選。

1
2
3
4
5
redis-sentinel1 | 1:X 23 Jul 2020 09:28:36.124 # +new-epoch 1
redis-sentinel1 | 1:X 23 Jul 2020 09:28:36.162 # +vote-for-leader 8ce97092515e4e1f53f5bdfa276676c664dc100b 1
redis-sentinel3 | 1:X 23 Jul 2020 09:28:36.162 # 8fc467a6fa4a1d798be4a054d78db4c5db10fb97 voted for 8ce97092515e4e1f53f5bdfa276676c664dc100b 1
redis-sentinel2 | 1:X 23 Jul 2020 09:28:36.162 # 8ce97092515e4e1f53f5bdfa276676c664dc100b voted for 8ce97092515e4e1f53f5bdfa276676c664dc100b 1
redis-sentinel2 | 1:X 23 Jul 2020 09:28:36.211 # +elected-leader master mymaster 172.30.0.2 6379

接著 Sentinel2 選出了 redis-slave1(slave 172.30.0.3:6379) 作為 Master,並且下了 slaveof no one 的指令使其解除 Slave 狀態變回獨立的 Master,隨後將 redis-slave1 升格為 Master。

1
2
3
4
5
redis-sentinel2 | 1:X 23 Jul 2020 09:28:36.211 # +failover-state-select-slave master mymaster 172.30.0.2 6379
redis-sentinel2 | 1:X 23 Jul 2020 09:28:36.268 # +selected-slave slave 172.30.0.3:6379 172.30.0.3 6379 @ mymaster 172.30.0.2 6379
redis-sentinel2 | 1:X 23 Jul 2020 09:28:36.268 * +failover-state-send-slaveof-noone slave 172.30.0.3:6379 172.30.0.3 6379 @ mymaster 172.30.0.2 6379
redis-sentinel2 | 1:X 23 Jul 2020 09:28:36.339 * +failover-state-wait-promotion slave 172.30.0.3:6379 172.30.0.3 6379 @ mymaster 172.30.0.2 6379
redis-sentinel2 | 1:X 23 Jul 2020 09:28:36.584 # +promoted-slave slave 172.30.0.3:6379 172.30.0.3 6379 @ mymaster 172.30.0.2 6379

設定完新的 Master 後,Sentinel2 讓原本的 Master 轉為 Slave,並且讓 redis-slave2(172.30.0.4:6379) 指向新的 Master。

1
2
redis-sentinel2 | 1:X 23 Jul 2020 09:28:36.584 # +failover-state-reconf-slaves master mymaster 172.30.0.2 6379
redis-sentinel2 | 1:X 23 Jul 2020 09:28:36.610 * +slave-reconf-sent slave 172.30.0.4:6379 172.30.0.4 6379 @ mymaster 172.30.0.2 6379

Sentinel1 和 Sentinel3 開始從 Sentinel2 取得設定然後更新自己的設定,至此整個故障轉移就完成了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
redis-sentinel1 | 1:X 23 Jul 2020 09:28:36.611 # +config-update-from sentinel 8ce97092515e4e1f53f5bdfa276676c664dc100b 172.30.0.7 26380 @ mymaster 172.30.0.2 6379
redis-sentinel3 | 1:X 23 Jul 2020 09:28:36.611 # +config-update-from sentinel 8ce97092515e4e1f53f5bdfa276676c664dc100b 172.30.0.7 26380 @ mymaster 172.30.0.2 6379
redis-sentinel3 | 1:X 23 Jul 2020 09:28:36.611 # +switch-master mymaster 172.30.0.2 6379 172.30.0.3 6379
redis-sentinel3 | 1:X 23 Jul 2020 09:28:36.611 * +slave slave 172.30.0.4:6379 172.30.0.4 6379 @ mymaster 172.30.0.3 6379
redis-sentinel3 | 1:X 23 Jul 2020 09:28:36.611 * +slave slave 172.30.0.2:6379 172.30.0.2 6379 @ mymaster 172.30.0.3 6379
redis-sentinel1 | 1:X 23 Jul 2020 09:28:36.611 # +switch-master mymaster 172.30.0.2 6379 172.30.0.3 6379
redis-sentinel1 | 1:X 23 Jul 2020 09:28:36.611 * +slave slave 172.30.0.4:6379 172.30.0.4 6379 @ mymaster 172.30.0.3 6379
redis-sentinel1 | 1:X 23 Jul 2020 09:28:36.611 * +slave slave 172.30.0.2:6379 172.30.0.2 6379 @ mymaster 172.30.0.3 6379
redis-sentinel2 | 1:X 23 Jul 2020 09:28:37.284 # -odown master mymaster 172.30.0.2 6379
redis-sentinel2 | 1:X 23 Jul 2020 09:28:37.284 * +slave-reconf-inprog slave 172.30.0.4:6379 172.30.0.4 6379 @ mymaster 172.30.0.2 6379
redis-sentinel2 | 1:X 23 Jul 2020 09:28:37.284 * +slave-reconf-done slave 172.30.0.4:6379 172.30.0.4 6379 @ mymaster 172.30.0.2 6379
redis-sentinel2 | 1:X 23 Jul 2020 09:28:37.355 # +failover-end master mymaster 172.30.0.2 6379
redis-sentinel2 | 1:X 23 Jul 2020 09:28:37.355 # +switch-master mymaster 172.30.0.2 6379 172.30.0.3 6379
redis-sentinel2 | 1:X 23 Jul 2020 09:28:37.355 * +slave slave 172.30.0.4:6379 172.30.0.4 6379 @ mymaster 172.30.0.3 6379
redis-sentinel2 | 1:X 23 Jul 2020 09:28:37.355 * +slave slave 172.30.0.2:6379 172.30.0.2 6379 @ mymaster 172.30.0.3 6379

此時因為 redis-master 還是處於關閉的狀態,所以三個哨兵還是會判斷其為主觀下線,但是因為他已經成為 Slave,所以不會進行故障轉移。

1
2
3
redis-sentinel1 | 1:X 23 Jul 2020 09:28:41.616 # +sdown slave 172.30.0.2:6379 172.30.0.2 6379 @ mymaster 172.30.0.3 6379
redis-sentinel3 | 1:X 23 Jul 2020 09:28:41.631 # +sdown slave 172.30.0.2:6379 172.30.0.2 6379 @ mymaster 172.30.0.3 6379
redis-sentinel2 | 1:X 23 Jul 2020 09:28:42.402 # +sdown slave 172.30.0.2:6379 172.30.0.2 6379 @ mymaster 172.30.0.3 6379

這時再次啟動 redis-master,Sentinel2 會正式的將 redis-master 轉換為 Slave。

1
redis-sentinel2 | 1:X 24 Jul 2020 01:02:17.484 * +convert-to-slave slave 172.30.0.2:6379 172.30.0.2 6379 @ mymaster 172.30.0.3 6379

叢集模式

叢集模式是用來提供 Redis 分散式的需求,以解決單台伺服器的記憶體容量有限的問題。前面介紹的主從複製模式仍然只有一台主伺服器在進行寫入,其他的附屬伺服器只是將數據備份過來幫忙分散讀取的工作而已。

然而叢集模式無法像主從複製模式實現讀寫分離,Slave 只負責備份資料不提供服務。

Cluster

資料分片

叢集模式採用了 Sharding(分片) 的技術,自動地將資料拆分到多台的伺服器上,以水平的擴充記憶體的容量。

Redis 引入了 Hash Slot(槽) 來實現資料的拆分,Hash Slot 是用於紀錄資料和節點之間的關聯,Redis 會將多個 Key 映射到一個 Hash Slot。

一個 Redis 叢集包含了 16384 個 Hash Slot,叢集內所有的節點會各負責一部份的 Hash Slot。每個節點會將自己負責哪些 Slot 傳送給其他節點,這些資訊稱為 Slots。確切的說,Slots 是一個長度為 16384 的 Binary Array,每個節點會在自己的 Slots 上標記負責的 Slot 為 1,其他則為 0。

Slots

呈上,每個節點因為會收到其他節點的 Slots,所以除了自己的 Slots 外還會有一個大家都長一樣的 Slots 來記錄這 16384 個 Slot 分別指派給哪個節點。

節點的資料結構與 Slots

到這裡你可能會對於兩個 Slots 有點分不太清楚,我們用下面這張圖來做說明。首先最右邊一排是每個節點最基本的資料結構稱為 ClusterNode,裡面會儲存這個節點的狀態和資訊。ClusterNode 裡會儲存 Slots,這個 Slots 就是紀錄自己負責哪些 Slot。

此外,每個節點也都還會有一個資料結構用來記錄叢集目前的狀態,稱為 ClusterState,就是下圖的最左邊。ClusterState 是以當前節點的視角來看整個叢集的狀態,例如叢集的節點數、上下線等等。

ClusterState 也會儲存 Slots,這個 Slots 就是上面提到的大家都一樣的用來記錄每個 Slot 分別是指派給哪個節點。下圖中間即為 ClusterState 的 Slots,可以看到它記錄的便是每個 Slot 指向哪個 ClusterNode。另外,ClusterState 裡有還一個屬性稱為 myself,myself 會直接指向當前節點的 ClusterNode。

ClusterState Slots

工作流程

  1. 當節點收到命令的時候,會先透過 CRC16 演算法取得這個 Key 是屬於哪個 Slot。
  2. 知道是哪個 Slot 後,節點會透過 ClusterState 的 Slots 取得這個 Slot 是由誰負責,接著再和 myself 比對是否相同。
  3. 如果相同代表是由當前的節點負責,那當前的節點就會執行命令。
  4. 如果不同代表不是由當前的節點負責,則節點會根據取得的 ClusterNode 所記錄的 IP 和 Port,回傳 MOVED 錯誤,並指引客戶端轉向到負責這個 Slot 的節點。

故障轉移

叢集模式的故障轉移不需要自己建立哨兵,節點之間會自行運作並執行類似哨兵的故障處理方式。

節點增加與刪除

節點的刪除與增加並不會影響資料也不會造成叢集下線。增加節點時,其他的節點會分配一些 Slot 到新的節點,讓新的節點幫忙負責一部份。而刪除也是同理,當有節點被刪除時,他所負責的 Slot 會被分配到其他剩餘的節點上幫忙負責。如果是想改變一個節點上的 Slot 數量也是同樣的方法進行重新分配。

範例

範例 github 連結

檔案結構

1
2
3
4
5
├─cluster
| ├─docker-compose.yml
| ├─redis
| | ├─Dockerfile
| | └redis.conf

首先先建立 Dockerfile,叢集模式因為節點較多 (至少要有 3 個主節點),所以不能再像哨兵模式的範例一一掛載 conf 檔,太麻煩也太浪費時間了。就用 Dockerfile 將 conf 直接複製進 Redis Container 中。

1
2
3
4
5
6
7
8
9
10
FROM redis:latest

ARG REDIS_PORT

COPY redis.conf /etc/redis/redis.conf

# 將 Port 改成傳進來的 Port 值
RUN sed -i 's/REDIS_PORT/'$REDIS_PORT'/g' /etc/redis/redis.conf

ENTRYPOINT redis-server /etc/redis/redis.conf

接著要建立 redis.conf (redis 下載),redis 預設是沒有開啟叢集模式的,所以必須要在 redis.conf 開啟或加入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 註解 bind 127.0.0.1,會影響 redis 節點連接
# bind 127.0.0.1

# Port 會在建立 Dockerfile 時以參數傳入並更改 redis.conf
port REDIS_PORT

# 啟用叢集模式
cluster-enabled yes

# 叢集的設定檔
cluster-config-file nodes.conf

# 請求超時預設 15000 毫秒
cluster-node-timeout 15000

最後要建立啟動 Redis Container 的 docker-compose.yml,可以看到每個節點在編譯 Dockerfile 時都有傳入 REDIS_PORT 這個參數來代表佔用的 Port 號。而網路模式是使用 hosthost 模式才能讓外部連接進來操作。

這個 yaml 檔最重要的是最後一個用來建立 Cluster 的 Container,這個 Container 啟動時會使用 redis-cli --cluster create 的指令來建立 Cluster,其後所接的是主機的 IP + 節點的 Port。

此外指令後面還有接 --cluster-replicas 1,這是代表每個 node 會有幾個 Slave,前面有提到 Cluster 至少要有 3 個主節點,所以 yaml 檔裡才會設定了 6 個節點 (3 Master, 3 Slave)。

指令最後的 --cluster-yes 是為了要自動輸入 yes 以完成建立 Cluster。

1
2
3
4
5
6
7
8
9
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
version: '3'
services:
node1:
container_name: redis-node1
build:
context: redis
args:
- REDIS_PORT=9001
volumes:
- ./data/node1/data:/data
network_mode: host
node2:
container_name: redis-node2
build:
context: redis
args:
- REDIS_PORT=9002
volumes:
- ./data/node2/data:/data
network_mode: host
node3:
container_name: redis-node3
build:
context: redis
args:
- REDIS_PORT=9003
volumes:
- ./data/node3/data:/data
network_mode: host
node4:
container_name: redis-node4
build:
context: redis
args:
- REDIS_PORT=9004
volumes:
- ./data/node4/data:/data
network_mode: host
node5:
container_name: redis-node5
build:
context: redis
args:
- REDIS_PORT=9005
volumes:
- ./data/node5/data:/data
network_mode: host
node6:
container_name: redis-node6
build:
context: redis
args:
- REDIS_PORT=9006
volumes:
- ./data/node6/data:/data
network_mode: host
cluster-creator:
image: redis
container_name: redis-cluster-creator
entrypoint: redis-cli --cluster create 192.168.100.4:9001 192.168.100.4:9002 192.168.100.4:9003 192.168.100.4:9004 192.168.100.4:9005 192.168.100.4:9006 --cluster-replicas 1 --cluster-yes
network_mode: host
depends_on:
- node1
- node2
- node3
- node4
- node5
- node6

上述步驟都完成後就可以啟動 Docker Compose 了,啟動後可以找到下面這段輸出。這段輸出就是在進行 Hash Slot 的分配,Hash Slot 切成了三段後,先將 node5、node6、node4 分別分配作為 node1、node2、node3 的 Slave,接著再將 3 段 Hash Slot 分配給 node1、node2、node3。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
redis-cluster-creator | >>> Performing hash slots allocation on 6 nodes...
redis-cluster-creator | Master[0] -> Slots 0 - 5460
redis-cluster-creator | Master[1] -> Slots 5461 - 10922
redis-cluster-creator | Master[2] -> Slots 10923 - 16383
redis-cluster-creator | Adding replica 192.168.100.4:9005 to 192.168.100.4:9001
redis-cluster-creator | Adding replica 192.168.100.4:9006 to 192.168.100.4:9002
redis-cluster-creator | Adding replica 192.168.100.4:9004 to 192.168.100.4:9003
redis-cluster-creator | >>> Trying to optimize slaves allocation for anti-affinity
redis-cluster-creator | [WARNING] Some slaves are in the same host as their master
redis-cluster-creator | M: ab3b241fa07cada74242f69e11c84494f3a87fea 192.168.100.4:9001
redis-cluster-creator | slots:[0-5460] (5461 slots) master
redis-cluster-creator | M: 38e1bf11a87384f4da113c78d7b40f78e6120da0 192.168.100.4:9002
redis-cluster-creator | slots:[5461-10922] (5462 slots) master
redis-cluster-creator | M: 8dc77a4d71b04a7edc4354737ae6426132c51526 192.168.100.4:9003
redis-cluster-creator | slots:[10923-16383] (5461 slots) master
redis-cluster-creator | S: e90b96dae4f591656469b29288344e6c4bb9d2eb 192.168.100.4:9004
redis-cluster-creator | replicates 38e1bf11a87384f4da113c78d7b40f78e6120da0
redis-cluster-creator | S: 1246d07a9f245eaebd9fd97b9043cd9c8405464b 192.168.100.4:9005
redis-cluster-creator | replicates 8dc77a4d71b04a7edc4354737ae6426132c51526
redis-cluster-creator | S: c7930763878fbf5fe9025191936bdedf35901dca 192.168.100.4:9006
redis-cluster-creator | replicates ab3b241fa07cada74242f69e11c84494f3a87fea
redis-cluster-creator | >>> Nodes configuration updated
redis-cluster-creator | >>> Assign a different config epoch to each node
redis-cluster-creator | >>> Sending CLUSTER MEET messages to join the cluster
redis-cluster-creator | Waiting for the cluster to join

再往下可以看到這段輸出代表建立成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
redis-node6 | 6:M 25 Jul 2020 15:22:23.601 # Cluster state changed: ok
redis-node3 | 6:M 25 Jul 2020 15:22:23.604 # Cluster state changed: ok
redis-node5 | 6:M 25 Jul 2020 15:22:23.628 # Cluster state changed: ok
redis-node4 | 10:M 25 Jul 2020 15:22:23.634 # Cluster state changed: ok
redis-node2 | 6:M 25 Jul 2020 15:22:23.646 # Cluster state changed: ok
redis-node1 | 6:M 25 Jul 2020 15:22:24.511 # Cluster state changed: ok
...
redis-cluster-creator | ..
redis-cluster-creator | >>> Performing Cluster Check (using node 192.168.100.4:9001)
redis-cluster-creator | M: ab3b241fa07cada74242f69e11c84494f3a87fea 192.168.100.4:9001
redis-cluster-creator | slots:[0-5460] (5461 slots) master
redis-cluster-creator | 1 additional replica(s)
redis-cluster-creator | S: 1246d07a9f245eaebd9fd97b9043cd9c8405464b 192.168.100.4:9005
redis-cluster-creator | slots: (0 slots) slave
redis-cluster-creator | replicates 8dc77a4d71b04a7edc4354737ae6426132c51526
redis-cluster-creator | S: c7930763878fbf5fe9025191936bdedf35901dca 192.168.100.4:9006
redis-cluster-creator | slots: (0 slots) slave
redis-cluster-creator | replicates ab3b241fa07cada74242f69e11c84494f3a87fea
redis-cluster-creator | S: e90b96dae4f591656469b29288344e6c4bb9d2eb 192.168.100.4:9004
redis-cluster-creator | slots: (0 slots) slave
redis-cluster-creator | replicates 38e1bf11a87384f4da113c78d7b40f78e6120da0
redis-cluster-creator | M: 8dc77a4d71b04a7edc4354737ae6426132c51526 192.168.100.4:9003
redis-cluster-creator | slots:[10923-16383] (5461 slots) master
redis-cluster-creator | 1 additional replica(s)
redis-cluster-creator | M: 38e1bf11a87384f4da113c78d7b40f78e6120da0 192.168.100.4:9002
redis-cluster-creator | slots:[5461-10922] (5462 slots) master
redis-cluster-creator | 1 additional replica(s)
redis-cluster-creator | [OK] All nodes agree about slots configuration.
redis-cluster-creator | >>> Check for open slots...
redis-cluster-creator | >>> Check slots coverage...
redis-cluster-creator | [OK] All 16384 slots covered.

最後,Master 會把資料同步到 Slave,至此便完成了 Cluster 的整個建立過程。

1
2
3
4
5
6
...
redis-node6 | 6:S 25 Jul 2020 15:22:25.611 * MASTER <-> REPLICA sync started
...
redis-node5 | 6:S 25 Jul 2020 15:22:25.637 * MASTER <-> REPLICA sync started
...
redis-node4 | 10:S 25 Jul 2020 15:22:25.642 * MASTER <-> REPLICA sync started

讀寫資料
Cluster 建立好後就可以試著將資料寫進 Redis Server,下面這個指令可以看到 redis-node3 判斷要寫入的 Slot 不歸自己管,便回傳了 MOVED 錯誤提醒使用者要寫在 redis-node2。

1
2
$ docker exec -it redis-node3 redis-cli -p 9003 set k4 v4
(error) MOVED 8455 192.168.100.4:9002

然而如果每次都要使用者手動切換到其他的節點那就無法自動化的完成工作了,所以可以在 redis-cli 後面加上 -c 來啟動 Cluster 支援,這樣 Cluster 就會自動的導向到正確的節點並完成操作。

1
2
3
4
5
6
7
$ docker exec -it redis-node3 redis-cli -p 9003 -c set k4 v4
OK

$ docker exec -it redis-node3 redis-cli -p 9003 -c
127.0.0.1:9003> set k4 v4
-> Redirected to slot [8455] located at 192.168.100.4:9002
OK

故障轉移
接著我們來測試一下 Cluster 的故障轉移,首先先把 redis-node1 停止,這時會有節點發現 redis-node1 發生故障。

1
2
3
4
redis-node1 exited with code 137
redis-node4 | 6:S 26 Jul 2020 00:21:28.770 * Connecting to MASTER 192.168.100.4:9001
redis-node4 | 6:S 26 Jul 2020 00:21:28.770 * MASTER <-> REPLICA sync started
redis-node4 | 6:S 26 Jul 2020 00:21:28.770 # Error condition on socket for SYNC: Operation now in progress

當有一定數量的節點認為 redis-node1 故障時,會像哨兵模式一樣自動開始啟動選舉並且重新分配誰該成為新的 Master。此範例最後是由 redis-node4 勝出成為新的 Master,如輸出的最後一行顯示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
redis-node4        | 6:S 26 Jul 2020 00:21:48.716 * Marking node c0d1d6b5b817c479942d199a3da2b89b89958d0e as failing (quorum reached).
redis-node4 | 6:S 26 Jul 2020 00:21:48.716 # Start of election delayed for 912 milliseconds (rank #0, offset 84).
redis-node4 | 6:S 26 Jul 2020 00:21:48.716 # Cluster state changed: fail
redis-node6 | 7:S 26 Jul 2020 00:21:48.716 * FAIL message received from 9e7a868b38f55314e8bb61a1f2f527a07d1a7626 about c0d1d6b5b817c479942d199a3da2b89b89958d0e
redis-node6 | 7:S 26 Jul 2020 00:21:48.716 # Cluster state changed: fail
redis-node5 | 6:S 26 Jul 2020 00:21:48.716 * FAIL message received from 9e7a868b38f55314e8bb61a1f2f527a07d1a7626 about c0d1d6b5b817c479942d199a3da2b89b89958d0e
redis-node5 | 6:S 26 Jul 2020 00:21:48.716 # Cluster state changed: fail
redis-node3 | 6:M 26 Jul 2020 00:21:48.716 * FAIL message received from 9e7a868b38f55314e8bb61a1f2f527a07d1a7626 about c0d1d6b5b817c479942d199a3da2b89b89958d0e
redis-node3 | 6:M 26 Jul 2020 00:21:48.716 # Cluster state changed: fail
redis-node2 | 12:M 26 Jul 2020 00:21:48.716 * FAIL message received from 9e7a868b38f55314e8bb61a1f2f527a07d1a7626 about c0d1d6b5b817c479942d199a3da2b89b89958d0e
redis-node2 | 12:M 26 Jul 2020 00:21:48.716 # Cluster state changed: fail
redis-node4 | 6:S 26 Jul 2020 00:21:48.816 * Connecting to MASTER 192.168.100.4:9001
redis-node4 | 6:S 26 Jul 2020 00:21:48.817 * MASTER <-> REPLICA sync started
redis-node4 | 6:S 26 Jul 2020 00:21:48.817 # Error condition on socket for SYNC: Operation now in progress
redis-node4 | 6:S 26 Jul 2020 00:21:49.718 # Starting a failover election for epoch 7.
redis-node2 | 12:M 26 Jul 2020 00:21:49.719 # Failover auth granted to 9e7a868b38f55314e8bb61a1f2f527a07d1a7626 for epoch 7
redis-node3 | 6:M 26 Jul 2020 00:21:49.719 # Failover auth granted to 9e7a868b38f55314e8bb61a1f2f527a07d1a7626 for epoch 7
redis-node4 | 6:S 26 Jul 2020 00:21:49.720 # Failover election won: I'm the new master.

新增節點
新增節點前要先用原本的 Dockerfile 來建立 Image。

1
docker build -t redis-node-image --build-arg REDIS_PORT=9007 .

建立好 Image 後就可以啟動新的節點。

1
docker run --name redis-node7 --net=host redis-node-image

接著使用 add-node 指令將新節點加入到 Cluster 中,第一個 IP:Port 是新節點,第二個則是 Cluster 中的任意節點的。若是新節點要作為 Slave 則要再加上 --cluster-slave --cluster-master-id <master-node-Id>

1
docker exec -it redis-node7 redis-cli --cluster add-node 192.168.100.4:9007 192.168.100.4:9006

新增完成後會看到以下的輸出。

1
2
3
4
5
6
7
...
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
>>> Send CLUSTER MEET to node 192.168.100.4:9007 to make it join the cluster.
[OK] New node added correctly.

此時查看一下所有節點的狀態可以看到 redis-node7 的 Connected 後面是空的,代表還沒被分配到 Hash Slot,所以我們要再進行重新的分片。

1
2
3
4
5
6
7
8
$ docker exec -it redis-node7 redis-cli -p 9007 cluster nodes
c0d1d6b5b817c479942d199a3da2b89b89958d0e 192.168.100.4:9001@19001 slave 9e7a868b38f55314e8bb61a1f2f527a07d1a7626 0 1595731538000 7 connected
953c929f0092a971e27d0c40b093c772ea9553bc 192.168.100.4:9002@19002 master - 0 1595731537975 2 connected 5461-10922
d1096e4d456e806d046461c108af4f20edee9f99 192.168.100.4:9006@19006 slave 3c3a2216147a692b61b1de2a6f3806e0afa7cc87 0 1595731537000 3 connected
af228461d8ab2c1d9998a62e684bdf23dda851c1 192.168.100.4:9007@19007 myself,master - 0 1595731536000 0 connected
9e7a868b38f55314e8bb61a1f2f527a07d1a7626 192.168.100.4:9004@19004 master - 0 1595731536000 7 connected 0-5460
66fa35f56d59289e0f3e2b0f531b5c590f44b189 192.168.100.4:9005@19005 slave 953c929f0092a971e27d0c40b093c772ea9553bc 0 1595731538975 2 connected
3c3a2216147a692b61b1de2a6f3806e0afa7cc87 192.168.100.4:9003@19003 master - 0 1595731536000 3 connected 10923-16383

重新分片可以使用 reshard 指令指定要分配多少到哪個 node,這裡我們用 rebalance 指令來平均的重新分配到各個節點上。Cluster 預設是平均分配不會分配到空的節點,所以加上 --cluster-use-empty-masters 就可以啟用分配到空節點上。

1
docker exec -it redis-node7 redis-cli -p 9007 --cluster rebalance 192.168.100.4:9001 --cluster-use-empty-masters

分配好後可以看到 redis-node7 已經有被分到一些 Slot,而其他 Master 的 Slot 也有減少。

1
2
3
4
5
6
7
8
$ docker exec -it redis-node7 redis-cli -p 9007 cluster nodes
c0d1d6b5b817c479942d199a3da2b89b89958d0e 192.168.100.4:9001@19001 slave 9e7a868b38f55314e8bb61a1f2f527a07d1a7626 0 1595733490000 7 connected
953c929f0092a971e27d0c40b093c772ea9553bc 192.168.100.4:9002@19002 master - 0 1595733488392 2 connected 6827-10922
d1096e4d456e806d046461c108af4f20edee9f99 192.168.100.4:9006@19006 slave 3c3a2216147a692b61b1de2a6f3806e0afa7cc87 0 1595733490401 3 connected
af228461d8ab2c1d9998a62e684bdf23dda851c1 192.168.100.4:9007@19007 myself,master - 0 1595733487000 8 connected 0-1364 5461-6826 10923-12287
9e7a868b38f55314e8bb61a1f2f527a07d1a7626 192.168.100.4:9004@19004 master - 0 1595733488000 7 connected 1365-5460
66fa35f56d59289e0f3e2b0f531b5c590f44b189 192.168.100.4:9005@19005 slave 953c929f0092a971e27d0c40b093c772ea9553bc 0 1595733489396 2 connected
3c3a2216147a692b61b1de2a6f3806e0afa7cc87 192.168.100.4:9003@19003 master - 0 1595733488000 3 connected 12288-16383

刪除節點
被刪除的節點其所分配的 Slot 必須為 0,所以要先用 reshard 指令將 Slot 分配給其中一個 Master,再進行刪除。執行 Reshard 時會詢問要移動的 Slot 數量,這裡就把要刪除的節點的 Slot 數量全部填上去,接著再填入要接收 Slot 的 node Id 和要移出 Slot 的 node-Id,最後輸入 done 即完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
$ docker exec -it redis-node7 redis-cli -p 9007 --cluster reshard 192.168.100.4:9007
>>> Performing Cluster Check (using node 192.168.100.4:9007)
M: af228461d8ab2c1d9998a62e684bdf23dda851c1 192.168.100.4:9007
slots:[0-1364],[5461-6826],[10923-12287] (4096 slots) master
S: c0d1d6b5b817c479942d199a3da2b89b89958d0e 192.168.100.4:9001
slots: (0 slots) slave
replicates 9e7a868b38f55314e8bb61a1f2f527a07d1a7626
M: 9e7a868b38f55314e8bb61a1f2f527a07d1a7626 192.168.100.4:9004
slots:[1365-5460] (4096 slots) master
1 additional replica(s)
S: d1096e4d456e806d046461c108af4f20edee9f99 192.168.100.4:9006
slots: (0 slots) slave
replicates 3c3a2216147a692b61b1de2a6f3806e0afa7cc87
M: 953c929f0092a971e27d0c40b093c772ea9553bc 192.168.100.4:9002
slots:[6827-10922] (4096 slots) master
1 additional replica(s)
S: 66fa35f56d59289e0f3e2b0f531b5c590f44b189 192.168.100.4:9005
slots: (0 slots) slave
replicates 953c929f0092a971e27d0c40b093c772ea9553bc
M: 3c3a2216147a692b61b1de2a6f3806e0afa7cc87 192.168.100.4:9003
slots:[12288-16383] (4096 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)? 4096
What is the receiving node ID? 9e7a868b38f55314e8bb61a1f2f527a07d1a7626
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: af228461d8ab2c1d9998a62e684bdf23dda851c1
Source node #2: done

此時再確認一下各節點的 Slot 數量,可以看到要刪除的 redis-node7 已經沒有 Slot,而 redis-node4 增加了許多 Slot。

1
2
3
4
5
$ docker exec -it redis-node7 redis-cli -p 9007 --cluster check 192.168.100.4:9007
192.168.100.4:9007 (af228461...) -> 0 keys | 0 slots | 0 slaves.
192.168.100.4:9004 (9e7a868b...) -> 0 keys | 8192 slots | 1 slaves.
192.168.100.4:9002 (953c929f...) -> 0 keys | 4096 slots | 1 slaves.
192.168.100.4:9003 (3c3a2216...) -> 0 keys | 4096 slots | 1 slaves.

現在可以使用 del-node 指令將節點正式刪除。

1
2
3
4
$ docker exec -it redis-node4 redis-cli -p 9004 --cluster del-node 192.168.100.4:9007 af228461d8ab2c1d9998a62e684bdf23dda851c1
>>> Removing node af228461d8ab2c1d9998a62e684bdf23dda851c1 from cluster 192.168.100.4:9007
>>> Sending CLUSTER FORGET messages to the cluster...
>>> Sending CLUSTER RESET SOFT to the deleted node.

Summary

本篇介紹了三種模式來實現多點資料庫,利用這些方式可以讓資料的可用性提升,但是相對的多點資料庫會導致資料一致性降低。

參考

[1] Redis 主從複製 原理與用法
[2] Redis面试篇 – Redis主從複製原理
[3] Redis replication 揭秘
[4] redis:詳解三種集群策略
[5] 深入剖析Redis系列(二) - Redis哨兵模式與高可用集群
[6] Redis 哨兵模式
[7] Redis 哨兵模式原理和部署
[8] Redis Cluster 槽(Slot)
[9] Redis學習筆記(十七) 集群(上)
[10] redis槽道原理
[11] 如何理解Redis集群的Slot映射
[12] Redis的高可用詳解:Redis哨兵、複製、集群的設計原理,以及區別
[13] Redis集群的原理和搭建
[14] Docker方式部署redis-cluster
[15] 解决redis集群./redis-cli 啟動 Connection refused
[16] 一文掌握Redis主從複製、哨兵、Cluster三種集群模式
[17] Redis 5.0 redis-cli --cluster help說明
[18] Redis Cluster的两種搭建和簡單使用
[19] 使用 Docker Compose 建立 Redis Cluster
[20] 分布式系统的 CAP 定理