目录

集群

数据分片算法

哈希求余

一致性哈希算法

哈希槽分区算法

redis集群搭建

1.创建目录和配置.

2.将上述redis节点.构建成集群

3.使用客户端连接集群

集群模式下的故障转移流程

1.故障判定

2.故障迁移

集群扩容

集群

广义上的集群,只要是多个机器,构成了分布式系统,都可以称为是一个"集群".前面的主从模式和哨兵模式也可以称为是广义的集群.

狭义的集群,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.故障判定

集群中的所有节点, 都会周期性的使⽤⼼跳包进⾏通信.

1. 节点 A 给 节点 B 发送 ping 包, B 就会给 A 返回⼀个 pong 包. ping 和 pong 除了 message type

属性之外, 其他部分都是⼀样的. 这⾥包含了集群的配置信息(该节点的id, 该节点从属于哪个分⽚,

是主节点还是从节点, 从属于谁, 持有哪些 slots 的位图...).

2. 每个节点, 每秒钟, 都会给⼀些随机的节点发起 ping 包, ⽽不是全发⼀遍. 这样设定是为了避免在节点很多的时候, ⼼跳包也⾮常多(⽐如有 9 个节点, 如果全发, 就是 9 * 8 有 72 组⼼跳了, ⽽且这是按照 N^2 这样的级别增⻓的).

3. 当节点 A 给节点 B 发起 ping 包, B 不能如期回应的时候, 此时 A 就会尝试重置和 B 的 tcp 连接, 看能否连接成功. 如果仍然连接失败, A 就会把 B 设为 PFAIL 状态(相当于主观下线).

4. A 判定 B 为 PFAIL 之后, 会通过 redis 内置的 Gossip 协议, 和其他节点进⾏沟通, 向其他节点确认 B 的状态. (每个节点都会维护⼀个⾃⼰的 "下线列表", 由于视⻆不同, 每个节点的下线列表也不⼀定相同).

5. 此时 A 发现其他很多节点, 也认为 B 为 PFAIL, 并且数⽬超过总集群个数的⼀半, 那么 A 就会把 B 标记成 FAIL (相当于客观下线), 并且把这个消息同步给其他节点(其他节点收到之后, 也会把 B 标记成FAIL).

⾄此, B 就彻底被判定为故障节点了.

2.故障迁移

上述例⼦中, B 故障, 并且 A 把 B FAIL 的消息告知集群中的其他节点.

• 如果 B 是从节点, 那么不需要进⾏故障迁移.

• 如果 B 是主节点, 那么就会由 B 的从节点 (⽐如 C 和 D) 触发故障迁移了.

所谓故障迁移, 就是指把从节点提拔成主节点, 继续给整个 redis 集群提供⽀持.

具体流程如下:

1. 从节点判定⾃⼰是否具有参选资格. 如果从节点和主节点已经太久没通信(此时认为从节点的数据和主节点差异太⼤了), 时间超过阈值, 就失去竞选资格.

2. 具有资格的节点, ⽐如 C 和 D, 就会先休眠⼀定时间. 休眠时间 = 500ms 基础时间 + [0, 500ms] 随机时间 + 排名 * 1000ms. offset 的值越⼤, 则排名越靠前(越⼩).

3. ⽐如 C 的休眠时间到了, C 就会给其他所有集群中的节点, 进⾏拉票操作. 但是只有主节点才有投票资格.

4. 主节点就会把⾃⼰的票投给 C (每个主节点只有 1 票). 当 C 收到的票数超过主节点数⽬的⼀半, C 就会晋升成主节点. (C ⾃⼰负责执⾏ slaveof no one, 并且让 D 执⾏ slaveof C).

5. 同时, 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]

从节点添加完毕!!!

相关链接

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: