一、Redis集群介绍

Redis Cluster维护一个0~16383固定范围的虚拟槽(slot)分区,通过将每个key进行CRC16校验后再对16384取模,决定将key放在哪个槽上,计算公式:HASH_SLOT = CRC16(key) mod 16384
槽(slot)是集群中数据管理和迁移的基本单位,集群中每个节点负责维护一定数量的槽,以及槽所映射的键值对数据。
为了保证在部分节点发生故障时集群仍然可用,在集群中采用主从复制结构。如果某个主节点不可用,将从该节点的从节点中选举出一个代替主节点继续提供服务。但是当一个主节点和该主节点的从节点都不可用时,分布在该主节点上的槽将无法找到,整个集群将不可用。
Redis Cluster的特点:

  • 在集群扩容和收缩时,只需要维护好槽分区和节点之间的映射关系,而不必关心数据和节点的关系,降低扩容和收缩难度;
  • 由于数据分布在各个槽中,Redis Cluster对数据键的批量操作有限,只支持对具有相同槽值的键执行批量操作;同样地,只支持key在同一节点上的事务操作;
  • key作为槽的最小粒度,无法将一个大的键值对象映射到不同的节点;
  • 在Redis Cluster中只能使用一个数据库空间,即db0;复制结构也只支持一层,即从节点只能复制主节点,不支持树状结构;
  • 由于异步复制的特性,主节点上的写指令在执行完成返回后,才将该写指令同步到从节点上,因此Redis Cluster并不能保证数据的强一致性,同样会遇到复制延迟、读取到过期数据等问题。

二、集群相关配置项

  • cluster-enabled [yes|no]:设置为“yes”时,将在该Redis实例上启用集群功能;
  • cluster-config-file “filename”:集群配置文件的名称,在启用集群功能时自动生成,不要手动修改它;
  • cluster-node-timeout [NUM]:集群节点不可用的最大时间,单位是毫秒。对于一个主节点,如果超过该时间不可用,将被从节点代替;
  • cluster-slave-validity-factor [NUM]:从节点有效性判断因子。如果设置为0,从节点在任何时候都可以参与故障转移;如果设置为正整数,例如设置为10,假如cluster-node-timeout为5秒,则从节点与主节点失联时间超过50秒时,该节点不参与故障转移;
  • cluster-migration-barrier [NUM]:主从节点切换需要的最小从节点个数;
  • cluster-require-full-coverage [yes|no]:如果设置为“yes”(默认值),在槽没有被完全分配时,集群将不接收写入请求;
  • cluster-allow-reads-when-down [yes|no]:如果设置为“no”(默认值),当集群中节点被下线或无法连接到法定主节点时将不会接收任何请求。

三、创建Redis Cluster

3.1、准备节点

一个完整高可用的Redis集群,至少需要六个节点。因为在下线一个主节点时,需要半数以上的主节点都同意才能真正下线;而为了保证集群的高可用,还需要为每个节点分配一个从节点,用来在主节点故障下线时替换主节点。
在一台服务器上,启动六个Redis实例来创建集群。首先准备需要的目录:

# mkdir -p /usr/local/redis/{conf,data,logs}

最小化配置,各个实例除监听端口不同外,六个实例分别监听6379、6380、6381、6382、6383、6384端口,其它配置相同。务必区分每个Redis实例的RDB文件和AOF文件,否则将导致Redis集群异常:

# vim /usr/local/redis/conf/redis-6379.conf
bind 127.0.0.1
port 6379
daemonize yes
pidfile /usr/local/redis/logs/redis_6379.pid
logfile "/usr/local/redis/logs/redis_6379.log"
dir /usr/local/redis/data
dbfilename dump_6379.rdb
appendonly yes
appendfilename "appendonly_6379.aof"
cluster-enabled yes
cluster-config-file /usr/local/redis/conf/node-6379.conf
cluster-node-timeout 15000

启动六个Redis实例:

# for port in {6379..6384};do redis-server /usr/local/redis/conf/redis-${port}.conf;done
# netstat -tlnp | grep redis

Redis实例以集群模式启动后,会根据配置项“cluster-config-file”的内容自动生成集群配置文件。文件内容如下:

# cat /usr/local/redis/conf/node-6379.conf 
b652e555d59f5fe35b2b5c66f6f629ea48152ae9 :0@0 myself,master - 0 0 0 connected
vars currentEpoch 0 lastVoteEpoch 0

文件记录了集群的初始状态,其中40位的十六进制字符串,唯一标识了每个Redis实例在集群中的身份ID。该ID号伴随在Redis集群的整个生命周期,并不会随着Redis实例重启而发生改变。
集群中的每个Redis实例,都会单独建立一个TCP通道,用于实例之间相互通信,通信端口号在基础端口号上加10000。

3.2、创建集群

在Redis 5以下版本,可以通过源码包的“src/”目录下的“redis-trib.rb”工具创建集群。而在Redis 5以上版本,通过redis-cli命令的“–cluster”选项来创建和管理集群。两者的用法基本一致。
使用redis-cli命令创建集群:

# 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

其中,“–cluster-replicas 1”表示将为每个主节点分配一个从节点。

3.3、集群创建过程

当创建集群的命令执行时,首先会给出槽和主从节点分配计划,如下:

>>> 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
M: b652e555d59f5fe35b2b5c66f6f629ea48152ae9 127.0.0.1:6379
   slots:[0-5460] (5461 slots) master
M: 27dfdf4ed79a4a421131e02afe75584a1d22bca9 127.0.0.1:6380
   slots:[5461-10922] (5462 slots) master
M: b465de2d2fdbb0d8ff9fe410c17dba69a8746c4f 127.0.0.1:6381
   slots:[10923-16383] (5461 slots) master
S: b0125a0935a16d82338be077bbb70631f7734a48 127.0.0.1:6382
   replicates 27dfdf4ed79a4a421131e02afe75584a1d22bca9
S: a1460b210b556c7ccb92ec3b77c373a5654873d4 127.0.0.1:6383
   replicates b465de2d2fdbb0d8ff9fe410c17dba69a8746c4f
S: 1b0128245109ed8d3cec973c778a283f174a869c 127.0.0.1:6384
   replicates b652e555d59f5fe35b2b5c66f6f629ea48152ae9
Can I set the above configuration? (type 'yes' to accept): yes

当手动键入“yes”时,表示同意自动分配计划,集群创建过程便开始了。
首先会在每个节点上执行cluster meet命令发送“meet”消息,与其它各个节点间握手通信,其它节点收到“meet”消息会回复“pong”消息,让彼此感知到对方的存在,从而将所有节点加入到集群,如下:

>>> Sending CLUSTER MEET messages to join the cluster
>Waiting for the cluster to join
.........

所有节点加入集群后,将16384个槽均匀地分布到三个主节点上,并检测集群状态。最后的输出报告说明:16384个槽全部被分配,集群创建成功,如下:

>>> Performing Cluster Check (using node 127.0.0.1:6379)
M: b652e555d59f5fe35b2b5c66f6f629ea48152ae9 127.0.0.1:6379
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
M: 27dfdf4ed79a4a421131e02afe75584a1d22bca9 127.0.0.1:6380
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
M: b465de2d2fdbb0d8ff9fe410c17dba69a8746c4f 127.0.0.1:6381
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
S: b0125a0935a16d82338be077bbb70631f7734a48 127.0.0.1:6382
   slots: (0 slots) slave
   replicates 27dfdf4ed79a4a421131e02afe75584a1d22bca9
S: a1460b210b556c7ccb92ec3b77c373a5654873d4 127.0.0.1:6383
   slots: (0 slots) slave
   replicates b465de2d2fdbb0d8ff9fe410c17dba69a8746c4f
S: 1b0128245109ed8d3cec973c778a283f174a869c 127.0.0.1:6384
   slots: (0 slots) slave
   replicates b652e555d59f5fe35b2b5c66f6f629ea48152ae9
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

3.4、集群完整性检查

集群完整性指所有的槽都被分配到存活的主节点上,通过redis-cli命令的“–cluster”选项指定任意一个Redis节点都可以完成整个集群的检查工作:

# redis-cli --cluster check 127.0.0.1:6379

默认情况下,只要有一个槽没有被分配,集群都是不完整的。这时,执行任何键命令都会返回“(error) CLUSTERDOWN Hash slot not served”错误。这在某个主节点下线,故障转移期间,整个Redis集群对业务都是不可用的。可以将配置项“cluster-require-full-coverage”设置为“no”,在故障转移期间,只影响该节点负责的槽的相关数据,不会影响其它节点的可用性。

3.5、查看集群信息

通过在集群中任一节点上执行“cluster nodes”命令,查看集群中的节点信息:

# redis-cli cluster nodes
8f4372bfc1f8dba6d9eb7b49b9441e5312f8424c 127.0.0.1:6384@16384 slave 31b6e8d6ea7400d6b8a1e5ec13764a96d8db1d60 0 1609134886755 6 connected
6723af847c2824ba3035f87b7d8c245397eb2184 127.0.0.1:6380@16380 master - 0 1609134886000 2 connected 5461-10922
16ec73242020c56766ec9250d528303a4fac31b1 127.0.0.1:6383@16383 slave c82b897ec2751058cfbf9473ccee3e20d9d9662c 0 1609134887000 5 connected
31b6e8d6ea7400d6b8a1e5ec13764a96d8db1d60 127.0.0.1:6379@16379 myself,master - 0 1609134886000 1 connected 0-5460
f953ed43eac4318ecb9f88da966cd924b6936ab2 127.0.0.1:6382@16382 slave 6723af847c2824ba3035f87b7d8c245397eb2184 0 1609134887763 4 connected
c82b897ec2751058cfbf9473ccee3e20d9d9662c 127.0.0.1:6381@16381 master - 0 1609134884740 3 connected 10923-16383

通过在集群中任一节点上执行“cluster info”命令,查看集群状态信息:

# redis-cli 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:7926
cluster_stats_messages_pong_sent:7946
cluster_stats_messages_sent:15872
cluster_stats_messages_ping_received:7941
cluster_stats_messages_pong_received:7926
cluster_stats_messages_meet_received:5
cluster_stats_messages_received:15872

四、集群中数据读写

在集群模式中,任何发送到Redis的键相关指令,都会先通过计算得出key对应的槽,然后通过自身维护的槽与节点的映射关系,找出槽对应的节点。如果节点是自身,则处理键命令;否则将返回“(error) MOVED”重定向错误,告知客户端key对应的槽,及槽对应的节点,如下:

# redis-cli -p 6379
127.0.0.1:6379> set foo bar
(error) MOVED 12182 127.0.0.1:6381

通过重定向返回的内容,得知“foo”对应的槽为“12182”,对应的节点为“127.0.0.1:6381”。这时客户端需要重新连接到“127.0.0.1:6381”节点上再次执行键指令:

# redis-cli -p 6381
127.0.0.1:6381> set foo bar
OK

在使用Redis客户端命令redis-cli时,通过“-c”选项可以自动完成重定向:

# redis-cli -c -p 6379
127.0.0.1:6379> get foo
-> Redirected to slot [12182] located at 127.0.0.1:6381
"bar"
127.0.0.1:6381> set hello world
-> Redirected to slot [866] located at 127.0.0.1:6379
OK

大多数开发语言都支持Redis集群客户端,在https://redis.io/clients可以找到。

五、集群伸缩

集群伸缩的过程,可以理解为槽和槽上的数据在不同节点之间移动的过程。在集群伸缩时,通过相关命令把槽和数据迁移至其它节点,完成集群伸缩。

5.1、集群扩容

扩容的步骤一般分为:准备新的节点 --> 新节点加入集群 --> 为新节点分配槽。
新加入的节点通常有两种用途:一是作为新的主节点为它迁移槽和数据实现集群扩容;二是作为其它主节点的从节点负责故障转移。
为了命令演示,通过6385和6386端口启动两个新的Redis实例,并将6385端口实例作为主节点,分配槽和数据;将6386端口实例作为6385端口实例的从节点。
1)以集群模式启动两个新的Redis实例:

# redis-server /usr/local/redis/conf/redis-6385.conf
# redis-server /usr/local/redis/conf/redis-6386.conf

2)将6385端口实例作为主节点加入到原有的Redis集群中:

# redis-cli --cluster add-node 127.0.0.1:6385 127.0.0.1:6379
>>> Adding node 127.0.0.1:6385 to cluster 127.0.0.1:6379
>>> Performing Cluster Check (using node 127.0.0.1:6379)
……
>>> Send CLUSTER MEET to node 127.0.0.1:6385 to make it join the cluster.
[OK] New node added correctly.

将新节点加入集群时,集群会自动进行一次完整性检查。如果新的节点已经加入其它集群或者已经有数据,则加入集群失败。
集群中的原节点通过“cluster meet”命令向新节点发送“meet”消息,从而集群中的所有节点都感知到新节点加入。
3)为6385端口实例分配槽。将6379端口实例的1000个槽分配为6385端口实例:

# redis-cli cluster nodes    #查看各个节点的nodeID
# redis-cli --cluster reshard 127.0.0.1:6379 --cluster-from 31b6e8d6ea7400d6b8a1e5ec13764a96d8db1d60 --cluster-to 8f7be1ea0f54d43bcf88c48b76904e5df9b8518e --cluster-slots 1000 --cluster-yes
  • 127.0.0.1:6379:集群中任一节点的IP:Port;
  • –cluster-from:持有槽的节点的nodeID;
  • –cluster-to:将槽转移至哪个节点的nodeID;
  • –cluster-slots:转移的槽的数量;
  • –cluster-yes:在出现询问提示时,自动回答“yes”。

4)将6386端口实例作为6385端口实例的从节点加入集群:

# redis-cli -p 6385 cluster nodes | grep myself      #获取6385端口实例的nodeID
# redis-cli --cluster add-node 127.0.0.1:6386 127.0.0.1:6379 --cluster-slave --cluster-master-id 8f7be1ea0f54d43bcf88c48b76904e5df9b8518e
  • –cluster-slave:作为从节点加入集群;
  • –cluster-master-id:主节点的nodeID。

5.2、集群收缩

在将节点从集群中删除时,必须保证该节点不再持有槽。持有槽的节点在下线前,必须先将槽迁移至其它节点。
为了演示,将主节点6381端口实例做下线操作。
首先查看6381端口实例的信息,如nodeID、持有槽的数量、从节点等:

# redis-cli -p 6381 cluster nodes | grep c82b897ec2751058cfbf9473ccee3e20d9d9662c
16ec73242020c56766ec9250d528303a4fac31b1 127.0.0.1:6383@16383 slave c82b897ec2751058cfbf9473ccee3e20d9d9662c 0 1609140627026 5 connected
c82b897ec2751058cfbf9473ccee3e20d9d9662c 127.0.0.1:6381@16381 myself,master - 0 1609140627000 3 connected 10923-16383

从命令输出的结果中得知,该节点持有槽的范围是10923~16383。6383端口实例作为该节点的从节点。
1)迁移该节点持有的槽:
通过执行三次reshard,将6381端口实例的5461个槽迁移到其它三个主节点上:

# redis-cli --cluster reshard 127.0.0.1:6379 --cluster-from c82b897ec2751058cfbf9473ccee3e20d9d9662c --cluster-to 31b6e8d6ea7400d6b8a1e5ec13764a96d8db1d60 --cluster-slots 2731 --cluster-yes
# redis-cli --cluster reshard 127.0.0.1:6379 --cluster-from c82b897ec2751058cfbf9473ccee3e20d9d9662c --cluster-to 8f7be1ea0f54d43bcf88c48b76904e5df9b8518e --cluster-slots 1365 --cluster-yes
# redis-cli --cluster reshard 127.0.0.1:6379 --cluster-from c82b897ec2751058cfbf9473ccee3e20d9d9662c --cluster-to 6723af847c2824ba3035f87b7d8c245397eb2184 --cluster-slots 1365 --cluster-yes

2)检查集群完整性:

# redis-cli --cluster check 127.0.0.1:6379
127.0.0.1:6381 (c82b897e...) -> 0 keys | 0 slots | 0 slaves.

从命令的输出结果,得知槽已经被全部迁移。
3)下线6381端口实例:

# redis-cli --cluster del-node 127.0.0.1:6379 c82b897ec2751058cfbf9473ccee3e20d9d9662c
>>> Removing node c82b897ec2751058cfbf9473ccee3e20d9d9662c from cluster 127.0.0.1:6379
>>> Sending CLUSTER FORGET messages to the cluster...
>>> SHUTDOWN the node.

4)从节点的处理:
主节点下线后,该主节点的从节点自动转变为其他主节点的从节点。可以删除该从节点,也可以将其设置为指定主节点的从节点。例如,将该节点设置为6385端口节点的从节点:

# redis-cli -p 6383 cluster replicate 8f7be1ea0f54d43bcf88c48b76904e5df9b8518e
OK
# redis-cli -p 6383 info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6385

六、故障转移

Redis集群采用Gossip(流言)协议,用于在节点之间彼此通信交换信息,一段时间后所有的节点都会知道集群的最新信息。
集群中的每个节点都会使用基础端口号加10000作为通信端口,与其它(可能是全部,也可能是部分)节点建立TCP通道,在固定周期内发送ping消息,接收到ping消息的节点用pong消息作为回应。

6.1、故障发现

当一个节点向某个节点发送的ping消息,在“cluster_node_timeout”时间后仍没有得到pong回应消息,它会用PFAIL(possible failure)来标识这个不可达的节点。当在经过一半“cluster_node_timeout”时间还没收到目标节点对于 ping包的回复的时候,还会马上尝试重连接该节点。
当集群中半数以上持有槽的主节点都将某个节点标记为PFAIL时,该节点的状态会升级为FAIL,这时才认为该节点真正下线,并在集群内部将节点FAIL的消息通知到所有节点。

6.2、故障恢复

如果被标记FAIL的节点为主节点,则其余主节点会在该节点的从节点中选举出一个替换它。
与主节点断线时间超过“cluster-node-time*cluster-slave-validity-factor”的从节点不具备参与选举的资格。
选举时,需要半数以上的主节点同意才能将某个节点提升为主节点,因此部署集群时所有主节点最少需要部署在3台物理机上才能避免单点故障。
新提升的主节点,会将故障节点的槽转移到自己身上。
当故障的主节点重新上线时,发现自己的槽已经被指派给了另外一个节点,它将作为新主节点的从节点运行。

6.3、手动故障转移

当需要对某个主节点进行升级、维护时,可以通过手动故障转移,执行“cluster failover”将它的从节点提升为主节点:

# redis-cli cluster nodes
064e2314569580f6885b4624862d8eae554c922e 127.0.0.1:6386@16386 slave 8f7be1ea0f54d43bcf88c48b76904e5df9b8518e 0 1609147135000 10 connected
16ec73242020c56766ec9250d528303a4fac31b1 127.0.0.1:6383@16383 slave 8f7be1ea0f54d43bcf88c48b76904e5df9b8518e 0 1609147135720 11 connected
8f7be1ea0f54d43bcf88c48b76904e5df9b8518e 127.0.0.1:6385@16385 master - 0 1609147135000 10 connected 0-999 13654-15018
# redis-cli -p 6383
127.0.0.1:6383> cluster failover
OK
127.0.0.1:6383> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6385,state=online,offset=10514,lag=1
slave1:ip=127.0.0.1,port=6386,state=online,offset=10514,lag=1