admin管理员组文章数量:1436886
初识Redis · 集群
前言:
在我们前文介绍的主从复制,主从模式+哨兵模式下,确实能够提高Redis的高可用性,但是不管是主从模式还是加上了哨兵,总的数据容量还是只有一个节点那么大,那么问题来了,在提高高可用的基础上,我们能不能有效的扩展Redis整体的存储容量呢?
答案当然是可以的,本章开始,我们就引入了集群,我们可以通过集群,来提高整体的存储容量,那么具体怎么实现的呢,请移步下文吧!
数据分片算法
在学习集群之间,我们可以先来看看集群的定义,从广义上来看,只要是多个机器,构成了分布式系统,那么就可以称之为一个集群,前面的主从模式,哨兵模式,就是多个主机一起构成了分布式系统,这些就可以称之为广义的集群。从狭义上来看,是Redis官方自带的一种原生分布式方案,是一种明确的部署架构。
当然,不管是狭义上还是广义上,都是要引入多台机器的,那么多个机器之间的数据怎么协同,数据怎么分配就成了一定的问题。当然了,引入了这么多台机器,每台机器我们称为一个分片,即我们后面考虑的是如何在分片上存储数据以及分片之间如何协同诗句。
比如我们最能理解的,有1000个数据,5台主机,每台主机分配200个数据就行,但是实际上的场景远比这个复杂,比如这200个数据是顺序分配还是随机分配?如果涉及到扩容了,涉及到了数据插入又该如何分配?
这种问题就是我们今天的重点,即数据分片算法。对于常见的数据分片算法分为了三种,我们依次介绍,分别是哈希求余,一致性哈希算法,哈希槽分区算法。
哈希求余
哈希求余借助的是哈希表的思想,即将key通过某种方式计算成16进制的数,然后模上总分片的个数,我们能够都到一个确定的数值,这个数值可以充当下标,根据下标来选择插入的分片。
比如有三个分片,分别是0 1 2,上述的某种方式可以是md5加密算法,通过md5加密算法来得到一个确定的16进制的值,然后这个值模上3,那么结果就是0 1 2的某一个,此时插入该数据即可。
说到md5,我们可以简单的了解一下md5加密算法:MD5在线加密/解密/破解—MD5在线
中间我们可以选择是16位的还是32位的。
md5具有以下的特点: 1.计算结果是定长的。2.计算结果是分散的。3.理论上可逆,但实际上很难还原出来。
定长的特点体现在不管我们输什么,最后的数字长度都是一样的,计算结果分散表现在很长的字符串中我们就修改一个字母也会造成很大的变化。
不过MD5 是不可逆的哈希函数,但实际可以通过暴力、彩虹表等手段“反查”原文,因此它已不再安全,推荐使用更现代、更强壮的哈希算法。
那么今天加密算法不是我们的主题,我们简单了解一下就可以了。
对于哈希求余的这个分片算法来讲,我们知道有一步是通过hash(key) mod N == ?那么问题来了,如果后面数据量起来了,那么我们势必要引入更多的分片,此时的N就变成了N+1,那么导致的后果就是数据量迁移是非常大的。
当N等于3的时候,100到120的数据是这么分布的,那么N等于4的时候,数据是这么分布的:
我们可以发现一共有20个数据,只有3个数据不需要迁移,那么如果是20亿个数据呢?这迁移量是非常恐怖的。
所以上述分片算法,在生产环境中几乎是不能使用的,只能通过替换的方式来扩容,比如我在原来三台机器的基础上,重新引入4台数据,将三台机器的数据拷贝到四台机器上并且插入新来的数据,不过这个成本是非常大的,所以哈希求余算法成功被pass掉了。
一致性哈希算法
在上面的哈希求余算法中,我们发现最大的弊端就是连续的数据是交替出现的,这就导致了搬运数据的时候成本较高,那么我们是否可以通过将交替出现改成连续出现进行优化呢?
当然是可以的,优化了之后,它的名字叫做一致性哈希算法:
它将整个哈希空间看作是一个按照顺时针的圆环,将key通过哈希函数映射到环上的某个点,哈希环的取值范围是【0,2^32-1】,那么范围为什么是2^32-1到0呢?
因为使用的是常见的32位的哈希函数,当然也有使用64的,不过64的空间有点大了,16位的又害怕哈希碰撞,32位的在大多数中型系统已经足够使用了。
在实际的数据分配中,会引入虚拟节点机制,用来平衡数据倾斜:
当我们引入新的节点之后,数据会迁移的就只有0号和3号分片本身,相对来说迁移的数据就非常少了。但是引入的问题就是这几个分片上的数据量就可以能不均匀了,从而导致某个节点的负载量过大。
一致性哈希的本质是为了解决节点动态变动时的数据最小迁移问题,但它在负载均衡、实现复杂度、多 key 操作等方面存在缺陷,因此 Redis Cluster 采用了更工程化的 slot 分区机制作为替代。
而Redis没有采取一致性哈希算法的原因大致如下:
问题类别 | 具体表现 |
---|---|
数据分布 | 物理节点分布不均导致负载倾斜;需依赖大量虚拟节点修正 |
数据可靠性 | 无内建副本机制,无法应对节点宕机或数据丢失 |
查询能力 | 仅适合单 key 精准定位,不适用于范围查询或多 key 操作 |
实现复杂度 | 环结构构造、查找、高效维护逻辑复杂;运维困难 |
可扩展性细节 | 虚拟节点增多后,查找效率下降,状态同步复杂 |
哈希函数依赖性 | 过度依赖哈希函数质量,选择不当会严重影响性能与平衡性 |
哈希槽分区算法
出于负载均衡,查找效率,数据保存等方面的问题的考虑,Redis官方使用了哈希槽分区算法。同样的,它用到了哈希算法crc16,引入了槽的概念hash_slot = crc(key) % 16384,我们假设一共有16384个槽,每个key对应一个槽,并且因为有16384,即2的14次方个位置,所以每个分片会引入一个数据结构来表示槽是否有数据:位图。
假设当前有三个分⽚, ⼀种可能的分配⽅式:
0 号分片: [0, 5461], 共 5462 个槽位
1 号分片: [5462, 10923], 共 5462 个槽位
2 号分片: [10924, 16383], 共 5460 个槽位
我们要注意了,这里的分片是非常灵活的,每个分片持有的槽位也不一定连续,而对于16384槽位来说需要使用2048个字节的内存空间来表示,几乎可以忽略了。
比如我们插入了一个三号分片,此时重新分配槽位,分配的结果是:
0 号分片: [0, 4095], 共 4096 个槽位
1 号分片: [5462, 9557], 共 4096 个槽位
2 号分片: [10924, 15019], 共 4096 个槽位
3 号分片: [4096, 5461] + [9558, 10923] + [15019, 16383], 共 4096 个槽位
分配了之后,我们发现结果是尽量保持原来的结果不变,每个分片尽量从自己这里截取等量的槽位用来分配给新的分片,这样的数据迁移量是非常少的。
所以Redis在这里有一个很重要的考虑点就是尽量减少数据的迁移量,而有的同学可能会好奇为什么槽位是16384个?
从Redis作者的角度看来的话:
Redis 作者的答案:
Normal heartbeat packets carry the full configuration of a node, that can be replaced in an
idempotent way with the old in order to update an old config. This means they contain the
slots configuration for a node, in raw form, that uses 2k of space with 16k slots, but would
use a prohibitive 8k of space using 65k slots.
At the same time, it is unlikely that Redis Cluster would scale to more than 1000 master
nodes because of other design tradeoffs.
So 16k was in the right range to ensure enough slots per master with a max of 1000 masters, but
a small enough number to propagate the slot configuration as a raw bitmap easily. Note that in
small clusters, the bitmap would be hard to compress, because when N is small, the bitmap
would have slots/N bits set. That is a large percentage of bits set.
翻译过来⼤概意思是:
节点之间通过⼼跳包通信. ⼼跳包中包含了该节点持有哪些 slots. 这个是使⽤位图这样的数据结构
表⽰的. 表⽰ 16384 (16k) 个 slots, 需要的位图⼤⼩是 2KB. 如果给定的 slots 数更多了, ⽐如 65536 个了, 此时就需要消耗更多的空间, 8 KB 位图表⽰了. 8 KB, 对于内存来说不算什么, 但是在频繁的网络心跳包中, 还是⼀个不小的开销的.
另一方面,Redis 集群⼀般不建议超过 1000 个分片. 所以 16k 对于最大1000 个分片来说是足够用
的, 同时也会使对应的槽位配置位图体积不至于很大.
以下是从各个角度的解析:
维度 | 说明 |
---|---|
数值选择 | 16384 = 2^14,是 2 的幂,方便用位运算优化 CRC16(key) % 16384 的计算 |
数据迁移粒度 | 槽数多于节点数,便于将槽平均分配到多个节点,同时也支持灵活的数据迁移(按槽级别) |
负载均衡 | 比直接用一致性哈希更细粒度,有利于将 key 分布更均匀地打散到不同节点 |
元信息开销可控 | 只需维护 16384 条槽位 → 节点映射表,系统开销远小于为每个 key 单独记录映射 |
与 CRC16 匹配 | Redis 使用 CRC16 作为哈希函数,输出范围 0∼65535,取模 16384 分布较均匀 |
集群扩展性 | 16384 个槽足以支撑上百个节点部署(每个节点管理几十~几百槽),满足大多数集群场景需求 |
实现复杂度低 | 固定槽数使得代码实现和运维更简单,不需要动态维护 hash 环结构或 slot 空间 |
支持多 key 操作 | 使用槽机制 + {} keytag,可以明确 key 是否落在同一槽,从而允许有限的 multi-key 操作 |
这里大家可能是对比直接用一致性哈希更细粒度不是很理解,这里的更细粒度代表了操作数据分布的最小数据单位,一致性哈希算法操作数据的时候是通过节点来控制数据如何分布的,并且因为一致性哈希算法只是更好的解决Key的映射问题,但是对于数据分布,怎么分布的,它解决的不是很好,它的基本数据操作单位就是节点,对于哈希槽算法来说,一个槽可以有多个key,大概是6100左右,它操作数据的基本单位就是槽,这可比一致性哈希算法细化太多了。
那么现在就清楚了三个分片算法,并且知道Redis采用的是哈希槽分区算法,我们现在就来使用docker简单模拟。
使用docker模拟集群
创建集群并使用
使用docker模拟的时候我们创建11个节点,其中两个节点用来展示后面扩容操作。我们构建的一个集群大致样子如下:
有三个分片,每个分片的有两个从节点,我们在保证拓展数据量的同时,还是要注意数据安全和一致性的。
我们首先创建一个redis-cluster目录,里面加上generate.sh和docker-compose.yml文件,一个脚本是我们用来快速创建每个节点对应的目录的,yml文件就不用多说了吧,里面的代码配置如下:
代码语言:javascript代码运行次数:0运行复制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
cluster-enabled yes 开启集群.
cluster-config-file nodes.conf 集群节点生成的配置.
cluster-node-timeout 5000 节点失联的超时时间.
cluster-announce-ip 172.30.0.101 节点自身ip.
cluster-announce-port 6379 节点自身的业务端口.
cluster-announce-bus-port 16379 节点自身的总线端口. 集群管理的信息交互是通过这个端⼝进⾏的.
执行bash generate.sh之后,我们的目录就变成了:
就有了这种结构,每个节点都有自己的目录和配置文件。
代码语言:javascript代码运行次数:0运行复制version: '3'
networks:
mynet:
ipam:
config:
- subnet: 172.30.0.0/24
services:
redis1:
image: 'redis:7.2'
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:7.2'
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:7.2'
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:7.2'
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:7.2'
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:7.2'
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:7.2'
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:7.2'
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:7.2'
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:7.2'
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:7.2'
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
这是docker-compose.yml的文件。
当我们的配置文件OK了之后,我们现在来启动1-9的容器,10和11后面做演示使用。
我们首先启动docker容器,然后输入以下命令:
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
然后我们就能得到以下集群:
输入yes之后开始确定:
这两个的含义其实是一样,我们通过第一张图片我们就能确定主从关系了,不过那只是redis给出的预备方案,只要我们输入yes,它才会构建真正的集群。
我们能清晰的得到槽位和replid,以及主从关系等。
那么集群我们就创建好了,我们任意进去一个客户端,使用cluster nodes查看当前集群的信息:
不过这里我们注意我们是可以通过指定docker内容器的ip来连接指定的redis,连接之后输入:
从这里我们能获取到replid 谁是主节点,主节点有多少槽位,它的从节点是谁等。
而有意思的是,我们是通过crc16算法来获取的哈希值并给到对应的槽位,如果我们连接的机器没有这个槽位怎么办?
很简单,我们启动的时候加上-c的选项,这样进行操作的时候会自动进行跳转:
这里就跳转到了另一个机器set,并且我们要注意一个点,在创建集群的时候,主从节点我们是没有办法指定的,所以主节点都是随机的。
当然了,在集群模式下不是所有的命令都能顺序执行的,比如我们使用mget,同时涉及到了多个机器,那么就会失败,这种情况我们可以通过打tag来解决,这里不做演示:
并且如果我们走的是从节点,在从节点上写的话,那么也会自动重定向:
以上我们验证了集群的set在主从节点和不同主节点之间的操作,接下来我们验证集群模式下的故障转移机制。
故障转移
我们先挂一个主节点:
挂了redis1节点之后,我们发现集群马上重新选取了一个master节点,并且分配了对应的槽位。而且redis1我们也看到了它的标识变成了fail。
当我们重启redis1节点之后,我们发现它不出所料的变成了从节点:
那么在哨兵机制中我们知道了故障转移的操作,在集群中也有类似的操作。
首先是故障判定:
1.节点 A 给 节点 B 发送 ping 包, B 就会给 A 返回⼀个 pong 包. ping 和 pong 除了 message type属性之外, 其他部分都是⼀样的. 这⾥包含了集群的配置信息(该节点的id, 该节点从属于哪个分片,是主节点还是从节点, 从属于谁, 持有哪些 slots 的位图...).
2.每个节点, 每秒钟, 都会给⼀些随机的节点发起 ping 包, 不是全发⼀遍. 这样设定是为了避免在节 点很多的时候, 心跳包也非常多(比如有 9 个节点, 如果全发, 就是 9 * 8 有 72 组心跳了
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)
总体来说还是一个先主观下线然后再客观下线的过程,并且这个过程每个节点都维护了自己的一份下线列表。
那么在故障迁移中,会涉及到raft算法,以下的描述较为粗糙,咱们可以借助理解:
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 还会把自己成为主节点的消息, 同步给其他集群的节点. ⼤家也都会更新自己保存的集群结构信息.
总体来说它的一个思想是从节点先确定自己是否有参选资格,然后按照休眠时间依次醒来,最早醒的节点就开始拉票,票数达到一定标准之后,就开始执行对应的命令和通知其他节点了。
那么既然有故障,也就有因为故障导致的集体宕机,大致分为以下三种情况:
某个分片, 所有的主节点和从节点都挂了.
某个分片, 主节点挂了, 但是没有从节点.
超过半数的 master 节点都挂了.
其实一二两种情况都是某个分片没有办法提供数据服务了,第三种是大规模的主节点罢工,也是非常严重了。
集群扩(缩)容
接下来我们演示集群扩容的操作,我们需要记住:集群扩容是一种风险较大,成本较高的操作。
输入redis-cli --cluster add-node 新加的节点:某个节点,后面的某个节点代表把这个节点加入到哪个集群中。
此时就有了对应的信息。
然后我们可以进入查看:
不过我们也发现了一个问题,它自己作为主节点的同时,它没有拥有任何槽位,此时我们就需要共享一下槽位:
输入命令redis-cli --cluster reshard 集群中某个节点的Ip。
此时就询问我们想要移动多少槽位,我们本着公平的原则就均分,所以输入4096:
后面还有两个选项,分别是接受槽位的主机id以及我们想从哪几个机器上移动,输入all代表所有的。
后面就开始移动槽位了。
此时我们也确实发现了,110拥有了自己的槽位了。
那么我们要注意,在移动槽位的时候,客户端访问的话大概率是会报错的,所以我们应该在夜深人静的时候悄悄移动~~
代码语言:javascript代码运行次数:0运行复制redis-cli --cluster add-node 172.30.0.111:6379 172.30.0.101:6379 \
--cluster-slave --cluster-master-id d2de62adad4758ebf6e831ebbdf3756f431bfca6
而110也应该有自己的从节点,我们使用上述命令给它添加,也好理解,先是添加到集群中,使用add-node,然后使用命令--cluster-slave表示它的从节点,然后指定master的id,就成功添加了!
那么对于集群缩容来说,就是减掉一些节点,减少分片的数量,当然,去掉之前是应该移动数据的,但是实际上很难用得上,所以我们了解就行。
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。 原始发表:2025-04-30,如有侵权请联系 cloudcommunity@tencent 删除迁移redis集群数据算法本文标签: 初识Redis集群
版权声明:本文标题:初识Redis · 集群 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/biancheng/1747433022a2696731.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论