七的博客

Redis集群模式研究

原理分析

Redis集群模式研究

上一篇文章讲过,Redis 集群模式是为了解决单个节点数据量大的问题,同时也解决节点故障问题。

这篇文章研究的点跟上篇差不多:

  • 模式的特点是什么。
  • 模式解决了什么问题。
  • 模式的简单原理是什么。
  • 尝试搭建一套对应模式的环境,进行一些测试等。
  • 概括下这种模式的优缺点。

1. 集群模式是什么

Redis 的集群模式是 Redis 提供的一种分布式的解决方案,用于应对并发请求量高、Redis 中存储的数据量大的应用场景。

集群模式会将数据分散存储在多个 Redis 节点上,这样可以容易实现 Redis 的水平拓展。

在 Redis 集群模式中,数据会被划分成多个分片 ( shard ) ,每个分片包含一部分数据。 这些分片在多个节点上,每个节点负责处理一部分请求。 当客户端发送请求时, Redis 会根据请求的 key 路由到对应的分片上进行处理。

基本的集群模式

上面这种模式是一种比较简单的集群模式,所有节点都是主节点。这样的话如果有一个主节点宕机,那么这个节点的数据就会丢失,无法进行访问。

所以生产环境的集群模式也会进行主从,就是每个主节点会有多个从节点。 从节点是主节点的副本,从节点实时从对应的主节点进行数据同步,这样保持跟主节点的数据一致。但是这里从节点不会处理写的操作,只是充当一个数据备份以及负载均衡读数据请求处理。

主从模式2

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哈希槽

假设现在有操作一个 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 ,那么直接访问大概率是访问不通的,这也会造成集群连接不上。