Redis集群模式研究
Redis集群模式研究
上一篇文章讲过,Redis 集群模式是为了解决单个节点数据量大的问题,同时也解决节点故障问题。
这篇文章研究的点跟上篇差不多:
- 模式的特点是什么。
- 模式解决了什么问题。
- 模式的简单原理是什么。
- 尝试搭建一套对应模式的环境,进行一些测试等。
- 概括下这种模式的优缺点。
1. 集群模式是什么
Redis 的集群模式是 Redis 提供的一种分布式的解决方案,用于应对并发请求量高、Redis 中存储的数据量大的应用场景。
集群模式会将数据分散存储在多个 Redis 节点上,这样可以容易实现 Redis 的水平拓展。
在 Redis 集群模式中,数据会被划分成多个分片 ( shard ) ,每个分片包含一部分数据。 这些分片在多个节点上,每个节点负责处理一部分请求。 当客户端发送请求时, Redis 会根据请求的 key 路由到对应的分片上进行处理。
上面这种模式是一种比较简单的集群模式,所有节点都是主节点。这样的话如果有一个主节点宕机,那么这个节点的数据就会丢失,无法进行访问。
所以生产环境的集群模式也会进行主从,就是每个主节点会有多个从节点。 从节点是主节点的副本,从节点实时从对应的主节点进行数据同步,这样保持跟主节点的数据一致。但是这里从节点不会处理写的操作,只是充当一个数据备份以及负载均衡读数据请求处理。
2. 集群模式原理
主从模式要解决的问题有下面几个,这也是核心原理:
- 怎么对数据进行分片,分到每个节点上。
- 节点之间怎么进行通信。
- 节点故障后怎么处理。
2.1 数据分片
Redis 集群将整个数据集分为 16384 个哈希槽 ( Hash solt )。 每个 Redis key 都可以通过 CRC16 校验算法计算出哈希值,并根据哈希值确定一个哈希槽。
每个 Redis 节点会负责管理一个哈希槽的范围,通过哈希槽就可以确定在哪个 Redis 节点。
比如下面一个有一个三个节点的 Redis 集群,三个 Redis 节点管理的哈希槽范围为:
- Redis Node1 :0 - 5000
- Redis Node2 :5001 - 10000
- Redis Node3 :10001 - 16383
假设现在有操作一个 Redis key 为 123,计算这个 Key 应该在哪个 Redis 节点的步骤如下:
- 先使用 CRC16 算法计算 123 的哈希值,CRC16(123) = 15562 。
- 对 16384 取模,这样可以确保哈希槽落在 16384 以内。 15562 % 16384 = 15562 ,15562 % 16384 = 15562 编号。
- 按照上面图中三个节点,编号 15562 落在 10001-16383 的范围内 , 所以 key 【123】应该存储在节点 3 上。
2.2 节点间通信
Redis 集群中的节点通信是使用 Gossip 协议进行通信,交换节点的状态,这样就可以实现节点的管理以及维护。每个 Redis 节点会维护一份集群的状态信息,包括其他节点的状态、负责的哈希槽范围等等。 节点间定期进行数据交换,最终就可以保证集群状态的唯一性。
集群节点之间也会有类似于心跳检测的机制,每个节点会定期向其他节点发送 ping 消息,然后根据其他节点是否返回 pong 消息来判断其他节点的状态。 如果某个节点因为网络故障或者其他原因没有响应,那么这个节点在急群众就可以标记为离线状态。
2.3 节点故障处理
上面有提过, Redis 集群为了保证数据的高可用性,每个主节点也会有一个或者多个从节点做备份。 这里的从节点更加主要的作用还是当做备份使用,当从节点故障的时候,集群会自动把某个从节点提升为主节点。 这样可以保证服务的稳定性,如果故障节点后面上线了,那么这个节点就只能当做从节点,不会再切换回主节点了。
这种自动提升从节点为主节点的机制,就是 Redis 集群的故障处理方案。
3. 集群模式实践
搭建一个 Redis 集群步骤也不会特别的复杂,这里假设以一个3主3从的 Redis 集群为例,搭建主要的步骤如下:
准备好 6 台服务器,同时每台服务器上安装好相同版本的 Redis 版本。
给每台服务器上的 redis.conf 文件中增加如下配置:
#表示开启集群模式
cluster-enabled yes
# 集群节点的配置文件,这个文件由 Redis 自动生成自动维护
cluster-config-file /etc/redis/nodes.conf
# Redis 集群节点通信超时时间,毫秒
cluster-node-timeout 5000
依次启动每一台服务器上的 Redis 实例。
检查好每一个 Redis 节点启动完成之后,登录到任意一台服务器上执行如下命令:
redis-cli -p 6379 --cluster create 172.16.238.12:6379 172.16.238.13:6379 172.16.238.14:6379 172.16.238.15:6379 172.16.238.16:6379 172.16.238.17:6379 --cluster-replicas 1
上面的命令是告诉 Redis 创建一个新的集群,后面一共配置了 6个 Redis 节点的 ip 以及端口号。–cluster-replicas 这个选项指定每个主节点有多少个从节点,这里配置的是 1, 所以每一个主节点会有一个从节点。 总共加起来一共是6个节点,分别是 3主3从。
到这里如果看到跟下面类似的提示就说明集群搭建成功:
>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 172.16.238.16:6379 to 172.16.238.12:6379
Adding replica 172.16.238.17:6379 to 172.16.238.13:6379
Adding replica 172.16.238.15:6379 to 172.16.238.14:6379
M: 4794fb35f7d5c9a733f4113bd2e5d4e1278a372e 172.16.238.12:6379
slots:[0-5460] (5461 slots) master
M: 5e3aed61ccc210d74000f5fbbc6038ce6a892806 172.16.238.13:6379
slots:[5461-10922] (5462 slots) master
M: db99f2ca6f03b311037e9e3f40400d0f5b2a6750 172.16.238.14:6379
slots:[10923-16383] (5461 slots) master
S: 0ba9643eed0a17945fadf98c23c37d814cae54b8 172.16.238.15:6379
replicates db99f2ca6f03b311037e9e3f40400d0f5b2a6750
S: 9fae69d582e084e4b2d79ad9975b025f4f9ebd08 172.16.238.16:6379
replicates 4794fb35f7d5c9a733f4113bd2e5d4e1278a372e
S: 9fae69d582e084e4b2d79ad9975b025f4f9ebd08 172.16.238.17:6379
replicates 5e3aed61ccc210d74000f5fbbc6038ce6a892806
Can I set the above configuration? (type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join
.
>>> Performing Cluster Check (using node 172.16.238.12:6379)
M: 4794fb35f7d5c9a733f4113bd2e5d4e1278a372e 172.16.238.12:6379
slots:[0-5460] (5461 slots) master
1 additional replica(s)
M: 5e3aed61ccc210d74000f5fbbc6038ce6a892806 172.16.238.13:6379
slots:[5461-10922] (5462 slots) master
S: 0ba9643eed0a17945fadf98c23c37d814cae54b8 172.16.238.15:6379
slots: (0 slots) slave
replicates db99f2ca6f03b311037e9e3f40400d0f5b2a6750
M: db99f2ca6f03b311037e9e3f40400d0f5b2a6750 172.16.238.14:6379
slots:[10923-16383] (5461 slots) master
1 additional replica(s)
S: 9fae69d582e084e4b2d79ad9975b025f4f9ebd08 172.16.238.16:6379
slots: (0 slots) slave
replicates 4794fb35f7d5c9a733f4113bd2e5d4e1278a372e
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
上面这段提示的意思是成功创建了一个 6 个节点的 Redis 集群,其中 3 个节点作为主节点,3 个节点作为从节点,每个主节点都有一个从节点作为副本。
槽分配情况:
Redis 集群将 16384 个槽(slots)分配给了 3 个主节点。
172.16.238.12:6379 负责槽 0-5460(共 5461 个槽)。
172.16.238.13:6379 负责槽 5461-10922(共 5462 个槽)。
172.16.238.14:6379 负责槽 10923-16383(共 5461 个槽)。
从节点设置: 每个主节点都有一个从节点作为副本。
- 172.16.238.16:6379 是 172.16.238.12:6379 的副本。
- 172.16.238.17:6379 是 172.16.238.13:6379 的副本。
- 172.16.238.15:6379 是 172.16.238.14:6379 的副本。
最后可以登录任意 Redis 主节点或者 Redis 从节点查询集群信息:
# 列出集群所有节点信息
127.0.0.1:6379> cluster nodes
5e3aed61ccc210d74000f5fbbc6038ce6a892806 172.16.238.13:6379@16379 master - 0 1552379116524 9 connected 5461-10922
4794fb35f7d5c9a733f4113bd2e5d4e1278a372e 172.16.238.12:6379@16379 myself,master - 0 1552379116000 7 connected 0-5460
0ba9643eed0a17945fadf98c23c37d814cae54b8 172.16.238.15:6379@16379 slave db99f2ca6f03b311037e9e3f40400d0f5b2a6750 0 1552379115922 3 connected
db99f2ca6f03b311037e9e3f40400d0f5b2a6750 172.16.238.14:6379@16379 master - 0 1552379116929 3 connected 10923-16383
9fae69d582e084e4b2d79ad9975b025f4f9ebd08 172.16.238.16:6379@16379 slave 4794fb35f7d5c9a733f4113bd2e5d4e1278a372e 0 1552379116627 7 connected
简单的解释下上面的输出的集群节点信息:
- 每一行最前的是节点ID,比如上面的 5e3aed61ccc210d74000f5fbbc6038ce6a892806。
- 172.16.238.13:6379@16379:节点的 IP 地址、端口号及集群总线端口(通常是 Redis 端口+10000)。
- master:该节点的角色,这里是主节点。slave就是从节点。
- 【-】:如果节点是从节点,这里会显示它的主节点 ID。- 表示该节点是主节点,所以没有主节点 ID。如果有主节点的话就会有主节点的ID。
- 0:上次发送 PING 的毫秒时间。
- 1552379116524: 上次接收到 PONG 响应的毫秒时间。
- connected:当前节点的连接状态, connected 表示连接正常。
- 5461-10922:该主节点负责的数据槽范围。如果没有槽信息,说明是从节点,从节点不直接负责管理数据槽。
这样一个完整的 Redis 集群节点就算是搭建完成。接下来可以使用一个简单的 Python 脚本来验证 Redis 的集群是否正常工作。
import random
import string
from rediscluster import RedisCluster
def random_key(length=10):
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length))
def verify_cluster(startup_nodes):
try:
rc = RedisCluster(startup_nodes=startup_nodes, decode_responses=True)
nodes = rc.cluster_nodes()
master_count = sum(1 for node in nodes.values() if node['role'] == 'master')
slave_count = sum(1 for node in nodes.values() if node['role'] == 'slave')
print(f"集群状态: {'正常' if master_count == 3 and slave_count == 3 else '异常'}")
print(f"主节点数量: {master_count}")
print(f"从节点数量: {slave_count}")
test_data = {}
for _ in range(1000):
# 这里随机生成一些数据,数据分布到哪个 Redis 节点不需要应用去关心
key = random_key()
value = random_key()
rc.set(key, value)
test_data[key] = value
# 这里从集群节点读取数据,看是否可以读取到
print("正在验证数据一致性...")
for key, value in test_data.items():
assert rc.get(key) == value, f"数据不一致: key={key}"
print("数据一致性验证通过")
except Exception as e:
print(f"验证过程中出错: {e}")
if __name__ == "__main__":
startup_nodes = [
{"host": "172.16.238.12", "port": "6379"},
{"host": "172.16.238.13", "port": "6379"},
{"host": "172.16.238.14", "port": "6379"},
{"host": "172.16.238.15", "port": "6379"},
{"host": "172.16.238.16", "port": "6379"},
{"host": "172.16.238.17", "port": "6379"}
]
verify_cluster(startup_nodes)
上面这个脚本主要是测试了客户端从 Redis 获取集群的信息进行验证,同时测试数据是否正确写入到不同的节点。按照预期应该会输出:
集群状态: 正常
主节点数量: 3
从节点数量: 3
正在验证数据一致性...
数据一致性验证通过
4. 集群模式总结
集群模式的优点:
通过主从复制以及自动主节点切换,可以提供比较高的服务可用性。
可以动态的添加 Redis 节点,也可以删除 Redis 节点。
大数据量情况下数据可以拆分到不同节点上,降低每个节点压力。
缺点:
- 集群模式运维跟管理起来更加复杂。
- 在目前的版本中,不支持跨节点的 mget 、mset 等操作。
- 添加或者删除节点的时候,需要进行数据迁移,这个对性能跟带宽都会有一定影响。
- 在实现客户端的时候会更加的复杂,当然这些复杂的活一些开源的客户端已经做的差不多了。
- 数据可能会出现不均匀的情况,因为是通过哈希值去划分到哪个槽上,所以数据具有一定的随机性。
5. 集群模式场景错误
5.1 组建集群的时候提示 Node xxx:6379 is not empty
比如执行下面的命令组建集群的时候:
redis-cli -p 6379 --cluster create 172.16.238.12:6379 172.16.238.13:6379 172.16.238.14:6379 172.16.238.15:6379 172.16.238.16:6379 172.16.238.17:6379 --cluster-replicas 1
[ERR] Node 172.16.238.12:6379 is not empty. Either the node already knows other nodes (check with CLUSTER NODES) or contains some key in database 0.
这里是因为添加到 Redis 集群时,有一部分节点已经有一些数据或已经是另一个集群的一部分。Redis 集群要求在将节点加入集群之前,节点必须是干净的,必须做到:
节点没有包含任何数据,节点的数据库 0 中不能有任何键。
节点没有加入其他集群,节点不能已经属于另一个集群。
解决方案是将有数据的节点数据清除掉:
redis-cli -h 172.16.238.12 -p 6379 FLUSHALL
或者是重置下集群状态:
redis-cli -h 172.16.238.12 -p 6379 CLUSTER RESET
这样可以完全重置节点的集群状态,并删除所有集群配置。这个命令不会删除节点上的数据,但会将节点从任何集群配置中移除,然后再重新添加节点。
5.2 ERR SELECT is not allowed in cluster mode
执行切换 db 的时候报错:
127.0.0.1:6379> select 2
(error) ERR SELECT is not allowed in cluster mode
注意: Redis 集群模式下是不能使用 SELECT 命令来切换数据库的,因为 Redis 集群模式下不支持多数据库的概念。在不是集群模式下,Redis 默认提供 16 个逻辑数据库,即编号从 0 到 15 ,可以通过 SELECT 命令切换到不同的数据库。
集群模式下,Redis 只允许使用数据库 0,因为集群模式的设计理念是通过分布式槽将数据分布在多个节点上,而不是依赖于多个逻辑数据库。
5.3 连接不上集群
这种情况通常出现在 K8s 或者容器内部的访问地址。Redis 客户端需要知道集群中多个节点的地址,这样在需要时重定向请求。客户端连接时,可以通过连接 Redis 集群中的任意一个节点来获取整个集群的信息,然后根据请求的键值,自动重定向到正确的节点。
但是这种场景下 Redis 返回的集群节点列表会是内部的 ip ,比如上面集群返回的就是:
172.16.238.12:6379
172.16.238.13:6379
172.16.238.14:6379
172.16.238.x 这种 ip 如果是容器的 ip ,那么直接访问大概率是访问不通的,这也会造成集群连接不上。