目录
1. 基本概念
哨兵模式提高了系统的可用性,但是真正存储数据的还是 master 和 slave 节点,所有的数据都需要存储在单个 master 和 slave 节点中,如果数据量很大,超出了 master 和 slave 所在机器的物理内存,就可能会出现严重问题,而 Redis 集群就是引入多组 master 和 slave 节点,每一组 master 和 slave 存储全部数据的一部分,从而构成一个更大的整体,也就是 Redis 集群
假设整个数据量是 1TB,引入三组 master / slave 来存储,那么每一组机器只需要存储整个数据量的 1 / 3 即可
master1 和 slave11 和 slave12 保存同样的数据,占总数据的 1 / 3
master2 和 slave21 和 slave22 保存同样的数据,占总数据的 1 / 3
master3 和 slave31 和 slave32 保存同样的数据,占总数据的 1 / 3
每个 slave 都是对应 master 的备份,如果 master 挂了,对应的 slave 会变成 master,三组机器对应三个分片,如果数据量进一步增加,只需要增加更多的分片即可解决问题
2. 数据分片算法
Redis cluster 的核心思路是用多组机器来存储数据的每个部分,所以只需要确定一个数据(一个具体的 key)应该存储在哪个分片上,读取的时候在哪个分片上读取就可以了
2.1 哈希求余
假设有 N 个分片,使用 [0,N-1] 这样的序号进行编号,针对某个给定的 key,先计算其 hash 值,只把得到的结果 %N,得到的结果就是分片编号
例如,N 为3,key 为 hello,计算 hello 的哈希值(使用 md5 算法),得到的结果为 bc4b2a76b9719d91,再把这个结果 %3(hash(hello) % 3),结果为 0,那么就把 hello 这个 key 放到 0 号分片上,后续如果要取某个 key 的记录,就是针对 key 计算 hash,再对 N 求余即可
优点:简单高效,数据分配均匀
缺点:一旦需要进行扩容,N 改变了,原有的规则都会被打破,搬运的数据量是非常多的,开销比较大
上述只是在 3 个分片的基础上增加了一个分片,大量的数据都经过了搬运,因此开销是非常之大
2.2 一致性哈希算法
一致性哈希算法是为了降低搬运的开销,能够更高效的扩容,key 映射到分片的过程不再是简单求余了,而是改成下面的过程
1)把 0 - 2^32-1 这个数据空间,映射到一个圆环上,数据按照顺时针方向增长
2)假设当前存在三个分片,就把分片放到圆环的某个位置上
3)当有一个 key 计算到的 hash 值为 H,就从 H 所在的为止顺时针往下找,找到第一个分片,就是这个 key 所属的分片
N 个分片的位置把整个圆环分成了 N 个管辖区域,key 的 hash 值落在某个区域,就归哪个区域管理
当分片进行扩容时,原有的分片在环上的位置不变,只需要在环上新安排一个分片的位置即可
只需要把0号分片上的部分数据搬运给3号分片即可,其他分片管理的区域不变
优点:降低了扩容时数据搬运的规模,提高了扩容操作的效率
缺点:数据分配不均匀
2.3 哈希槽分区算法(Redis 使用)
为了解决搬运成本高和数据分配不均匀的问题,Redis cluster 引入了哈希槽(hahs slots)算法
hash_slot = crc16(key) % 16384
相当于把整个哈希值映射到 16384(也就是2^14)个槽位上,也就是[0,16383],然后把这些槽位比较均匀的分配给每个分片,每个分片的节点都需要记录自己持有哪些分片
假设当前有三个分片:
0 号分片:[0,5461],共 5462 个槽位
1 号分片:[5462,10923],共 5462 个槽位
2 号分片:[10924,16383],共 5460 个槽位
每个分片的节点使用位图来表示自己持有哪些槽位,对于 16384 个槽位来说,需要 2048 个字节(2 kb)大小的内存空间表示
如果需要进行扩容,比如新增一个 3 号分片,就针对原有的槽位进行重新分配,一种可能的分配方式如下:
0 号分片:[0,4095],共 4096 个槽位
1 号分片:[5462,9557],共 4096 个槽位
2 号分片:[10924,15019],共 4096 个槽位
3 号分片:[4096,5461] + [9558,10923] + [15019,16383],共 4096 个槽位
分片的规则很灵活,每个分片所持有的槽位不一定连续,在实际使用 Redis 集群时,不需要手动指定哪些槽位分配给某个分片,只需要告诉某个分片应该持有多少槽位即可,Redis 会自动完成后续的槽位分配,以及对应的 key 搬运的工作
问题一:Redis 集群最多有 16384 个分片吗?
如果有1.6w个分片,整个数据服务器的集群规模是非常庞大的,一个系统越复杂,出现故障的概率越高,实际上 Redis 的作者建议集群分片数不应该超过 1000
问题二:为什么是 16384 个槽位?
1)节点之间通过心跳包通信,心跳包中包含了该节点持有哪些 slots(槽位),并且使用位图这样的数据结构来表示,16384(16k)个 slots,需要的位图大小为 2kb,如果给定的 slots 数更多了,比如 65536 个,此时就需要消耗更多的空间,8kb位图来表示,8kb对于在频繁的网络心跳包中是一个不小的开销
2)Redis 集群一般不建议超过 1000 个分片,所以 16k 对于最大 1000 个分片来说足够使用
3. 集群搭建
基于 docker 搭建一个集群,每个节点都是一个容器,拓扑结构如下:
1)创建目录和配置
创建 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.confcat << 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
├── redis2
│ └── redis.conf
├── redis3
│ └── redis.conf
├── redis4
│ └── redis.conf
├── redis5
│ └── redis.conf
├── redis6
│ └── redis.conf
├── redis7
│ └── redis.conf
├── redis8
│ └── redis.conf
└── redis9
└── redis.conf├── redis10
│ └── redis.conf
├── redis11
│ └── 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
2)编写 docker-compose.yml
先编写 networks,并分配网段 172.30.0.0/24
配置每个节点,注意配置文件映射,端口映射以及容器的 ip 地址,设定成固定 ip 方便后续的观察和操作
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
3-11 的配置类似
3)启动容器
docker-compose up -d
4)构建集群
启动一个 docker 客户端,此处是把前面 9 个主机构建成集群,3 主 6 从
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:6379Adding 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 构建集群, 出现[OK]说明集群构建完成
此时,使用客户端连上集群中的任何一个节点,就相当于连上了整个集群,客户端后面要加上 -c 选项,否则如果没有 key 落到当前节点上,是不呢个操作的, -c 会自动把请求重定向在对应的节点上
redis-cli -h 172.30.0.101 -p 6379 -c
4. 主节点宕机
此处和哨兵模式的主节点宕机一样,主节点挂了,会在属于该主节点的从节点中选出一个从节点作为主节点,当挂了的主节点连接之后,会成为从节点
可以使用 cluster failover 进行集群恢复,把挂了的主节点重新设置成 master
4.1 故障判定
在集群中的所有结点,都会周期性的使用心跳包进行通信
1)节点 A 给节点 B 发送 ping 包,B就会给 A 返回 pong 包
2)每个节点每秒钟都会给一些随机的节点发起 ping 包,不是全发一遍,这样设定是为了避免在节点过多的时候,心跳包也很多
3)当节点 A 给节点 B 发起 ping 包,B 不能如期回应,此时 A 就会尝试重置和 B 的 tcp 连接,看能否连接成功,如果不能,A 就会把 B 设成 PFALL 状态(相当于主观下线)
4)A 判定 B 为 PFALL 之后,会通过 redis 内置的 Gossip 协议,和其他节点进行沟通,向其他节点确认 B 的状态
5)此时 A 发现其他很多节点也认为 B 为 PFAIL,并且数目超过总集群个数的⼀半,那么 A 就会把B 标记成 FAIL (相当于客观下线),并且把这个消息同步给其他节点
此时 B 就彻底被判定为故障节点了
以下三种情况会出现集群宕机
某个分片所有的主节点和从节点都挂了
某个分片主节点挂了,但是没有从节点
超过半数的 master 节点都挂了
4.2 故障迁移
上述中 B 故障,并且 A 把 B FALL 的消息告知集群中的其他节点,如果 B 是从节点,那么不需要进行故障迁移,B 是主节点,就会由 B 的从节点(例如 C 和 D)触发故障迁移,所谓的故障迁移就是把从节点提升为主节点
具体流程:
1)从节点判定自己是否有参选资格,如果从节点和主节点已经太久没有通信,那么从节点的数据和主节点差异就会很大,就失去资格
2)具有资格的节点,例如 C 和 D,就会休眠一定的时间,休眠时间 = 500ms 基础时间 + [0,500ms] 随机时间 + 排名 * 1000ms,offset 的值越大,则排名越靠前
3)例如 C 休眠的时间到了,C 就会给其他所有集群中的节点,进行拉票操作,只有主节点才能投票
4)直接点把自己的票投给 C,当 C 超过主节点数目一般的票数,C 晋升为主节点
5)C 会把自己成为主节点的消息,同步给其他集群的节点,大家也会更新自己保存的集群结构信息