Redis Cluster
在学习Redis Cluster之前,我们先了解为什么需要集群,当遇到单机内存、并发、流量等瓶颈时,单机已经无法满足我让节点7000和7001等节点进们的要求的时候,可以采用Cluster架构方案达到负载均衡的目的。
数据分区概论
分布式数据库首先要解决把整个数据集按照分区规则映射到多个节点的问题,即把数据集划分到多个节点上,每个节点负责整体数据的一个子集。
常见的分区规则有哈希分区和顺序分区两种。
首先看一下对比
分布方式 | 特点 | 典型产品 |
---|---|---|
哈希分区 | 数据分散度高、键值分布无业务无关、无法顺序访问、支持批量操作。 | 一致性哈希:Mecache、Redis Cluster ... |
顺序分区 | 数据分散度易倾斜、键值业务相关、可以顺序访问、支持批量操作。 | BigTable、HBase |
顺序分区
比如:1-100个数字,要保存到3个节点上,每个节点平均存储,1-33存储在第1个节点,34-66存储到2节点,剩余存储到3节点。
顺序存储常用在关系型存储上。
哈希分区
因为Redis Cluster采用的哈希分区,所以我们看一下常见的哈希分区有哪几种。
节点取余分区
比如100个数据,对每个数据进行hash运算之后,再于节点数进行取余运算,根据余数保存在不同节点上。
缺点就是:当节点数量变化时,如扩容或收缩节点,数据节点映射关系需要重新计算,会导致数据的重新迁移。
一致性哈希分区
为系统中每个节点分配一个token,范围一般在0~2的32次方,这些token构成一个哈希环。数据读写执行节点查找操作时,先根据key计算hash值,然后顺时针找到第一个大于等于该哈希值的token节点,如下图所示
这种方式相比节点取余最大的好处在于加入和删除节点只影响哈希环中相邻的节点,对其他节点无影响。
但一致性哈希也存在一些问题:
- 加减节点会造成哈希环中部分数据无法命中(例如一个key增减节点前映射到第n2个节点,因此它的数据是保存在第n2个节点上的;当我们增加一个节点后被映射到n5节点上了,此时我们去n5节点上去找这个key对应的值是找不到的,见下图),需要手动处理或者忽略这部分数据,因此一致性哈希常用于缓存场景。
- 当使用少量节点时,节点变化将大范围影响哈希环中数据映射,因此这种方式不适合少量数据节点的分布式方案。
- 普通的一致性哈希分区在增减节点时需要增加一倍或减去一半节点才能保证数据和负载的均衡。
虚拟槽分区
Redis Cluster采用的就是虚拟槽分区。槽的范围是0~16383,将16384个槽平均分配给节点,由节点进行管理。
每次将key进行hash运算,对16383进行取余,然后去redis对应的槽进行查找。
槽是集群内数据管理和迁移的基本单位。采用大范围槽的主要目的是为了方便数据拆分和集群扩展。每个节点会负责一定数量的槽。
比如我们现在有5个集群,每个节点平均大约负责3276个槽。Redis Cluster 计算公式:slot=CRC16(key)&16383。每一个节点负责维护一部分槽以及槽所映射的键值数据。
Redis虚拟槽分区的特点:
- 解耦数据和节点之间的关系,简化了节点扩容和收缩难度。
- 节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据。
- 支持节点、槽、键之间的映射查询,用于数据路由、在线伸缩等场景。
准备节点
Redis集群一般由多个节点组成,节点数量至少为6个才能保证组成完整高可用的集群。每个节点需要开启配置cluster-enabled yes
,让Redis运行在集群模式下。
首先我们在redis文件中创建三个文件夹:config
、data
、log
。分别存放配置、数据和日志相关文件。
配置相关redis.conf
#节点端口
port ${port}
# 守护进程模式启动(可选)
daemonize yes
# 开启集群模式
cluster-enabled yes
# 节点超时时间,单位毫秒
cluster-node-timeout 15000
# 集群内部配置文件
cluster-config-file /usr/local/redis/config/nodes-${port}.conf
# 节点宕机后是否整个集群不可用
cluster-require-full-coverage no
dir /usr/local/redis/data/
dbfilename dump-${port}.rdb
logfile ${port}.log
# 其余的配置与redis.conf默认配置文件一致即可
6个节点全部配完成后就可以开启了。
[root@localhost config]# ls
redis-7000.conf redis-7001.conf redis-7002.conf redis-7003.conf redis-7004.conf redis-7005.conf
[root@localhost redis]# redis-server config/redis-7000.conf
[root@localhost redis]# cd config
[root@localhost config]# cat nodes-7000.conf
f4deba14aac6494e95e3e4ad060c94b8c82df7ec :0 myself,master - 0 0 0 connected
vars currentEpoch 0 lastVoteEpoch 0
[root@localhost config]# cd ..
[root@localhost redis]# redis-server config/redis-7001.conf
[root@localhost redis]# redis-server config/redis-7002.conf
[root@localhost redis]# redis-server config/redis-7003.conf
[root@localhost redis]# redis-server config/redis-7004.conf
[root@localhost redis]# redis-server config/redis-7005.conf
[root@localhost redis]# cd config
[root@localhost config]# ll
总用量 288
-rw-r--r--. 1 root root 112 12月 17 04:00 nodes-7000.conf
-rw-r--r--. 1 root root 112 12月 17 04:00 nodes-7001.conf
-rw-r--r--. 1 root root 112 12月 17 04:00 nodes-7002.conf
-rw-r--r--. 1 root root 112 12月 17 04:00 nodes-7003.conf
-rw-r--r--. 1 root root 112 12月 17 04:00 nodes-7004.conf
-rw-r--r--. 1 root root 112 12月 17 04:00 nodes-7005.conf
-rw-r--r--. 1 root root 41650 12月 17 03:59 redis-7000.conf
-rw-r--r--. 1 root root 41649 12月 17 03:59 redis-7001.conf
-rw-r--r--. 1 root root 41651 12月 17 03:59 redis-7002.conf
-rw-r--r--. 1 root root 41651 12月 17 03:59 redis-7003.conf
-rw-r--r--. 1 root root 41651 12月 17 03:59 redis-7004.conf
-rw-r--r--. 1 root root 41651 12月 17 03:59 redis-7005.conf
[root@localhost config]# cat nodes-7005.conf
d1e8e8e42be8d3b2f3f44d197138e54d91170442 :0 myself,master - 0 0 0 connected
vars currentEpoch 0 lastVoteEpoch 0
[root@localhost config]#
检查节点日志是否正确:
sudo cat /usr/local/redis/conf/nodes-${port}.conf
文件内容记录了集群初始状态,这里最重要的是节点ID,它是一个40位16进制字符串,用于唯一标识集群内一个节点,之后很多集群操作都要借助于节点ID来完成。需要注意是,节点ID不同于运行ID:节点ID在集群初始化 时只创建一次,节点重启时会加载集群配置文件进行重用,而Redis的运行ID每次重启都会变化。
我们现在启动6个节点,但每个节点彼此并不知道对方的存在,下面通过节点握手让6个节点彼此建立联系从而组成一个集群。
[root@localhost redis]# ps -ef |grep redis
root 1388 1 0 09:10 ? 00:00:00 redis-server *:7000 [cluster]
root 1392 1 0 09:10 ? 00:00:00 redis-server *:7001 [cluster]
root 1396 1 0 09:10 ? 00:00:00 redis-server *:7002 [cluster]
root 1400 1 0 09:10 ? 00:00:00 redis-server *:7003 [cluster]
root 1404 1 0 09:10 ? 00:00:00 redis-server *:7004 [cluster]
root 1408 1 0 09:10 ? 00:00:00 redis-server *:7005 [cluster]
节点握手
节点握手是指一批运行在集群模式下的节点通过Gossip协议彼此通信, 达到感知对方的过程
节点握手是集群彼此通信的第一步,由客户端发起下面的命令:
cluster meet {ip} {port}
[root@localhost redis]# redis-cli -h 127.0.0.1 -p 7000 cluster meet 127.0.0.1 7001
OK
[root@localhost redis]# redis-cli -h 127.0.0.1 -p 7000 cluster meet 127.0.0.1 7002
OK
[root@localhost redis]# redis-cli -h 127.0.0.1 -p 7000 cluster meet 127.0.0.1 7003
OK
[root@localhost redis]# redis-cli -h 127.0.0.1 -p 7000 cluster meet 127.0.0.1 7004
OK
[root@localhost redis]# redis-cli -h 127.0.0.1 -p 7000 cluster meet 127.0.0.1 7005
OK
[root@localhost redis]# redis-cli -h 127.0.0.1 -p 7000 cluster meet 127.0.0.1 7006
OK
上面执行命令之后让节点7000和7001等节点进行握手通信。cluster meet
命令是一个异步命令,执行之后立刻返回。内部发起与目标节点进行握手通信。
- 节点7000本地创建7001节点信息对象,并发送meet消息。
- 节点7001接受到meet消息后,保存7000节点信息并回复pong消息。
- 之后节点7000和7001彼此定期通过ping/pong消息进行正常的节点通信。
这个时候我们再执行cluster nodes
可以看到已经检测到其它节点了。
[root@localhost redis]# redis-cli -h 127.0.0.1 -p 7000 cluster nodes
d1e8e8e42be8d3b2f3f44d197138e54d91170442 127.0.0.1:7005 master - 0 1609463858135 4 connected
9a8abb84bcc8301a8f11c664471159dc0bf23a62 127.0.0.1:7001 master - 0 1609463860149 1 connected
f4deba14aac6494e95e3e4ad060c94b8c82df7ec 127.0.0.1:7000 myself,master - 0 0 0 connected
d5f317fc4597dbaac8b26a5897d801a72e45512e 127.0.0.1:7003 master - 0 1609463857127 3 connected
7dbbf232c72405a66416d2a0c335bd072f740644 127.0.0.1:7004 master - 0 1609463859143 5 connected
d438b4689776cb6cd6b6d0eaecb7576669c7b3fe 127.0.0.1:7002 master - 0 1609463861156 2 connected
节点建立握手之后集群还不能正常工作,这时集群处于下线状态,所有的数据读写都被禁止。通过如下命令可以看到:
[root@localhost redis]# redis-cli -h 127.0.0.1 -p 7000
127.0.0.1:7000> set jack hello
(error) CLUSTERDOWN The cluster is down
通过cluster info
命令可以获取集群当前状态:
127.0.0.1:7000> cluster info
cluster_state:fail
cluster_slots_assigned:0
cluster_slots_ok:0
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:0
cluster_current_epoch:5
cluster_my_epoch:0
cluster_stats_messages_sent:670
cluster_stats_messages_received:521
可以看到我们现在的状态是fail
,被分配的槽 cluster_slots_assigned
是0,由于目前所有的槽没有分配到节点,因此集群无法完成槽到节点的映射。只有当16384个槽全部分配给节点后,集群才进入在线状态。
分配槽
Redis集群把所有的数据映射到16384个槽中。每个key会映射为一个固定的槽,只有当节点分配了槽,才能响应和这些槽关联的键命令。通过cluster addslots
命令为节点分配槽。因为我们有6个节点,我们是三主三从的模式,所以只用给三个主节点进行配置即可。
redis-cli -h 127.0.0.1 -p 7000 cluster addslots {0..5461}
redis-cli -h 127.0.0.1 -p 7001 cluster addslots {5462..10922}
redis-cli -h 127.0.0.1 -p 7002 cluster addslots {10923..16383}
配置成功后,我们再进入节点看一下:
[root@localhost redis]# redis-cli -h 127.0.0.1 -p 7000
127.0.0.1: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:5
cluster_my_epoch:0
cluster_stats_messages_sent:1384
cluster_stats_messages_received:1235
可以看到,cluster_state
和 cluster_slots_assigned
都没有问题。
设置主从
目前还有三个节点没有使用,作为一个完整的集群,每个负责处理槽的节点应该具有从节点,保证当它出现故障时可以自动进行故障转移。
集群模式下,Reids节点角色分为主节点和从节点。首次启动的节点和被分配槽的节点都是主节点,从节点负责复制主节点槽信息和相关的数据。使用cluster replicate {node-id}
命令让一个节点成为从节点。其中命令执行必须在对应的从节点上执行,node-id是要复制主节点的节点ID。
我们首先找到三个已经配置槽的节点的node-id。
[root@localhost redis]# redis-cli -h 127.0.0.1 -p 7000 cluster nodes
d1e8e8e42be8d3b2f3f44d197138e54d91170442 127.0.0.1:7005 master - 0 1609464545892 4 connected
9a8abb84bcc8301a8f11c664471159dc0bf23a62 127.0.0.1:7001 master - 0 1609464547906 1 connected 5462-10922
f4deba14aac6494e95e3e4ad060c94b8c82df7ec 127.0.0.1:7000 myself,master - 0 0 0 connected 0-5461
d5f317fc4597dbaac8b26a5897d801a72e45512e 127.0.0.1:7003 master - 0 1609464546899 3 connected
7dbbf232c72405a66416d2a0c335bd072f740644 127.0.0.1:7004 master - 0 1609464549923 5 connected
d438b4689776cb6cd6b6d0eaecb7576669c7b3fe 127.0.0.1:7002 master - 0 1609464548916 2 connected 10923-16383
[root@localhost redis]# redis-cli -h 127.0.0.1 -p 7003 cluster replicate f4deba14aac6494e95e3e4ad060c94b8c82df7ec
OK
[root@localhost redis]# redis-cli -h 127.0.0.1 -p 7004 cluster replicate 9a8abb84bcc8301a8f11c664471159dc0bf23a62
OK
[root@localhost redis]# redis-cli -h 127.0.0.1 -p 7005 cluster replicate d438b4689776cb6cd6b6d0eaecb7576669c7b3fe
OK
完成后我们查看是否已经ok。
[root@localhost redis]# redis-cli -h 127.0.0.1 -p 7000 cluster nodes
d1e8e8e42be8d3b2f3f44d197138e54d91170442 127.0.0.1:7005 slave d438b4689776cb6cd6b6d0eaecb7576669c7b3fe 0 1609464847442 4 connected
9a8abb84bcc8301a8f11c664471159dc0bf23a62 127.0.0.1:7001 master - 0 1609464846435 1 connected 5462-10922
f4deba14aac6494e95e3e4ad060c94b8c82df7ec 127.0.0.1:7000 myself,master - 0 0 0 connected 0-5461
d5f317fc4597dbaac8b26a5897d801a72e45512e 127.0.0.1:7003 slave f4deba14aac6494e95e3e4ad060c94b8c82df7ec 0 1609464849456 3 connected
7dbbf232c72405a66416d2a0c335bd072f740644 127.0.0.1:7004 slave 9a8abb84bcc8301a8f11c664471159dc0bf23a62 0 1609464848449 5 connected
d438b4689776cb6cd6b6d0eaecb7576669c7b3fe 127.0.0.1:7002 master - 0 1609464850468 2 connected 10923-16383
目前为止,我们依照Redis协议手动建立一个集群。它由6个节点构成, 3个主节点负责处理槽和相关数据,3个从节点负责故障转移。
Redis自动化安装
我们之前分别使用命令搭建了一个完整的集群,但是命令过多,当集群节点众多时,必然会加大搭建集群的复杂度和运维成本。因此redis还提供了redis-cli --cluster来搭建集群。
首先我们还是启动六个单独的节点。
使用下面命令进行安装,--cluster-replicas 1
指定集群中每个主节点配备几个从节点,这里设置为1。并且该命令会自己创建主节点和分配从节点,其中前3个是主节点,后3个是从节点,后3个从节点分别复制前3个主节点。
redis-cli --cluster create --cluster-replicas 1 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005
最后的输出报告说明:16384个槽全部被分配,集群创建成功。这里需要注意命令中节点的地址必须是不包含任何槽/数据的节点,否则会拒绝创建集群。
如果不想要从节点则不填写该参数即可--cluster-replicas 1
。
最后我们可以使用下面命令进行查看是否已经ok。
redis-cli --cluster check 127.0.0.1:7000
集群伸缩原理
Redis集群提供了灵活的节点扩容和收缩方案。在不影响集群对外服务的情况下,可以为集群添加节点进行扩容也可以下线部分节点进行缩容。原理可抽象为槽和对应数据在不同节点之间灵活移动。
当我们现在有三个节点,此时想增加6385节点,也就是每个节点把一部分槽和数据迁移到新的节点6385,每个节点负责的槽和数据相比之前变少了从而达到了集群扩容的目的。
扩容集群实操
准备节点
之前我们有6个节点,7000~7005节点。
现在我们增加两个单独的节点也就是7006和7007。然后7006节点当做主节点,7007当做从节点。新节点跟集群内的节点配置保持一致,便于管理统一。
随后我们进行启动
[root@localhost redis]# redis-server config/redis-7006.conf
[root@localhost redis]# redis-server config/redis-7007.conf
这个时候我们的两个新的节点只是单独运行,并没有加入集群中。可以看到下面并没有7006和7007节点。
[root@localhost redis]# redis-cli -h 127.0.0.1 -p 7000 cluster nodes
d1e8e8e42be8d3b2f3f44d197138e54d91170442 127.0.0.1:7005 slave d438b4689776cb6cd6b6d0eaecb7576669c7b3fe 0 1609467765084 4 connected
9a8abb84bcc8301a8f11c664471159dc0bf23a62 127.0.0.1:7001 master - 0 1609467769137 1 connected 5462-10922
f4deba14aac6494e95e3e4ad060c94b8c82df7ec 127.0.0.1:7000 myself,master - 0 0 0 connected 0-5461
d5f317fc4597dbaac8b26a5897d801a72e45512e 127.0.0.1:7003 slave f4deba14aac6494e95e3e4ad060c94b8c82df7ec 0 1609467767119 3 connected
7dbbf232c72405a66416d2a0c335bd072f740644 127.0.0.1:7004 slave 9a8abb84bcc8301a8f11c664471159dc0bf23a62 0 1609467768127 5 connected
d438b4689776cb6cd6b6d0eaecb7576669c7b3fe 127.0.0.1:7002 master - 0 1609467766110 2 connected 10923-16383
结构图如下:
加入集群
redis-cli -h 127.0.0.1 -p 7000 cluster meet 127.0.0.1 7006
redis-cli -h 127.0.0.1 -p 7000 cluster meet 127.0.0.1 7007
集群内新旧节点经过一段时间的ping/pong消息通信之后,所有节点会发现新节点并将它们的状态保存到本地。
随后我们再进行查看cluster nodes
。
[root@localhost redis]# redis-cli -h 127.0.0.1 -p 7000 cluster nodes
d1e8e8e42be8d3b2f3f44d197138e54d91170442 127.0.0.1:7005 slave d438b4689776cb6cd6b6d0eaecb7576669c7b3fe 0 1609468208783 4 connected
9a8abb84bcc8301a8f11c664471159dc0bf23a62 127.0.0.1:7001 master - 0 1609468204768 1 connected 5462-10922
f4deba14aac6494e95e3e4ad060c94b8c82df7ec 127.0.0.1:7000 myself,master - 0 0 0 connected 0-5461
d5f317fc4597dbaac8b26a5897d801a72e45512e 127.0.0.1:7003 slave f4deba14aac6494e95e3e4ad060c94b8c82df7ec 0 1609468210798 3 connected
35f9f0abd365bb0fc424dbdaa849f1f1c71163bb 127.0.0.1:7006 master - 0 1609468209790 6 connected
55b028fbd0a0207b6acc6e2b1067bf79f3090534 127.0.0.1:7007 master - 0 1609468206777 7 connected
7dbbf232c72405a66416d2a0c335bd072f740644 127.0.0.1:7004 slave 9a8abb84bcc8301a8f11c664471159dc0bf23a62 0 1609468205773 5 connected
d438b4689776cb6cd6b6d0eaecb7576669c7b3fe 127.0.0.1:7002 master - 0 1609468206274 2 connected 10923-16383
然后我们把7007设置为7006的从节点
redis-cli -h 127.0.0.1 -p 7007 cluster replicate 35f9f0abd365bb0fc424dbdaa849f1f1c71163bb
再次查看已经OK。
[root@localhost redis]# redis-cli -h 127.0.0.1 -p 7000 cluster nodes
d1e8e8e42be8d3b2f3f44d197138e54d91170442 127.0.0.1:7005 slave d438b4689776cb6cd6b6d0eaecb7576669c7b3fe 0 1609470748800 4 connected
9a8abb84bcc8301a8f11c664471159dc0bf23a62 127.0.0.1:7001 master - 0 1609470750824 1 connected 5462-10922
f4deba14aac6494e95e3e4ad060c94b8c82df7ec 127.0.0.1:7000 myself,master - 0 0 0 connected 0-5461
d5f317fc4597dbaac8b26a5897d801a72e45512e 127.0.0.1:7003 slave f4deba14aac6494e95e3e4ad060c94b8c82df7ec 0 1609470745778 3 connected
35f9f0abd365bb0fc424dbdaa849f1f1c71163bb 127.0.0.1:7006 master - 0 1609470746785 6 connected
55b028fbd0a0207b6acc6e2b1067bf79f3090534 127.0.0.1:7007 slave 35f9f0abd365bb0fc424dbdaa849f1f1c71163bb 0 1609470751833 7 connected
7dbbf232c72405a66416d2a0c335bd072f740644 127.0.0.1:7004 slave 9a8abb84bcc8301a8f11c664471159dc0bf23a62 0 1609470749817 5 connected
d438b4689776cb6cd6b6d0eaecb7576669c7b3fe 127.0.0.1:7002 master - 0 1609470747795 2 connected 10923-16383
槽迁移计划
上面我们添加了两个新节点:7006、7007。其中7006作为主节点存储数据,7007作为从节点复制7006。下面我们要把其他节点的槽和数据迁移到7006这个节点中。
再迁移后原有节点负责的槽数量变为4096个。
迁移数据
数据迁移过程是逐个槽进行的。流程如下:
- 对目标节点发送:
cluster setslot {slot} importing {sourceNodeId}
命令,让目标节点准备导入槽数据。 - 对源节点发送:
cluster setslot {slot} migrating {targetNodeId}
命令,让源节点准备迁出槽数据。 - 源节点循环执行:
cluster getkeysinslot {slot} {count}
命令,每次获取count个属于槽的键。 - 在源节点上执行:
migrate {targetIP} {targetPort} key 0 {timeout}
命令,把指定的key迁移。 - 重复执行步骤3和步骤4,直到槽下所有的键值数据迁移到目标节点。
- 向集群内所有主节点发送:
cluster setslot {slot} node {targetNodeId}
命令,通知槽分配给目标节点。
伪代码如下:
def move_slot(source,target,slot):
# 目标节点准备导入槽
target.cluster("setslot",slot,"importing",source.nodeId);
# 源节点准备全出槽
source.cluster("setslot",slot,"migrating",target.nodeId);
while true :
# 批量从源节点获取键
keys = source.cluster("getkeysinslot",slot,pipeline_size);
if keys.length == 0:
# 键列表为空时,退出循环
break;
# 批量迁移键到目标节点
source.call("migrate",target.host,target.port,"",0,timeout,"keys",keys);
# 向集群所有主节点通知槽被分配给目标节点
for node in nodes:
if node.flag == "slave":
continue;
node.cluster("setslot",slot,"node",target.nodeId);
redis-cli cluster进行迁移
redis-cli --cluster reshard host:port --from --to --slots --yes --timeout
--pipeline
host
:port:必传参数,集群内任意节点地址,用来获取整个集群信息。--from
:制定源节点的id,如果有多个源节点,使用逗号分隔,如果是all源节点变为集群内所有主节点,在迁移过程中提示用户输入。--to
:需要迁移的目标节点的id,目标节点只能填写一个,在迁移过程 中提示用户输入。--slots
:需要迁移槽的总数量,在迁移过程中提示用户输入。--yes
:当打印出reshard执行计划时,是否需要用户输入yes确认后再执行reshard。--timeout
:控制每次migrate操作的超时时间,默认为60000毫秒。·--pipeline
:控制每次批量迁移键的数量,默认为10。
开始迁移:
redis-cli --cluster reshard 127.0.0.1:7000
输入需要迁移的槽数量,此处我们输入4096。
目标节点ID,只能指定一个,因为我们需要迁移到7006中,因此下面输入7006的ID。
之后输入源节点的ID,redis会从这些源节点中平均取出对应数量的槽,然后迁移到6385中,下面我们分别输入7000、7001、7002的节点ID。最后要输入done
表示结束。
最后输入yes即可。
我们可以检查一下节点之间的平衡性
redis-cli --cluster rebalance 127.0.0.1:6380
所有主节点负责的槽数量差异在2%以内,就算集群节点数据相对均匀,无需调整。
收缩集群
- 首先需要确定下线节点是否有负责的槽,如果是,需要把槽迁移到 其他节点,保证节点下线后整个集群槽节点映射的完整性。
- 当下线节点不再负责槽或者本身是从节点时,就可以通知集群内其 他节点忘记下线节点,当所有的节点忘记该节点后可以正常关闭。
收缩正好和扩容迁移方向相反,7006变为源节点,其他主节点变为目标节点,源节点需要把自身负责的4096个槽均匀地迁移到其他主节点上。
具体步骤和上述扩容类似,这里就不演示。
请求重定向
在集群模式下,Redis接收任何键相关命令时首先计算键对应的槽,再根据槽找出所对应的节点,如果节点是自身,则处理键命令;否则回复MOVED重定向错误,通知客户端请求正确的节点。
命中槽
因为我们执行cluster keyslot hello
之后,发现槽的位置在866,在我们之中,所以直接返回。
127.0.0.1:7000> set hello world
OK
127.0.0.1:7000> cluster keyslot hello
(integer) 866
127.0.0.1:7000> get hello
"world"
未命中槽
由于键对应槽是6918,不属于7000节点,则回复MOVED {slot} {ip} {port}格式重定向信息:
127.0.0.1:7000> set test hello
(error) MOVED 6918 127.0.0.1:7001
我们可以切换到7001发送命令即可成功。
127.0.0.1:7001> set test hello
OK
用redis-cli命令时,可以加入-c参数支持自动重定向,简化手动发起重定向操作。
[root@localhost config]# redis-cli -h 127.0.0.1 -p 7000 -c
127.0.0.1:7000> set test hello
-> Redirected to slot [6918] located at 127.0.0.1:7001
OK
ASK重定向
Redis集群支持在线迁移槽(slot)和数据来完成水平伸缩,当slot对应的数据从源节点到目标节点迁移过程中,客户端需要做到智能识别,保证键命令可正常执行。例如当一个slot数据从源节点迁移到目标节点时,期间可能出现一部分数据在源节点,而另一部分在目标节点。
当出现上述情况时,客户端键命令执行流程将发生变化,如下所示:
- 客户端根据本地slots缓存发送命令到源节点,如果存在键对象则直 接执行并返回结果给客户端。
- 如果键对象不存在,则可能存在于目标节点,这时源节点会回复 ASK重定向异常。格式如下:
(error) ASK {slot} {targetIP}:{targetPort}
。 - 客户端从ASK重定向异常提取出目标节点信息,发送
asking
命令到目标节点打开客户端连接标识,再执行键命令。如果存在则执行,不存在则返回不存在信息。
ASK和MOVED区别
- ASK重定向说明集群正在进行slot数据迁移,客户端无法知道什么时候迁移完成,因此只能是临时性的重定向,客户端不会更新slots缓存。
- 但是MOVED重定向说明键对应的槽已经明确指定到新的节点,因此需要更新slots缓存。
故障发现
- 当集群内某个节点出现问题时,需要通过一种健壮的方式保证识别出节点是否发生了故障。Redis集群内节点通过ping/pong消息实现节点通信,消息不但可以传播节点槽信息,还可以传播其他状态如:主从状态、节点故障等。
因此故障发现也是通过消息传播机制实现的,主要环节包括:
- 主观下线 (pfail):指某个节点认为另一个节点不可用,即下线状态,这个状态并不是最终的故障判定,只能代表一个节点的意见,可能存在误判情况。
- 客观下线(fail):指标记一个节点真正的下线,集群内多个节点都认为该节点不可用,从而达成共识的结果。如果是持有槽的主节点故障,需要为该节点进行故障转移。
主观下线
集群中每个节点都会定期向其他节点发送ping消息,接收节点回复pong消息作为响应。如果在cluster-node-timeout时间内通信一直失败,则发送节点会认为接收节点存在故障,把接收节点标记为主观下线(pfail)状态。
主观下线流程:
- 节点a发送ping消息给节点b,如果通信正常将接收到pong消息,节点a更新最近一次与节点b的通信时间。
- 如果节点a与节点b通信出现问题则断开连接,下次会进行重连。如果一直通信失败,则节点a记录的与节点b最后通信时间将无法更新。
- 节点a内的定时任务检测到与节点b最后通信时间超过
cluster-nodetimeout
时,更新本地对节点b的状态为主观下线(pfail)。
客观下线
当半数以上持有槽的主节点都标记某节点主观下线。
客观下线流程:
- 当消息体内含有其他节点的pfail状态会判断发送节点的状态,如果发送节点是主节点则对报告的pfail状态处理,从节点则忽略。
- 找到pfail对应的节点结构,更新clusterNode内部下线报告链表。
- 根据更新后的下线报告链表告尝试进行客观下线。
尝试客观下线
- 首先统计有效的下线报告数量,如果小于集群内持有槽的主节点总数的一半则退出。
- 当下线报告大于槽主节点数量一半时,标记对应故障节点为客观下线状态。
- 向集群广播一条fail消息,通知所有的节点将故障节点标记为客观下线,fail消息的消息体只包含故障节点的ID。
故障恢复
故障节点变为客观下线后,如果下线节点是持有槽的主节点则需要在它的从节点中选出一个替换它,从而保证集群的高可用。下线主节点的所有从节点承担故障恢复的义务,当从节点通过内部定时任务发现自身复制的主节点进入客观下线时,将会触发故障恢复流程。
检查资格
- 每个从节点都要检查最后与主节点断线时间,判断是否有资格替换故障的主节点。
- 如果从节点与主节点断线时间超过
cluster-node-timeout * cluster-slave-validity-factor
,则当前从节点不具备故障转移资格。参数cluster-slavevalidity-factor
用于从节点的有效因子,默认为10。
准备选举时间
当从节点符合故障转移资格后,更新触发故障选举的时间,只有到达该时间后才能执行后续流程。
主节点b进入客观下线后,它的三个从节点根据自身复制偏移量设置延迟选举时间,如复制偏移量最大的节点slave b-1延迟1秒执行,保证复制延迟低的从节点优先发起选举。
选举投票
- 只有持有槽的主节点才会处理故障选举消息。
- 投票过程其实是一个领导者选举的过程,如集群内有N个持有槽的主节点代表有N张选票。由于在每个配置纪元内持有槽的主节点只能投票给一个从节点,因此只能有一个从节点获得N/2+1的选票,保证能够找出唯一的从节点。
- Redis集群没有直接使用从节点进行领导者选举,主要因为从节点数必须大于等于3个才能保证凑够N/2+1个节点,将导致从节点资源浪费。使用集群内所有持有槽的主节点进行领导者选举,即使只有一个从节点也可以完成选举过程。
- 当从节点收集到N/2+1个持有槽的主节点投票时,从节点可以执行替换主节点操作,例如集群内有5个持有槽的主节点,主节点b故障后还有4个, 当其中一个从节点收集到3张投票时代表获得了足够的选票可以进行替换主节点操作。
替换主节点
当从节点收集到足够的选票之后,触发替换主节点操作:
- 当前从节点取消复制变为主节点。
- 执行
clusterDelSlot
操作撤销故障主节点负责的槽,并执行clusterAddSlot
把这些槽委派给自己。 - 向集群广播自己的pong消息,表明已经替换了故障从节点。