目录
1. 集群的基本概念
(1)哨兵模式提高了系统的可用性。但是真正用来存储数据的还是master和slave节点。所有的数据都需要存储在单个master和slave节点中。
(2)如果数据量很大,接近超出了master / slave所在机器的物理内存,就可能出现严重问题了。虽然硬件价格在不断降,一些中大厂的服务器内存已经可以达到TB级别了,但是1TB在当前这个"大数据"时代,俨然不算什么,有的时候我们确实需要更大的内存空间来保存更多的数据。
(3)如何获取更大的空间?加机器即可!所谓"大数据"的核心,其实就是一台机器搞不定了就用多台机器来搞。
(4)Redis的集群就是在上述的思路之下,引入多组Master / Slave,每一组Master / Slave存储数据全集的一部分,从而构成一个更大的整体,称为Redis集群(Cluster)。
(5)假定整个数据全集是1TB,引入三组Master / Slave来存储。那么每一组机器只需要存储整个数据全集的1/3即可:
(6)在上述图中:
- Master1 和Slave11 和Slave12 保存的是同样的数据。占总数据的 1/3。
- Master2和Slave21和Slave22保存的是同样的数据。占总数据的1/3。
- Master3 和Slave31 和Slave32 保存的是同样的数据。占总数据的 1/3。
(7)这三组机器存储的数据都是不同的:
- 每个Slave都是对应Master的备份(当Master挂了,对应的Slave会补位成Master)。
- 每个红框部分都可以称为是一个分片(Sharding)。
- 如果全量数据进一步增加,只要再增加更多的分片,即可解决。
2. 数据分片算法
Redis cluster的核心思路是用多组机器来存数据的每个部分。那么接下来的核心问题就是,给定一个数据(一个具体的key),那么这个数据应该存储在哪个分片上? 读取的时候又应该去哪个分片读取?围绕这个问题,业界有三种比较主流的实现方式:
2.1 哈希求余
(1)设有N个分片,使用[0, N-1]这样序号进行编号:
- 针对某个给定的key, 先计算hash值,再把得到的结果% N,得到的结果即为分片编号。
- 例如N为3。给定key为hello,对hello计算hash值(比如使用md5算法),得到的结果为bc4b2a76b9719d91,再把这个结果%3,结果为0,那么就把hello这个key放到0号分片上。
- 当然,实际工作中涉及到的系统,计算hash的方式不- -定是md5,但是思想是一致的。
(2)后续如果要取某个key的记录,也是针对key计算hash,再对N求余,就可以找到对应的分片编号了。
- 优点:简单高效,数据分配均匀。
- 缺点:一旦需要进行扩容,N改变了,原有的映射规则被破坏,就需要让节点之间的数据相互传输,重新排列,以满足新的映射规则。此时需要搬运的数据量是比较多的,开销较大。
(3)N为3的时候,[100,120]这21个hash值的分布(此处假定计算出的hash值是一个简单的整数,方便肉眼观察)当引入一个新的分片,N从3=>4时,大量的key都需要重新映射(某个key%3和%4的结果不一样,就映射到不同机器上了)。
如上图可以看到,整个扩容一共21个key,只有3个key没有经过搬运,其他的key都是搬运过的。
2.2 一致性哈希算法
(1)为了降低上述的搬运开销,能够更高效扩容,业界提出了"一致性哈希算法"。key映射到分片序号的过程不再是简单求余了,而是改成以下过程:
- 第一步,把0-> 2^32-1这个数据空间,映射到一个圆环上。数据按照顺时针方向增长:
- 第二步,假设当前存在三个分片,就把分片放到圆环的某个位置上:
- 第三步,假定有一个key,计算得到hash值H,那么这个key映射到哪个分片呢?规则很简单,就是从H所在位置,顺时针往下找,找到的第一个分片即为该key所从属的分片:
- 这就相当于,N个分片的位置把整个圆环分成了N个管辖区间。Key的hash值落在某个区间内,就归对应区间管理:
-
上述⼀致性哈希算法的过程,类似于去高铁站取票。现在的高铁站都可以直接刷身份证了。但是以前的时候需要网上先购票,然后再去高铁站的取票机上把票取出来。想象下列场景:
-
在这个情况下,如果扩容一个分片,如何处理呢?原有分片在环上的位置不动,只要在环上新安排一个分片位置即可:
(2)此时只需要把0号分片上的部分数据搬运给3号分片即可。1号分片和2号分片管理的区间都是不变。
- 优点:大大降低了扩容时数据搬运的规模,提高了扩容操作的效率。
- 缺点:数据分配不均匀(有的多有的少,数据倾斜)。
2.3 哈希槽分区算法(Redis采用的方案)
(1)为了解决上述问题(搬运成本高和数据分配不均匀),Redis cluster引入了哈希槽(hash slots)算法:
hash_slot = crc16(key) % 16384
- 其中crc16也是一种hash算法。16384其实是16* 1024,也就是2^14。相当于是把整个哈希值,映射到16384个槽位上,也就是[0, 16383]。
- 然后再把这些槽位比较均匀的分配给每个分片。每个分片的节点都需要记录自己持有哪些分片。
(2)假设当前有三个分片,一种可能的分配方式:
- 0号分片:[0, 5461]共5462个槽位。
- 1号分片:[5462, 10923]共5462个槽位。
- 2号分片:[10924, 16383]共5460个槽位。
这里的分片规则是很灵活的。每个分片持有的槽位也不一定连续。每个分片的节点使用位图来表示自己持有哪些槽位。对于16384个槽位来说,需要2048个字节(2KB)大小的内存空间表示。
(3)如果需要进行扩容,比如新增一个3号分片,就可以针对原有的槽位进行重新分配。比如可以把之前每个分片持有的槽位,各拿出一点分给新分片。一种可能的分配方式:
- 0号分片:[0, 4095]共4096个槽位。
- 1号分片:[5462, 9557]共4096个槽位。
- 2号分片:[10924, 15019]共4096个槽位。
- 3号分片:[4096, 5461] + [9558, 10923] + [15019, 16383]共4096个槽位。
我们在实际使用Redis集群分片的时候,不需要手动指定哪些槽位分配给某个分片,只需要告诉某个分片应该持有多少个槽位即可,Redis会自动完成后续的槽位分配,以及对应的key搬运的工作。
(4)此处还有两个问题:
- 问题一:Redis集群是最多有16384个分片吗??并非如此。如果一个分片只有一个槽位,这对于集群的数据均匀其实是难以保证的。实际上Redis的作者建议集群分片数不应该超过1000。而且,16000这么大规模的集群,本身的可用性也是一个大问题。一个系统越复杂,出现故障的概率是越高的。
- 问题二:为什么是16384个槽位??Redis作者的答案: https://github.com/antirez/redis/issues/2576。
-
Normal heartbeat packets carry the full configuration of a node, that can be replaced in anidempotent way with the old in order to update an old config. This means they contain theslots configuration for a node, in raw form, that uses 2k of space with 16k slots, but would use a prohibitive 8k of space using 65k slots.
-
At the same time, it is unlikely that Redis Cluster would scale to more than 1000 masternodes because of other design tradeoffs.
-
So 16k was in the right range to ensure enough slots per master with a max of 1000 masters, but a small enough number to propagate the slot configuration as a raw bitmap easily. Note that in small clusters, the bitmap would be hard to compress, because when N is small, the bitmap would have slots/N bits set. That is a large percentage of bits set.
-
翻译过来大概意思是:节点之间通过心跳包通信。心跳包中包含了该节点持有哪些slots。这个是使用位图这样的数据结构表示的。表示16384 (16k)个slots,需要的位图大小是2KB。如果给定的slots数更多了,比如65536个了,此时就需要消耗更多的空间,8 KB位图表示了8 KB,对于内存来说不算什么,但是在频繁的网络心跳包中,还是一个不小的开销的。
-
另一方面,Redis集群一般不建议超过1000个分片。所以16k对于最大1000个分片来说是足够用的,同时也会使对应的槽位配置位图体积不至于很大。
-
3. 集群的搭建(基于docker)
(1)接下来基于docker搭建一个集群。每个节点都是一个容器。拓扑结构如下:
- 注意:此处我们先创建出11个redis节点。其中前9个用来演示集群的搭建。后两个用来演示集群扩容。
(2)第一步:创建目录和配置。
- 创建redis-cluster目录。内部创建两个文件:
redis-cluster/
├── docker-compose.yml
└── generate.sh
- generate.sh内容如下:
for port in $(seq 1 9); \
do \
mkdir -p redis${port}/
touch redis${port}/redis.conf
cat << EOF > redis${port}/redis.conf
port 6379
bind 0.0.0.0
protected-mode no
appendonly yes
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
cluster-announce-ip 172.30.0.10${port}
cluster-announce-port 6379
cluster-announce-bus-port 16379
EOF
done
# 注意 cluster-announce-ip 的值有变化.
for port in $(seq 10 11); \
do \
mkdir -p redis${port}/
touch redis${port}/redis.conf
cat << EOF > redis${port}/redis.conf
port 6379
bind 0.0.0.0
protected-mode no
appendonly yes
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
cluster-announce-ip 172.30.0.1${port}
cluster-announce-port 6379
cluster-announce-bus-port 16379
EOF
done
- 执行命令:
bash generate.sh
- 生成目录如下:
redis-cluster/
├── docker-compose.yml
├── generate.sh
├── redis1
│ └── redis.conf
├── redis10
│ └── redis.conf
├── redis11
│ └── redis.conf
├── redis2
│ └── redis.conf
├── redis3
│ └── redis.conf
├── redis4
│ └── redis.conf
├── redis5
│ └── redis.conf
├── redis6
│ └── redis.conf
├── redis7
│ └── redis.conf
├── redis8
│ └── redis.conf
└── redis9
└── redis.conf
- 其中redis.conf每个都不同。以redis1为例:区别在于每个配置中配置的cluster-announce-ip 是不同的,其他部分都相同。
port 6379
bind 0.0.0.0
protected-mode no
appendonly yes
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
cluster-announce-ip 172.30.0.101
cluster-announce-port 6379
cluster-announce-bus-port 16379
后续会给每个节点分配不同的ip地址。
- 配置说明:
- cluster-enabled yes 开启集群。
- cluster-config-file nodes.conf集群节点生成的配置。
- cluster-node-timeout 5000 节点失联的超时时间。
- cluster-announce-ip 172.30.0.101节点自身ip。
- cluster-announce-port 6379节点自身的业务端口。
- cluster- announce-bus-port 16379 节点自身的总线端口.集群管理的信息交互是通过这个端口进行的。
(3)第二步:编写docker-compose.yml。
- 先创建networks并分配网段为172.30.0.0/24。
- 配置每个节点。注意配置文件映射,端口映射,以及容器的ip地址。设定成固定ip方便后续的观察和操作。此处的端口映射不配置也可以,配置的目的是为了可以通过宿主机ip +映射的端口进行访问。通过容器自身ip:6379的方式也可以访问。
version: '3.7'
networks:
mynet:
ipam:
config:
- subnet: 172.30.0.0/24
services:
redis1:
image: 'redis:5.0.9'
container_name: redis1
restart: always
volumes:
- ./redis1/:/etc/redis/
ports:
- 6371:6379
- 16371:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.101
redis2:
image: 'redis:5.0.9'
container_name: redis2
restart: always
volumes:
- ./redis2/:/etc/redis/
ports:
- 6372:6379
- 16372:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.102
redis3:
image: 'redis:5.0.9'
container_name: redis3
restart: always
volumes:
- ./redis3/:/etc/redis/
ports:
- 6373:6379
- 16373:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.103
redis4:
image: 'redis:5.0.9'
container_name: redis4
restart: always
volumes:
- ./redis4/:/etc/redis/
ports:
- 6374:6379
- 16374:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.104
redis5:
image: 'redis:5.0.9'
container_name: redis5
restart: always
volumes:
- ./redis5/:/etc/redis/
ports:
- 6375:6379
- 16375:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.105
redis6:
image: 'redis:5.0.9'
container_name: redis6
restart: always
volumes:
- ./redis6/:/etc/redis/
ports:
- 6376:6379
- 16376:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.106
redis7:
image: 'redis:5.0.9'
container_name: redis7
restart: always
volumes:
- ./redis7/:/etc/redis/
ports:
- 6377:6379
- 16377:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.107
redis8:
image: 'redis:5.0.9'
container_name: redis8
restart: always
volumes:
- ./redis8/:/etc/redis/
ports:
- 6378:6379
- 16378:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.108
redis9:
image: 'redis:5.0.9'
container_name: redis9
restart: always
volumes:
- ./redis9/:/etc/redis/
ports:
- 6379:6379
- 16379:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.109
redis10:
image: 'redis:5.0.9'
container_name: redis10
restart: always
volumes:
- ./redis10/:/etc/redis/
ports:
- 6380:6379
- 16380:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.110
redis11:
image: 'redis:5.0.9'
container_name: redis11
restart: always
volumes:
- ./redis11/:/etc/redis/
ports:
- 6381:6379
- 16381:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.111
(4)第三步:启动容器。
docker-compose up -d
(5)第四步:构建集群。
- 启动一个docker客户端。此处是把前 9个主机构建成集群,3主6从。后2个主机暂时不用。
redis-cli --cluster create 172.30.0.101:6379 172.30.0.102:6379
172.30.0.103:6379 172.30.0.104:6379 172.30.0.105:6379 172.30.0.106:6379
172.30.0.107:6379 172.30.0.108:6379 172.30.0.109:6379 --cluster-replicas 2
–cluster create表示建立集群。后面填写每个节点的ip和地址。
–cluster-replicas 2表示每个主节点需要两个从节点备份。
- 执行之后,容器之间会进行加入集群操作。日志中会描述哪些是主节点,哪些从节点跟随哪个主节点。
>>> Performing hash slots allocation on 9 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 172.30.0.105:6379 to 172.30.0.101:6379
Adding replica 172.30.0.106:6379 to 172.30.0.101:6379
Adding replica 172.30.0.107:6379 to 172.30.0.102:6379
Adding replica 172.30.0.108:6379 to 172.30.0.102:6379
Adding replica 172.30.0.109:6379 to 172.30.0.103:6379
Adding replica 172.30.0.104:6379 to 172.30.0.103:6379
M: e4f37f8f0ea0dafc584349999795716613910e51 172.30.0.101:6379
slots:[0-5460] (5461 slots) master
M: 5f71983ad52cc7077ce8874ae1c4f9c23d9f502c 172.30.0.102:6379
slots:[5461-10922] (5462 slots) master
M: b3c0a96f6a206088ecea639147b6fcf903afe872 172.30.0.103:6379
slots:[10923-16383] (5461 slots) master
S: 85025819223f12615046c54d89f510e9cd0444a1 172.30.0.104:6379
replicates b3c0a96f6a206088ecea639147b6fcf903afe872
S: 2e5dc211288784ba55d554a377b87bfe2b5398db 172.30.0.105:6379
replicates e4f37f8f0ea0dafc584349999795716613910e51
S: 29f05d98982bd3df05d0222091e4b8ef9569f424 172.30.0.106:6379
replicates e4f37f8f0ea0dafc584349999795716613910e51
S: 3584840ac704c3ee016f3bdcca3f7ebe6f6e8e80 172.30.0.107:6379
replicates 5f71983ad52cc7077ce8874ae1c4f9c23d9f502c
S: 0a889103b35db2a6e82e8c09904bbef310cff3b1 172.30.0.108:6379
replicates 5f71983ad52cc7077ce8874ae1c4f9c23d9f502c
S: 00ba82bed6abeb015116d51d1af7fcb1609d03ad 172.30.0.109:6379
replicates b3c0a96f6a206088ecea639147b6fcf903afe872
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 172.30.0.101:6379)
M: e4f37f8f0ea0dafc584349999795716613910e51 172.30.0.101:6379
slots:[0-5460] (5461 slots) master
2 additional replica(s)
M: 5f71983ad52cc7077ce8874ae1c4f9c23d9f502c 172.30.0.102:6379
slots:[5461-10922] (5462 slots) master
2 additional replica(s)
S: 2e5dc211288784ba55d554a377b87bfe2b5398db 172.30.0.105:6379
slots: (0 slots) slave
replicates e4f37f8f0ea0dafc584349999795716613910e51
S: 0a889103b35db2a6e82e8c09904bbef310cff3b1 172.30.0.108:6379
slots: (0 slots) slave
replicates 5f71983ad52cc7077ce8874ae1c4f9c23d9f502c
S: 3584840ac704c3ee016f3bdcca3f7ebe6f6e8e80 172.30.0.107:6379
slots: (0 slots) slave
replicates 5f71983ad52cc7077ce8874ae1c4f9c23d9f502c
S: 85025819223f12615046c54d89f510e9cd0444a1 172.30.0.104:6379
slots: (0 slots) slave
replicates b3c0a96f6a206088ecea639147b6fcf903afe872
S: 00ba82bed6abeb015116d51d1af7fcb1609d03ad 172.30.0.109:6379
slots: (0 slots) slave
replicates b3c0a96f6a206088ecea639147b6fcf903afe872
S: 29f05d98982bd3df05d0222091e4b8ef9569f424 172.30.0.106:6379
slots: (0 slots) slave
replicates e4f37f8f0ea0dafc584349999795716613910e51
M: b3c0a96f6a206088ecea639147b6fcf903afe872 172.30.0.103:6379
slots:[10923-16383] (5461 slots) master
2 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
- 见到下方的[OK]说明集群建立完成。此时,使用客户端连上集群中的任何一个节点,都相当于连上了整个集群。
- 客户端后面要加上-c选项,否则如果key没有落到当前节点上,是不能操作的。 -c会自动把请求重定向到对应节点。
- 使用cluster nodes可以查看到整个集群的情况。
# redis-cli -h 172.30.0.101 -p 6379 -c
172.30.0.101:6379> CLUSTER nodes
5f71983ad52cc7077ce8874ae1c4f9c23d9f502c 172.30.0.102:6379@16379 master - 0 1682
2e5dc211288784ba55d554a377b87bfe2b5398db 172.30.0.105:6379@16379 slave e4f37f8f0
0a889103b35db2a6e82e8c09904bbef310cff3b1 172.30.0.108:6379@16379 slave 5f71983ad
3584840ac704c3ee016f3bdcca3f7ebe6f6e8e80 172.30.0.107:6379@16379 slave 5f71983ad
85025819223f12615046c54d89f510e9cd0444a1 172.30.0.104:6379@16379 slave b3c0a96f6
e4f37f8f0ea0dafc584349999795716613910e51 172.30.0.101:6379@16379 myself,master -
00ba82bed6abeb015116d51d1af7fcb1609d03ad 172.30.0.109:6379@16379 slave b3c0a96f6
29f05d98982bd3df05d0222091e4b8ef9569f424 172.30.0.106:6379@16379 slave e4f37f8f0
b3c0a96f6a206088ecea639147b6fcf903afe872 172.30.0.103:6379@16379 master - 0 1682
172.30.0.101:6379> set k1 1
-> Redirected to slot [12706] located at 172.30.0.103:6379
OK
172.30.0.103:6379> get k1
"1"
4. 主节点宕机
4.1 主节点宕机效果演示
(1)演示效果:手动停止一个master节点,观察效果。比如上述拓扑结构中,可以看到redis1 redis2 redis3是主节点,随便挑一个停掉。
docker stop redis1
- 连上redis2,观察结果。
172.30.0.102:6379> CLUSTER NODES
e4f37f8f0ea0dafc584349999795716613910e51 172.30.0.101:6379@16379 master,fail - 1
0a889103b35db2a6e82e8c09904bbef310cff3b1 172.30.0.108:6379@16379 slave 5f71983ad
29f05d98982bd3df05d0222091e4b8ef9569f424 172.30.0.106:6379@16379 slave 2e5dc2112
00ba82bed6abeb015116d51d1af7fcb1609d03ad 172.30.0.109:6379@16379 slave b3c0a96f6
5f71983ad52cc7077ce8874ae1c4f9c23d9f502c 172.30.0.102:6379@16379 myself,master -
b3c0a96f6a206088ecea639147b6fcf903afe872 172.30.0.103:6379@16379 master - 0 1682
2e5dc211288784ba55d554a377b87bfe2b5398db 172.30.0.105:6379@16379 master - 0 1682
3584840ac704c3ee016f3bdcca3f7ebe6f6e8e80 172.30.0.107:6379@16379 slave 5f71983ad
85025819223f12615046c54d89f510e9cd0444a1 172.30.0.104:6379@16379 slave b3c0a96f6
可以看到, 101已经提示fail,然后原本是slave的105成了新的master。
- 然后重新启动redis1:
docker start redis1
- 再次观察结果。可以看到101启动了,仍然是slave。
172.30.0.102:6379> CLUSTER NODES
e4f37f8f0ea0dafc584349999795716613910e51 172.30.0.101:6379@16379 slave 2e5dc2112
0a889103b35db2a6e82e8c09904bbef310cff3b1 172.30.0.108:6379@16379 slave 5f71983ad
29f05d98982bd3df05d0222091e4b8ef9569f424 172.30.0.106:6379@16379 slave 2e5dc2112
00ba82bed6abeb015116d51d1af7fcb1609d03ad 172.30.0.109:6379@16379 slave b3c0a96f6
5f71983ad52cc7077ce8874ae1c4f9c23d9f502c 172.30.0.102:6379@16379 myself,master -
b3c0a96f6a206088ecea639147b6fcf903afe872 172.30.0.103:6379@16379 master - 0 1682
2e5dc211288784ba55d554a377b87bfe2b5398db 172.30.0.105:6379@16379 master - 0 1682
3584840ac704c3ee016f3bdcca3f7ebe6f6e8e80 172.30.0.107:6379@16379 slave 5f71983ad
85025819223f12615046c54d89f510e9cd0444a1 172.30.0.104:6379@16379 slave b3c0a96f6
可以使用cluster failover 进行集群恢复。也就是把101重新设定成master(登录到101上执行)。
4.2 故障处理流程
(1)故障判定:集群中的所有节点,都会周期性的使用心跳包进行通信。
- 节点A给节点B发送ping包,B就会给A返回一个pong包。ping和pong除了message type属性之外,其他部分都是一样的。这里包含了集群的配置信息(该节点的id,该节点从属于哪个分片,是主节点还是从节点,从属于谁,持有哪些slots的位图…
- 每个节点,每秒钟,都会给一些随机的节点发起ping包,而不是全发一遍。这样设定是为了避免在节点很多的时候,心跳包也非常多(比如有9个节点,如果全发,就是9* 8有72组心跳了,而且这是按照N^2这样的级别增长的)。
- 当节点A给节点B发起ping包,B不能如期回应的时候,此时A就会尝试重置和B的tcp连接,看能否连接成功。如果仍然连接失败,A就会把B设为PFAIL状态(相当于主观下线)。
- A判定B为PFAIL之后,会通过redis内置的Gossip协议,和其他节点进行沟通,向其他节点确认B的状态(每个节点都会维护一个自己的"下线列表",由于视角不同,每个节点的下线列表也不一定相同)。
- 此时A发现其他很多节点,也认为B为PFAIL,并且数目超过总集群个数的一半,那么A就会把B标记成FAIL (相当于客观下线),并且把这个消息同步给其他节点(其他节点收到之后,也会把B标记成FAIL)。至此,B就彻底被判定为故障节点了。
(2)某个或者某些节点宕机,有的时候会引起整个集群都宕机(称为fail状态)。以下三种情况会出现集群宕机:
- 某个分片,所有的主节点和从节点都挂了。
- 某个分片,主节点挂了,但是没有从节点。
- 超过半数的master节点都挂了。
(3)故障迁移:上述例子中,B故障,并且A把B FAIL的消息告知集群中的其他节点。如果B是从节点,那么不需要进行故障迁移.如果B是主节点,那么就会由B的从节点(比如C和D)触发故障迁移了。所谓故障迁移,就是指把从节点提拔成主节点,继续给整个redis集群提供支持.具体流程如下:
- 从节点判定自己是否具有参选资格。如果从节点和主节点已经太久没通信(此时认为从节点的数据和主节点差异太大了),时间超过阈值,就失去竞选资格。
- 具有资格的节点,比如C和D,就会先休眠一定时间。休眠时间= 500ms基础时间+ [0, 500ms]随机时间+排名* 1000ms。offset的值越大,则排名越靠前(越小)。
- 比如C的休眠时间到了,C就会给其他所有集群中的节点,进行拉票操作。但是只有主节点才有投票资格。
- 主节点就会把自己的票投给C (每个主节点只有1票)。当C收到的票数超过主节点数目的一半,C就会晋升成主节点(C自己负责执行slaveof no one,并且让D执行slaveof C)。
- 同时C还会把自己成为主节点的消息,同步给其他集群的节点。大家也都会更新自己保存的集群结构信息。
(4)上述选举的过程, 称为Raft算法,是一种在分布式系统中广 泛使用的算法。在随机休眠时间的加持下,基本上就是谁先唤醒,谁就能竞选成功。
5. 集群扩容
(1)扩容是一个在开发中比较常遇到的场景。随着业务的发展,现有集群很可能无法容纳日益增长的数据。此时给集群中加入更多新的机器,就可以使存储的空间更大了。所谓分布式的本质,就是使用更多的机器,引入更多的硬件资源。
(2)第一步:把新的主节点加入到集群。上面已经把redis1 - redis9重新构成了集群。接下来把redis10和redis11也加入集群。此处我们把redis10作为主机,redis11作为从机。
redis-cli --cluster add-node 172.30.0.110:6379 172.30.0.101:6379
- add-node后的第一组地址是新节点的地址。第二组地址是集群中的任意节点地址。执行结果:
>>> Adding node 172.30.0.110:6379 to cluster 172.30.0.101:6379
>>> Performing Cluster Check (using node 172.30.0.101:6379)
M: 00d319e23ef76a4d51e74600c42ee2a371ae81f6 172.30.0.101:6379
slots:[0-5460] (5461 slots) master
2 additional replica(s)
S: e34911c57d7605903de84ec05b3deac611aaef7e 172.30.0.105:6379
slots: (0 slots) slave
replicates 00d319e23ef76a4d51e74600c42ee2a371ae81f6
S: 6cf48cc11d0171b6ab1b418808473167acd7986e 172.30.0.106:6379
slots: (0 slots) slave
replicates 00d319e23ef76a4d51e74600c42ee2a371ae81f6
S: fd18c7f164b09ec563f4573ec9d6466e6769221e 172.30.0.108:6379
slots: (0 slots) slave
replicates b3f2ba758318f4bd54031c98c01d7a6155ff43d3
M: 579282abe81b3f20ffd17d5a1956cdca3b0e71b0 172.30.0.103:6379
slots:[10923-16383] (5461 slots) master
2 additional replica(s)
S: e9ea79b1326ea5a75a1701d5c12a0f6081c1d043 172.30.0.109:6379
slots: (0 slots) slave
replicates 579282abe81b3f20ffd17d5a1956cdca3b0e71b0
S: 628d1ec9ceef6760b9038c4fbc83ee92430062ac 172.30.0.107:6379
slots: (0 slots) slave
replicates b3f2ba758318f4bd54031c98c01d7a6155ff43d3
M: b3f2ba758318f4bd54031c98c01d7a6155ff43d3 172.30.0.102:6379
slots:[5461-10922] (5462 slots) master
2 additional replica(s)
S: 2a248acb47f0036655397897f9800c70ea22514f 172.30.0.104:6379
slots: (0 slots) slave
replicates 579282abe81b3f20ffd17d5a1956cdca3b0e71b0
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
>>> Send CLUSTER MEET to node 172.30.0.110:6379 to make it join the cluster.
[OK] New node added correctly.
- 此时的集群状态如下,可以看到172.30.0.110这个节点已经成为了集群中的主节点:
127.0.0.1:6379> CLUSTER NODES
b3f2ba758318f4bd54031c98c01d7a6155ff43d3 172.30.0.102:6379@16379 master - 0 1683
e34911c57d7605903de84ec05b3deac611aaef7e 172.30.0.105:6379@16379 slave 00d319e23
522a1bd88a1a9084e6919fa88f4bf1c3655ad837 172.30.0.110:6379@16379 master - 0 1683
579282abe81b3f20ffd17d5a1956cdca3b0e71b0 172.30.0.103:6379@16379 master - 0 1683
00d319e23ef76a4d51e74600c42ee2a371ae81f6 172.30.0.101:6379@16379 master - 0 1683
6cf48cc11d0171b6ab1b418808473167acd7986e 172.30.0.106:6379@16379 slave 00d319e23
e9ea79b1326ea5a75a1701d5c12a0f6081c1d043 172.30.0.109:6379@16379 myself,slave 57
fd18c7f164b09ec563f4573ec9d6466e6769221e 172.30.0.108:6379@16379 slave b3f2ba758
2a248acb47f0036655397897f9800c70ea22514f 172.30.0.104:6379@16379 slave 579282abe
628d1ec9ceef6760b9038c4fbc83ee92430062ac 172.30.0.107:6379@16379 slave b3f2ba758
(3)第二步:重新分配slots。
redis-cli --cluster reshard 172.30.0.101:6379
- reshard后的地址是集群中的任意节点地址。另外,注意单词拼写,是reshard (重新切分),不是reshared (重新分享) ,不要多写个e。执行之后,会进入交互式操作,redis会提示用户输入以下内容:
- 多少个slots要进行reshard ? (此处我们填写4096)。
- 哪个节点来接收这些slots ? (此处我们填写172.30.0.110这个节点的集群节点id)。
- 这些slots从哪些节点搬运过来? (此处我们填写all,表示从其他所有的节点都进行搬运)。
- 执行结果如下:
How many slots do you want to move (from 1 to 16384)? 4096
What is the receiving node ID? 522a1bd88a1a9084e6919fa88f4bf1c3655ad837
Please enter all the source node IDs.
Type 'all' to use all the nodes as source nodes for the hash slots.
Type 'done' once you entered all the source nodes IDs.
Source node #1: all
- 确定之后,会初步打印出搬运方案,让用户确认。之后就会进行集群的key搬运工作。这个过程涉及到数据搬运。可能需要消耗一定的时间。在搬运key的过程中,对于那些不需要搬运的key,访问的时候是没有任何问题的。但是对于需要搬运的key,进行访问可能会出现短暂的访问错误(key的位置出现了变化)。随着搬运完成,这样的错误自然就恢复了。
(4)第三步:给新的主节点添加从节点。
- 光有主节点了,此时扩容的目标已经初步达成。但是为了保证集群可用性,还需要给这个新的主节点添加从节点,保证该主节点宕机之后,有从节点能够顶上。
redis-cli --cluster add-node 172.30.0.111:6379 172.30.0.101:6379 --cluster-slave
执行完毕后,从节点就已经被添加完成了。