5. 集群
5.1 引入
广义上的集群:只要是多台机器,构成了分布式系统,都可以称为是一个 "集群"。
狭义上的集群:redis 的集群模式,该模式主要是解决,存储空间不足的问题,即 拓展存储空间。
前面的哨兵每一个节点都存储了整个数据,存储的范围有限。
redis 的集群模式,即 完整的数据,每个机器(每个机器还要配从节点)分别存一部分,这样能存储的数据量就提升了。
5.2 数据划分方式
如果数据被划分到 3 套存储机器(假定一主二从)上,那么这里的一套机器及数据称为一个分片。
主流分片方式有三种:
5.2.1 哈希求余
原理类似于散列表,假定有三个分片,编号 0,1,2。
对要插入的数据的 key (redis 都是键值对数据结构)计算hash 值(例如使用 md5),然后将值对 3 取余,余数即为该 key 所在的分片。
md5 是一个广泛使用的 hash 算法,其结果是一串 16进制 的数字。
特点:
- 结果定长 ------ 无论原字符串有多长,结果都一样长。
- 计算结果分散 ------ 字符串即使差别很小,对应结果也相差甚远
- 计算结果不可逆 ------ 即通过 md5 得到结果后,很难通过结果还原原始字符串,因而可以用于加密
缺点 :扩容成本高。
现实中随着业务的增长,数据也会变多,原先的片数不足以支持,就要增加分片,扩大容量。
但是分片数增加了,之前数据的 哈希求余 的结果就会改变,因而就要对所有的 key 重新计算,重新分配。

就像上图这样(一共 20 个 key),扩容后只有三个 key 所在分片不变,其余全部都要搬运。
如果有 20 亿数据,就要搬运 17 亿数据,这开销可不小。 现实中如果真有这样的扩容,往往不能在生产环境中直接操作,只能再搞 4 台(以上图为例),从这三台中迁移数据,而不是这三台之间相互搬运。迁移完成,用新的这四台,替换旧的那三台。
但是这样,就要更多的机器,这些机器都要再提前配置,同样成本不低。
5.2.2 一致性哈希算法
前面的哈希求余,key 属于哪个分片是交替的,而一致性哈希中,key 属于哪个分片是连续的。
将 0 - 2^32-1 这个数据空间,映射到圆环上,数值顺时针递增。

假设有 三 个分片(编号 0,1,2),将分片放到圆环的某个位置上(尽量将圆等分)

key 通过 hash 函数计算得到 hash 值后,从该值于圆上所在位置,顺时针找到的第一分片,就是 key 所属分片。

扩容时,原有分片位置不动,只要在环上,新添一个分片即可。如下:

这样扩容时,1 & 2 分片上的数据,不用动,只要动 0 号 分片是即可。
一致性哈希
优点:大大降低扩容时,搬运数据的规模,提高了扩容操作的效率
缺点:数据分配不均,一些机器上数据多,一些机器上数据少。
5.2.3 哈希槽分区算法
该算法是 redis 采用的分片算法,本质是前两种的结合。
bash
hash_slot = crc16(key) % 16384
- crc16 是一种 hash 算法
- 16384 = 16 * 1024 即 2 ^ 14
把 hash 值映射到 16384 个槽位上( 【0,16383】),然后将这些槽位比较均匀的分配给每一个分片,分片都有一个 16384 个bit 位 的位图(2048 个字节,即 2 KB 大小),用于记录自己持有哪些槽位。
假定 三 分片,一种可能的分片方式
0号:【0,5461】 共 5462 个槽位
1号:【5462.10923】共 5462 个槽位
2号:【10924,16383】共 5460个槽位
注:分片持有的槽位不一定要连续
需要扩容时,需要将原有槽位进行重新分配即可,例如:每个分片都拿出一部分给新片。
相关问题
- 实际中 redis 集群最大有 16384 个分片吗?
- 如果一个分片一个槽位,那么数据很难均匀分布,redis 作者建议分片数超过 1000 个。
- 为什么是 16384 个槽位?
- 节点间通过心跳包通信, 里面包含了该节点持有哪些槽位(位图),16384(16k) 个槽位,位图大小位 2KB;如果 槽位更多如 65536, 就是 8 KB 大小了;虽然对内存来说不算什么,但是在心跳包的网络通信中是个不小的开销
- redis 集群一般不超过 1000 个分片,16384(16k)对于 1000 个分片来说够用了,同时对应位图的体积也不至于很大
5.3 基于 docker 的集群搭建
由于只有一台服务器,所以用 docker 来进行集群搭建,每一个节点都是一个容器。
要部署的集群结构如下:

一共 11 个 redis 节点,9 个用于集群搭建,剩下 2 个用于演示扩容。
记得将之前启动的 redis 容器停掉。
5.3.1 前置准备
目录结构
bash
redis-cluster/
├── docker-compose.yml
└── generate.sh
创建对应文件夹和文件,
bash
mkdir redis-cluster
cd redis-cluster/
touch generate.sh
touch docker-compose.yml
linux上,这个 .sh 文件称为 "shell 脚本"。
linux 是通过一些命令进行操作的,这就可以将命令写到一个文件里,进行批量执行,同时还能加入 条件,循环,函数 等机制。这个就是 shell 脚本。
要创建的 11 个 redis 节点,这些 redis 的配置文件内容,大同小异,所以就可以用脚本来批量生成。
shell 文件内容
bash
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
for port in $(seq 1 9); \类似于 java 中的 for each 循环,seq是 linux 中的一个命令,(seq 1 9)即,生成 【1,9】

所以,这里就是 port 从 1 到 9 循环 9 次。
\是 续行符,就是将下一行内容,和当前行合并。因为 shell 默认情况,要求所有代码都写在一行,但这写起来太难受了,所有用\进行续行。
do & done shell 中用 {} 表示变量,不是表示代码块,对于 for,用 do 和 done 表示代码块的开始和结束。
redis${port} shell 中拼接字符串直接写在一起,不需要用 + 拼接。
bash
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
cluster-enabled yes开启集群模式cluster-config-file nodes.conf不用手动写,redis 自动生成,后续启动节点后,会配置一些 redis 集群信息cluster-node-timeout 5000超时时间cluster-announce-ip 172.30.0.10${port}用来告诉集群其他 节点 到哪个 ip 找自己cluster-announce-port 6379节点自己的业务端口cluster-announce-bus-port 16379节点自己的总线端口 。集群管理的信息交互就是通过该端口,也称 管理端口。
业务端口:用于业务数据通信,响应 redis 客户端请求
管理端口:用于一些完成管理任务的通信,例如:某分片的主节点挂了,要让从节点晋升,就要通过该端口进行操作
这里的 ip ,port 就是 redis 节点在 docker 容器中的 ip,port(注:要在 yml 中配置对应静态)
运行结果
bash
bash generate.sh

docker-compose 文件配置
yml
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
yml 文件内容如上。
内容字段讲解
yml
networks:
mynet:
ipam:
config:
- subnet: 172.30.0.0/24
这个是为这个局域网分配了一个网段,方便后续创建静态 ip 和前面对应。
172.30.0.0/24 24 指子网掩码左边 24 位是 1, 右边 8 位为 0,即 255.255.255.0。8 位主机位,意味着除了 172.30.0.0 网络地址,172.30.0.255 广播地址,一共可以给 254 个主机分配地址。
yml
networks:
mynet:
ipv4_address: 172.30.0.111
容器在网段中的地址。
启动容器,docker-compose up -d 。

5.3.2 构建集群
bash
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 表示每个主节点有两个从节点

运行之后,它会告诉你接下来集群中 槽位的分配方式,和节点间主从关系。但是只有 yes 之后,才会真正执行。
集群搭建好后,这九个节点就是一个整体了,连上其中任意一个就相当于连上集群。
此前,配置了静态 ip 和端口映射,因而采用下面任意一种连接方式均可
bash
#方式一
redis-cli -h 172.30.0.101 -p 6379
#方式二
redis-cli -p 6379
使用 cluster nodes 命令即可查看当前集群的相关信息

前面的一串是节点的身份标识,后面的一串是从节点的主节点的身份标识,主节点是 0。 在 master 节点后还会显示持有的 槽位。
这时候还不能直接在里面操作数据,就像下图

如果 当前的 key 不属于该分片,就会提示应该在哪个分片。
解决措施:在启动的时候,加上 -c 选项,redis 客户端就会根据 key 所属槽位,自动匹配分片,并且换到对应客户端。

5.4 故障转移
集群中出现节点挂了的情况,怎么办?
如果是从节点,则没有什么问题。
如果是主节点,就要进行处理,因为只有主节点才能处理写操作。在从节点上写,redis 会自动将 写 重定向到主节点,并不是 从节点真的能写了。
关掉主节点 redis1 ,模拟挂了的情况。发现 集群中 redis1 的信息显示为 fail,而从节点 172.30.0.106 成为了新的主节点。

这里的故障迁移和哨兵不太一样。
具体流程
-
故障判定------判定是否真挂了
- 节点间会发送心跳包,包里面除了
message type属性,其他部分都是一样的。里面含有集群的配置信息(该节点的 id,节点属于哪个分片,是主还是从,从的话又从属于谁,持有的 槽位 的位图) - 每个节点,每秒都会随机给一些节点发送 ping 包,不是全发,因为全发的话太多了,对网络压力不小。这里有 九个节点,全发就要 8 * 9 = 72 组了。
- 假定 A,B 两个节点,当 A 给 B 发送 ping包 后,B 一直没有回应;A 就会尝试重置和 B 的 TCP 连接。如果仍连接失败,就会将 B 标记为 PFAIL
- A 认为 B fail 后,就会通过 redis 内置的 gossip 协议 ,和其他节点交流,从其他节点处确定 B 的状态 (每个节点都维护的有 "下线列表",不同节点的 "下线列表" 可能不同)。
- 如果 A 发现集群中超一半节点,认为 B "PFAIL" ,就会将 B 标记为 "FAIL ",并通知其他节点,其他节点就会将 B 标为 FAIL
- 节点间会发送心跳包,包里面除了
-
故障迁移
- 如果 B 是从节点,就不需要进行故障迁移,如果是主节点就会触发故障迁移。
- 从节点判定自己是否有资格参选,如果主从节点之间太久没有通信(从节点数据和主节点数据相差太多),时间超过阈值,就失去竞选资格。
- 具有资格的从节点,就会先休眠一定时间(时间 = 500ms 基础时间 + 【0,500】随机时间 + 排名*1000ms),offset 越大,排名越靠前,时间就短。
- 如果有从节点醒了,其就会通知集群中其他节点进行拉票,但是只有主节点才有票资格,且每个只有一票
- 当票数超过主节点数目的一半,该节点就会晋升为主节点(该节点自己负责执行 slaveof no one,并让没竞选上的从节点更改从属)
- 同时,C 还会将自己成为 主节点的消息,同步给集群中其他各个节点,其他节点就会更改自己保存的集群信息。
重启 redis1 节点,该节点就会以从节点的方式加入集群。

集群会宕机的情况
- 某分片的主从节点都挂了
- 某分片主节点挂了,但是没有从节点
- 超半数主节点挂了
5.5 集群扩容
新节点加入集群
bash
redis-cli --cluster add-node 172.30.0.110:6379 172.30.0.101:6379
前一个是新节点地址,后一个集群中任意节点的地址。

进入任意一个 redis 客户端,可以看到该节点已成功加入集群中。

重新分配 slots(槽位)
bash
redis-cli --cluster reshard 172.30.0.101:6379
注意是 reshard 切分 ,不是 reshared 重新分享 ,不要多写 e。
reshard 后面写 集群中任意一节点的地址。
输入命令之后,如下:

首先,它会问你要移动多少个槽位,这里我们要均分,就填 4096。
然后,问你要将这些槽位移动到哪个节点,这里我们填写刚加入的节点 的 id。
最后,会问你这些槽位你准备从哪些节点节点中取,这里我们填 all,即从其他所有节点中取。
这之后,它会输出将要进行操作,yes 同意即可。

接入集群,可以看到新节点已经有槽位了

新节点添加从节点
bash
redis-cli --cluster add-node 172.30.0.111:6379 172.30.0.101:6379 --cluster-slave --cluster-master-id [172.30.1.110 节点的 nodeId]
可以看到已经成功加入

5.6 集群缩容
实际中一般都是扩容,很少有缩容,所以了解即可。
一、 删除从节点
bash
redis-cli --cluster del-node [集群中任⼀节点ip:port] [要删除的从机节点 nodeId]

二、重新分配 slots
要删除的主节点,有 4096 个 slots,就将这些槽位分成三份分给其他三个主节点(1365 + 1365 + 1366),多一两个,少一两个槽位无所谓。
和前面分配槽位类似,这里只展示一个的处理。

三、删除主节点
bash
redis-cli --cluster del-node [集群中任⼀节点ip:port] [要删除的从机节点 nodeId]