目录
集群
1.三种分片算法
1.哈希求余算法
2.一致性哈希算法
3.哈希槽分区算法
2.搭建集群环境
3.集群故障处理
4.集群扩容
集群
上篇文章我们了解Redis哨兵的相关操作,使用哨兵只是解决了主节点瘫痪,从节点不能自动变为主节点的问题,并没有解决“单点问题”,(写操作只能由主节点完成)。 为了能存储更多的数据,分担主从节点的读/写压力,于是Redis引入了集群的相关操作, 引⼊多组 Master / Slave ,每⼀组 Master / Slave 存储数据全集的 ⼀部分,从⽽构成⼀个更⼤的整体,称为 Redis 集群 (Cluster)。
1.三种分片算法
如上图所示,在把全集数据平均分成多个部分,交给多个Redis主从服务器存储数据,这就是Redis集群,其中每个master与其对应的slave保存的是同样的数据,占总数据的1/3。
每个 Slave 都是对应 Master 的备份(当 Master 挂了, 对应的 Slave 会补位成 Master)。每个Redis主节点与其对应的从节点称为是⼀个 分⽚ (Sharding)。如果全量数据进⼀步增加, 只要再增加更多的分⽚, 即可解决。我们需要把全量数据尽可能的平均分给每个分片,以免造成Redis服务器压力不均衡,这个时候就需要用到分片算法了。Redis在设计时使用的便是哈希槽分区算法。
1.哈希求余算法
例如, N 为 3. 给定 key 为 hello, 对 hello 计算 hash 值(⽐如使⽤ md5 算法), 得到的结果为bc4b2a76b9719d91 , 再把这个结果 % 3, 结果为 0, 那么就把 hello 这个 key 放到 0 号分片上。
后续如果要取某个 key 的记录, 也是针对 key 计算 hash , 再对 N 求余, 就可以找到对应的分⽚编号了。
2.一致性哈希算法
- 第⼀步, 把 0 -> 2^32-1 这个数据空间, 映射到⼀个圆环上. 数据按照顺时针方向增⻓.
- 第⼆步, 假设当前存在三个分⽚, 就把分⽚放到圆环的某个位置上.
- 第三步, 假定有⼀个 key, 计算得到 hash 值 H, 那么这个 key 映射到哪个分⽚呢? 规则很简单, 就是从 H所在位置, 顺时针往下找, 找到的第⼀个分⽚, 即为该 key 所从属的分片.
这就相当于, N 个分⽚的位置, 把整个圆环分成了 N 个管辖区间. Key 的 hash 值落在某个区间内, 就归对应区间管理.
在这种情况下,扩容只需要原有分片在环上的位置不动, 只要在环上新安排⼀个分⽚位置即可。
3.哈希槽分区算法
hash_slot = crc16(key) % 16384
crc16是一种哈希算法,16384其实就是2^14。
- 0 号分⽚: [0, 5461], 共 5462 个槽位
- 1 号分⽚: [5462, 10923], 共 5462 个槽位
- 2 号分⽚: [10924, 16383], 共 5460 个槽位
这⾥的分⽚规则是很灵活的. 每个分⽚持有的槽位也不⼀定连续.每个分⽚的节点使⽤ 位图 来表⽰⾃⼰持有哪些槽位. 对于 16384 个槽位来说, 需要 2048 个字节(2KB) 大小的内存空间表⽰.
如果需要进⾏扩容, ⽐如新增⼀个 3 号分⽚, 就可以针对原有的槽位进⾏重新分配。
比如可以把之前每个分⽚持有的槽位, 各拿出⼀点, 分给新分片。
⼀种可能的分配方式:
- 0 号分⽚: [0, 4095], 共 4096 个槽位
- 1 号分⽚: [5462, 9557], 共 4096 个槽位
- 2 号分⽚: [10924, 15019], 共 4096 个槽位
- 3 号分⽚: [4096, 5461] + [9558, 10923] + [15019, 16383], 共 4096 个槽位
2.搭建集群环境
接下来给大家教一种基于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.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
配置说明:
- 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 节点⾃⾝的总线端⼝. 集群管理的信息交互是通过这个端⼝进⾏的.
2.编写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
3.启动容器
使用docker-compose up -d命令启动容器。
4.构建集群
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 表⽰每个主节点需要两个从节点备份
执⾏之后, 容器之间会进⾏加⼊集群操作.
- 客⼾端后⾯要加上 -c 选项, 否则如果 key 没有落到当前节点上, 是不能操作的. -c 会⾃动把请求重定向到对应节点.
- 使⽤ cluster nodes 可以查看到整个集群的情况。
3.集群故障处理
故障判定
集群中的所有节点, 都会周期性的使⽤⼼跳包进⾏通信。
- 节点 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 就彻底被判定为故障节点了.
某个或者某些节点宕机, 有的时候会引起整个集群都宕机 (称为 fail 状态).以下三种情况会出现集群宕机:
- 某个分⽚, 所有的主节点和从节点都挂了.
- 某个分⽚, 主节点挂了, 但是没有从节点.
- 超过半数的 master 节点都挂了.
故障迁移
一个节点A发生了故障,如果A是从节点不会发生故障迁移,但是如果A是主节点,那么便会触发故障迁移,所谓故障迁移,就是指把从节点提拔成主节点,继续给整个 redis 集群提供⽀持。
- 从节点判定⾃⼰是否具有参选资格. 如果从节点和主节点已经太久没通信(此时认为从节点的数据和主节点差异太⼤了), 时间超过阈值, 就失去竞选资格.
- 具有资格的节点, ⽐如 C 和 D, 就会先休眠⼀定时间. 休眠时间 = 500ms 基础时间 + [0, 500ms] 随机时间 + 排名 * 1000ms. offset 的值越⼤, 则排名越靠前(越⼩).
- 比如 C 的休眠时间到了, C 就会给其他所有集群中的节点, 进⾏拉票操作. 但是只有主节点才有投票资格.
- 主节点就会把⾃⼰的票投给 C (每个主节点只有 1 票). 当 C 收到的票数超过主节点数⽬的⼀半, C 就会晋升成主节点. (C ⾃⼰负责执⾏ slaveof no one, 并且让 D 执⾏ slaveof C).
- 同时, C 还会把⾃⼰成为主节点的消息, 同步给其他集群的节点. ⼤家也都会更新⾃⼰保存的集群结构信息.
上述选举过程被称为Raft算法,是一种在分布式系统中广泛使用的算法。在随机休眠时间的加持下, 基本上就是谁先唤醒, 谁就能竞选成功。
4.集群扩容
所谓分布式的本质, 就是使⽤更多的机器, 引⼊更多的硬件资源。
集群扩容我们可以分为三个步骤:
第一步:把新的主节点加⼊到集群
redis-cli --cluster add-node 172.30.0.110:6379 172.30.0.101:6379
redis-cli --cluster reshard 172.30.0.101:6379
- 多少个 slots 要进⾏ reshard ? (此处我们填写 4096)
- 哪个节点来接收这些 slots ? (此处我们填写 172.30.0.110 这个节点的集群节点 id)
- 这些 slots 从哪些节点搬运过来? (此处我们填写 all, 表示从其他所有的节点都进行搬运)
第三步: 给新的主节点添加从节点
redis-cli --cluster add-node 172.30.0.111:6379 172.30.0.101:6379 --clusterslave --cluster-master-id [172.30.1.110 节点的 nodeId]
❤️😍😍😍😍😍😍😍😍😍😍😍😍😍😍😍😍😍
🍔我是小皮侠,谢谢大家都能看到这里!!
🦚主页已更新Java基础内容,数据结构基础,数据库,算法,Redis相关内容。
🚕未来会更新Java项目,SpringBoot,docker,mq,微服务以及各种Java路线会用到的技术。
🎃求点赞!求收藏!求评论!求关注!
🤷♀️谢谢大家!!!!!!!!