🌸个人主页:https://blog.csdn.net/2301_80050796?spm=1000.2115.3001.5343
🏵️热门专栏:
🧊 Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm=1001.2014.3001.5482
🍕 Collection与数据结构 (92平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm=1001.2014.3001.5482
🧀线程与网络(96平均质量分) https://blog.csdn.net/2301_80050796/category_12643370.html?spm=1001.2014.3001.5482
🍭MySql数据库(93平均质量分)https://blog.csdn.net/2301_80050796/category_12629890.html?spm=1001.2014.3001.5482
🍬算法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12676091.html?spm=1001.2014.3001.5482
🍃 Spring(97平均质量分)https://blog.csdn.net/2301_80050796/category_12724152.html?spm=1001.2014.3001.5482
🎃Redis(97平均质量分)https://blog.csdn.net/2301_80050796/category_12777129.html?spm=1001.2014.3001.5482
🐰RabbitMQ(97平均质量分) https://blog.csdn.net/2301_80050796/category_12792900.html?spm=1001.2014.3001.5482
感谢点赞与关注~~~
目录
- 1. 集群概念介绍
- 2. 数据分片方式与规则
- 2.1 哈希求余算法
- 2.2 一致性哈希算法
- 2.3 哈希槽分区算法(redis使用)
- 3. 基于docker搭建集群
- 4. 主节点宕机自动处理流程
- 5. 集群扩容
1. 集群概念介绍
集群这个词,可以从广义上来理解,也可以从狭义上来理解,其中广义的集群,只要是多个机器,构成了分布式系统。我们前面学过的哨兵模式,也可以算作是一种广义上的集群。狭义上的集群,是redis提供的一种集群模式,在这个集群模式之下,这个集群模式之下,主要解决存储空间不足的问题,即拓展存储空间。
我们前面的哨兵模式提高了系统的可用性,哨兵模式中,本质上还是redis主从节点存储数据,其中要是请求一个主节点/从节点,就得存储整个数据的“全集”。但是集群中主要要解决的问题就是要引入多台机器,每台机器存储一部分数据.只要机器规模足够大,就可以存储任意数据的大小了.比如整个数据全集是1TB,引入三组Master/Salve来存储(注意是三组集群,每组集群中由一个主节点和若干从结点组成,集群中各个服务器存储的数据是相同的).那么每一组机器只需要存储整个数据全集的1/3即可.
这其中每个服务器集群上都需要存储一定规模的数据,也就是把数据分为了多份,但是数据在分成多份的时候,应该怎么分?这就是我们接下来需要主要研究的问题.
2. 数据分片方式与规则
上面的每个红框中的集群,都可以算是一个数据分片.下面是三种主流的数据分片方式.
2.1 哈希求余算法
所谓哈希求余,就是借鉴了哈希表的基本思想,借助哈希函数,把一个key映射到整数,再针对数据的长度,求余就可以得到一个数组的下标.
哈希求余的分片算法具体是什么样子的呢?
比如有三个分片,编号0 1 2,此时就可以针对要插入的数据的key计算出一个哈希值(计算哈希值的算法,一般是针对一个字符串里面的内容进行一系列的变换,比如md5算法就是一种哈希算法,他是把一个字符串变换成为一个16进制的数字).在把这个哈希值余上一个分片个数,就会得到一个下标.此时就可以把这个数据放到该下标对应的分片中了.
比如,hash(key) % N => 0,由于求出的余数是0,此时这个key对应的数据就要存储在0号分片中.后续查询key的时候也是同样的算法.只要key是一样的,hash函数式一样的,得到的分片的值就是一样的.
随着业务逐渐增长,数据变多的时候,仅有的几个分片就不足以保存数据了,这就需要对集群进行扩容,引入新的分片.**这时候,哈希求余算法的缺点就体现出来了,就是在增加集群容量,需要对数据进行搬运的时候,开销非常大 **.需要对数据进行搬运的原因就是,集群的数量增加,取余的N会增加,这就使得有些数据不应该待在当前的分片了,就需要重新进行分配.
而这些数据在搬运的时候,大部分的数据都是需要搬运的,这就是扩容的开销非常大.
从上图中可以看到,其中的21个数据,只有3个key没有被搬运,其他的key都是搬运过的.
当然,哈希求余的分片算法也是有一定优点的,那就是简单高效,数据分配比较均匀.
2.2 一致性哈希算法
哈希求余这种操作中,当前的key属于哪个分片是交替的.
比如某些数据的hash值分别是102,103,104,这些数据在余3之后,得到的值分别是0,1,2,它们哈希求余求出的值是交替的,交替分布在不同的集群中.在一致性哈希这种算法中,我们就把数据交替存储改进成了连续存储.以下的过程就是一致性哈希算法的过程:
- 首先把数据空间全部映射到一个圆环上,数据按照顺时针方向增长.
- 假设当前存在三个分片,把分片放到圆环的某个位置.
- 假定有一个key,通过哈希函数计算得到的哈希值H,之后把计算出的H映射到圆盘上对应数据的位置.之后从H所在的位置,顺时针向下找,找到的第一个分片,就是该key所从属的分片.
这就相当于,N个分片的位置,把整个圆环分成了N个管辖的区间,key的哈希值落在某个区间内,就归对应区间管理.
在这种一致性哈希算法情况下,如果想要对数据进行扩容,我们需要如何处理呢?原有分片在环上的位置不动,只需要在换上安排一个新的分片即可.
此时只需要把0号分片上的部分数据搬运给3号分片即可,1号分片和2号分片管理的区间都是不变的.
这种算法的搬运成本相对于哈希求余的方式确实低了不少.虽然搬运的成本低了,但是缺点就是这几个分片的数据量就可能会发生某种程度上的不均匀,也就是发生数据倾斜.
2.3 哈希槽分区算法(redis使用)
这种算法是redis真正采用的算法,为了解决上述问题,redis集群就引入了哈希槽算法
hash_slot = crc16(key) % 16384
其中crc也是一种哈希算法,16384其实是16*1024,也就是2^14
其实这种算法就是把一致性哈希和哈希求余两种方式结合一下.也就是把哈希值映射到16384个槽位上.
然后再把这些槽位均匀地分给每个分片,每个分片的接地那都需要记录自己持有哪些分片.每个分片都会使用"位图"这样的数据结构来表示出当前有多少槽位号,每一位用0/1来区分自己这个分片当前是否持有该槽位号
假设当前有三个分片,一种可能得分配方式:
• 0号分片:[0,5461],共5462个槽位
• 1号分片:[5462,10923],共5462个槽位
• 2号分片:[10924,16383],共5460个槽位
这里只是一种可能得分片方式,实际上分片方式是很灵活的.每个分片持有的槽位号,是可以连续,也可以不连续的.这里的分配虽然不是严格意义上的"均匀",但是差异却非常小了.此时这三个分片上的数据已经在某种程度上比较均匀了.
如果后需要进行扩容,比如新增一个3号分片,就可以针对原有的槽位进行重新分配.比如可以把之前的每个分片持有的槽位各拿出一点,分给新的分片:
• 0号分片:[0,4095],共4096个槽位
• 1号分片:[5462,9557],共4096个槽位
• 2号分片:[10924,15019],共4096个槽位
• 3号分片:[4096,5461]+[9558,10923]+[15019,16383],共4096个槽位
此外,还有一些其他的问题:
- redis集群最多有16384个分片吗?
key是要先映射到槽位上,在映射到分片上的.如果每个分片包含的槽位比较多,槽位个数相当,就可以认为包含的key的个数是相当的,那么就可以认为数据分布式均匀的.如果每个分片包含的槽位非常少,槽位个数不一定直观反应到key的数目,这种情况之下,数据均匀性是很难保证的.而且如果每个分片中包含的槽数非常少,这就会使得集群的服务器规模非常庞大,服务器越多,出现故障的概率就越大.
在redis官方文档中提到,作者建议我们集群的分片数不应该超过1000. - 为什么是16384个槽位?
这是由于结点之间需要通过心跳包进行通信,心跳包通过网络来在结点之间发送,心跳包中包含了该结点持有了哪些槽位,如果每个心跳包中给定的槽位数更多了,这时候心跳包就会变大,会增加网络传输上的开销.
3. 基于docker搭建集群
在搭建docker集群的时候,一定要记得把之前启动的redis容器给停止掉.否则可能会产生端口上的冲突.我们需要搭建的集群环境如下:
由于我们需要创建的redis结点较多,redis结点之间大同小异.在Linux操作系统的环境之下,我们需要引入shell脚本来批量创建.
就是把shell命令操作写入了一个文件中,批量化执行,同时还可以加入,条件,循环,函数机制等.
- 首先批量生成redis配置文件.创建redis-cluster目录.内部创建shell脚本文件,命名为
generate.sh
注意文件后缀必须是sh
.
generate文件的内容如下:
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
- 之后在redis-cluster目录之下执行命令执行脚本.
bash generate.sh
.注意想要在指定的目录中生成文件,必须在指定的目录之下执行命令,于是就会在目录之下批量生成指定目录和配置文件.
redis-cluster/
├── 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每个都不同.区别在与每个配置中配置的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
我们对conf文件中的这些字段来解释一下:
- cluster-enabled: 开启集群
- cluster-config-file: 启动结点之后,redis自动配置的一些集群信息,这些是redis自动生成的,不需要手写.
- cluster-node-timeout: 多个结点之间需要进行交互,保持联络,如果发送心跳包之后,超过这个时间没有回应,则会主观判定为宕机.
- cluster-announce-ip: 该redis接地那所在主机的ip地址.当前是使用docker容器模拟主机,此处写的应该是docker容器的ip.
- cluster-announce-port: 业务端口,用来进行业务数据通信
- cluster-announce-bus-port: 管理端口,为了完成一些管理上的任务来进行通信.
3. 之后就需要使用docker来创建出redis容器了.在redis-cluster目录之下创建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,5,6...同理
- subnet: 此处为了后续创建静态的ip,就需要先手动创建出网络,同时给这个网段分配ip,这里分配的网络号是
172.30.0
,其中这个网络号是私网格式的一种,需要注意的是,不能和当前主机上现有的其他网段冲突,0/24
是主机号. - volumes: 配置文件映射
- ports: 端口映射,需要注意的是,映射右边的端口必须和配置文件中的保持一致.
- ipv4_address: 静态配置的网络ip,这里的ip也必须和之前的配置文件中的一致.
- 启动容器
docker-compose up -d
- 接下来就需要把这些服容器全部构建为集群
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表示每个主节点需要两个从结点备份.设置了这个配置之后,redis就会知道,每3个结点是一伙的(一个分片).但是分配的时候,谁是主节点,谁是从结点,都是随机的.
执行之后,容器之间会进行加入集群操作.之后系统会弹出日志,描述集群的搭建是增怎样的.输入yes之后才会真正创建.见到[OK]之后说明集群建立完成.
- 使用客户端连上集群中的任何一个结点,都相当于连上了整个集群.
redis-cli -h 172.30.0.101 -p 6379
,在客户端中,我们使用cluster nodes来查看当前集群的信息,但是存在一个问题,我们知道redis集群中存储数据的时候是使用哈希槽分区算法来分配数据的,如果我们以这种姿势启动客户端的话,如果set的key不属于所在主节点的集群的话,就会报错.
所以我们在启动客户端的时候,我们就需要在后面加上一个-c
,这时候如果所添加的key不属于当前的集群的话,就会重定向到其他对应分片上的主机中.后期获取也是可以直接获取到的.
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. 主节点宕机自动处理流程
如果集群中的主节点发生了宕机,此处集群做的工作就和之前哨兵做的工作类似了,就会自动的把该主节点旗下的从结点给挑出来一提拔为主节点.
但是此处的故障处理转移,具体的流程还和哨兵有一些区别.
-
故障判定,通过周期性发送心跳包来判定某个结点是否挂掉.
结点A首先给结点B发送ping包,如果B不能如期回应的时候,A就会尝试重置和B的tcp连接,看能否连接成功,如果仍然连接失败,A就会把B设置为PFAIL(主观下线)状态.== A判定B为主观下线之后,会通过redis内置的gossip协议,和其他节点进行沟通==,向其他节点确认B的状态.此时A发现其他很多结点都认为B主观下线了,并且数目超过了集群总个数的一半,那么A就会把B标记为客观下线.并且把这个消息同步给其他的结点,其他节点收到之后,也会把B标记为客观下线. -
故障迁移
如果B是从结点,就不需要进行故障迁移.如果B是主节点,那么就会由B的从结点(比如C和D)触发故障迁移.把从结点提拔为主节点.
从结点首先判定自己是否具有参选资格.如果主节点和从结点太久没有通信,那么就证明主节点和从结点的数据差异太大了,就会失去竞选资格.具有资格的结点,比如C和D,就会先休眠一定的时间.休眠时间=500ms基础时间+[0,500ms]随机时间+排名*1000ms. 其中offset越大,则排名越靠前(越小),offset越大,数据就越靠近主节点,排名就会更靠前休眠时间也更短.比如C的休眠时间到了,C就会给其他所有集群中的结点进行拉票操作.但是只有主节点才有投票的资格.主节点就会把自己的票投给C.当C收到的票数超过主节点的一半的时候,C就会晋升为主节点.同时C还会把自己成为主节点的消息通知其他集群的结点,大家也都会更新自己保存的集群结构的信息.
上述的选举过程符合Raft算法,在随机休眠时间的加持之下,基本上谁先唤醒,谁就可以竞选成功.
上述和哨兵最明显的区别就是,哨兵是选取一个Leader来进行提拔主节点,而这里是直接通过主节点投票选举出主节点
5. 集群扩容
扩容会涉及到大量数据的迁移操作,所以扩容是一件风险比较高,成本比较大的操作.
- 把新的主节点加入到集群中
redis-cli --cluster add-node 172.30.0.110:6379 172.30.0.101:6379
add-node之后的第一组地址是新节点的地址,第二组是集群中的任意节点地址,表示把新的结点添加到哪个集群中去. - 重新分配哈希槽位
把之前Master上面的slots拎出来一些,分配给新的master.
redis-cli --cluster reshard 172.30.0.101:6379
reshard之后的地址是集群中任意一个结点的地址.
执行之后,会进入交互式操作,redis会询问一下内容:- 多少个槽位需要reshard?
- 哪个结点来接收槽位?此处填写新加入的结点.
- 这些槽位从哪些结点搬运过来?此处如果填写all,表示的就是每个主节点都会拿一点槽位过来,如果手动指定,就会动指定的某个或者是几个节点来搬运槽位.
- 之后会打印初步分配方案,让用户确认,确定的时候输入yes即可.在搬运真正开始的时候,此时不仅仅是槽位的重新划分,也会把槽位上对应的数据搬运到新的主机上,此时是一个比较重量的操作,可能需要消耗一定的时间.
注意在搬运的时候,在迁移的槽位不可被访问,其他未搬运的key是可以正常访问的
- 给新的主节点添加从结点
redis-cli --cluster add-node 172.30.0.111:6379 172.30.0.101:6379 --clusterslave --cluster-master-id [172.30.1.110 节点的 nodeId]
此时扩容目标初步达成,但是为了保证整个集群的可用性,还需要给主节点添加一些从结点,保证主节点宕机之后,有接班人.