xyZGHio

本是青灯不归客,却因浊酒恋风尘

0%

浅谈Redis Cluster集群的可用性

本文介绍Redis Cluster模式下集群的可用性

abstract.png

搭建集群

利用Docker Compose搭建一个3主3从的Redis Cluster集群,docker-compose.yml文件如下

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# 构建一个3主3从的Redis集群
# Compose 版本
version: '3.8'

# 定义服务
services:

Redis-Service-1:
image: redis:7.0
container_name: node-1
command: [ "redis-server", "/etc/redis/redis.conf" ]
ports:
- "6371:6379"
- "16371:16379"
volumes:
- /Users/zgh/Docker/RedisCluster_3M3S/RedisConf/redis.conf:/etc/redis/redis.conf
networks:
# 使用名为redis_cluster_3m3s_net的网络并设置容器IP
redis_cluster_3m3s_net:
ipv4_address: 120.120.120.11

Redis-Service-2:
image: redis:7.0
container_name: node-2
command: [ "redis-server", "/etc/redis/redis.conf" ]
ports:
- "6372:6379"
- "16372:16379"
volumes:
- /Users/zgh/Docker/RedisCluster_3M3S/RedisConf/redis.conf:/etc/redis/redis.conf
networks:
# 使用名为redis_cluster_3m3s_net的网络并设置容器IP
redis_cluster_3m3s_net:
ipv4_address: 120.120.120.12

Redis-Service-3:
image: redis:7.0
container_name: node-3
command: [ "redis-server", "/etc/redis/redis.conf" ]
ports:
- "6373:6379"
- "16373:16379"
volumes:
- /Users/zgh/Docker/RedisCluster_3M3S/RedisConf/redis.conf:/etc/redis/redis.conf
networks:
# 使用名为redis_cluster_3m3s_net的网络并设置容器IP
redis_cluster_3m3s_net:
ipv4_address: 120.120.120.13

Redis-Service-4:
image: redis:7.0
container_name: node-4
command: [ "redis-server", "/etc/redis/redis.conf" ]
ports:
- "6374:6379"
- "16374:16379"
volumes:
- /Users/zgh/Docker/RedisCluster_3M3S/RedisConf/redis.conf:/etc/redis/redis.conf
networks:
# 使用名为redis_cluster_3m3s_net的网络并设置容器IP
redis_cluster_3m3s_net:
ipv4_address: 120.120.120.14

Redis-Service-5:
image: redis:7.0
container_name: node-5
command: [ "redis-server", "/etc/redis/redis.conf" ]
ports:
- "6375:6379"
- "16375:16379"
volumes:
- /Users/zgh/Docker/RedisCluster_3M3S/RedisConf/redis.conf:/etc/redis/redis.conf
networks:
# 使用名为redis_cluster_3m3s_net的网络并设置容器IP
redis_cluster_3m3s_net:
ipv4_address: 120.120.120.15

Redis-Service-6:
image: redis:7.0
container_name: node-6
command: [ "redis-server", "/etc/redis/redis.conf" ]
ports:
- "6376:6379"
- "16376:16379"
volumes:
- /Users/zgh/Docker/RedisCluster_3M3S/RedisConf/redis.conf:/etc/redis/redis.conf
networks:
# 使用名为redis_cluster_3m3s_net的网络并设置容器IP
redis_cluster_3m3s_net:
ipv4_address: 120.120.120.16

# 定义网络
networks:
# 定义一个名为redis_cluster_3m3s_net的网络
redis_cluster_3m3s_net:
ipam:
config:
# 设置网段
- subnet: 120.120.120.0/24

Redis指定版本的配置文件redis.conf可通过下述链接下载对应版本的安装包后解压后获取

1
http://download.redis.io/releases/

修改Redis配置文件redis.conf

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
...

# 注释bind配置项
# bind 127.0.0.1 -::1

# 设为 no, 关闭保护模式
protected-mode no

# 端口设为6379
port 6379

# 设为no, 因为docker启动时会通过-d参数来让其实现后台运行
daemonize no

# 修改数据库数量, 用于验证配置文件是否生效
databases 5

# 设置访问主库时的密码
masterauth "52996"

# 设置Redis密码
requirepass 52996

# 使能集群模式
cluster-enabled yes

# 集群是否要求槽全覆盖
# yes:当负责某个槽的主库下线且没有相应的从库进行故障恢复时,集群整体不可用。即其他槽的节点在线也无法访问
# no:当负责某个槽的主库下线且没有相应的从库进行故障恢复时,集群仍然可用。即可访问其他槽的数据
cluster-require-full-coverage yes

...

这里对于集群中的6个节点,均使用上述同一个配置文件。目录结构如下所示

figure 1.jpg

现在,进行docker-compose.yml文件所在目录,利用Docker Compose命令创建容器

1
docker-compose up -d

figure 2.jpg

执行下述命令组建集群

1
2
3
4
5
6
docker exec -it node-1 \
redis-cli -p 6379 -a 52996 --cluster create \
120.120.120.11:6379 120.120.120.12:6379 \
120.120.120.13:6379 120.120.120.14:6379 \
120.120.120.15:6379 120.120.120.16:6379 \
--cluster-replicas 1

命令输出结果如下所示,其中在分配好每个节点的槽范围后,需要输入yes进行确认,以便完成集群构建

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
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 120.120.120.15:6379 to 120.120.120.11:6379
Adding replica 120.120.120.16:6379 to 120.120.120.12:6379
Adding replica 120.120.120.14:6379 to 120.120.120.13:6379
M: 375df7b2d433e5deeb790a665b4c71bf05351d39 120.120.120.11:6379
slots:[0-5460] (5461 slots) master
M: 9a6d9f2b8bcd3194e9aa6c3b49981b2c06cc4c2a 120.120.120.12:6379
slots:[5461-10922] (5462 slots) master
M: ee7167cfb5db69f4ade001433d93b103a5083410 120.120.120.13:6379
slots:[10923-16383] (5461 slots) master
S: 274433887a0d2e6d16c481d6d1f5e966b1394e11 120.120.120.14:6379
replicates ee7167cfb5db69f4ade001433d93b103a5083410
S: 442b5c8ed244a51150f3926992e34aa9ebee32b1 120.120.120.15:6379
replicates 375df7b2d433e5deeb790a665b4c71bf05351d39
S: a9cdb504940f70f55e1421999573beae90f38653 120.120.120.16:6379
replicates 9a6d9f2b8bcd3194e9aa6c3b49981b2c06cc4c2a
Can I set the above configuration? (type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join
.
>>> Performing Cluster Check (using node 120.120.120.11:6379)
M: 375df7b2d433e5deeb790a665b4c71bf05351d39 120.120.120.11:6379
slots:[0-5460] (5461 slots) master
1 additional replica(s)
S: a9cdb504940f70f55e1421999573beae90f38653 120.120.120.16:6379
slots: (0 slots) slave
replicates 9a6d9f2b8bcd3194e9aa6c3b49981b2c06cc4c2a
S: 442b5c8ed244a51150f3926992e34aa9ebee32b1 120.120.120.15:6379
slots: (0 slots) slave
replicates 375df7b2d433e5deeb790a665b4c71bf05351d39
S: 274433887a0d2e6d16c481d6d1f5e966b1394e11 120.120.120.14:6379
slots: (0 slots) slave
replicates ee7167cfb5db69f4ade001433d93b103a5083410
M: ee7167cfb5db69f4ade001433d93b103a5083410 120.120.120.13:6379
slots:[10923-16383] (5461 slots) master
1 additional replica(s)
M: 9a6d9f2b8bcd3194e9aa6c3b49981b2c06cc4c2a 120.120.120.12:6379
slots:[5461-10922] (5462 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.

此时我们即可在任意一个节点容器中执行redis-cli命令来访问集群了

1
2
3
4
5
6
7
8
# 以集群方式启动redis-cli(使用-c选项),可以自动进行重定向;反之,不行
docker exec -it node-1 redis-cli -c -p 6379 -a 52996

# redis 命令:查看集群信息
cluster info

# redis 命令:查看集群节点信息
cluster nodes

figure 3.jpg

现在向集群中添加key分别为age、name、country的数据。不难看出其分别存储到Master节点node1、node2、node3当中

figure 4.jpg

如果期望从某Slave节点中访问其所存储的相应数据,需先发送readonly命令。如下所示

figure 5.jpg

下文将会利用该集群对Redis Cluster的可用性进行探索。至此我们不难得出该集群的拓扑结构关系及部分key的分布,如下所示

figure 6.jpg

Master下线

一个Master下线

Redis Cluster模式下,当某个Master被判定下线后,会进行故障转移Failover。基本流程如下:

  1. 从已经下线的Master节点所属的所有Slave节点中,挑选一个Slave节点作为新Master
  2. 让已经下线的Master节点所属的所有Slave节点,指向新Master。即从新Master进行复制
  3. 将已经下线的Master节点设置为新Master的从节点。这样当这个已经下线的Master节点后续重新上线时,其就会成为新Master的从节点。以避免某个槽中同时出现两个主节点的情形

对于上面的搭建3主3从的集群而言,如果我们停止Master节点node 2的容器使其下线。从下图不难看出,集群状态依然可用。node 2节点的状态为fail。node 6节点此前是node 2节点的Slave节点,现在则成为新Master,对外提供服务了

figure 7.jpg

通过对key为name的数据进行访问、修改。进一步证明了node 6节点是Master节点了

figure 8.jpg

此时,我们如果将node 2节点容器启动,会是什么情形呢?不难看出,node 2节点已经是新Master node 6节点的从节点了。因为当我们在node 6节点中修改key为name的数据后,可以从node 2节点中感知到变化

figure 9.jpg

半数以上Master同时下线

对于上面的搭建3主3从的集群而言,如果我们将半数以上的Master同时下线,会怎么样呢?这里我们选择停止2个Master节点容器——node 2、node 3,会发生什么呢?

1
2
3
4
5
# 停止 node 2、node 3 节点容器
docker container stop node-2; docker container stop node-3

# 查看正在运行的容器
docker ps --filter status=running

figure 10.jpg

现在我们通过node 1节点查看集群、节点状态,不仅node 2、node 3节点下线,而且整个集群都不可用。甚至对于node 1中key为age的数据都无法访问

figure 11.jpg

我们知道node 2、node 3节点都有各自的slave节点——node 6、node 4。为什么在该场景下未进行故障转移Failover,使node 6、node 4成为新Master呢?原因其实很简单,在Redis Cluster模式下是不需要Sentinel哨兵的。故对于一个含有N个Master的集群而言,当一个Slave提升为新Master的要求,是需要至少获得集群中 N/2+1 个Master(即半数以上的Master)的投票数。换言之,当集群中可用Master节点不足半数以上,会导致集群不可用

对于这里含有3个Master的集群而言,半数以上Master投票数的要求是 N/2+1 = 3/2+1 = 2。当我们同时停止两个Master节点容器后,集群中只剩一个Master可以进行投票。使得node 6、node 4节点均无法满足最低投票数要求。进而导致整个集群不可用

原Master在故障转移后依次下线

从上文不难看出在Redis Cluster中,节点的角色不是固定的。因为当Master节点下线后,其下的Slave节点可以被提升为新Master节点。对于上面的搭建3主3从的集群而言,如果我们对原Master节点node 1、node 2、node 3依次进行下线,但保证每次下线某个原Master节点时,之前的下线节点已经完成了故障转移

具体地:

  • 先下线原Master节点node 1,等待故障转移完成,使得node 5成为新Master
  • 再下线原Master节点node 2,等待故障转移完成,使得node 6成为新Master
  • 最后下线原Master节点node 3,等待故障转移完成,使得node 4成为新Master

如下所示,不难看到当某个原Master下线。由于集群中可用Master的数量(包括原Master、新Master)总是可以满足半数以上的要求,故其下的Slave总是可以会被提升为新Master。例如,当原Master节点node 2下线时,集群中可用的Master有两个:原Master节点node 3、新Master节点node 5

figure 12.jpg

可以看到,原Master在故障转移后依次下线。此时集群中只有node 5、node 6、node 4这3个新Master节点了。自然集群依然是可用的

figure 13.jpg

某个槽对应的主从节点全部下线

对于上面的搭建3主3从的集群而言,我们先将Master节点node 2下线,然后让node 6节点成为新Master

figure 14.jpg

现在我们将node 6节点继续下线,这样该槽对应的所有节点(node 2、node 6)都已经全部下线了。此时,由于Redis集群中某个槽完全无法对外提供服务,会导致集群整体不可用。不仅无法访问存在于node 2、node 6中key为name的数据,而且对存在于node 1、node 3中key为age、country的数据也无法访问

figure 15.jpg

事实上,这正是redis.conf配置文件中cluster-require-full-coverage配置项的作用。此前我们将该配置项设置为yes。即,当负责某个槽的主库下线且没有相应的从库进行故障恢复时,集群整体不可用。即使其他槽的节点未下线,也无法访问其中的数据

而这里,如果我们期望某个槽对应的所有节点(node 2、node 6)全部下线后。集群仍然可用,即可以访问其他槽(例如node1、node3)的数据。则可将cluster-require-full-coverage配置项修改为no,然后重启整个集群使之生效。这样当负责某个槽的主库下线且没有相应的从库进行故障恢复时,集群仍然可用。即可访问其他槽的数据

修改配置重启后,我们停止了node 2、node 6节点。此时集群状态依然可用。虽然无法访问存在于node 2、node 6中key为name的数据,但对存在于node 1、node 3中key为age、country的数据可以正常访问

figure 16.jpg

总结

通过上文不难看出,Redis Cluster模式下集群不可用有两种情形:

  1. 当集群中可用Master节点不足半数以上,会导致集群不可用
  2. 当cluster-require-full-coverage配置项为yes时,某个槽对应的主从节点全部下线,会导致集群不可用;当cluster-require-full-coverage配置项为no时,某个槽对应的主从节点全部下线,集群中其他槽的数据可以正常访问
请我喝杯咖啡捏~

欢迎关注我的微信公众号:青灯抽丝