Redis 集群的实现原理是什么?

Sherwin.Wei Lv7

Redis 集群的实现原理是什么?

回答重点

Redis 集群(Redis cluster)是通过多个 Redis 实例组成的,每个实例存储部分的数据(即每个实例之间的数据是不重复的)

具体是采用哈希槽(Hash Slot)机制来分配数据,将整个键空间划分为 16384 个槽(slots)。每个 Redis 实例负责一定范围的哈希槽,数据的 key 经过哈希函数计算后对 16384 取余即可定位到对应的节点。

客户端在发送请求时,会通过集群的任意节点进行连接,如果该节点存储了对应的数据则直接返回,反之该节点会根据请求的键值计算哈希槽并路由到正确的节点。

简单来说,集群就是通过多台机器分担单台机器上的压力。

如果不理解集群原理,可以看下扩展知识中的两个示例

扩展知识

Redis 集群中节点之间的信息如何同步?

Redis 集群内每个节点都会保存集群的完整拓扑信息,包括每个节点的 ID、IP 地址、端口、负责的哈希槽范围等。

节点之间使用 Gossip 协议进行状态交换,以保持集群的一致性和故障检测。每个节点会周期性地发送 PING 和 PONG 消息,交换集群信息,使得集群信息得以同步。

Gossip 的优点

  • 快速收敛:Gossip 协议能够快速传播信息,确保集群状态的迅速更新。
  • 降低网络负担:由于信息是以随机节点间的对话方式传播,避免了集中式的状态查询,从而降低了网络流量。

Gossip 协议

Gossip 主要特点

  1. 分布式信息传播:每个节点定期向其他节点发送其状态信息,确保所有节点对集群的状态有一致的视图。
  2. 低延迟和高效率:Gossip 协议设计为轻量级的通信方式,能够快速传播信息,减少单点故障带来的风险。
  3. 去中心化:没有中心节点,所有节点平等地参与信息传播,提高了系统的鲁棒性。

工作原理

  1. 状态报告:每个节点在特定的时间间隔内,向随机选择的其他节点发送其自身的状态信息,包括节点的主从关系、槽位分布等。
  2. 信息更新:接收到状态信息的节点会根据所接收到的数据更新自己的状态,并将更新后的状态继续传播给其他节点。
  3. 节点检测:通过周期性交换状态信息,节点可以检测到其他节点的存活状态。如果某个节点未能在预定时间内响应,则该节点会被标记为故障节点。
  4. 容错处理:在检测到节点故障后,集群中的其他节点可以采取措施(如重新分配槽位)以保持系统的高可用性。

Redis 集群分片原理图示

Redis 集群会将数据分散到 16384 (2 ^ 14)个哈希槽中,集群中的每个节点负责一定范围的哈希槽,在 Redis 集群中,使用 CRC16 哈希算法计算键的哈希槽,以确定该键应存储在哪个节点。

集群哈希槽分片如下图所示:

image.png

每个节点会拥有一部分的槽位,然后对应的键值会根据其本身的 key,映射到一个哈希槽中,其主要流程如下:

  • 根据键值的 key,按照 CRC 16 算法计算一个 16 bit 的值,然后将 16 bit 的值对 16384 进行取余运算,最后得到一个对应的哈希槽编号。
  • 根据每个节点分配的哈希槽区间,对应编号的数据落在对应的区间上,就能找到对应的分片实例。

为了方便大家理解,我这里画一个对应的关系图,以三个节点为例:

image.png

这里还有一点需要强调下,redis 客户端可以访问集群中任意一台实例,正常情况下这个实例包含这个数据。

但如果槽被转移了,客户端还未来得及更新槽的信息,当前实例没有这个数据,则返回 MOVED 响应给客户端,将其重定向到对应的实例(因 Gossip 集群内每个节点都会保存集群的完整拓扑信息)

Redis 集群中存储 key 示例

假设我们有一个 Redis 集群,包含三个主节点(Node1、Node2、Node3),它们分别负责以下哈希槽:

  • Node1: 哈希槽 0-5460
  • Node2: 哈希槽 5461-10922
  • Node3: 哈希槽 10923-16383

现在要存储一个键为 user:1001 的数据。

计算哈希槽

  1. 使用 CRC16 哈希算法计算 user:1001 的 CRC16 值。
  2. 假设计算结果为 12345。
  3. 然后,计算该值对应的哈希槽:
    • 哈希槽 = 12345 % 16384 = 12345。

确定目标节点

  • 12345 落在 Node3 的负责范围(10923-16383),因此,user:1001 会被存储在 Node3 中。

Redis 集群中请求 key 示例(客户端直接连接的并不是对应 key 的节点)

如果客户端连接的是集群的 Node1,但需要访问存储在 Node3 的键 user:1001,查询过程如下:

查询过程

1)计算哈希槽

  • 客户端使用 CRC16 算法计算 user:1001 的哈希值(假设为 12345)。
  • 计算哈希槽:12345 % 16384 = 12345。

2)查询请求

  • 因为客户端连接的是集群中的 node1,所以客户端发送查询命令 GET user:1001 到 Node1。

3)Node1 响应

  • Node1 检测到请求的键 user:1001 属于 Node3,返回一个 MOVED 错误,指示客户端请求的键在另一个节点上。MOVED 错误会中返回目标节点的信息(例如,Node3 的 IP 和端口)

4)重新连接

  • 客户端根据返回的目标节点信息,建立与 Node3 的连接。

5)再次发送查询请求

  • 客户端向 Node3 发送 GET user:1001

6)获取结果

  • Node3 查询到 user:1001 的值(假设为 {"name": "面试鸭", "age": 18}),并返回结果。

为什么 Redis 哈希槽节点的数目是 16384 呢?

github 上有人向作者提问过:

image.png

简单总结分析一下:

1)首先是消息大小的考虑

正常的心跳包需要带上节点完整配置数据,心跳还是比较频繁的,所以需要考虑数据包的大小,如果使用 16384 数据包只要 2k,如果用了 65535 则需要 8k。

实际上槽位信息使用一个长度为 16384 位的数组来表示,节点拥有哪个槽位,就将对应位置的数据信息设置为 1,否则为 0。

心跳数据包就包含槽位信息如下图所示:

下载.jpg

这里我们看到一个重点,即在消息头中最占空间的是 myslots[CLUSTER_SLOTS/8]。

  • 当槽位为65536时,这块的大小是: 65536÷8÷1024=8kb
  • 当槽位为16384时,这块的大小是: 16384÷8÷1024=2kb

如果槽位为 65536 ,这个 ping 消息的消息头就太大了,浪费带宽。

2)集群规模的考虑

集群不太可能会扩展超过 1000 个节点,16384 够用且使得每个分片下的槽又不会太少。

Comments