Redis集群

集群概念

哨兵模式提高了系统的可用性,但本质上还是redis主从节点存储数据,其中就要求一个主节点/从节点,就得存储整个数据的"全集"

如果数据量很⼤,接近超出了master/slave所在机器的物理内存,就可能出现严重问题了

此处关键问题,就是引入多台机器,每台机器存储一部分数据

假设1TB的数据要存储,拿两台机器存,每个机器只需要存521G;拿四台机器存,每个机器只需要300+G

只要机器规模足够多,就可以存储任意大小的数据

当时,要给每个存储数据的机器搭配若干个从节点,防止一台挂了

redis 的集群就是在上述的思路之下,引⼊多组Master/Slave,每⼀组Master/Slave存储数据全集的 ⼀部分,从⽽构成⼀个更⼤的整体,称为Redis集群(Cluster).

广义:

只要是多个机器,构成了分布式系统,都可以称为是一个"集群"

狭义:

redis提供了集群模式,主要解决空间不足的问题(拓展存储空间)

这三组机器存储的数据都是不同的 ,每个红框部分都可以称为是⼀个分⽚

把数据分成多份,具体怎么分?

下面看三种主流的分片方式

数据分片算法

1) 哈希求余

借助hash函数,把一个key映射到数组下标上,再针对数组长度求余,得到一个数组下标

比如说三个分片,编号0 1 2

此时就针对要插入的key,计算hash值(比如md5),再把这个hash值余上分片个数,就得到了一个下标,此时就可以把这个数据放到该下标对应的分片中了

hash(key)%N=>0  此时这个key就要存储在0号分片中

后续查询key,也是同样的算法

如果key一样,hash函数一样,得到的分片值就是一样的

MD5是一个广泛使用的hash算法

特点:

1.md5计算结果是定长的

无论输入的原字符串多次,最终算出的结果都是固定长度

2.md5计算结果是分散的[哈希函数]

两个原字符串,哪怕大部分相同,只要一个小地方不同,算出来的md5值也会差别很大

3.md5计算结果不可逆[加密]

给出原字符串,可以很容易算出md5的值

但给md5值,很难还原出原始的字符串(理论上不可行)

优点:

简单⾼效,数据分配均匀

缺点:

一旦服务器集群需要扩容,就需要更高的成本了

分片主要目的是为了提高存储能力,分片越多,能存的数据越多,成本也越高

一般都是先少弄几个分片(3个),但是随着业务注解增长,3个分片就不足以保存了,需要扩容

引入新的分片,N就改变了

hash(key) % N =>0 hash函数和key都不变,整体的分片结果仍然会变

如果发现某个数据,在扩容之后,不应该待在当前的分片中了,就得重新进行分配


扩容 

此时列出的这些值,假设为hash(key),计算完后,数值是100-200

扩容后,主节点带着从节点去搬运: 

 

上述可以看出:

一共20个数据,只有三个数据不需要搬运,搬运了17个数据

如果数据更多呢?? ?

上述级别的扩容,开销是极大的,往往是不能直接在生产环境上操作的,只能通过替换的方式来实现扩容,依赖的机器更多,成本更高,操作步骤更复杂

2) 一致性哈希算法

①把0->2^32-1这个数据空间,映射到⼀个圆环上.数据按照顺时针⽅向增⻓:

②假设当前存在三个分⽚,就把分⽚放到圆环的某个位置上:

③假定有⼀个key,计算得到hash值H,那么这个key映射到哪个分⽚呢?

规则很简单,就是从H 所在位置,顺时针往下找,找到的第⼀个分⽚,即为该key所从属的分⽚:

这就相当于,N个分⽚的位置,把整个圆环分成了N个管辖区间.Key的hash值落在某个区间内,就归对 应区间管理.

在hash求余这种操作中,当前key属于哪个分片,是交替的,103=>0 104=>1 105=>2 交替出现,导致搬运成本大了

在一致性哈希这样的设定下,把交替出现,改进成连续出现

类似于取票:

假设,⼀个⼈每次来⾼铁站,都会停⻋在同⼀个位置.(不同的⼈停⻋位置不同).

每个人下车后,都往右手方向走,遇到第一个取票机就取票

此时是均匀的分布

扩容

原有分⽚在环上的位置不动,只要在环上新安排⼀个分⽚位置即可:

增加一个3号分片,本来是0号分片,只需要把0号分片上的数据搬运到3号分片上就好,1,2号分片的数据不变

这种搬运方式的成本比hash求余低了很多

虽然搬运的成本低了,但是这几个分片上的数据量不均匀,出现了数据倾斜

此时如果一次扩容多个分片,确实可以避免刚才的数据倾斜的情况,总的搬运数据量仍然是比hash求余的方式更少,但是最大的问题是,机器太多了

3) 哈希槽分区算法(Redis使用)

hash_slot = crc16(key) % 16384

crc也:一种计算hash值的算法

hash_slot :哈希槽

将上述的哈希槽,分配到不同的分片上


假设当前有三个分片,一种可能的分配方式:

  • 0号分⽚:[0,5461],共5462个槽位
  • 1号分⽚:[5462,10923],共5462个槽位
  • 2号分⽚:[10924,16383],共5460个槽位 

差异不大,认为这三个分片上的数据比较均匀

这种算法本质就是把一致性哈希和哈希求余这两种方式结合

这里只是一种可能的分片方式,实际上分片是非常灵活的,每个分片的槽位号,可以连续,也可以不连续

 每个分⽚的节点使⽤位图来表⽰⾃⼰持有哪些槽位.对于16384个槽位来说,需要2048个字节(2KB)⼤⼩的内存空间表⽰.(1字节,8bit)

16384个bit位,每一位用0/1来区分自己这个分片当前是否持有该槽位

扩容

一种可能的分配方式: 

  • 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个分片嘛?

并不是,如果每个分片上就只有一个槽位,此时很难保证数据在每个分片上的均衡性

key要先映射到槽位,再映射到分片上

如果每个分片包含的槽位比较多,如果槽位个数相当,就可以认为是包含的key数量相当

如果每个分片包含的槽位非常少,槽位个数不一定能直观反应key的数目

有的槽位可能是由多个key,有的槽位可能没有key

实际上,redis作者建议集群分片数不应该超过1000

问题二:为什么是16384个槽位?

节点之间通过⼼跳包通信.⼼跳包中包含了该节点持有哪些slots.这个是使⽤位图这样的数据结构表⽰的.

表⽰16384(16k)个slots,需要的位图⼤⼩是2KB.如果给定的slots数更多了,⽐如65536 个了,此时就需要消耗更多的空间,8KB位图表⽰了.8KB,对于内存来说不算什么,但是在频繁的⽹络⼼跳包中,还是⼀个不⼩的开销的.

另⼀⽅⾯,Redis集群⼀般不建议超过1000个分⽚.所以16k对于最⼤1000个分⽚来说是⾜够⽤的,同时也会使对应的槽位配置位图体积不⾄于很⼤

4)优缺点

算法优点缺点
哈希求余简单⾼效,数据分配均匀⼀旦需要进⾏扩容,N改变了,原有的映射规则被破坏,就需要让节点之间的数据相互传输,重新排列,以满⾜新的映射规则.此时需要搬运的数据量是⽐较多的,开销较⼤
⼀致性哈希算法⼤⼤降低了扩容时数据搬运的规模,提⾼了扩容操作的效率数据分配不均匀(有的多有的少,数据倾斜)
哈希槽分区算法结合了上述两个算法的优点

 

基于docker搭建出redis集群

拓扑结构 

此处我们先创建出11个redis节点.其中前9个⽤来演⽰集群的搭建. 后两个⽤来演⽰集群扩容.

 


先把之前启动的redis容器,给停止,不然就可能出现端口冲突

 

第⼀步:创建⽬录和配置

创建redis-cluster ⽬录.内部创建两个⽂件

redis-cluster/

├── docker-compose.yml

└── generate.sh

在linux中,sh后缀结尾的文件,称为"shell脚本"

使用linux的时候,都是通过一些命令来进行操作的

使用命令操作,就非常适合把命令写到一个文件中,批量化执行,同时还能加入条件,循环等...

此时需要创建11个redis节点,这些redis的配置文件内容,大同小异

就可以使用脚本批量生成

 

generate.sh 内容如下:

for port in $(seq 1 9); \
do \
mkdir -p redis${port}/
touch redis${port}/redis.conf
cat << EOF > redis${port}/redis.conf
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

 

内容解析:

  • seq是一个linux命令,生成[1,9]
  • \是续航符,把下一行的内容和当前行,合并成一行,shell默认情况下,要求把所有的代码都写到一行里,使用续航符来换行
  • shell中拼接字符串是直接写的一起,而不需要使用+ redis${port}
  • shell中{ }用来表示变量,不是代码快
  • 对于for,就是使用do和done来表示代码块开始和结束

预期结果:得到11个目录,每个目录里都有一个配置文件,ip地址各不相同

  • cluster-enabled yes:开启集群
  • cluster-config-file nodes.conf:这个配置不需要手动写,redis会自动生成
  • cluster-node-timeout 5000:多个节点之间相互交互,保存联络,节点失联的超时时间
  • cluster-announce-ip:该redis节点所在主机的ip地址(当前使用docker容器模拟的主机,此处写的应该是docker容器的ip)
  • cluster-announce-port 6379 :redis节点自身绑定的端口(容器内的端口,业务端口),不同的容器内部可以有相同的端口,后续进行端口映射,再把这些容器内的端口映射到容器外的不同端口即可.
  • cluster-announce-bus-port 16379 :节点⾃⾝的总线端⼝.集群管理的信息交互是通过这个端⼝进⾏的,管理端口

bash 执行命令

查看目录:

第⼆步:编写docker-compose.yml

docker-compose.yml:

先创建networks,并分配⽹段为 172.30.0.0/24

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


shell脚本创建的目录,这里就包含配置文件


此处不进行端口映射也可以,映射是为了在容器外面通过客户端直接进行访问


此处配置静态ip,网络号要和前面的网段一致

主机部分,可以随便配置(1-255,保证不重复)

就是按照之前配置文件中写的101-111


进行配置时,一定要保证前后的一致性:

第三步:启动容器

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

create:创建集群,列出每个参与构建集群的ip和端口号,端口都是写容器内部的端口号

cluster-replicas 2:描述集群的每个节点,应该是2个从节点

这个配置设置了之后,就知道三个节点是一伙的,一共9个节点,一共是3个分片了

注意:

  • redis在构建集群的时候,谁是主节点,谁是从节点,谁和谁是一个分片,都不固定
  • 本身从集群的角度来看,提供的这些节点之间本身就是等价的

 

主从分配:

槽位分配:

输入yes才会真正构建集群:

总结:

1)生成每个redis节点的配置文件

2)使用docker创建出11个redis节点,并且启动容器

3)使用redis-cli执行构建集群命令

两种连接方式:

 从101-109九个节点,现在是一个整体,使用客户端连上任意一个节点,本质上都是等价的

cluster nodes查看当前集群的信息:

使用集群来存储数据:

 

设置成集群模式之后,当前数据就分片了

k1这个key通过hash'计算后,得到slot 12706,属于103这个分片

可以在启动redis-cli时,加上-c选项,此时客户端发现当前key的操作不在当前分片上,就能自动的重定向到对应的分片主机

此时就把请求转发给了103这个节点

有些指令,能够操作多个key,但如果key是分散在不同的分片上,就可能出现问题了:

主节点宕机

如果集群中,有节点挂了,怎么办??

如果挂了从节点,无影响

如果挂了主节点,有影响,从节点不能写

当尝试在从节点上操作,就会自动的被重定向到指定的主节点上:

此处,集群就和之前的哨兵类似了,就会自动把该主节点旗下的从节点,挑出来一个,提拔为主节点

模拟

把redis1挂了

之前101是主节点,105和106是它的从节点

当101挂了之后,106就成了新的主节点,代替了原来101的位置

 

101节点重启:

docker start 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是从节点,那么不需要进⾏故障迁移.

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

  • 从节点判定⾃⼰是否具有参选资格.如果从节点和主节点已经太久没通信(此时认为从节点的数据和主节点差异太⼤了),时间超过阈值,就失去竞选资格.
  • 具有资格的节点,⽐如C和D,就会先休眠⼀定时间.休眠时间=500ms基础时间+[0,500ms]随机 时间+排名*1000ms.offset的值越⼤,则排名越靠前(越⼩),休眠时间越短.
  • ⽐如C的休眠时间到了,C就会给其他所有集群中的节点,进⾏拉票操作.但是只有主节点才有投票 资格.
  • 主节点就会把⾃⼰的票投给C(每个主节点只有1票).当C收到的票数超过主节点数⽬的⼀半,C就 会晋升成主节点.(C⾃⼰负责执⾏slaveofnoone,并且让D执⾏slaveofC).
  • 同时,C还会把⾃⼰成为主节点的消息,同步给其他集群的节点.⼤家也都会更新⾃⼰保存的集群结构 信息.

这里是之间投票出新的主节点,

而哨兵,是先竞选出leader,leader负责找一个节点升级成主节点

上述选举的过程,称为Raft算法,是⼀种在分布式系统中⼴泛使⽤的算法.

在随机休眠时间的加持下,基本上就是谁先唤醒,谁就能竞选成功.

 

集群宕机

  • 某个分⽚,所有的主节点和从节点都挂了.(该分片无法提供数据服务了)
  • 某个分⽚,主节点挂了,但是没有从节点.
  • 超过半数的master节点都挂了 

此时master挂了,但是后面还有slave做补充

如果突然一系列的master都挂了,此时说明集群遇到了非常严重的情况,需要赶快停下来,检查问题

集群扩容 

101-109 9个主机,构成了3主6从结构的集群

下面把110和111加入集群中:110为master 111为slave,把数据分片3=>4

集群扩容操作,是一件风险较高,成本较大的操作

1.新的主节点(110)加入到集群中

redis-cli --cluster add-node 172.30.0.110:6379 172.30.0.101:6379

前面是被新增的节点

后面是集群中的任意一个节点,表示要把新节点加入到哪个集群

2.重新分配slots

把之前三组master上面的slots拎出来一些,分配给新的master

redis-cli --cluster reshard 172.30.0.101:6379 

reshard 后的地址是集群中的任意节点地址

此处会先打印出每个集群的情况,并且要求用户输入要移动多少个slots:

4个分片,一共是16384,/4得到4096

接下来,会询问让哪个节点来接收,粘贴110这个主机的id:

输入要从哪些节点来移动slots:

1)all:表示从其它每个持有的slots的master都拿过来点

2)手动指定:从某一个或者某几个节点来移动slots(以done为结尾)

输入yes,搬运:

此时不仅是slots重新划分,也会把slots对应的数据,也搬运到新的主机上(比较重量的操作)

再次查看,此时这四个master都持有slots了:

问:在搬运slots/key的过程中,此时客户端能否访问集群呢?

搬运key的过程,大部分的key是不用搬运的

针对这些未搬运的key,此时是可以访问的

针对正在搬运的key,是有可能出现访问出错的情况的

假设客户端访问k1,集群通过分片算法,得到k1是第一个分片的数据,就会重定向到第一个分片的节点,可能在重定向之后,k1被搬走了,自然就无法访问了

可以在无人访问的时候,进行扩容,将损失降到最低

要想追求更高的可用性,让扩容对用户影响更小,就需要搞一组新的机器,重新搭建集群,把数据导入进来,使用新集群代替旧集群(成本最高)

3. 把从节点也添加到集群

redis-cli --cluster add-node 172.30.0.111:6379 172.30.0.101:6379 --cluster slave --cluster-master-id [172.30.1.110 节点的 nodeId]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值