集群介绍
哨兵模式提高了系统的可用性.但是真正用来存储数据的还是master和slave节点,所有的数据都需要存储在单个master和slave节点中, 如果数据量很大,大到超出了master/slave所在机器的物理内存,就可能出现严重问题了
为了获取更大的空间,就需要加机器
Redis集群就是在上述的思路之下,引入多组Master/Slave,每一组Master/Slave存储数据全集的一部分,从而构成一个更大的整体,称为Redis集群(Cluster)
比如我们要存储1T的数据,有3台机器,每台就只需要存储三分之一台机器即可
数据分片算法
redis集群的核心思路是把数据分发到不同的机器上存储,那么,给定一个具体的数据,这个数据应该存储到哪个机器(分片)上,读取的时候又应该去哪个机器(分片)上读取?
有三种主流的实现方式
1.哈希求余
设有N个分片,使用[0,N-1]这样序号进行编号
针对某个给定的key,先计算hash值,再把得到的结果%N,得到的结果即为分片编号
比如有三台机器(分片),N为3,对数据hello计算hash值,再把结果 % N, 结果为0,就把数据hello存在 0号分片上。读取的时候也是同样的方式
优点:
- 数据分布较为均衡,负载较为均匀。
- 扩展性好,可以较容易地增加或减少分片。
缺点:
- 扩展分片数量时,因为取模值变化,原来的绝大部分数据都需要迁移,性能低
- 无法保持数据的自然排序。
2.一致性哈希算法
一致性哈希算法是一种特殊的哈希算法,旨在解决节点动态变化(如增加或减少节点)时,尽量减少数据的重新分配。一致性哈希将所有可能的哈希值组织成一个虚拟的环,将节点分布在环上,然后将数据映射到环上相应的位置。
第一步,把0->2^32-1这个数据空间,映射到一个圆环上,数据按照顺时针方向增长
第二步,把分片映射到圆上,假设有三个分片
第三步,当有数据时,计算数据的哈希值,映射到圆上,顺时针往后找,找到的第一个分片,就是所属的分片
当需要增加分片时,只要在环上新安排一个分片位置即可
优点:
- 扩展性非常好,增加或移除节点时,只有一小部分数据需要重新分配。
缺点:
- 需要更复杂的算法和数据结构支持。
- 数据分配不均匀,数据倾斜
3.哈希槽分区算法(redis使用)
把整个哈希值,映射到16384个槽位上,也就是[0,16383], 然后再把这些槽位比较均匀的分配给每个分片.每个分片的节点都需要记录自己持有哪些分片
假设有一个三节点的 Redis 集群:
- 0号分片 负责哈希槽 [0,5461] 共5462个槽位
- 1号分片 负责哈希槽 [5462,10923] 共5462个槽位
- 2号分片 负责哈希槽 [10924,16383]共5460个槽位
这里的分片规则是很灵活的,每个分片持有的槽位也不一定连续
每个分片的节点使用位图来表示自己持有哪些槽位,对于16384个槽位来说,需要2048个字节(2KB)大小的内存空间表示。
如果需要扩容,新增一个分片
可以把之前的每个分片持有的槽位各自拿出来一点,分给新分片
新增一个节点可能的分配方式:
- 0号分片 负责哈希槽 [0,4095]共4096个槽位
- 1号分片 负责哈希槽 [5462,9557]共4096个槽位
- 2号分片 负责哈希槽 [10924,15019]共4096个槽位
- 3号分片 负责哈希槽 [4096,5461] + [9558,10923] + [15020,16383]共4096个槽位
我们在实际使用Redis集群分片的时候,不需要手动指定哪些槽位分配给某个分片,只需要告诉某个分片应该持有多少个槽位即可,Redis会自动完成后续的槽位分配,以及对应的key搬运的工作
虽然Redis设计了16384个槽位,但并不意味着可以弄16384个分片,这对于集群的数据均匀和可用性是难以保证的(Redis作者建议分片数不超过1000)
至于为什么是16384呢,Redis作者的答案是:
大致意思是:
节点之间通过心跳包通信,心跳包中包含了该节点持有哪些slots.这个是使用位图数据结构表示的,表示16384(16k)个slots,需要的位图大小是2KB,如果给定的slots数更多了,就需要消耗更多的空间,比如65535个槽需要8kb位图,因为心跳包的传输非常频繁,多几kb也是很大的开销
另一方面,Redis集群一般不建议超过1000个分片,所以16k对于最大1000个分片来说是足够用的,同时也会使对应的槽位配置位图体积不至于很大
集群搭建
这是我们准备搭建的集群,共3个分片,每个分片是1主2从结构,另外我们还会创建两个redis节点,用于后面演示集群扩容
但是,在本次演示中,因为资源有限,所有演示都在同一台机器上运行,作学习使用,但开发环境下,把节点放在一个机器上没有任何意义。
创建目录和配置
创建redis-cluster 目录
内部创建两个文件
redis-cluster/
├── docker-compose.yml
└── generate.sh
编写generate.sh
内容如下
注意bind也要改,因为我们会给每个容器配置不同的ip,那bind也要bind对应的ip
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 172.30.0.10${port}
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 172.30.0.1${port}
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
这个脚本的意思是:
1.循环创建编号为 1 到 9 的节点的配置文件
把下面的配置追加到对应的配置文件里
配置里的ip是172.30.0.10x 其中x是1-9
2.下面和上面的区别是,ip不同,因为从10开始占两位,所以是172.30.0.1x 其中x是10-11
cluster-enabled yes
- 启用集群模式。
no
(默认值)禁用集群模式。cluster-config-file nodes.conf 这个参数指定了集群配置文件的名称和位置。Redis 将在运行目录下创建和更新 nodes.conf 文件。
- 用途:Redis 集群使用这个文件来存储节点的配置和状态信息,包括节点 ID、集群拓扑结构、槽分配等。
- 文件内容:该文件包含集群节点的信息以及节点之间的连接关系,它由 Redis 自动管理和更新。
cluster-node-timeout 5000 这个参数指定了集群节点超时的时间,单位是毫秒。表示如果节点在 5 秒内没有响应,它将被认为是不可用的。
- 用途:定义节点之间通信的超时时间。如果一个节点在这个时间范围内没有响应,它可能会被其他节点标记为不可用。
- 影响:超时值设置得太低可能会导致正常节点被误判为失联,而设置得太高可能会延迟故障检测和恢复过程。
cluster-announce-ip 172.30.0.1${port} 这个参数指定了当前节点在集群中的公告 IP 地址。表示其他节点将使用 IP 地址
172.30.0.1
${port} 来与当前节点通信。
- 用途:集群中的其他节点将使用这个 IP 地址与当前节点进行通信。特别是在 NAT 或多网卡环境下,该参数非常重要。
- 设置:应设置为当前节点的实际 IP 地址,确保集群中的其他节点可以通过这个 IP 地址访问当前节点。
cluster-announce-port 6379 这个参数指定了当前节点在集群中的公告端口号。表示其他节点将使用端口号
6379
来连接当前节点的 Redis 服务。
- 用途:其他节点将使用这个端口号来连接当前节点。
- 设置:应设置为当前节点的实际 Redis 服务端口号。
cluster-announce-bus-port 16379 这个参数指定了当前节点在集群中的总线端口号。表示集群节点将使用端口号
16379
进行内部通信。
- 用途:Redis 集群使用两个端口进行通信,一个是 Redis 服务端口(如 6379),另一个是总线端口(服务端口 + 10000,如 16379)。总线端口用于集群节点之间的内部通信,包括心跳和故障检测。
- 设置:通常设置为服务端口号加 10000 的值。
现在使用bash generate.sh执行脚本
编写docker-compose.yml
先创建networks,并分配网段为 172.30.0.0/24
配置每个节点.注意配置文件映射,端口映射,以及容器的ip地址.设定成固定ip方便后续的观察和操作
这里只展示创建network一个redis节点的配置,其余节点大差不差,注意修改ip、端口和名字
注意文件的缩进,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
启动容器
docker-compose up -d
构建集群
此处是把前9个主机构建成集群,3主6从.后2个主机暂时不用
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个是一个分片的。
这里谁当主节点,谁和谁分为同一片,由redis随机决定
可以看出,这里列出了三个主节点,分配了槽,分别是[0,5460][5461,10922][10923,16383]
然后后面列出了各个节点的id和ip、端口。从节点也列出了主节点id
现在,只是列了出来,还没有构建集群,输入yes开始构建
输入yes后,节点会开始分配拷贝数据,建立集群、主从关系
构建成功
查看集群信息
先连到集群任意一个节点上,现在九个节点本质上是一个整体的
CLUSTER NODES
查看集群信息
从这里可以看出各个节点主从关系,槽位信息
这时候,插入数据,可能会因为没有对应的槽而插入失败
可以在连接时用-c选项,让插入的数据重定向插入到对应的节点中
这里显示槽位是9189重定向到172.30.0.102
我们去这个机器上看看
数据存在,说明插入数据重定向成功
节点宕机
如果集群中宕机的是从节点,那影响不大,如果是主节点呢?
现在随便手动停止一个主节点
docker stop redis2
可以看见,172.30.0.102显示fail 已经挂了
而172.30.0.107变成了主节点,接手了对应的槽
重新启动102后,102变成了从节点
故障判定
在Redis集群中,节点之间会定期通过心跳包进行通信。
1.当节点A向节点B发送ping包时,节点B会回复pong包。除了消息类型外,ping和pong包的内容几乎相同,包含了集群的配置信息,如节点ID、所属分片、主从角色以及持有的槽位信息。
2.每个节点每秒会向一些随机节点发送ping包,而不是向所有节点发送。这样设计可以避免在节点数量较多时心跳包数量呈二次方级增长的问题。
3.当节点A发送ping包给节点B,但B未能及时回复时,节点A会尝试重置与B的TCP连接,查看是否可以重新连接。如果连接仍然失败,节点A会将节点B标记为PFAIL状态(主观下线)。
4.一旦节点A将节点B标记为PFAIL后,它会通过Redis内置的Gossip协议与其他节点进行沟通,确认节点B的状态。每个节点维护自己的下线列表,因此每个节点的视角可能不同。
5.如果多个节点都认为节点B处于PFAIL状态,并且超过了总集群节点数的一半,则节点A会将节点B标记为FAIL(客观下线)。节点A会将这一信息同步给其他节点,其他节点收到后也会将节点B标记为FAIL。
因此,这些步骤最终导致节点B被完全判定为故障节点。
以下三种情况会出现集群宕机:
某个分片,所有的主节点和从节点都挂了.
某个分片,主节点挂了,但是没有从节点.
超过半数的master节点都挂了
故障转移
在Redis集群中,当主节点B发生故障并被标记为FAIL后,如果B是主节点,就会触发故障迁移过程,将从节点提升为新的主节点,以继续提供服务给整个集群。以下是具体的故障迁移流程:
1.判断从节点的参选资格: 从节点(例如C和D)会首先判断自己是否具备参选成为新主节点的资格。如果从节点与主节点(B)的通信已经很久没有发生(超过了预设的阈值),从节点会失去参选资格,因为此时从节点的数据可能与主节点的数据有较大的差异。
2.休眠时间设定: 有资格参选的节点(如C和D)会进入休眠状态,休眠时间由一个基础时间(例如500ms)加上一个随机值(0到500ms之间的随机数),再加上一个根据节点在集群中的排名所决定的偏移时间(偏移时间越大,排名越靠前,休眠时间越短)。
3.拉票操作: 休眠时间结束后,节点C(假设)会向集群中的所有节点发起拉票操作,但只有主节点有投票资格。
4.投票过程: 主节点会将自己的投票投给候选节点C(每个主节点只有一票)。当候选节点C收到的票数超过集群中主节点数目的一半时,节点C就会被晋升为新的主节点。节点C会执行命令 slaveof no one
,表示不再是任何节点的从属,同时让从节点D成为它的从节点(执行 slaveof C
)。
5.同步消息: 新的主节点C会将自己成为主节点的消息同步给集群中的其他节点。其他节点接收到消息后也会更新自己保存的集群结构信息,确保整个集群对新的主节点状态的一致认知。
这样,通过故障迁移过程,Redis集群能够在主节点故障时自动调整,确保持续的可用性和服务能力。
选举流程也被称为Raft算法,是一种在分布式系统中广泛使用的算法
集群扩容
之前我们的redis10机器还没有用到,现在作为新节点加入到集群
redis-cli --cluster add-node 172.30.0.110:6379 172.30.0.101:6379
表示把110机器加入到101的集群中,当然,只要是集群中任意一个都行
加入新节点
可以看到110已经作为主节点加入集群,但是并没有分配槽
重新分配槽slots
redis-cli --cluster reshard 172.30.0.101:6379
reshard后面跟集群中任意地址
现在,redis跟我们交互,问我们需要重新切分多少槽slots?
我们填4096
redis问接收槽的id是什么?
我们把上面列出的110的id填上去
redis问我们从哪个节点搬运过来
我们输入all ,表示从所有节点搬(每个人都给点~)
然后redis会列出移动的槽序号,问是否确定,然后输入yes开始执行
运行结束我们再看集群信息
发现110被分配了槽,分别是0-1364,5461-6826,10923-12287
分配完成
在搬运key的过程中,对于那些不需要搬运的key,访问的时候是没有任何问题的.
但是对于需要搬运的key,进行访问可能会出现短暂的访问错误(key的位置出现了变化),随着搬运完成,这样的错误自然就恢复了.
添加从节点
为了保证集群可用性,还需要给这个新的主节点添加从节点,保证该主节点宕机之后,有从节点能够顶上
redis-cli --cluster add-node 172.30.0.111:6379 172.30.0.101:6379 --cluster-slave
再观察集群信息
发现111已经作为110的从节点加入到集群了