前言
Redis 是我們目前大規模使用的緩存中間件,由於他強調高效而又便捷的功能,得到了廣泛的使用。單節點的Redis已經達到了很高的性能,為了提高可用性我們可以使用Redis 集群。本文參考了Rdis的官方文檔和使用Redis官方提供的Redis Cluster工具搭建Rdis集群。
Redis 集群的概念
介紹
Redis 集群是一個可以在多個 Redis 節點之間進行數據共享的設施(installation)。
Redis 集群不支持那些需要同時處理多個鍵的 Redis 命令, 因為執行這些命令需要在多個 Redis 節點之間移動數據, 並且在高負載的情況下, 這些命令將降低 Redis 集群的性能, 並導致不可預測的錯誤。
Redis 集群通過分區(partition)來提供一定程度的可用性(availability): 即使集群中有一部分節點失效或者無法進行通訊, 集群也可以繼續處理命令請求。
Redis 集群提供了以下兩個好處:
將數據自動切分(split)到多個節點的能力。
當集群中的一部分節點失效或者無法進行通訊時, 仍然可以繼續處理命令請求的能力。
數據分片
Redis 集群使用數據分片(sharding)而非一致性哈希(consistency hashing)來實現: 一個 Redis 集群包含 16384 個哈希槽(hash slot), 數據庫中的每個鍵都屬於這 16384 個哈希槽的其中一個, 集群使用公式 CRC16(key) % 16384 來計算鍵 key 屬於哪個槽, 其中 CRC16(key) 語句用於計算鍵 key 的 CRC16 校驗和 。
集群中的每個節點負責處理一部分哈希槽。 舉個例子, 一個集群可以有三個哈希槽, 其中:
- 節點 A 負責處理 0 號至 5500 號哈希槽。
- 節點 B 負責處理 5501 號至 11000 號哈希槽。
- 節點 C 負責處理 11001 號至 16384 號哈希槽。
這種將哈希槽分布到不同節點的做法使得用戶可以很容易地向集群中添加或者刪除節點。 比如說:
我現在想設置一個key,叫my_name:
set my_name zhangguoji
按照Redis Cluster的哈希槽算法,CRC16('my_name')%16384 = 2412 那么這個key就被分配到了節點A上
同樣的,當我連接(A,B,C)的任意一個節點想獲取my_name這個key,都會轉到節點A上
再比如
如果用戶將新節點 D 添加到集群中, 那么集群只需要將節點 A 、B 、 C 中的某些槽移動到節點 D 就可以了。
增加一個D節點的結果可能如下:
- 節點A覆蓋1365-5460
- 節點B覆蓋6827-10922
- 節點C覆蓋12288-16383
- 節點D覆蓋0-1364,5461-6826,10923-1228
與此類似, 如果用戶要從集群中移除節點 A , 那么集群只需要將節點 A 中的所有哈希槽移動到節點 B 和節點 C , 然后再移除空白(不包含任何哈希槽)的節點 A 就可以了。
因為將一個哈希槽從一個節點移動到另一個節點不會造成節點阻塞, 所以無論是添加新節點還是移除已存在節點, 又或者改變某個節點包含的哈希槽數量, 都不會造成集群下線。
所以,Redis Cluster的模型大概是這樣的形狀
主從復制模型
為了使得集群在一部分節點下線或者無法與集群的大多數(majority)節點進行通訊的情況下, 仍然可以正常運作, Redis 集群對節點使用了主從復制功能: 集群中的每個節點都有 1 個至 N 個復制品(replica), 其中一個復制品為主節點(master), 而其余的 N-1 個復制品為從節點(slave)。
在之前列舉的節點 A 、B 、C 的例子中, 如果節點 B 下線了, 那么集群將無法正常運行, 因為集群找不到節點來處理 5501 號至 11000號的哈希槽。
另一方面, 假如在創建集群的時候(或者至少在節點 B 下線之前), 我們為主節點 B 添加了從節點 B1 , 那么當主節點 B 下線的時候, 集群就會將 B1 設置為新的主節點, 並讓它代替下線的主節點 B , 繼續處理 5501 號至 11000 號的哈希槽, 這樣集群就不會因為主節點 B 的下線而無法正常運作了。
不過如果節點 B 和 B1 都下線的話, Redis 集群還是會停止運作。
Redis一致性保證
Redis 並不能保證數據的強一致性. 這意味這在實際中集群在特定的條件下可能會丟失寫操作:第一個原因是因為集群是用了異步復制. 寫操作過程:
-
客戶端向主節點B寫入一條命令.
-
主節點B向客戶端回復命令狀態.
-
主節點將寫操作復制給他得從節點 B1, B2 和 B3
主節點對命令的復制工作發生在返回命令回復之后, 因為如果每次處理命令請求都需要等待復制操作完成的話, 那么主節點處理命令請求的速度將極大地降低 —— 我們必須在性能和一致性之間做出權衡。 注意:Redis 集群可能會在將來提供同步寫的方法。 Redis 集群另外一種可能會丟失命令的情況是集群出現了網絡分區, 並且一個客戶端與至少包括一個主節點在內的少數實例被孤立。
舉個例子 假設集群包含 A 、 B 、 C 、 A1 、 B1 、 C1 六個節點, 其中 A 、B 、C 為主節點, A1 、B1 、C1 為A,B,C的從節點, 還有一個客戶端 Z1 假設集群中發生網絡分區,那么集群可能會分為兩方,大部分的一方包含節點 A 、C 、A1 、B1 和 C1 ,小部分的一方則包含節點 B 和客戶端 Z1 .
Z1仍然能夠向主節點B中寫入, 如果網絡分區發生時間較短,那么集群將會繼續正常運作,如果分區的時間足夠讓大部分的一方將B1選舉為新的master,那么Z1寫入B中得數據便丟失了.
注意, 在網絡分裂出現期間, 客戶端 Z1 可以向主節點 B 發送寫命令的最大時間是有限制的, 這一時間限制稱為節點超時時間(node timeout), 是 Redis 集群的一個重要的配置選項
Redis Cluster 部署文檔
updated: 24/05/2019
1 說明
Redis Cluster 的主要特點如下:
- 無中心結構,客戶端與 redis 節點直連,不需要中間代理層
- 節點冗余設計,slave->master 選舉,集群容錯
- 數據分片存儲,且支持在線分片
- ASK / MOVED 轉向機制,可通過任意節點,讀寫不屬於本節點的數據
本文以部署一個可實現高可用的最小集群為例,集群部署在三台主機上,包含 M1、M2、M3、S1、S2、S3 六個節點。
M1、M2、M3 為主節點對應 Redis 實例:7000,7001,7002
S1、S2、S3 為從節點對應 Redis 實例:7003,7004,7005
主從節點交叉連接,對應關系為:
M1 -> S2
M2 -> S3
M3 -> S1
2 環境准備
系統環境:
主機 | IP | 節點-角色-實例(端口) |
---|---|---|
redis1 | 192.168.0.100 | M1-master-7000、S1-slave-7003 |
redis2 | 192.168.0.101 | M2-master-7001、S2-slave-7004 |
redis3 | 192.168.0.102 | M3-master-7002、S3-slave-7005 |
3 安裝軟件
分別在三台主機上編譯安裝 redis 5.0.4 版本。
安裝完畢建議將 redis bin 目錄加入 PATH 環境變量
4 配置集群
參考步驟 4.1 - 4.3,分別在三台主機上進行 Redis 集群配置。
4.1 創建集群目錄
創建集群各節點配置和數據目錄。
mkdir -p /data/redis-cluster/{7000,7001}
以 redis1 為例,目錄結構如下:
/data/redis-cluster/
├── 7000
│ ├── redis.conf # redis 實例配置文件
│ └── nodes.conf # redis 集群節點的配置文件(由集群自動創建)
└── 7003
│ ├── redis.conf # redis 實例配置文件
│ └── nodes.conf # redis 集群節點的配置文件(由集群自動創建)
4.2 創建實例配置文件
分別復制 redis 源碼目錄下的 redis.conf 至節點配置和數據目錄。
編輯 redis 實例配置文件中的各項配置。redis 默認未開啟集群功能,需修改下面幾個配置開啟:
port 7000
bind 0.0.0.0 # 允許其他主機連接
dir /data/redis-cluster/7000 # 節點實例配置目錄
cluster-enabled yes # 開啟集群
cluster-config-file nodes.conf # 集群配置文件
cluster-node-timeout 5000 # 超時時間
appendonly yes # 並開啟AOF模式
請根據生產環境性能需求和實際部署情況修改相關配置項,注意每個節點的實例配置文件中端口不同。
4.3 防火牆設置
根據情況修改服務器防火牆配置,允許 Redis 主機互相連接 7000-7005, 17000-17005 端口,允許所有業務服務器連接 Redis 服務器的 7000-7005 端口。
5 啟動集群
5.1 啟動實例
分別在三台主機上啟動全部 6 個 redis 節點實例,啟動時指定配置文件為各自節點配置目錄中的 redis.conf。
5.2 啟動集群
在任意一台 redis 節點主機上執行如下命令啟動集群:
redis-cli --cluster create \
--cluster-replicas 1 \
192.168.0.100:7000 \
192.168.0.100:7001 \
192.168.0.101:7002 \
192.168.0.101:7003 \
192.168.0.102:7004 \
192.168.0.102:7005
注意修改命令中的 IP 為 redis 節點實例端口對應的真實主機 IP。
根據提示輸入yes
,出現下列信息則表示集群創建成功。
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
6 測試
查看集群信息
在任意一個 redis 節點的主機上執行以下命令:
# redis-cli -c -h 192.168.0.100 -p 7000 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:6
cluster_my_epoch:1
cluster_stats_messages_ping_sent:101
cluster_stats_messages_pong_sent:98
cluster_stats_messages_sent:199
cluster_stats_messages_ping_received:93
cluster_stats_messages_pong_received:101
cluster_stats_messages_meet_received:5
cluster_stats_messages_received:199
列出集群節點
在任意一個 redis 節點的主機上執行以下命令,列出集群當前已知的所有節點(node),以及這些節點的相關信息。
# redis-cli -c -h 192.168.0.100 -p 7000 cluster nodes
輸出類似以下信息:
50725018cd7f5f20214b0ed462975258397dfe29 192.168.0.102:7005@17005 slave 39e335386bb48f012f433287ed853174009114e5 0 1554876136000 6 connected
8874f69c2f5747cce3a02387167a00cdaba43d3c 192.168.0.100:7001@17001 master - 0 1554876137380 2 connected 5461-10922
c9cb48efbc1a83098cb730a5295402d9cdb343c2 192.168.0.101:7003@17003 slave 917301c74a9ed1ed52918b8e4c7f88de9743c361 0 1554876136375 4 connected
5633e918818033552b1adc089d99fbe9bcf36589 192.168.0.102:7004@17004 slave 8874f69c2f5747cce3a02387167a00cdaba43d3c 0 1554876136072 5 connected
917301c74a9ed1ed52918b8e4c7f88de9743c361 192.168.0.100:7000@17000 myself,master - 0 1554876136000 1 connected 0-5460
39e335386bb48f012f433287ed853174009114e5 192.168.0.101:7002@17002 master - 0 1554876136878 3 connected 10923-16383
查看集群數據槽分配
在任意一個 redis 節點的主機上執行以下命令,顯示集群當前所有數據槽的分配情況。
# redis-cli -c -h 192.168.0.100 -p 7000 cluster slots
輸出類似以下信息:
1) 1) (integer) 5461
2) (integer) 10922
3) 1) "127.0.0.1"
2) (integer) 7001
4) 1) "127.0.0.1"
2) (integer) 7004
2) 1) (integer) 0
2) (integer) 5460
3) 1) "127.0.0.1"
2) (integer) 7000
4) 1) "127.0.0.1"
2) (integer) 7003
3) 1) (integer) 10923
2) (integer) 16383
3) 1) "127.0.0.1"
2) (integer) 7002
4) 1) "127.0.0.1"
2) (integer) 7005