Redis 进阶(六)—— 集群

5. 集群

5.1 引入

广义上的集群:只要是多台机器,构成了分布式系统,都可以称为是一个 "集群"。

狭义上的集群:redis 的集群模式,该模式主要是解决,存储空间不足的问题,即 拓展存储空间。

前面的哨兵每一个节点都存储了整个数据,存储的范围有限。

redis 的集群模式,即 完整的数据,每个机器(每个机器还要配从节点)分别存一部分,这样能存储的数据量就提升了。

5.2 数据划分方式

如果数据被划分到 3 套存储机器(假定一主二从)上,那么这里的一套机器及数据称为一个分片。

主流分片方式有三种:

5.2.1 哈希求余

原理类似于散列表,假定有三个分片,编号 0,1,2。

对要插入的数据的 key (redis 都是键值对数据结构)计算hash 值(例如使用 md5),然后将值对 3 取余,余数即为该 key 所在的分片。

md5 是一个广泛使用的 hash 算法,其结果是一串 16进制 的数字。

特点:

  1. 结果定长 ------ 无论原字符串有多长,结果都一样长。
  2. 计算结果分散 ------ 字符串即使差别很小,对应结果也相差甚远
  3. 计算结果不可逆 ------ 即通过 md5 得到结果后,很难通过结果还原原始字符串,因而可以用于加密

缺点 :扩容成本高。

现实中随着业务的增长,数据也会变多,原先的片数不足以支持,就要增加分片,扩大容量。

但是分片数增加了,之前数据的 哈希求余 的结果就会改变,因而就要对所有的 key 重新计算,重新分配。

就像上图这样(一共 20 个 key),扩容后只有三个 key 所在分片不变,其余全部都要搬运。

如果有 20 亿数据,就要搬运 17 亿数据,这开销可不小。 现实中如果真有这样的扩容,往往不能在生产环境中直接操作,只能再搞 4 台(以上图为例),从这三台中迁移数据,而不是这三台之间相互搬运。迁移完成,用新的这四台,替换旧的那三台。

但是这样,就要更多的机器,这些机器都要再提前配置,同样成本不低。

5.2.2 一致性哈希算法

前面的哈希求余,key 属于哪个分片是交替的,而一致性哈希中,key 属于哪个分片是连续的。

将 0 - 2^32-1 这个数据空间,映射到圆环上,数值顺时针递增。

假设有 三 个分片(编号 0,1,2),将分片放到圆环的某个位置上(尽量将圆等分)

key 通过 hash 函数计算得到 hash 值后,从该值于圆上所在位置,顺时针找到的第一分片,就是 key 所属分片。

扩容时,原有分片位置不动,只要在环上,新添一个分片即可。如下:

这样扩容时,1 & 2 分片上的数据,不用动,只要动 0 号 分片是即可。

一致性哈希

优点:大大降低扩容时,搬运数据的规模,提高了扩容操作的效率

缺点:数据分配不均,一些机器上数据多,一些机器上数据少。

5.2.3 哈希槽分区算法

该算法是 redis 采用的分片算法,本质是前两种的结合。

bash 复制代码
hash_slot = crc16(key) % 16384
  • crc16 是一种 hash 算法
  • 16384 = 16 * 1024 即 2 ^ 14

把 hash 值映射到 16384 个槽位上( 【0,16383】),然后将这些槽位比较均匀的分配给每一个分片,分片都有一个 16384 个bit 位 的位图(2048 个字节,即 2 KB 大小),用于记录自己持有哪些槽位。

假定 三 分片,一种可能的分片方式

0号:【0,5461】 共 5462 个槽位

1号:【5462.10923】共 5462 个槽位

2号:【10924,16383】共 5460个槽位

注:分片持有的槽位不一定要连续

需要扩容时,需要将原有槽位进行重新分配即可,例如:每个分片都拿出一部分给新片。

相关问题

  1. 实际中 redis 集群最大有 16384 个分片吗?
    • 如果一个分片一个槽位,那么数据很难均匀分布,redis 作者建议分片数超过 1000 个。
  2. 为什么是 16384 个槽位?
    • 节点间通过心跳包通信, 里面包含了该节点持有哪些槽位(位图),16384(16k) 个槽位,位图大小位 2KB;如果 槽位更多如 65536, 就是 8 KB 大小了;虽然对内存来说不算什么,但是在心跳包的网络通信中是个不小的开销
    • redis 集群一般不超过 1000 个分片,16384(16k)对于 1000 个分片来说够用了,同时对应位图的体积也不至于很大

5.3 基于 docker 的集群搭建

由于只有一台服务器,所以用 docker 来进行集群搭建,每一个节点都是一个容器。

要部署的集群结构如下:

一共 11 个 redis 节点,9 个用于集群搭建,剩下 2 个用于演示扩容。

记得将之前启动的 redis 容器停掉。

5.3.1 前置准备

目录结构

bash 复制代码
redis-cluster/
├── docker-compose.yml
└── generate.sh

创建对应文件夹和文件,

bash 复制代码
mkdir redis-cluster
cd redis-cluster/
touch generate.sh
touch docker-compose.yml

linux上,这个 .sh 文件称为 "shell 脚本"。

linux 是通过一些命令进行操作的,这就可以将命令写到一个文件里,进行批量执行,同时还能加入 条件,循环,函数 等机制。这个就是 shell 脚本。

要创建的 11 个 redis 节点,这些 redis 的配置文件内容,大同小异,所以就可以用脚本来批量生成。

shell 文件内容

bash 复制代码
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 是 linux 中的一个命令,(seq 1 9) 即,生成 【1,9】

    所以,这里就是 port 从 1 到 9 循环 9 次。
    \ 是 续行符,就是将下一行内容,和当前行合并。因为 shell 默认情况,要求所有代码都写在一行,但这写起来太难受了,所有用 \ 进行续行。

do & done shell 中用 {} 表示变量,不是表示代码块,对于 for,用 do 和 done 表示代码块的开始和结束。

redis${port} shell 中拼接字符串直接写在一起,不需要用 + 拼接。

bash 复制代码
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
  • cluster-enabled yes 开启集群模式
  • cluster-config-file nodes.conf 不用手动写,redis 自动生成,后续启动节点后,会配置一些 redis 集群信息
  • cluster-node-timeout 5000 超时时间
  • cluster-announce-ip 172.30.0.10${port} 用来告诉集群其他 节点 到哪个 ip 找自己
  • cluster-announce-port 6379 节点自己的业务端口
  • cluster-announce-bus-port 16379 节点自己的总线端口 。集群管理的信息交互就是通过该端口,也称 管理端口

业务端口:用于业务数据通信,响应 redis 客户端请求

管理端口:用于一些完成管理任务的通信,例如:某分片的主节点挂了,要让从节点晋升,就要通过该端口进行操作

这里的 ip ,port 就是 redis 节点在 docker 容器中的 ip,port(注:要在 yml 中配置对应静态)

运行结果

bash 复制代码
bash generate.sh


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

yml 文件内容如上。

内容字段讲解

yml 复制代码
networks:
  mynet:
    ipam:
      config:
        - subnet: 172.30.0.0/24

这个是为这个局域网分配了一个网段,方便后续创建静态 ip 和前面对应。

172.30.0.0/24 24 指子网掩码左边 24 位是 1, 右边 8 位为 0,即 255.255.255.0。8 位主机位,意味着除了 172.30.0.0 网络地址,172.30.0.255 广播地址,一共可以给 254 个主机分配地址。

yml 复制代码
networks:
      mynet:
        ipv4_address: 172.30.0.111

容器在网段中的地址。

启动容器,docker-compose up -d

5.3.2 构建集群

bash 复制代码
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 表示每个主节点有两个从节点

运行之后,它会告诉你接下来集群中 槽位的分配方式,和节点间主从关系。但是只有 yes 之后,才会真正执行。

集群搭建好后,这九个节点就是一个整体了,连上其中任意一个就相当于连上集群。

此前,配置了静态 ip 和端口映射,因而采用下面任意一种连接方式均可

bash 复制代码
#方式一
redis-cli -h 172.30.0.101 -p 6379
#方式二
redis-cli  -p 6379

使用 cluster nodes 命令即可查看当前集群的相关信息

前面的一串是节点的身份标识,后面的一串是从节点的主节点的身份标识,主节点是 0。 在 master 节点后还会显示持有的 槽位。

这时候还不能直接在里面操作数据,就像下图

如果 当前的 key 不属于该分片,就会提示应该在哪个分片。

解决措施:在启动的时候,加上 -c 选项,redis 客户端就会根据 key 所属槽位,自动匹配分片,并且换到对应客户端。

5.4 故障转移

集群中出现节点挂了的情况,怎么办?

如果是从节点,则没有什么问题。

如果是主节点,就要进行处理,因为只有主节点才能处理写操作。在从节点上写,redis 会自动将 写 重定向到主节点,并不是 从节点真的能写了。

关掉主节点 redis1 ,模拟挂了的情况。发现 集群中 redis1 的信息显示为 fail,而从节点 172.30.0.106 成为了新的主节点。

这里的故障迁移和哨兵不太一样。

具体流程

  1. 故障判定------判定是否真挂了

    • 节点间会发送心跳包,包里面除了 message type 属性,其他部分都是一样的。里面含有集群的配置信息(该节点的 id,节点属于哪个分片,是主还是从,从的话又从属于谁,持有的 槽位 的位图)
    • 每个节点,每秒都会随机给一些节点发送 ping 包,不是全发,因为全发的话太多了,对网络压力不小。这里有 九个节点,全发就要 8 * 9 = 72 组了。
    • 假定 A,B 两个节点,当 A 给 B 发送 ping包 后,B 一直没有回应;A 就会尝试重置和 B 的 TCP 连接。如果仍连接失败,就会将 B 标记为 PFAIL
    • A 认为 B fail 后,就会通过 redis 内置的 gossip 协议 ,和其他节点交流,从其他节点处确定 B 的状态 (每个节点都维护的有 "下线列表",不同节点的 "下线列表" 可能不同)。
    • 如果 A 发现集群中超一半节点,认为 B "PFAIL" ,就会将 B 标记为 "FAIL ",并通知其他节点,其他节点就会将 B 标为 FAIL
  2. 故障迁移

    • 如果 B 是从节点,就不需要进行故障迁移,如果是主节点就会触发故障迁移。
    1. 从节点判定自己是否有资格参选,如果主从节点之间太久没有通信(从节点数据和主节点数据相差太多),时间超过阈值,就失去竞选资格。
    2. 具有资格的从节点,就会先休眠一定时间(时间 = 500ms 基础时间 + 【0,500】随机时间 + 排名*1000ms),offset 越大,排名越靠前,时间就短。
    3. 如果有从节点醒了,其就会通知集群中其他节点进行拉票,但是只有主节点才有票资格,且每个只有一票
    4. 当票数超过主节点数目的一半,该节点就会晋升为主节点(该节点自己负责执行 slaveof no one,并让没竞选上的从节点更改从属)
    5. 同时,C 还会将自己成为 主节点的消息,同步给集群中其他各个节点,其他节点就会更改自己保存的集群信息。

重启 redis1 节点,该节点就会以从节点的方式加入集群。

集群会宕机的情况

  1. 某分片的主从节点都挂了
  2. 某分片主节点挂了,但是没有从节点
  3. 超半数主节点挂了

5.5 集群扩容

新节点加入集群

bash 复制代码
redis-cli --cluster add-node 172.30.0.110:6379 172.30.0.101:6379

前一个是新节点地址,后一个集群中任意节点的地址。

进入任意一个 redis 客户端,可以看到该节点已成功加入集群中。

重新分配 slots(槽位)

bash 复制代码
redis-cli --cluster reshard 172.30.0.101:6379

注意是 reshard 切分 ,不是 reshared 重新分享 ,不要多写 e

reshard 后面写 集群中任意一节点的地址。

输入命令之后,如下:

首先,它会问你要移动多少个槽位,这里我们要均分,就填 4096。

然后,问你要将这些槽位移动到哪个节点,这里我们填写刚加入的节点 的 id。

最后,会问你这些槽位你准备从哪些节点节点中取,这里我们填 all,即从其他所有节点中取。

这之后,它会输出将要进行操作,yes 同意即可。

接入集群,可以看到新节点已经有槽位了

新节点添加从节点

bash 复制代码
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]

可以看到已经成功加入

5.6 集群缩容

实际中一般都是扩容,很少有缩容,所以了解即可。

一、 删除从节点

bash 复制代码
redis-cli --cluster del-node [集群中任⼀节点ip:port] [要删除的从机节点 nodeId]

二、重新分配 slots

要删除的主节点,有 4096 个 slots,就将这些槽位分成三份分给其他三个主节点(1365 + 1365 + 1366),多一两个,少一两个槽位无所谓。

和前面分配槽位类似,这里只展示一个的处理。

三、删除主节点

bash 复制代码
redis-cli --cluster del-node [集群中任⼀节点ip:port] [要删除的从机节点 nodeId]
相关推荐
山峰哥17 小时前
3000字深度解析:SQL调优如何让数据库查询效率提升10倍
java·服务器·数据库·sql·性能优化·编辑器
iAkuya17 小时前
(leetcode)力扣100 35 LRU 缓存(双向链表&哈希)
leetcode·链表·缓存
tyatyatya17 小时前
MySQL Group Replication(MGR)集群部署,实现自动故障切换
数据库·mysql
b***594317 小时前
mysql 迁移达梦数据库出现的 sql 语法问题 以及迁移方案
数据库·sql·mysql
木风小助理17 小时前
MySQL中COUNT()、COUNT(1)与COUNT
数据库
不想上班的小吕17 小时前
采购申请创建(BAPI_PR_CREATE/BAPI_REQUISITION_CREATE)
java·服务器·数据库
j***894617 小时前
MySQL官网驱动下载(jar包驱动和ODBC驱动)【详细教程】
数据库·mysql
emma羊羊17 小时前
Vulhub-Mysql靶场
数据库·mysql