前言
相关系列
- 《Redis & 目录》(持续更新)
- 《Redis & 集群 & 源码》(学习过程/多有漏误/仅作参考/不再更新)
- 《Redis & 集群 & 总结》(学习总结/最新最准/持续更新)
- 《Redis & 集群 & 问题》(学习解答/持续更新)
概述
简介
Redis集群通过数据分片来分摊单台Redis的读/写压力。如果说主从同步是出于安全方面的考量而执行的行为,那么集群就是出于性能方面的考量所做出的应对。当面对海量数据的高并发读/写时,仅依靠单台Redis实例的吞吐量是远远无法满足实际场景的使用需求的。诚然我们可以通过提升硬件配置方式来垂直拓展其执行能力,但单机的性能终究是有极限的,因此多机器协作共行的水平拓展才是高并发的最终解决方案,而集群便是该方案的具体产物。在集群中,数据会通过某些方式被均匀分片到内部的多个节点上,即每个节点都只负责保存整体数据的一部分。而当客户端试图以键访问目标数据时,集群会将访问请求引导至目标数据的物理/逻辑所在节点以完成读/写,从而实现多Redis实例协作共行的效果。
Redis集群是去中心化的。所谓去中心化是指没有Redis集群没有独立的组件来专职处理访问的引导问题,如此设计的原因是因为单机既然会成为高并发读/写的性能瓶颈,那么其自然也会成为高并发引导的性能瓶颈。因此与其独立运行倒不如直接将该功能嵌入所有节点中以同步实现引导组件的集群化,故而Redis集群是去中性化的,这使得所有节点都具备了引导请求至正确节点的能力。而事实上Redis的设计其实更加精妙,因为其允许发起访问的客户端也同时具备一定程度的引导能力,因此除非是完全无法引导的首次请求或数据重分片导致引导能力失效,否则Redis其实是可以直接对目标节点发起访问的,而这就更进一步保证了Redis的读/写性能…该知识点会在下文讲解重定向时详述。
Redis集群支持高效扩/缩容。扩/缩容是集群必须考虑的问题,其本质是在集群中加入/移除节点。对于集群来说无法扩/缩容的概念是不存在的,因为集群的构建本身也是扩/缩容。但想要实现高效扩/缩容却相当有难度,因为你必须同时满足“动态扩/缩容”及“高效重分片”两个要求。所谓动态扩/缩容是与等价于重新构建的静态扩/缩容对立的概念,具体是指在不间断集群运行的情况下将节点动态地加入/移除;而所谓“高效重分片”则是指节点的加入/移除可能会导致数据的存储节点发生变化,因此便需要在确保新请求/数据正确落户的同时将旧数据“高效地”迁移至新节点中。为此Redis引入了哈希槽的概念,其核心作用是将数据重分片的被动性转为了主动性,从而令开发者可以自行制定高效的迁移方案,即自行决定“迁移哪几台节点的数据/迁移多少数据/迁移到哪几个节点”…该知识点会在下文讲解数据重分片是详述。
Redis集群支持自动主从同步/故障转移。对于组成集群的Redis实例,集群会根据要求自行挑选合适数量的Redis实例以完成主/从关系的构建,因此开发者并无需自行添加/修改/执行主从同步的相关配置/指令。此外Redis集群还内嵌了哨兵并全自动执行,因此开发者也是无需自行搭建哨兵来实现故障转移的自动化。
缺点
- 不支持多键读/写,因为这些键对应的数据可能在不同节点上;
- 不支持跨节点的事务,原因相同。
使用
构建
Redis对集群的支持需要通过添加/修改部分配置开启,由于Redis集群会自动选定/构建主/从关系,因此所有Redis的实例其配置除了端口外理应都是相同的…相关配置项及完整的配置文件如下所示:
名称:cluster-enabled
作用:设置是否开启集群支持(yes:是,no:否)。
默认:无配置,等价于no
名称:cluster-config-file
作用:设置Redis的集群配置文件名称,该名称在集群中必须唯一,而配置文件则会在Redis实例加入集群成为节点后自行在工作目录生成/维护。
默认:无配置
名称:cluster-node-timeout
作用:设置执行故障转移的主节点超时时间。
默认:无配置
名称:cluster-require-full-coverage
作用:设置集群是否在部分主节点(及其全部从节点)宕机时全面宕机(yes:是,no:否)。
默认:无配置,等价于yes
# ---- 继承:继承原始配置文件的一切配置项。
include /usr/local/redis-6.2.1/redis.conf
# ---- 端口:指定端口。
port 6379
# ---- 目录:指定工作目录。
dir /usr/local/redis-6.2.1/cluster/
# ---- 持久化:开启RDB/AOF,指定RDB快照/AOF日志文件名称。
save 300 100
dbfilename dump-6379.rdb
appendonly yes
appendfilename "appendonly-6379.aof"
# ---- 进程:指定进程文件。
pidfile /var/run/redis-6379.pid
# ---- 日志:指定日志文件。
# logfile "./redis-master-6379.log"
# ---- 集群:开启集群/指定节点配置文件/设置节点超时时间/获取不全覆盖。
cluster-enabled yes
cluster-config-file node-6379.conf
cluster-node-timeout 15000
// ---- 允许集群在部分主节点(及其所有从节点)宕机时继续工作。
cluster-require-full-coverage no
按上述配置启动端口依次为6379/6380/6381/6382/6383/6384的六台Redis实例。
将所有Redis实例启动后,需要再通过相关指令将之统一绑定为集群…相关指令具体如下:
- redis-cli --cluster create : : … : [–cluster-replicas ]:将指定Redis实例集构建为集群。
------------------------- 入参 -------------------------
ip… @ IP集:参与集群构建的Redis实例IP集;
port… @ 端口集:参与集群构建的Redis实例端口集。
------------------------- 选项 -------------------------
–cluster-replicas :设置此次构建会以1:replicas的比例从指定Redis实例集中选定主/从节点并自动构建主从关系,否则全都为主节点。
// ---- 将127.0.0.1:6379/127.0.0.1:6380/127.0.0.1:6381/127.0.0.1:6382/
// 127.0.0.1:6383/127.0.0.1:6384六台Redis实例构建为集群,并为每台主节点分配一台从节点。
stephan@shuoshuren ~ % redis-cli --cluster create 127.0.0.1:6379 127.0.0.1:6380 127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384 --cluster-replicas 1
// ---- 将16384个哈希槽均匀分割,用于后续分配给主节点。由于要为每台主节点分配一台从节点,
// 因此按1:1的比例主/从节点各有三台,因此哈希槽会被分为相对均匀的三份。哈希槽可以"近似"
// 理解可存储空间,一台没有被分配哈希槽的主节点是无法保存数据的。
>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
// ---- 预设主从关系。
Adding replica 127.0.0.1:6383 to 127.0.0.1:6379
Adding replica 127.0.0.1:6384 to 127.0.0.1:6380
Adding replica 127.0.0.1:6382 to 127.0.0.1:6381
// ---- 优化从节点分配,这可能改变上方预设的主从关系。
>>> Trying to optimize slaves allocation for anti-affinity
[WARNING] Some slaves are in the same host as their master
// ---- 将分割好的三段哈希槽分配给三台主节点,每段哈希槽的总数大致是相同的。
// IP:端口前字符串是集群为主/从节点生成的ID。
M: c47afeb8640fb9ffa11e24c82411185a3f30fdb5 127.0.0.1:6379slots:[0-5460] (5461 slots) master
M: ec4e3249f88e517da684b40e9c2c60716ec072c8 127.0.0.1:6380slots:[5461-10922] (5462 slots) master
M: b78bc221bd5a925149bf2a077afe0a327e8975f3 127.0.0.1:6381slots:[10923-16383] (5461 slots) master
// ---- 确定最终的主从关系。
S: 2c6748a8e93c5b26b5d6ecc29d119567e8bc63b7 127.0.0.1:6382replicates c47afeb8640fb9ffa11e24c82411185a3f30fdb5
S: 1f44ff8cb1b394bf01d82ce0bc1d3c1f1a7af93f 127.0.0.1:6383replicates ec4e3249f88e517da684b40e9c2c60716ec072c8
S: f6112d66cdc6f17c0a1b0a3e23d43e200b239949 127.0.0.1:6384replicates b78bc221bd5a925149bf2a077afe0a327e8975f3
// ---- 确定是否接受上述自动生成的集群结构,这里需要手动输入yes确定,确定后会便会正式按此结构构建集群。
Can I set the above configuration? (type 'yes' to accept): yes
// ---- 生成各个节点的配置文件。
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
// ---- 发送CLUSTER MEET指令至各个Redis实例并等待,该指令用于令Redis实例加入集群成为节点。
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join
.
// ---- 选中一台Redis实例检查集群的构建情况,并打印主/从节点的的信息。
>>> Performing Cluster Check (using node 127.0.0.1:6379)
M: c47afeb8640fb9ffa11e24c82411185a3f30fdb5 127.0.0.1:6379slots:[0-5460] (5461 slots) master1 additional replica(s)
M: ec4e3249f88e517da684b40e9c2c60716ec072c8 127.0.0.1:6380slots:[5461-10922] (5462 slots) master1 additional replica(s)
S: 1f44ff8cb1b394bf01d82ce0bc1d3c1f1a7af93f 127.0.0.1:6383slots: (0 slots) slavereplicates ec4e3249f88e517da684b40e9c2c60716ec072c8
S: 2c6748a8e93c5b26b5d6ecc29d119567e8bc63b7 127.0.0.1:6382slots: (0 slots) slavereplicates c47afeb8640fb9ffa11e24c82411185a3f30fdb5
M: b78bc221bd5a925149bf2a077afe0a327e8975f3 127.0.0.1:6381slots:[10923-16383] (5461 slots) master1 additional replica(s)
S: f6112d66cdc6f17c0a1b0a3e23d43e200b239949 127.0.0.1:6384slots: (0 slots) slavereplicates b78bc221bd5a925149bf2a077afe0a327e8975f3
[OK] All nodes agree about slots configuration.
// ---- 检查所有的哈希槽是否已全部分配。
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
构建完成后,执行{redis-cli --cluster check/info}执行检查/查看集群状态/信息…相关指令具体如下:
- redis-cli --cluster check : [–cluster-search-multiple-owners]:检查指定集群的状态。
------------------------- 入参 -------------------------
ip @ IP:指定集群中任意节点的IP;
port @ 端口:指定集群中任意节点的端口。
------------------------- 选项 -------------------------
–cluster-search-multiple-owners:设置此次检查需要判断是否存在哈希槽被分配给多个主节点的情况。
// ---- 检查指定集群,并需要判断是否存在哈希槽被分配给多个主节点的情况。
stephan@shuoshuren ~ % redis-cli --cluster check 127.0.0.1:6379 --cluster-search-multiple-owners
// ---- 主节点IP:端口/主节点ID/键数量/分配哈希槽的数量/从节点数量。
127.0.0.1:6379 (9b55ef69...) -> 0 keys | 5461 slots | 1 slaves.
127.0.0.1:6380 (957ccf94...) -> 0 keys | 5462 slots | 1 slaves.
127.0.0.1:6381 (779014eb...) -> 0 keys | 5461 slots | 1 slaves.
[OK] 0 keys in 3 masters.
0.00 keys per slot on average.
// ---- 主/从节点的各类信息。
>>> Performing Cluster Check (using node 127.0.0.1:6379)
M: c47afeb8640fb9ffa11e24c82411185a3f30fdb5 127.0.0.1:6379slots:[0-5460] (5461 slots) master1 additional replica(s)
M: ec4e3249f88e517da684b40e9c2c60716ec072c8 127.0.0.1:6380slots:[5461-10922] (5462 slots) master1 additional replica(s)
S: 1f44ff8cb1b394bf01d82ce0bc1d3c1f1a7af93f 127.0.0.1:6383slots: (0 slots) slavereplicates ec4e3249f88e517da684b40e9c2c60716ec072c8
S: 2c6748a8e93c5b26b5d6ecc29d119567e8bc63b7 127.0.0.1:6382slots: (0 slots) slavereplicates c47afeb8640fb9ffa11e24c82411185a3f30fdb5
M: b78bc221bd5a925149bf2a077afe0a327e8975f3 127.0.0.1:6381slots:[10923-16383] (5461 slots) master1 additional replica(s)
S: f6112d66cdc6f17c0a1b0a3e23d43e200b239949 127.0.0.1:6384slots: (0 slots) slavereplicates b78bc221bd5a925149bf2a077afe0a327e8975f3
// ---- 检查哈希槽的覆盖情况。
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
// ---- 检查是否有哈希槽被分配给多个主节点,这个检查只有使用了
// --cluster-search-multiple-owners选项才会执行。
>>> Check for multiple slot owners...
[OK] No multiple owners found.
- redis-cli --cluster info : [–cluster-search-multiple-owners]:查看指定集群的信息。
------------------------- 入参 -------------------------
ip @ IP:指定集群中任意节点的IP;
port @ 端口:指定集群中任意节点的端口。
// ---- 查看指定集群的信息,相当于简略版的Check指令。
stephan@shuoshuren ~ % redis-cli --cluster info 127.0.0.1:6379
// ---- 主节点IP:端口/主节点ID/键数量/分配哈希槽的数量/从节点数量。
127.0.0.1:6379 (9b55ef69...) -> 0 keys | 5461 slots | 1 slaves.
127.0.0.1:6380 (957ccf94...) -> 0 keys | 5462 slots | 1 slaves.
127.0.0.1:6381 (779014eb...) -> 0 keys | 5461 slots | 1 slaves.
[OK] 0 keys in 3 masters.
0.00 keys per slot on average.
在确定集群已正常构建后,我们选择端口为6379的Redis实例执行写指令,会发现其中部分数据会转发至其它Redis实例以达成数据分片。
// ---- 设置名称,该键对应的哈希槽为5798,而该哈希槽对应的Redis实例
// 为6380,因此数据会被转发至端口为6380的Redis实例保存。
> SET name 说淑人
MOVED 5798 127.0.0.1:6380
// ---- 设置性别/年龄,这两个数据都在当前端口为6379的Redis实例上保存。
> SET sex 男
OK
> SET age 28
OK
// ---- 设置身高,该键对应的哈希槽为8223,而该哈希槽对应的Redis实例
// 为8223,因此数据会被转发至端口为6380的Redis实例保存。
> SET height 178
MOVED 8223 127.0.0.1:6380
// ---- 设置体重,该键对应的哈希槽为8223,而该哈希槽对应的Redis实例
// 为16280,因此数据会被转发至端口为6380的Redis实例保存。
> SET weight 74
MOVED 16280 127.0.0.1:6381
加入/扩容
未在初始构建时加入集群的Redis实例可通过外部执行{redis-cli --cluster add-node}指令或内部执行{CLUSTER MEET}指令来在后期动态加入集群,这其中{redis-cli --cluster add-node}指令是通过在指定Redis实例内部执行{CLUSTER MEET}指令及其它指令来实现的…相关指令详细具体如下:
- redis-cli --cluster add-node <redis_ip>:<redis_port> <node_ip>:<node_port> [–cluster-slave] [–cluster-master-id ]:将指定Redis实例加入指定集群。
------------------------- 入参 -------------------------
redis_ip @ Redis IP:指定Redis实例的IP;
redis_port @ Redis端口:指定Redis实例的端口;
node_ip @ 节点IP:指定集群中任意节点的IP;
node_port @ 节点端口:指定集群中任意节点的端口。
------------------------- 选项 -------------------------
–cluster-slave:设置指定Redis实例会以从节点的身份加入指定集群,在不配合–cluster-master-id选项使用时其主节点将由集群自行指定;
–cluster-master-id :设置指定Redis实例会以指定主节点(以ID标志)的从节点身份加入指定集群,该选项需配合–cluster-slave选项一同使用。
// ---- 将端口为6385的Redis实例以从节点的身份加入端口为6379的Redis实例所在
// 的集群中,并将其主节点设置为端口为6379的Redis实例
stephan@shuoshuren ~ % redis-cli --cluster add-node 127.0.0.1:6385 127.0.0.1:6379 --cluster-slave --cluster-master-id c47afeb8640fb9ffa11e24c82411185a3f30fdb5
>>> Adding node 127.0.0.1:6386 to cluster 127.0.0.1:6379
// ---- 检查集群的情况,并输出主/从节点的信息。
>>> Performing Cluster Check (using node 127.0.0.1:6379)
M: c47afeb8640fb9ffa11e24c82411185a3f30fdb5 127.0.0.1:6379slots:[0-5460] (5461 slots) master1 additional replica(s)
M: ec4e3249f88e517da684b40e9c2c60716ec072c8 127.0.0.1:6380slots:[5461-10922] (5462 slots) master1 additional replica(s)
S: 1f44ff8cb1b394bf01d82ce0bc1d3c1f1a7af93f 127.0.0.1:6383slots: (0 slots) slavereplicates ec4e3249f88e517da684b40e9c2c60716ec072c8
S: 2c6748a8e93c5b26b5d6ecc29d119567e8bc63b7 127.0.0.1:6382slots: (0 slots) slavereplicates c47afeb8640fb9ffa11e24c82411185a3f30fdb5
M: b78bc221bd5a925149bf2a077afe0a327e8975f3 127.0.0.1:6381slots:[10923-16383] (5461 slots) master1 additional replica(s)
S: f6112d66cdc6f17c0a1b0a3e23d43e200b239949 127.0.0.1:6384slots: (0 slots) slavereplicates b78bc221bd5a925149bf2a077afe0a327e8975f3
[OK] All nodes agree about slots configuration.
// ----
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
// ---- 向端口为6385的Redis发送CLUSTER MEET指令以令之以主节点的身份加入集群。
>>> Send CLUSTER MEET to node 127.0.0.1:6385 to make it join the cluster.
Waiting for the cluster to join
// ---- 将端口为6385的主节点设置为端口为6379的从节点。如果在指令中
// 没有舒勇--cluster-slave/--cluster-master-id选项的话是不会有这步输出的。
>>> Configure node as replica of 127.0.0.1:6379.
[OK] New node added correctly.
- CLUSTER MEET :将当前Redis实例以主节点身份加入指定集群。
------------------------- 入参 -------------------------
ip @ IP:指定集群中任意节点的IP;
port @ 端口:指定集群中任意节点的端口。
// ---- 将当前(端口为6385)Redis实例以主节点身份加入指定集群中。
> CLUSTER MEET 127.0.0.1 6379
OK
- CLUSTER REPLICATE :将当前Redis实例设置为所在集群中指定主节点的从节点。
------------------------- 入参 -------------------------
node_id @ 节点ID:所在集群中指定主节点的ID。
// ---- 将当前(端口为6385)Redis实例加入指定集群中。
> CLUSTER REPLICATE c47afeb8640fb9ffa11e24c82411185a3f30fdb5
OK
Redis实例以主节点身份加入集群后并无法立即对外提供服务,原因是此时其尚未被分配哈希槽。由于未被分配哈希槽的主节点是无法保存数据的,因此想要新主节点可用就必须先外部执行{redis-cli --cluster rebalance}指令来为之分配哈希槽。{redis-cli --cluster rebalance}指令的作用是平衡集群中各主节点的哈希槽数量至大致均等…该指令的详情具体如下。而除此以外{redis-cli --cluster reshard}指令也可用于为主节点分配哈希槽,但其与{redis-cli --cluster rebalance}指令的差别在于其需要人为控制从集群中的哪台主节点上拆出多少哈希槽分配给新主节点,因此如果没有特殊需求那么使用全自动的{redis-cli --cluster rebalance}指令来执行哈希槽分配才是基于负载均衡考虑的最佳选择,而{redis-cli --cluster reshard}指令则更适用于在移除主节点前迁移其所有哈希槽/数据至其它主节点…该知识点会在下文讲述移除时详述:
- redis-cli --cluster rebalance : [–cluster-simulate] [–cluster-use-empty-masters] [–cluster-replace] [–cluster-threshold ] [–cluster-timeout ] [–cluster-pipeline ] [–cluster-weight <node1=w1…nodeN=wN>]:平衡指定集群中各主节点的哈希槽数量至大致均等,并在各主节点的从节点数量失调时合理调整。该指令的执行时间可能较长,因为哈希槽的重分配会导致数据重分片。
------------------------- 入参 -------------------------
ip @ IP:指定集群中任意节点的IP;
port @ 端口:指定集群中任意节点的端口。
------------------------- 选项 -------------------------
–cluster-simulate:设置此次重平衡只是评估影响的模拟,并不会真的执行;
–cluster-use-empty-masters:设置此次重平衡允许将空(无哈希槽)主节点作为哈希槽的分配对象,默认情况下重平衡只会在非空主节点上执行;
–cluster-replace:设置此次重平衡导致的数据重分片会在目标主节点存在相同键时覆盖原数据;
–cluster-threshold :设置此次重平衡只在各主节点哈希槽数量相差超过指定阈值时执行。该选项未使用时也会隐式生效,默认值为2%;
–cluster-timeout :设置此次重平衡只在指定时间(ms)内执行;
–cluster-pipeline :设置此次重平衡导致的数据重分片一次可迁移的数据量。该选项未使用时也会隐式生效,默认值为10;
–cluster-weight <node1=w1…nodeN=wN>:设置此次重平衡中各主节点(以ID标志)哈希槽的分配权重,权重越高意味着被分配的哈希槽数量也越多,用于在不同硬件能力的主节点之间实现更细粒度的负载均衡。
// ---- 重平衡端口为6379的Redis实例所在的集群,从而令各主节点绑定的哈希槽数量大致相同。
// 允许对无哈希槽的主节点进行分配,触发重分配的各主节点哈希槽数量相差百分比为1%,并且
// 设置端口为6379/6380/6381/6386的主节点(用ID标志)的哈希槽分配权重为1:1:1:1。
stephan@shuoshuren ~ % redis-cli --cluster rebalance 127.0.0.1:6379 --cluster-use-empty-masters --cluster-threshold 1 --cluster-weight c47afeb8640fb9ffa11e24c82411185a3f30fdb5=1 ec4e3249f88e517da684b40e9c2c60716ec072c8=1 b78bc221bd5a925149bf2a077afe0a327e8975f3=1 d37f01a5a68909e86792b32fcb36ae1bff870f77=1
>>> Performing Cluster Check (using node 127.0.0.1:6379)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
// ---- 端口为6379/6380/6381的主节点各自迁移了1366个哈希槽至端口为6386的主节点,
// 该主节点原本没有哈希槽。
>>> Rebalancing across 4 nodes. Total weight = 4.00
Moving 1366 slots from 127.0.0.1:6380 to 127.0.0.1:6386
######################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################
Moving 1365 slots from 127.0.0.1:6381 to 127.0.0.1:6386
#####################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################
Moving 1365 slots from 127.0.0.1:6379 to 127.0.0.1:6386
#####################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################
- CLUSTER SLAVES/REPLICAS <node_id>:获取集群中指定主节点的从节点集。
------------------------- 入参 -------------------------
node_id @ 节点ID:集群中指定的主节点ID。
// ---- 获取当前(端口为6379)的从节点列表。
> CLUSTER SLAVES c47afeb8640fb9ffa11e24c82411185a3f30fdb5
2c6748a8e93c5b26b5d6ecc29d119567e8bc63b7 127.0.0.1:6382@16382 slave c47afeb8640fb9ffa11e24c82411185a3f30fdb5 0 1727367097000 1 connected
585b42e54f49502c7de83a72619cc56bfcb00c7c 127.0.0.1:6385@16385 slave c47afeb8640fb9ffa11e24c82411185a3f30fdb5 0 1727367096000 1 connected
> CLUSTER REPLICAS c47afeb8640fb9ffa11e24c82411185a3f30fdb5
2c6748a8e93c5b26b5d6ecc29d119567e8bc63b7 127.0.0.1:6382@16382 slave c47afeb8640fb9ffa11e24c82411185a3f30fdb5 0 1727367614116 1 connected
585b42e54f49502c7de83a72619cc56bfcb00c7c 127.0.0.1:6385@16385 slave c47afeb8640fb9ffa11e24c82411185a3f30fdb5 0 1727367612106 1 connected
移除/缩容
Redis支持通过外部执行{redis-cli --cluster del-node}指令或内部执行{CLUSTER FORGET}指令将节点从集群中移除,而这其中{redis-cli --cluster del-node}指令通过{CLUSTER FORGET}指令实现。需要注意的是:上述两个指令都不具备自动迁移哈希槽/数据能力,因此如果移除的是非主节点,那么在正式移除前应当先外部执行{redis-cli --cluster reshard}指令来将至哈希槽/数据的迁移至其它主节点…相关指令详情具体如下:
- redis-cli --cluster reshard : [–cluster-yes] [–cluster-replace] [–cluster-from ] [–cluster-to ] [–cluster-slots ] [–cluster-timeout ] [–cluster-pipeline ]:从指定来源主节点集中迁移指定数量的哈希槽至目标主节点,并在某来源主节点的哈希槽被全部迁移时将之自动转变为集群中任意主节点的从节点。该指令的执行时间可能较长,因为哈希槽的重分配会导致数据重分片。
------------------------- 入参 -------------------------
ip @ IP:指定集群中任意节点的IP;
port @ 端口:指定集群中任意节点的端口。
------------------------- 选项 -------------------------
–cluster-yes:设置此次分配的所有询问步骤都会自动同意;
–cluster-replace:设置此次分配导致的数据重分片会在目标主节点存在相同键时覆盖原数据;
–cluster-from :设置此次分配的来源主节点集,主节点之间以“,”分割,all表示全部主节点;
–cluster-to :设置此次分配的目标主节点;
–cluster-slots :设置此次分配来源主节点集要迁移至目标主节点的哈希槽“总”数量;
–cluster-timeout :设置此次分配只在指定时间(ms)内执行;
–cluster-pipeline :设置此次分配导致的数据重分片一次可迁移数据的量。该选项未使用时也会隐式生效,默认值为10。
// ---- 将端口为6386的主节点的哈希槽全部(数量要自己算)迁移端口为6385的主节点,
stephan@shuoshuren ~ % redis-cli --cluster reshard 127.0.0.1:6379 --cluster-replace --cluster-from d37f01a5a68909e86792b32fcb36ae1bff870f77 --cluster-to c47afeb8640fb9ffa11e24c82411185a3f30fdb5 --cluster-slots 4096
// ---- 检查集群状态。
>>> Performing Cluster Check (using node 127.0.0.1:6379)
M: c47afeb8640fb9ffa11e24c82411185a3f30fdb5 127.0.0.1:6379slots:[4096-6826],[10923-12287] (4096 slots) master1 additional replica(s)
S: 585b42e54f49502c7de83a72619cc56bfcb00c7c 127.0.0.1:6385slots: (0 slots) slavereplicates c47afeb8640fb9ffa11e24c82411185a3f30fdb5
M: ec4e3249f88e517da684b40e9c2c60716ec072c8 127.0.0.1:6380slots:[6827-10922] (4096 slots) master1 additional replica(s)
S: 1f44ff8cb1b394bf01d82ce0bc1d3c1f1a7af93f 127.0.0.1:6383slots: (0 slots) slavereplicates ec4e3249f88e517da684b40e9c2c60716ec072c8
M: d37f01a5a68909e86792b32fcb36ae1bff870f77 127.0.0.1:6386slots:[0-4095] (4096 slots) master1 additional replica(s)
S: 2c6748a8e93c5b26b5d6ecc29d119567e8bc63b7 127.0.0.1:6382slots: (0 slots) slavereplicates d37f01a5a68909e86792b32fcb36ae1bff870f77
M: b78bc221bd5a925149bf2a077afe0a327e8975f3 127.0.0.1:6381slots:[12288-16383] (4096 slots) master1 additional replica(s)
S: f6112d66cdc6f17c0a1b0a3e23d43e200b239949 127.0.0.1:6384slots: (0 slots) slavereplicates b78bc221bd5a925149bf2a077afe0a327e8975f3
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
// ---- 指定哈希槽迁移计划,包括来源主节点/目标主节点/迁移哪些哈希槽等。
Ready to move 4096 slots.Source nodes:M: d37f01a5a68909e86792b32fcb36ae1bff870f77 127.0.0.1:6386slots:[0-4095] (4096 slots) master1 additional replica(s)Destination node:M: c47afeb8640fb9ffa11e24c82411185a3f30fdb5 127.0.0.1:6379slots:[4096-6826],[10923-12287] (4096 slots) master1 additional replica(s)Resharding plan:Moving slot 0 from d37f01a5a68909e86792b32fcb36ae1bff870f77// ---- 这里有所有的计划日志,太长我省略了。...Moving slot 4095 from d37f01a5a68909e86792b32fcb36ae1bff870f77
// ---- 确定是否执行迁移计划,这里需要手动输入yes,因为指令上没有携带--cluster-yes选项。
Do you want to proceed with the proposed reshard plan (yes/no)? yes
// ---- 正式迁移哈希槽,
Moving slot 0 from 127.0.0.1:6386 to 127.0.0.1:6379:
// ---- 这里有所有的执行日志,太长我省略了。
...
Moving slot 4095 from 127.0.0.1:6386 to 127.0.0.1:6379:
// ---- 这里有一个报错,因为迁移完成后会执行{CLUSTER SETSLOT STABLE}指令来清除
// 哈希槽的迁移/导入状态。但这个指令只能在主节点上执行,而我们迁移的是6386端口主节点的
// 所有哈希槽,因此在迁移完成后其就自动变为从节点了,因此{CLUSTER SETSLOT STABLE}
// 指令执行就会报错。但这不影响数据槽的迁移,算是一个小BUG吧。
Node 127.0.0.1:6386 replied with error:
ERR Please use SETSLOT only with masters.
- redis-cli --cluster del-node : <node_id>:将指定空主节点/从节点从指定集群中移除。
------------------------- 入参 -------------------------
ip @ IP:指定集群中任意节点的IP;
port @ 端口:指定集群中任意节点的端口;
node_id @ 节点ID:欲移除的节点ID。
// ---- 将端口为6386的从节点(因为上文迁移了所有哈希槽而自动变为了从节点)从集群中移除。
stephan@shuoshuren ~ % redis-cli --cluster del-node 127.0.0.1:6379 d37f01a5a68909e86792b32fcb36ae1bff870f77
>>> Removing node d37f01a5a68909e86792b32fcb36ae1bff870f77 from cluster 127.0.0.1:6379
// ---- 内部执行{CLUSTER FORGET}指令来移除指定节点。
>>> Sending CLUSTER FORGET messages to the cluster...
// ---- 内部执行{CLUSTER RESET}指令清除各节点对其他节点的信息缓存,目的是将移
// 除的空主节点/从节点信息彻底从集群中清除。
>>> Sending CLUSTER RESET SOFT to the deleted node.// ---- 将端口为6379的非空主节点从集权中移除,这会返回失败。
stephan@shuoshuren ~ % redis-cli --cluster del-node 127.0.0.1:6379 c47afeb8640fb9ffa11e24c82411185a3f30fdb5
>>> Removing node c47afeb8640fb9ffa11e24c82411185a3f30fdb5 from cluster 127.0.0.1:6379
[ERR] Node 127.0.0.1:6379 is not empty! Reshard data away and try again.
- CLUSTER FORGET :将指定空主节点/从节点从当前集群中移除。
------------------------- 入参 -------------------------
node-id @ 节点ID:当前集群中指定空主节点/从节点的IP。
// ---- 将端口为6386的从节点从当前集群中移除。
> CLUSTER FORGET d37f01a5a68909e86792b32fcb36ae1bff870f77
OK// ---- 将端口为6379的非空主节点从集权中移除,这会返回失败。
> CLUSTER FORGET c47afeb8640fb9ffa11e24c82411185a3f30fdb5
ERR Can't forget my master!
- CLUSTER RESET [SOFT|HARD]:重置当前节点,即删除内部的各类数据。该指令对于已存有数据的主节点无效。
------------------------- 选项 -------------------------
SOFT:设置此次重置为软重置。软重置会删除当前节点缓存的其它节点数据以及其内部维护的哈希槽与主节点的映射关系。此外如果当前节点为从节点,那么其数据会被全部删除并转变为空主节点,但不会生成新的节点ID。
HARD:设置此次重置为硬重置。硬重置会执行软重置的所有操作及生成新节点ID,并将Current Epoch @ 当前纪元(用于实现故障转移)和Config Epoch @ 配置纪元(用于维护哈希槽与各主节点的映射关系)重置为0。
// ---- 对空主节点或从节点执行会软/硬重置。
> CLUSTER RESET SOFT
OK
// ---- 对存在数据的主节点执行会报错。
> CLUSTER RESET SOFT
ERR CLUSTER RESET can't be called with master nodes containing keys
修复
Redis集群可能因为哈希槽的迁移,节点的软/硬重置等原因而损坏,这种情况下开发者可以尝试外部执行{redis-cli --cluster fix}指令来进行修复。但注意该指令无法保证集群必然被修复,较为复杂的损坏依然需要人为寻根并针对性处理…相关指令详解具体如下:
- redis-cli --cluster fix : [–cluster-search-multiple-owners] [–cluster-fix-with-unreachable-masters ]:尝试修复指定集群。
------------------------- 入参 -------------------------
ip @ IP:指定集群中任意节点的IP;
port @ 端口:指定集群中任意节点的端口。
------------------------- 选项 -------------------------
–cluster-search-multiple-owners:设置此次修复需要处理哈希槽被分配给多个主节点的问题;
–cluster-fix-with-unreachable-masters :设置此次修复会将不可用且故障转移失败(无从节点或从节点也不可用)的主节点上的哈希槽分配至其它主节点,但数据则因为主节点不可用而无法迁移。
// ---- 修复指定集群,并需要处理哈希槽被分配给多个主节点的问题和迁移不可用主节点的哈希槽。
stephan@shuoshuren ~ % redis-cli --cluster fix 127.0.0.1:6379 --cluster-search-multiple-owners --cluster-fix-with-unreachable-masters
// ---- 检查集群中各主/从节点的信息。
127.0.0.1:6379 (9b55ef69...) -> 0 keys | 5461 slots | 1 slaves.
127.0.0.1:6380 (957ccf94...) -> 0 keys | 5462 slots | 1 slaves.
127.0.0.1:6381 (779014eb...) -> 0 keys | 5461 slots | 1 slaves.
[OK] 0 keys in 3 masters.
0.00 keys per slot on average.
>>> Performing Cluster Check (using node 127.0.0.1:6379)
M: 9b55ef69296899e3e1ea37d7ecb10e05fa2cc874 127.0.0.1:6379slots:[0-5460] (5461 slots) master1 additional replica(s)
M: 957ccf94807e9fce968c4d3f808cd8a14c050d2a 127.0.0.1:6380slots:[5461-10922] (5462 slots) master1 additional replica(s)
S: 8d3452c5cf4fd9fdd237990dc880e26bf672ee4a 127.0.0.1:6383slots: (0 slots) slavereplicates 9b55ef69296899e3e1ea37d7ecb10e05fa2cc874
S: 8b2cca31334e7d029871170955e205d37a4470bf 127.0.0.1:6384slots: (0 slots) slavereplicates 957ccf94807e9fce968c4d3f808cd8a14c050d2a
M: 779014ebdcf836c17f2df1b5860845bccec353f1 127.0.0.1:6381slots:[10923-16383] (5461 slots) master1 additional replica(s)
S: 0f292914acaaf51ca69e55e97ca3d5b6e9b3dc52 127.0.0.1:6382slots: (0 slots) slavereplicates 779014ebdcf836c17f2df1b5860845bccec353f1
[OK] All nodes agree about slots configuration.
// ---- 总体没有损耗,因此不需要修复。
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
>>> Check for multiple slot owners...
[OK] No multiple owners found.
实现
哈希槽/数据分片/配置纪元
Redis Cluster @ 集群会将所有数据挂靠在2^14 @ 16384个哈希槽上。Redis集群会将数据库(即所有主节点)在“逻辑”上整体分割为16384个(固定)哈希槽,再按一定规则将这些哈希槽划分/归类为与主节点数量相同的组并建立1:1映射,从而令各个主节点去专属保存挂靠在自身对应哈希槽上的数据,以达到数据分片效果。那所谓挂靠又是什么意思呢?实际上这也是一种映射关系。当客户端试图以某键于Redis集群中访问数据时,Redis先会通过一致性哈希算法(即同条件下重复计算将得到相同的结果)计算得到该键对应的哈希槽,从而再从该哈希槽对应的主/从节点中来写入/读取数据,因此这种数据与哈希槽的映射关系便被称为“挂靠”…键用于计算得到哈希槽的CRC16算法如下:
- slot = CRC16(key) mod 16384
Redis不支持将数据强制挂靠在指定哈希槽上。网上部分文章有这样的说法:Redis可以强制将数据挂靠在指定哈希槽上,并将该功能称为Tag @ 标签机制。但很遗憾这样的功能/机制在Redis中是不存在的,这些文章所说的标签机制实际上就在键中添加相应的前缀/标签以使之可以计算得到预设想的哈希槽,即直接将数据的键设计为可计算得到指定哈希槽的字符串而已…而很显然这对大部分并不熟知CRC16算法的开发者来说是无法做到的。
哈希槽设计有助于Redis集群支持更大程度的动态拓展。在数据分片的设计中,最常见的做法是根据数据与实际容器数量的模来进行划分。例如如果集群中有三台主节点,那么上述方案就是先对数据键进行“哈希值 mod 3”取模,然后再将数据保存在与模对应(对应关系自行定义,例如可为主节点赋予0/1/2的序号,再将序号与模相等的主节点视作与数据对应)的主节点中。这种做法虽然可以较为快速/简单的实现数据的均匀分片,但却难以支持集群的动态拓展,即难以支持节点低耗/高效地加入/退出集群。例如如果我们在集群中加入序号为3的主节点,那么集群就必须根据新模(通过“哈希值 mod 4”得到)对已有数据进行全局性的重分片(迁移)来保证其所在主节点的正确性,但很显然这种全局性的重分片在体量/开销/性能上都是难以令人接收的。而哈希槽设计就完美地避免了模的变化,因为无论有多少主节点加入/退出了集群,哈希槽的数量都会固定保持16384不变,从而使得模的值也永远保持不变,并进一步达到集群动态拓展低耗/高效的效果。
哈希槽总数的16384是经验值。所谓经验值是指没有/缺少理论依据,但在现实场景的绝大多数方面都能取得良好运行效果的阈值。因此想要给出哈希槽总数被设置为16384的具体原理是无法/难以做到的,我们只能从不限于以下几种角度去讲述其在程序运行中的各类良好效果:
- 计算效率高:计算机对于2的幂次方数字处理更快,并且该数字对于CRC16算法也有优化加成;
- 负载均衡强/迁移更灵活:对于实际开发场景中的主节点数量而言,16384个哈希槽不但可以做到对各主节点的大致均匀分配以实现负载均衡,还能保证单个主节点具备较高的哈希槽/数据颗粒度以实现迁移的灵活性,即能够选择的迁移范围更加多样化;
- 内存开销小/通讯影响少/增加集群体量:Redis集群的节点数量是很难超过1000个的,其“主要”原因是集群中的节点两两之间都会进行通讯,因此节点数量的增加就意味着“单个节点需要通讯的其它节点数量/通讯心跳包中包含的各节点数据(主要指各节点的运行状态,用于综合反应集群状态)”也同步增加,从而在节点数量接近1000时因为出现网络拥堵现象而难以继续扩容。而在这心跳包中除了各节点数据外还有一项非常重要的哈希槽数据,还数据的本质是当前节点被分配的哈希槽。由于以位图形式(具体如下图)保存且哈希槽总数为16384的原因,该哈希槽数据会固定占有较小的2(16384/8/1024)KB大小。但如果将哈希槽总数设计的更大,那么心跳包的大小自然也会水涨船高,从而就会导致网络拥堵现象出现的更早,并进一步导致可支持的节点数量也更少。
通讯/心跳/配置纪元
在Redis集群下,每个节点需要开放6379/16379(可配置)两个端口。这其中6379端口用于对外提供服务,而16379端口则用于进行节点间的通讯,该通讯机制被称为Cluster Bus @ 集群公交。Redis集群可基于集群公交在进行故障检测/配置更新/故障转移时进行即时通讯,其采用了gossip协议用于在节点间进行高效的数据交换,从而减少网络带宽和处理时间…节点间的信息交互主要分为以下两项:
- 心跳:节点会持续发送心跳(即{PING}指令)来监测集群中其他节点的运行状态,如果未能在规定时间内收到响应,那么它会认为该节点可能已经宕机;
- 心跳包:节点用于对外共享以维护集群整体状态的信息,这其中包含了数据槽数据/各节点运行状态/配置纪元等各项数据。
节点会根据配置纪元选择是否接受心跳包中的数据。所谓配置纪元是指节点内部用于记录哈希槽版本的整型变量,而所谓哈希槽版本则是指节点内部维护的哈希槽与主节点的映射关系(下文简称节点/槽映射)发生变化的次数。节点会在自身负责的哈希槽发生变化时递增配置纪元,并将其格式化为位图与配置纪元一起置于心跳包中发送至其它节点。而其它节点在接收到心跳包中会先判断配置纪元是否大于自身配置纪元(这里不太理解,每个节点的哈希槽变化情况不一样的,二者的配置纪元单纯比较大小似乎并不合理),是则将之位图更新至自身维护的节点/槽映射中,否则直接舍弃。
数据重分片/重定向
客户端/节点都会缓存哈希槽与主节点的映射关系。虽说都会缓存节点/槽映射,但客户端与节点的缓存目的却并不相同:客户端缓存是为了在计算出哈希槽后能够快速定位对应的主节点并发起访问请求;而节点缓存的目的则是为了在客户端无缓存/缓存过期时与之共享。客户端的初始节点/槽映射是在首次访问集群时从任意节点处获取的,由于此时其并无法定位哈希槽对应的主节点,因此会随机请求任意节点来获取映射关系以构建本地缓存,随后再通过本地缓存向目标节点发起请求。
客户端/节点的节点/槽映射都存在过期可能。客户端的初始节点/槽映射并无法保证永久有效,因此实际的节点/槽映射可能会因为集权扩容/缩容/平衡/迁移等原因而发生变化,因此客户端基于本地缓存发起的请求是可能出现定位错误情况的,因为相应的哈希槽实际已经被分配给其它主节点了。而在这种情况下被错误请求的节点会将自身的节点/槽映射共享给客户端以供之建立新本地缓存,从而令其可以基于新缓存向正确节点重新发起访问请求。不过需要注意的是:虽然集群节点之间的通讯确实会持续更新各自节点/槽映射缓存,但这种通讯终究还是有延迟的,故而节点的节点/槽映射同样可能过期,因此客户端可能会多次执行上述的“重定向”行为,直至访问到指定哈希槽真正所在的主节点为止。这一点在上文讲述的集群构造过程中应该是有所体现的,因为当我向6379主节点发送访问请求时其就返回过“MOVED”回应来表示重定向到了其它节点…具体摘录如下所示:
// ---- 设置名称,该键对应的哈希槽为5798,而该哈希槽对应的Redis实例
// 为6380,因此数据会被转发至端口为6380的Redis实例保存。
> SET name 说淑人
MOVED 5798 127.0.0.1:6380
// ---- 设置性别/年龄,这两个数据都在当前端口为6379的Redis实例上保存。
> SET sex 男
OK
> SET age 28
OK
// ---- 设置身高,该键对应的哈希槽为8223,而该哈希槽对应的Redis实例
// 为8223,因此数据会被转发至端口为6380的Redis实例保存。
> SET height 178
MOVED 8223 127.0.0.1:6380
// ---- 设置体重,该键对应的哈希槽为8223,而该哈希槽对应的Redis实例
// 为16280,因此数据会被转发至端口为6380的Redis实例保存。
> SET weight 74
MOVED 16280 127.0.0.1:6381
故障转移
Redis集群支持自动故障转移。下图展示了在关闭主节点6381后自动故障转移其从节点6382为主节点的输出日志。