【Redis】集群(Cluster)

之前哨兵模式的存在,我们解决了节点间的高可用性。但是真正用来存储数据的还是 master 和 slave 单个节点. 所有的数据 都需要存储在单个 master 和 slave 节点中.

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

那有人说,我们垂直扩展,直接将master,slave所在的机器的性能,以及内存,给它整成1TB不就行了?那也有上限啊,所以这种方案不可行。

所以上述问题就是,**所有数据都存放在单个节点!**主从复制和哨兵模式只是保证了高可用性!

所以我们需要多组master/slave节点来进行扩展,将数据平摊到多个节点,即水平扩展。

🐼什么是集群

什么是集群呢?我们之前学习的哨兵模式,只要有多个机器构成了一个分布式"系统",那么都可以成为是集群。前面学习的哨兵,主从结构的模式,可以称为是"广义的集群"。

而狭义的集群,也是这里我们所说的集群模式,即这个集群模式下,主要解决存储空间不足的问题(扩展存储空间)。

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

假定整个数据全集是 1 TB, 引⼊三组 Master / Slave 来存储. 那么每⼀组机器只需要存储整个数据全集的 1/3 即可.如图:

是光引入多组master吗?不是,还要有对应的多个slave,即每一组都是master和slave相互配合。


🐼如何分片?

可是,对于上述问题,如何进行分数据呢?我怎么把数据分到每一组master/slave上,合理呢?

我们把这种分数据的模式,每一组master/slave叫做一个分片。

主要有三种分片算法:

✅哈希求余

这种分片算法,大致思想为,设有 N 个分片.针对某个给定的 key, 先计算 hash 值, 再把得到的结果 % N(N就是分片数), 得到的结果即为分片编号.

复制代码
index = hash(key) % 分片的组数

所以,通过以上方式,我们就能算出一个index,即分片的下标,该key属于第几个分片,此时就可以把数据插入到这个分片了。这种思想有点类似于哈希表的映射。其中hash算法,可以有很多算法,这里一md5算法为例。

  1. MD5算法有以下特点:

  2. 计算结果是定长的。MD5计算结果是不可逆的(加密)

  3. 计算结果是分散的,尽管相差一个字母,但是经过MD5算法后的结果差别很大。

了解了MD5算法,举个例子:

比如:N 为 3. 给定 key 为 hello, 对 hello 计算 hash 值(⽐如使⽤ md5 算法), 得到的结果为 bc4b2a76b9719d91 , 再把这个结果 % 3, 结果为 0, 那么就把 hello 这个 key 放到 0 号分片上.

分析一下这种算法的优缺点:

💮优点:简单高效, 数据分配均匀。

可是,如果当我们3组分片的总存储量满足不了我们的需求了,需要4组分组,即扩容,这种方式还合理吗?根据我们上面的映射法则,hash(key)是不变的,但是分片的组数变化了 ,最后的index计算结果就会差异很大!如果发现某个数据,扩容之后,不应该待在当前的分片了 ,就需要重新分配,搬运数据。如图:

我们可以看出来这种算法的缺点,即本上都需要搬运,真正没有搬运的数据仅仅是3/20。

⭕缺点: 一旦需要进行扩容, N 改变了, 原有的映射规则被破坏, 就需要让节点之间的数据相互传输, 重新排列, 以满足新的映射规则. 此时需要搬运的数据量是比较多的, 开销较大.

.


✅一致性哈希算法

基于上述算法,我们来分析一下问题在哪,就是重新计算映射,n发生了变化,当前key属于哪个分片,是交替的。因此这里引入了一致性哈希算法来解决上述问题,把交替变为了连续。但同时又引入了新问题。

我们来看一下一致性哈希算法实现的分区过程:

第⼀步, 把 0 -> 2^32-1 这个数据空间, 映射到⼀个圆环上. 数据按照顺时针方向增长.假设当前存在三个分片, 就把分片放到圆环的某个位置上.如图:

假定有⼀个 key, 计算得到 hash 值 H, 那么这个 key 映射到哪个分⽚呢? 规则很简单, 就是从 H 所在位置, 顺时针往下找, 找到的第⼀个分片, 即为该 key 所从属的分片,如图:

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

在这个情况下, 如果扩容⼀个分片, 如何处理呢?原有分片在环上的位置不动, 只要在环上新安排⼀个分片位置即可.如图:

此时, 只需要把 0 号分片上的部分数据, 搬运给 3 号分片即可 . 1 号分片和 2 号分片管理的区间都是不变的。但是我们发现,原本属于0号分片的数据此时被切分后,0,1,2,3,的数据没有被均分了。1,2,的数据多余0,3的,这就发生了数据倾斜。

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

缺点: 数据分配不均匀 (有的多有的少, 数据倾斜).


✅哈希槽分区算法

为了解决上述问题 (搬运成本高和 数据分配不均匀), Redis cluster 引⼊了哈希槽 (hash slots) 算法.

cpp 复制代码
hash_slot = crc16(key) % 16384
  1. 计算槽位。其中crc16也是一种计算哈希值的算法。其中计算结果就是槽位。然后再把这些槽位比较均匀的分配给每个分片. 每个分片的节点都需要记录自已持有哪些槽位.

  2. 进行分片。然后根据实际需求,进行分片:

假设当前有三个分⽚, ⼀种可能的分配⽅式:

0 号分片: [0, 5461], 共 5462 个槽位

1 号分片: [5462, 10923], 共 5462 个槽位

2 号分片: [10924, 16383], 共 5460 个槽位

之后,我们拿着key,计算出槽位,就能知道是在哪个分片上。所以哈希槽分区算法相当于结合了哈希求余和一致性哈希

🌞注意:这里的分片规则是很灵活的. 每个分片持有的槽位也不⼀定连续.每个分片的节点使用位图 来表示自已有哪些槽位,0表示不拥有,1表示拥有.

如果遇到扩容问题呢?

比如新增⼀个 3 号分⽚, 就可以针对原有的槽位进⾏重新分配. 比如可以把之前每个分⽚持有的槽位, 各拿出⼀点, 分给新分片.

⼀种可能的分配方式:

• 0 号分片: [0, 4095], 共 4096 个槽位

• 1 号分片: [5462, 9557], 共 4096 个槽位

• 2 号分片: [10924, 15019], 共 4096 个槽位

• 3 号分片: [4096, 5461] + [9558, 10923] + [15019, 16383], 共 4096 个槽位

由于槽位是固定的,而哪个分区上有哪些槽位对应的我们也知道,实现了均匀分配槽位,并且搬移量上述例子也就搬移了1/4,解决了之前的问题。

现在想想哈希槽分区算法解决了哈希求余扩容搬运次数太多的问题。解决了一致性分区算法数据分配不均的问题。

🌞我们在实际使⽤ Redis 集群分片的时候, 不需要⼿动指定哪些槽位分配给某个分片, 只需要告诉某个分片应该持有多少个槽位即可, Redis 会自动完成后续的槽位分配, 以及对应的 key 搬运的工作.

还有两个问题:

🚀Redis集群中最多有16384个分片吗?

可以有,但不建议!因为16384个分片,意味着每个分片仅对应一个槽位。而有的槽位没有key,有的槽位有多个key,这样不就导致了分片不均衡了吗?导致了有的分片上没有key!

所以Redis的作者,建议分片数不超过1000.

🚀Redis集群中为什么是16384个槽位吗?

在实际cluster集群中,节点和节点间会发送心跳包,这是非常频繁的,表示该分片有多少槽位,持有哪些槽位。如果每个bit位对应一个槽位,那么16384即对应16384/1024/8 = 2kb。这个值对于网络带宽,不大,且基本够用。如果使用65536个槽位,理应分片数更多但 :位图大小 = 65536 / 8 / 1024 = 8KB。8Kb相比于2Kb也不大,但对于频繁的网络传输,这个代价是巨大的。


🐼集群搭建 (基于 docker)

下面,我们来看看Redis集群是如何搭建的。

此处我们先配置redis的配置文件

由于每个redis节点的配置文件大同小异,这里我们直接做一个脚本,帮我们配置了。

创建出 11 个 redis 节点. 其中前 9 个用来集群的搭建.后2个用来演示集群扩容。

首先先创建一个工作空间和脚本文件.

bash 复制代码
mkdir - p redis-cluster
cd redis-cluster
touch generate.sh

generate.sh内容是:

cpp 复制代码
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.conf 每个都不同. 以 redis1 为例:

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

区别在于每个配置中配置的 cluster-announce-ip 是不同的, 其他部分都相同.后续会用docker给每个节点分配不同的 ip 地址.

我们再来看一下这些配置的选项:

bash 复制代码
• cluster-enabled yes  开启集群.  
• cluster-config-file nodes.conf  集群节点⽣成的配置. 
• cluster-node-timeout 5000  节点失联的超时时间.  为了保持节点间保持联络
• cluster-announce-ip 172.30.0.101  节点⾃⾝ip.这个是我们在后续docker中配置的  
• cluster-announce-port 6379  节点⾃⾝的业务端⼝.  
• cluster-announce-bus-port 16379  节点⾃⾝的管理. 集群管理的信息交互
是通过这个端⼝进⾏的.  

我们发现上述配置cluster节点是有两个端口,一个为业务端口(6379),一个为管理端口(16379),这两个端口有什么区别呢?

业务端口是用来业务通信的。我们用redis-cli连接的,为用户服务的。

管理端口是为了管理方面的一些通信的,这里就是集群内部之间的信息交互。

这样执行完,按理说我们已经配置了11个redis节点的配置(目录及其对应的文件)。


下面我们再用docker容器创建redis容器。

此处我们为了控制我们的redis节点的网络号。我们用docker配置了一个静态的网段。然后将ip分配个每个redis节点。

我们这里以前两个节点一(redis1, redis2)的配置为例

docker.compose文件内容:

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

我们先配置了一个静态IP(这次我们自已分),一个网段。之后我们所有的redis节点的对外ip都是这个网络里面的。

剩下节点配置类似,只需要改一下对外的两个端口号和ip和我们刚刚配置文件的路径和redis名等..。

⭐️配置前需要注意的是:

你可以先ifconfig查一下你本地的网段,确定172.30.0.0/24这个网段没有被占用。如果被占用,只要是内网的网络号都可以替换(比如:172.16.*-172-31.* / / 192.168.* // 10.*)

其次需要注意:注意你的节点的mynet的ip地址的网络号要和配置的网段号一致。

然后redis节点要启动的配置文件我们已经配置好了,这里只需要指定就好了。

最后,注意,在启动之前,需要把之前的redis的所有进程都干掉。

启动容器:

bash 复制代码
docker-compose up -d

现在我们只是启动了11个单独的节点,还没有将他们配置为集群。下面,我们需要配置为他们为集群(注意,配置的命令选项去查就行,具体配置可能也不是100%一样的,所以具体问题具体分析)

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-replicas 2表示集群中的每个节点,有两个从节点,因为我们这里有9个节点,redis就知道要分3片,一共9个节点。

谁当主节点谁当从节点,这是完全取决于redis的,我们默认接受上述redis给我们分配的配置即可。如果不同意,可以下次让redis再随机配置

这样我们就配置了一个集群,我们可以任意连接一个节点,然后输入

bash 复制代码
cluster nodes 

我们同样根据这里slave依靠的replicationid也能知道,master的两个从节点是谁。

然后,我们之前所有学习的命令都是适用的,redis的主节点用于写/读,从节点只负责读。并且每个分片保存一部分数据。

可是我们这里发现,如果我们设置的key不在当前分片上,那么就会报错!

因此此时这个key不规这个分片管:

我们需要在启动客户端时带上一个-c,表示,如果key不在当前分片,帮我自动的重定向到指定的分片吧! 发生重定向,重定向到对应的分片上!

如果在当前分片,那么直接设置即可。

并且,我们尽管在分片一设置的,重定向到分片3,在分片3也能查找。

这也就说明了集群的特点,每个分片保存了整个数据的一部分,如果当前数据不在自已保存的范围内,就交给其他分片保存。而这对用户来说是"透明"的,用户不需要知道数据到底保存在哪个分片,尽管在master上写即可,master/slave读即可

不过这里还需要注意一点,就是如果我们使用mset,mget这种一次性操作多个key,可能不成功,因为key分散到了不同的分片上。就可能出现问题了。

Redis 集群的一个重要限制:MSET 等跨槽位批量操作需要所有 key 映射到同一个哈希槽

当然也可以解决,比如使用hashtag等强制到一个槽位等方式。


😐并且如果从节点挂了,如果主节点挂了,此处集群所做的工作,就和之前哨兵类似了,会从主机点的旗下选出一个从节点,然后提拔成主节点!

我们来看一下这个流程:

⼿动停止⼀个 master 节点

cpp 复制代码
docker stop redis1

连上 redis2

可以看到redis1已经挂了,并且它的从节点,redis5提拔为了主节点。

如果redis1重新启动,那么他会变为从节点,连接到redis5上!

可以使用cluster failover 进⾏集群恢复. 也就是把 101 重新设定成 master. (登录到 101 上执行)

此时redis1又恢复为了主节点!


✅如何判定一个主节点挂了?

首先,集群中的所有节点, 都会周期性的使⽤心跳包进行通信.

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 就彻底被判定为故障节点了.

某个或者某些节点宕机, 有的时候会引起整个集群都宕机 (称为 fail 状态).

以下三种情况会出现集群宕机:

• 某个分片, 所有的主节点和从节点都挂了.

• 某个分片, 主节点挂了, 但是没有从节点.

• 超过半数的 master 节点都挂了.

上述任意一种情况的发生都是极其危险的,都视为整个集群宕机。

核心原则是保证每个 slots 都能正常⼯作(存取数据).


✅如何提拔一个从节点为主节点?

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,再选master。这里就直接选出master了。


🐼集群扩容

首先需要知道,集群扩容是一件十分危险的事情。一不小心就把整个集群搞挂了。并且成本较大。所以,要找个夜深人静的晚上,偷偷的扩容,并且要十分有把握!

以下操作仅针对我当前集群的这种情况,具体扩容还需要具体分析。

扩容流程。

1. 第⼀步: 把新的主节点加⼊到集群,我们这里以redis10作为主节点.

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

add-node 后的第⼀组地址是新节点的地址. 第⼆组地址是集群中的任意节点地址,表示把新节点加入到哪个集群中。

2. 第二步:重新给新的主节点分配 slots

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

我们需要把之前三组的master每个上面取一点给新节点master。

现在我们一共有3+1 = 4个分片。总共16384个槽位,所以现在每个分片需要4096个槽位。具体操作如下:

执行之后, 会进⼊交互式操作, redis 会提示用户输⼊以下内容:

• 多少个 slots 要进⾏ reshard ? (此处我们填写 4096)

• 哪个节点来接收这些 slots ? (此处我们填写 172.30.0.110 这个节点的集群节点 id)

• 这些 slots 从哪些节点搬运过来? (此处我们填写 all, 表示从其他所有的节点都进行搬运)

当我们输入yes,那么就是一个比较重量级的搬运操作了。

3.第三步: 给新的主节点添加从节点

光有主节点了, 此时扩容的⽬标已经初步达成. 但是为了保证集群可⽤性, 还需要给这个新的主节点添加从节点, 保证该主节点宕机之后, 有从节点能够顶上.

这里为要指定101?可以把101想象为一个引路人,告诉"集群在哪里?"(地址)

cpp 复制代码
redis-cli --cluster add-node 172.30.0.111:6379 172.30.0.101:6379 --clusterslave --cluster-master-id [172.30.1.110 节点的 nodeId]

最后我们成功的又添加了一组分片,主节点是redis10,从节点是redis11.我们的集群实现共4组分片了。

现在有个问题,搬运过程中,客户端是否能够访问?

如果搬运的key,是不能访问的。但是大部分key是不需要搬运的,所以可以访问!

当然redis也支持缩容,缩容很少见,这里不讨论了

相关推荐
雨笋情缘2 小时前
未开启binlog时mysql全量备份
数据库·mysql
知识即是力量ol2 小时前
口语八股:MySQL 核心原理系列(二):事务与锁篇
java·数据库·mysql·事务·八股·原理·
Leon-Ning Liu2 小时前
Oracle云平台基础设施文档-控制台仪表板篇1
数据库·oracle
程序员敲代码吗2 小时前
MySQL崩溃问题:根源与解决方案
数据库·mysql
·云扬·2 小时前
MySQL Undo Log 深度解析:事务回滚与 MVCC 的底层支柱
android·数据库·mysql
海山数据库2 小时前
移动云大云海山数据库(He3DB)存算分离架构下Page页存储正确性校验框架介绍
数据库·架构·he3db·大云海山数据库·移动云数据库
SQL必知必会2 小时前
SQL 数据分析终极指南
数据库·sql·数据分析
SQL必知必会2 小时前
SQL 优化技术精要:让查询飞起来
数据库·sql
少云清2 小时前
【安全测试】5_应用服务器安全性测试 _SQL注入和文件上传漏洞
数据库·sql·安全性测试