目录
集群
广义上的集群,只要是多个机器,构成了分布式系统,都可以称为是一个"集群".前面的主从模式和哨兵模式也可以称为是广义的集群.
狭义的集群,redis提供的集群模式,在这个集群模式之下,主要是姐姐,存储空间不足的问题.
redis集群基本概念
上述的哨兵模式,虽然提高了系统的可用性,但是真正用来存储数据的还是master和slave节点,所有的数据都需要存储在单个的master和slave节点中.
如果数据量很大,超出了master/slave所在机器的物理内存,就可能出现严重问题了.
那么如何获得更大的空间,加机器即可!!!
redis集群就是在上述思路下,引入多组master/slave存储数据全集的一部分,从而构成一个更大的整体,称为是redis集群.
假定整个数据全集是1TB,此时就可以引入三组master/slave,每一组master/slave只需要存储整个数据全集的三分之一即可.
其中每一组主从节点保存的是同样的数据,占数据全集的三分之一.
每个从节点都是对应主节点的备份,当主节点挂了,对应的slave会补位成主节点.
每组主从节点都可以称为是一个分片(sharding).
如果全量数据进一步增加,只要在增加更多的分片即可.
数据分片算法
Redis cluster的核⼼思路是⽤多组机器来存数据的每个部分.那么接下来的核⼼问题就是,给定⼀个数据(⼀个具体的key),那么这个数据应该存储在哪个分⽚上?读取的时候⼜应该去哪个分⽚读取?
围绕这个问题,业界有三种⽐较主流的实现⽅式.
哈希求余
借助了哈希表的基本思想,借助hash函数,把一个key映射到整数,在针对数组的长度,求余,就可以得到一个数组的下标.
比如有三个分片,编号为0,1,2.
此时就可以针对要插入的数据key计算hash值(比如使用MD5计算hash值),在把这个hash值余上分片的个数,就得到了一个编号,此时就可以把这个数据放到对应的下标对应分片中了.
md5是一个计算hash值的算法.它能够针对一个字符串里面的内容进行一系列的数学演算,最终得到一个整数.
它是一个非常广泛使用的hash算法.特点:
- 1.md5计算结果是定长的,无论输入的原字符串有多长,最终算出的结果就是固定长度.
- 2.md5计算结果是分散的,两个源字符串,哪怕只有一个地方不同,算出来的md5值也会差别很大.
- 3.md5计算结果是不可逆的.字符串->md5值是很容易得到,而根据md5值还原出原始字符串是很困难的,理论上是不可行的.
如果计算出hash(key)%3==0,此时这个key就要存储在0号分片中,后续查询key的时候,也是同样的算法.
数据搬运
一旦服务器集群需要扩容,就需要更高的成本了.分片的主要目的是为了提高存储能力,分片越多,能存的数据也就越多,但是成本也就更高.
如果随着业务的增长,原先的三个分片已经不够用了,那么此时就要"扩容",引入更多的分片.
引入新的分片的后,hash(key)%N中的N就变了,加入这里新引入一个分片,N就从3变为了4.
当hash函数和key都不变的情况下,如果N变了,整体的分片结果仍然会改变.
如果发现某个数据,在扩容之后,不应该存储在当前的分片中了,就需要重新进行分配这个数据,这个过程就叫做数据搬运.
从上图可以看出,一共20个数据,经过扩容之后,只有3个数据不需要搬运,17个数据需要搬运!!!
由此我们知道采用哈希求余算法需要搬运的数据的比例是很高的.如果在生产环境上扩容,开销是极大的.所以我们往往不能直接在生产环境上操作上述过程,只能通过替换的方式来实现扩容.但是替换也就意味着依赖的机器更多了,成本更高,操作步骤也非常复杂!!!
一致性哈希算法
在hash求余这种操作下,当前的key属于哪个分片,是交替的.
102->0,103->1,104->2,105->0......,交替出现,就导致数据搬运的成本很大.
在一致性hash算法中,把交替出现,改进成了连续出现.降低了数据搬运的开销,能够高效扩容.
一致性hash算法过程
1.把0->2^31-1这个数据空间,映射到一个圆环上,数据按照顺时针方向增长.
2.假设当前存在三个分片,就把分片放到圆环的某个位置上.
3.假定有一个key,计算得到hash值H,就从H所在位置,顺时针往下找,找到的第一个分片,即为该key所从属的分片.
这就相当于,N个分片的位置,把整个圆环分成了N个管辖空间,key的hash值落在某个区间内,就归对应的区间管理.
在这种情况下,如果扩容一个分片,原有分片在环上的位置不动,只要在环上新安排一个分片位置即可.
此时,只要把0号分片上的部分数据,搬运到3号分片上即可,1号分片和2号分片管理的区间上的数据都是不变的.
虽然搬运的成本低了,但是这几个分片上的数据量,就可能步均匀了,就造成了数据倾斜的问题!!!
哈希槽分区算法
此种算法是redis真正采用的分片算法.
为了解决搬运成本高和数据分配不均匀的问题,reids cluster引入了哈希槽算法.
hash_slot = crc16(key) % 16384
其中crc也是一种hash算法.
相当于把整个哈希值,映射到16384个槽位上,也就是[0,16384].
然后把这些槽位均匀的分配给每个分片,每个分片的节点都需要记录自己持有哪些分片.
这种算法,本质就是把一致性hash和哈希求余两种方式结合一下.
假设现在有三个分片,一种可能的分配方式:
0号分片:[0,5461],共5462个槽位;
1号分片:[5462,10923],共5462个槽位.
2号分片:[10924,16383],共5460个槽位.
虽然不是严格意义的均匀,但是差异非常小,此时这三个分片上的数据就是比较均匀的了.
上述只是一种可能的分片方式,实际上分片是非常灵活的,每个分片持有的槽位号,可以是连续的,也可以是不连续的.
此处,每个分片都会使用位图这样的数据结构,来表示出当前持有的槽位.16384个bit位(2KB),用每一位的0或者1来区分这个分片是否持有这个槽位.
如果需要扩容,比如新增一个3号分片,就可以针对原有的槽位进行重新分配.
比如可以把之前每个分片持有的槽位,各拿出一点,分给新的分片.
• 0号分⽚:[0,4095],共4096个槽位
• 1号分⽚:[5462,9557],共4096个槽位
• 2号分⽚:[10924,15019],共4096个槽位
• 3号分⽚:[4096,5461]+[9558,10923]+[15019,16383],共4096个槽位.
注意,我们在使用redis集群分片的时候,不需要手动指定哪些槽位分配给某个分片,只需要告诉redis某个分片应该持有多少个槽位即可,redis会自动完成后续的槽位分配,以及key对应的搬运工作.
关于哈希槽分区算法的两个问题
redis集群是最多有16384个分片吗???
其实不然,如果一个分片上只有一个槽位,这对于集群的数据均匀是难以保证的.而且16384个分片这么大规模的集群,本身的可用性是一个大问题.
实际上redis的作者建议分片的数目不应该超过1000.
为什么是16384个槽位???
节点之间通过⼼跳包通信.⼼跳包中包含了该节点持有哪些slots.这个是使⽤位图这样的数据结构
表⽰的.表⽰16384(16k)个slots,需要的位图⼤⼩是2KB.如果给定的slots数更多了,⽐如65536个了,此时就需要消耗更多的空间,8KB位图表⽰了.8KB,对于内存来说不算什么,但是在频繁的⽹络⼼跳包中,还是⼀个不⼩的开销的.
另⼀⽅⾯,Redis集群⼀般不建议超过1000个分⽚.所以16k对于最⼤1000个分⽚来说是⾜够⽤
的,同时也会使对应的槽位配置位图体积不⾄于很⼤.
总结来说,就是这些个槽位基本上是够用的,同时占用的网络带宽也不是很大.
redis集群搭建
在这里由于只有一台云服务器,所以也是基于docker搭建.
实际工作中,一般是通过主机的方式,来搭建集群.
在搭建之前,一定要把之前启动的redis容器,给停止掉!!!在redis-data目录和redis-sentinel目录下分别执行docker-compose down命令.
在这里我们创建出11个redis节点,其中9个用于集群的搭建,2个用于集群的扩容.
1.创建目录和配置.
创建redis-cluster目录,内部创建两个文件.
在linux上以.sh为后缀结尾的文件,称为是shell脚本.shell脚本里可以批量化执行命令,并且还能加入条件,循环,函数等机制,来完成更加复杂的工作.
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
for port in $(seq 1 9);表明这是一个基于范围的循环.类似于java的for each.
seq也是一个命令,seq 1 9表示生成1-9闭区间内的数据.
\是续行符,把下一行的内容和当前行,合并成一行.shell默认情况下,要求把所有的代码都写到一行里,使用续行符来换行.
对于for来说,用do和done表示代码块的开始和结束,shell中{}用来表示变量了,不表示代码块.
shell中拼接字符是直接写到一起,而不需要使用+.
因此上述第一个循环就表示,创建9个目录,在这些目录下创建一个文件,将内容写到文件中去.
这些内容只有在配置集群的ip的时候是不一致的,
cluster-announce-ip 172.30.0.10${port},会生成101-109的ip.
经过上述两个循环,就会得到11个目录,每个目录里都有一个配置文件,配置文件中ip地址各不相同.
cluster-enabled yes表示开启集群
cluster-config-file nodes.conf//不需要手动写,redis自动生成,后续启动节点之后,会配置一些redis集群信息,写入到此文件中.
cluster-node-timeout 5000//多个节点保持联络的心跳包的超时时间
cluster-announce-ip 172.30.0.10${port}//该redis节点所在主机的ip,当前是使用docker容器模拟的主机,此处写的应该是docker容器的ip.
cluster-announce-port 6379//redis节点自身绑定的端口(容器内的端口),属于是业务端口.
cluster-announce-bus-port 16379//该redis节点的管理端口.
一个服务器,可以绑定多个端口号.
业务端口是用来完成业务数据通信的,响应redis客户端的请求.
管理端口:为了完成一些管理上的任务来进行通信的端口,如果某个分片的redis主节点挂了,就需要从节点成为主节点,此过程就需要管理端口来完成对应的操作.
完成上述操作之后,使用bash命令执行shell脚本.
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
此处为了后续创建静态ip,要先手动创建出网络,同时给这个网络也分配ip.
创建完配置文件之后,启动容器.
2.将上述redis节点.构建成集群
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表示每个主节点需要2个从节点备份.
redis在构建集群的时候,谁是主节点谁是从节点,哪些节点是一个分片不是固定的.
执行命令.
输入yes.
集群构造完毕.
3.使用客户端连接集群
从101-109九个节点,现在是一个整体,使用客户都安连上任意一个节点,都是在操作整个集群,本质上都是等价的.
使用cluster nodes命令查看当前集群的信息.
使用集群来存储数据.
设置成集群模式之后,当前数据就要分片存储了,k1这个key通过hash计算之后,得到slot为12706,属于103这个分片,所以就报错了.
我们可以在启动redis客户端的时候,加上-c选项,此时客户端如果发现当前的key的操作不在当前分片上,就能够自动的重定向到对应的分片主机.
请求转发给了103这个节点,进一步完成了数据存储的操作.
使用集群之后,之前学过的操作多个key的命令有时候就不能正常使用了,此时如果key分布在多个分片上,就有可能出现问题.
如果集群中,有节点挂了怎么办?
如果挂了的是从节点,没有多大影响.
如果挂了的是主节点,因为只有主节点才能处理写操作(如果在从节点上尝试写操作,此时就会自动的被重定向到指定的主节点上),此时集群做的工作就和哨兵做的类似了,集群会自动的把该主节点旗下的从节点,选拔一个出来,晋升为主节点.
我们先使用docker stop redis1命令停掉redis1.
在连上一个客户端查看集群信息.
可以看出,106成了新的主节点,并且105成了106的从节点.
然后我们在使用docker start redis1恢复redis1节点.
再次查看集群信息.
101成了从节点,从属于106.
通过上述过程,我们可以看出,集群机制具有故障转移的机制.
集群模式下的故障转移流程
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.故障迁移
上述例⼦中, 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 还会把⾃⼰成为主节点的消息, 同步给其他集群的节点. ⼤家也都会更新⾃⼰保存的集群结构信息.
上述选举的过程, 称为 Raft 算法, 是⼀种在分布式系统中⼴泛使⽤的算法. 在随机休眠时间的加持下, 基本上就是谁先唤醒, 谁就能竞选成功.
注意和哨兵的区别,哨兵实现出leader,leader负责找一个从节点升级成主节点.而集群是直接投票选出新的主节点.
集群扩容
101-109九个主机,构成了3主6从结构的集群.
现在将110和111两个节点也加入到集群当中,以110为主节点,111为从节点,同时数据分片从3变为4.
1.新的主节点110加入到集群中
redis-cli --cluster add-node 172.30.0.110:6379 172.30.0.101:6379
add-node后的第一组地址是新节点的地址,第二组地址是集群中任意节点的地址,代表整个集群.
此时通过cluster nodes命令查看到110已经成为主节点了,但是还有槽位分配给它.
2.重新分配slots
把之前三组的master上面的槽位各自分出一些来,给到新的主节点.
redis-cli --cluster reshard 172.30.0.101:6379
reshard后的地址是集群中任意节点的地址,reshard代表重新切分的意思.
执行此命令之后,会进入交互式操作,redis会提示用户输入以下内容:
1).多少个slots要进行reshard?
此处我们填写4096.
2).哪个节点接收这些slots?
此处我们填写172.30.0.110这个节点的集群节点的id,上方会有打印,直接粘贴即可.
3).这些slots从哪些节点搬运过来?
此处我们填写all,意思是每个主节点都分一些槽位过来.
也可以手动指定,从某一个或者某几个节点来移动slots,输入以done结尾.
当输入all之后,给出的搬运计划还没有真正开始,当输入yes之后,搬运才真正开始.
此时不仅仅是slots的重新划分,也会把slots上对应的数据,也搬运到新的主机上,这是比较重量的操作!!!
注意,在搬运key的过程中,对于哪些不需要搬运的key,客户端进行访问的时候是没有问题的,但是对于需要搬运的key,进行访问可能会出现短暂的访问错误(因为key的位置发生了变化),随着搬运完成,这样的错误也就自然恢复了.
搬运完成后,就可以看到它的槽位信息了.
3.给新的主节点添加从节点
光有主节点了,此时扩容的⽬标已经初步达成.但是为了保证集群可⽤性,还需要给这个新的主节点添加,从节点,保证该主节点宕机之后,有从节点能够顶上.
redis-cli --cluster add-node 172.30.0.111:6379 172.30.0.101:6379 --cluster-slave --cluster-master-id [172.30.0.110节点的nodeid]
从节点添加完毕!!!