Redis【有与无】【R2】Redis Cluster规范

Redis Cluster规范:Redis Cluster中使用的行为和算法的更正式描述。

目录

1.前提说明

2.设计的主要特点和原理

2.1.集群目标

2.2.实现子集

3.群集协议中的客户机和服务器角色

3.1.写安全

3.2.可用性

3.3.性能

3.4.为什么避免合并操作

4.Redis Cluster主要组件概述

4.1.键分配模型

4.2.键哈希标签(Keys hash tags)

4.3.集群节点属性

4.4.集群总线(Cluster bus)

4.5.集群拓扑(Cluster topology)

4.6.节点握手

5.重定向和重新分片

5.1.移动重定向(MOVED Redirection)

5.2.集群实时重新配置

5.3.ASK重定向

5.4.客户端首次连接和重定向处理

5.5.多键操作(Multiple keys operations)

5.6.使用复制节点扩展读取

6.容错能力

6.1.心跳和八卦(gossip)消息

6.2.心跳包内容

6.3.故障检测

7.配置处理,传播和故障转移

7.1.集群当前时代(Cluster current epoch)

7.2.配置时代(Configuration epoch)

7.3.复制节点选举和晋升

7.4.复制节点等级

7.5.主节点回复复制节点投票要求

7.6.分区期间配置时代(epoch)实用性的实际示例

7.7.哈希插槽配置传播

7.8.UPDATE消息,仔细查看

7.9.节点如何重新加入集群

7.10.复制节点迁移(Replica migration)

7.11.复制节点迁移算法(Replica migration algorithm)

7.12.configEpoch冲突解决算法

7.13.节点重置

7.14.从集群中删除节点

8.发布/订阅(Publish/Subscribe)

9.附录

附录A:ANSI C中的CRC16参考实现


1.前提说明

本文基于Redis 6.0.9版本,前提至少 Redis 3.0或更高版本。

欢迎使用 Redis 集群规范。在这里,您将找到关于 Redis Cluster 的算法和设计原理的信息。

八卦: gossip  闲聊
时代:epoch 

2.设计的主要特点和原理

2.1.集群目标

Redis Cluster 是 Redis 的一个分布式实现,按照设计的重要性顺序,它有以下目标:

  • 高性能和1000个节点的线性可扩展性。没有代理,使用异步复制,并且不对值执行合并操作
  • 可接受的写安全程度:系统尝试(尽最大努力)保留源自与大多数主节点连接的客户端的所有写操作。 通常情况下,在小窗口中会丢失已确认的写入。 当客户端位于少数分区中时,丢失确认写入的窗口会更大。
  • 可用性:Redis Cluster能够在大多数主节点都可访问且每个不再可用的主节点上至少有一个可访问的复制节点的分区中生存。 此外,使用复制节点迁移,不再由任何复制节点复制的主将从一个由多个复制节点覆盖的主接收一个。

2.2.实现子集

Redis Cluster实现了Redis的非分布式版本中所有可用的单键命令。 只要所有键都散列到同一插槽,就可以执行执行复杂的多键操作(如Set类型并集或交集)的命令。

Redis Cluster 实现了一个称为散列标记(hash tags)的概念,可以使用这个概念强制某些键存储在同一散列槽中。然而,在手动分片过程中,多个键操作可能在一段时间内不可用,而单个键操作始终可用。

Redis Cluster不支持多个数据库,例如独立版本的Redis。 只有数据库0,不允许使用SELECT命令。

3.群集协议中的客户机和服务器角色

在Redis群集中,节点负责保存数据并获取群集状态,包括将键映射到正确的节点。 群集节点还能够自动发现其他节点,检测不工作的节点,并在需要时将复制节点升级为主节点,以便在发生故障时继续运行。

为了执行其任务,所有群集节点都使用TCP总线和称为Redis群集总线(Redis Cluster Bus)的二进制协议进行连接。 使用群集总线,每个节点都连接到群集中的每个其他节点。 节点使用gossip协议传播有关群集的信息,以便发现新节点,发送ping数据包以确保所有其他节点都正常工作,并发送信号以指示特定情况。 群集总线还用于在用户请求时跨群集传播Pub/Sub消息,并在用户请求时编排手动故障转移(手动故障转移不是Redis Cluster故障检测器而是由系统管理员直接发起的故障转移)。

由于群集节点无法代理请求,因此可以使用重定向错误-MOVED和-ASK将客户端重定向到其他节点。 从理论上讲,客户端可以自由地将请求发送到集群中的所有节点,并在需要时进行重定向,因此不需要客户端保持集群的状态。 但是,能够在键和节点之间缓存映射的客户端可以以合理的方式提高性能。

3.1.写安全

Redis Cluster在节点之间使用异步复制,最后一次故障转移将获得隐式合并功能。 这意味着最后选择的主数据集最终将替换所有其他复制节点。 在分区期间可能丢失写操作的时间总是很长。 但是,对于连接到大多数主节点的客户端和连接到少数主节点的客户端,这些窗口有很大的不同。

与在少数主节点上执行的写操作相比,Redis Cluster会更努力地保留与大多数主节点连接的客户端执行的写操作。 以下是导致故障期间导致大多数分区中收到的已确认写入丢失的方案示例:

  • 写入可能到达主节点,但是尽管主节点可能能够答复客户端,但是写入可能不会通过在主节点和复制节点节点之间使用的异步复制传播到复制节点。 如果主节点死亡,而写入没有到达复制节点,则如果主节点无法访问足够长的时间以致提升其一个复制节点,则写入将永远丢失。 在主节点完全突然故障的情况下,这通常很难观察到,因为主节点尝试在大约同一时间尝试答复客户端(带有写入的确认)和复制节点(传播写入)。 但是,这是现实世界中的失败模式。
  • 从理论上讲,丢失写操作的另一种可能的失败模式是:
    • 主节点由于分区而无法访问。
    • 它被其中一个复制节点进行了故障转移。
    • 一段时间后,它可能会再次可达。
    • 具有过期路由表的客户端可能会先写入旧的主节点,再由集群将其转换为新主节点的复制节点。

第二种故障模式不太可能发生,因为无法与足够多的其他主节点通信的主节点有足够的时间来进行故障转移,并且该主节点将不再接受写操作,并且在分区固定后,仍会在少量时间内拒绝写操作,允许其他节点通知配置更改。 此故障模式还要求客户端的路由表尚未更新。

针对分区少数方的写操作有一个较大的可丢失窗口。例如,在有少数主节点和至少一个或多个客户机的分区上,Redis群集丢失了大量写操作,因为如果主节点在多数端失败,发送给主节点的所有写操作都可能丢失。

具体来说,要使一个主节点进行故障转移,至少在NODE_TIMEOUT之前,大多数主节点必须无法访问该主节点,因此,如果在该时间之前分区已固定,则不会丢失任何写操作。 当分区的持续时间超过NODE_TIMEOUT时,直到该点为止在少数端执行的所有写操作都可能会丢失。 但是,在经过NODE_TIMEOUT时间后,Redis群集的少数派一方将开始拒绝写操作,而不与多数派联系,因此存在一个最大窗口,在此之后,少数派将不再可用。 因此,在那之后,不会再有任何写入被接受或丢失。

3.2.可用性

Redis群集在分区的少数部分不可用。 在分区的大多数方面,假设每个不可达的主节点至少有多数主节点和一个复制节点,则在NODE_TIMEOUT时间加上复制节点被选举并对其主节点进行故障转移所需的几秒钟之后,群集将再次变得可用( 故障转移通常在1或2秒内执行)。

这意味着Redis群集旨在承受群集中少数几个节点的故障,但是对于那些需要大量网络拆分而需要可用性的应用程序而言,它并不是一个合适的解决方案。

在由N个主节点组成的群集的示例中,每个节点都有一个复制节点,只要将单个节点分开,群集的大部分将保持可用状态,并且将以1-(1/(N*2-1)),当两个节点被分割开时(第一个节点发生故障后,我们总共剩下N*2-1个节点,唯一没有复制节点的主节点出现故障的概率为1/(N*2-1))
例如,在具有5个节点且每个节点有一个复制节点的群集中,在两个节点从多数分区中分离出来后,该群集将不再可用的概率为(1/(5*2-1)= 11.11%) 。

由于Redis群集具有称为复制节点迁移的功能,因此在许多实际情况下,由于复制节点迁移到了孤立的主节点(不再具有复制节点的主节点),群集的可用性得以提高。 因此,在每个成功的故障事件中,群集都可以重新配置复制节点布局,以便更好地抵抗下一次故障。

3.3.性能

在Redis Cluster中,节点不会将命令代理到给定键的正确节点,而是将客户端重定向到服务于键空间给定部分的正确节点。

最终,客户端获得群集的最新表示,以及哪个节点提供键的哪个子集,因此在正常操作期间,客户端直接联系正确的节点以发送给定命令。

由于使用了异步复制,因此节点不必等待其他节点的写入确认(如果未使用WAIT命令明确请求)。

另外,由于多键命令仅限于近键,因此除非重新分片,否则数据永远不会在节点之间移动。

正常操作的处理方式与单个Redis实例的处理方式完全相同。 这意味着在具有N个主节点的Redis集群中,随着设计线性扩展,您可以期望获得与单个Redis实例乘以N相同的性能。 同时,查询通常是在单次往返中执行的,因为客户端通常会保留与节点的持久连接,因此延迟时间也与单个独立Redis节点的情况相同。

Redis Cluster的主要目标是在保持脆弱但合理的数据安全性和可用性形式的同时,具有很高的性能和可伸缩性。

3.4.为什么避免合并操作

与Redis数据模型的情况一样,Redis Cluster设计避免了多个节点中相同键值对的版本冲突。 Redis中的值通常很大; 通常会看到具有数百万个元素的列表或排序集。 数据类型在语义上也很复杂。 传输和合并这些类型的值可能是一个主要瓶颈,and/or 可能需要应用程序侧逻辑,存储元数据的额外内存等不小的参与。

这里没有严格的技术限制。 CRDTs或同步复制的状态机可以对类似于Redis的复杂数据类型进行建模。 但是,此类系统的实际运行时行为将与Redis Cluster不同。 Redis Cluster的设计旨在涵盖非集群Redis版本的确切用例。

4.Redis Cluster主要组件概述

4.1.分配模型

键空间被划分为16384个插槽,有效地设置了16384个主节点的群集大小的上限(但是建议的最大节点大小约为1000个节点)。

群集中的每个主节点都处理16384个哈希槽的子集。 当没有正在进行的集群重新配置时(即,哈希槽从一个节点移动到另一个节点时),该集群是稳定的。 当群集稳定时,单个哈希槽将由单个节点提供服务(但是,服务节点可以具有一个或多个复制节点设备,在发生网络分裂或故障的情况下可以替换该复制节点设备,并且可以用于扩展) 可接受过时数据的读取操作)。

下面是用于将键映射到哈希槽的基本算法(有关此规则的哈希标签异常,请阅读下一段):

HASH_SLOT = CRC16(key) mod 16384

CRC16的规定如下:

  • Name: XMODEM(也称为ZMODEM或CRC-16 / ACORN)
  • Width: 16 bit
  • Poly: 1021 (那实际上是 x16 + x12 + x5 + 1)
  • Initialization: 0000
  • Reflect Input byte: False
  • Reflect Output CRC: False
  • Xor constant to output CRC: 0000
  • Output for "123456789": 31C3

在16个CRC16输出位中使用了14个(这就是为什么在上式中有16384模运算的原因)。

在我们的测试中,CRC16在16384个插槽中均匀分布各种键方面表现出色。

注意:本文附录A中提供了所用CRC16算法的参考实现。

4.2.键哈希标签(Keys hash tags)

为了实现哈希标记(hash tags)而使用的哈希槽计算存在例外。 哈希标记(hash tags)是一种确保在同一哈希槽中分配多个键的方法。 这是为了在Redis Cluster中实现多键操作。

为了实现哈希标签,在某些情况下,键的哈希槽以略有不同的方式计算。 如果键包含“ {...}”模式,则仅对 { } 之间的子字符串进行哈希处理以获得哈希槽。 但是,由于可能出现 {} 多次,因此以下规则很好地指定了该算法:

  • 如果键包含 字符。
  • 并且如果在 的右边有一个 字符
  • 并且如果在 的第一次出现和 的第一次出现之间存在一个或多个字符。

然后,不对哈希进行哈希处理,仅对 的第一次出现和 的随后第一次出现之间的内容进行哈希处理。

例子:

  • 这有两个键 {user1000}.following 和 {user1000}.followers 将散列到同一散列槽,因为只有子字符串 user1000  将被散列以便计算散列槽。
  • 对于键 foo {} {bar},整个键将按通常的方式进行散列处理,因为右侧的第一个{ 后面是  },中间没有字符。
  • 对于键foo {{bar}} zap,子字符串 {bar 将被散列,因为它是 { 的第一个出现和其右边的 } 第一次出现之间的子字符串。
  • 对于键foo {bar} {zap},子字符串bar将被散列,因为算法会在{和}的第一个有效或无效(内部没有字节)匹配处停止。
  • 从该算法得出的结论是,如果键以{}开头,则可以保证将其作为一个整体进行哈希处理。 当使用二进制数据作为键名时,这很有用。

添加哈希标签异常,以下是Ruby和C语言的HASH_SLOT函数的实现。

Ruby 示例代码:

def HASH_SLOT(key)
    s = key.index "{"
    if s
        e = key.index "}",s+1
        if e && e != s+1
            key = key[s+1..e-1]
        end
    end
    crc16(key) % 16384
end

C示例代码:

unsigned int HASH_SLOT(char *key, int keylen) {
    int s, e; /* start-end indexes of { and } */

    /* Search the first occurrence of '{'. */
    for (s = 0; s < keylen; s++)
        if (key[s] == '{') break;

    /* No '{' ? Hash the whole key. This is the base case. */
    if (s == keylen) return crc16(key,keylen) & 16383;

    /* '{' found? Check if we have the corresponding '}'. */
    for (e = s+1; e < keylen; e++)
        if (key[e] == '}') break;

    /* No '}' or nothing between {} ? Hash the whole key. */
    if (e == keylen || e == s+1) return crc16(key,keylen) & 16383;

    /* If we are here there is both a { and a } on its right. Hash
     * what is in the middle between { and }. */
    return crc16(key+s+1,e-s-1) & 16383;
}

4.3.集群节点属性

每个节点在集群中都有唯一的名称。 节点名称是160位随机数的十六进制表示形式,是在节点首次启动时获得的(通常使用/dev/urandom)。 节点会将其ID保存在节点配置文件中,并将永久使用相同的ID,或者至少在系统管理员未删除节点配置文件或通过CLUSTER RESET命令请求硬重置的情况下使用该ID。

节点ID用于标识整个集群中的每个节点。 给定节点可以更改其IP地址,而无需也更改节点ID。 群集还能够检测 IP/port 的更改,并使用在群集总线上运行的gossip协议进行重新配置。

节点ID并不是与每个节点关联的唯一信息,而是唯一始终全局一致的信息。 每个节点还具有以下关联的信息集。 一些信息与该特定节点的集群配置详细信息有关,并且最终在整个集群中保持一致。 相反,某些其他信息(例如上次对节点执行ping操作)位于每个节点本地。

每个节点都维护以下有关群集中其他节点的信息:节点ID,节点的IP和端口,一组标志,如果上次被标记为复制节点,则该节点的主节点是什么? 对该节点执行ping操作,最后一次接收到pong时,将显示该节点的当前配置时期(在本规范的后面部分进行说明),链接状态以及最后一个哈希槽集合。

CLUSTER NODES文档中描述了所有节点字段的详细说明。

以下是发送到三个节点的小型集群中的主节点的CLUSTER NODES命令的示例输出。

$ redis-cli cluster nodes
d1861060fe6a534d42d8a19aeb36600e18785e04 127.0.0.1:6379 myself - 0 1318428930 1 connected 0-1364
3886e65cc906bfd9b1f7e7bde468726a052d1dae 127.0.0.1:6380 master - 1318428930 1318428931 2 connected 1365-2729
d289c575dcbc4bdd2931585fd4339089e461a27d 127.0.0.1:6381 master - 1318428931 1318428931 3 connected 2730-4095

在上面的清单中,不同的字段按顺序排列:节点ID,地址:端口,标志,发送的最后ping,收到的最后pong,配置时期,链接状态,插槽。 一旦我们谈到Redis Cluster的特定部分,将涵盖上述字段的详细信息。

4.4.集群总线(Cluster bus)

每个Redis Cluster节点都有一个额外的TCP端口,用于接收来自其他Redis Cluster节点的传入连接。 此端口与用于从客户端接收传入连接的普通TCP端口处于固定偏移量。 要获取Redis Cluster端口,应在常规命令端口中添加10000。 例如,如果Redis节点正在端口6379上侦听客户端连接,则群集总线端口16379也将打开。

节点到节点(Node-to-node)的通信仅使用群集总线和群集总线协议进行:群集二进制协议由不同类型和大小的帧组成。 未公开记录集群总线二进制协议,因为它不打算供外部软件设备使用该协议与Redis Cluster节点通信。 但是,您可以通过阅读Redis Cluster源代码中的cluster.h和cluster.c文件来获取有关Cluster总线协议的更多详细信息。

4.5.集群拓扑(Cluster topology)

Redis Cluster是一个完整的网格,其中每个节点都使用TCP连接与其他每个节点连接。

在N个节点的群集中,每个节点都有N-1个传出TCP连接和N-1个传入连接。

这些TCP连接始终保持活动状态,并且不会按需创建。 当节点期望响应集群总线中的ping进行pong响应时,在等待足够长的时间以将该节点标记为不可访问之前,它将尝试通过从头重新连接来刷新与该节点的连接。

虽然Redis Cluster节点形成一个完整的网格,但是节点使用gossip 协议和配置更新机制,以避免在正常情况下,在节点之间交换太多消息,因此交换的消息数量不是指数级的。

4.6.节点握手

节点始终接受群集总线端口上的连接,即使收到ping节点不受信任,甚至会在收到ping时回复ping。 但是,如果不将发送节点视为群集的一部分,则所有其他数据包都将被接收节点丢弃。

一个节点仅以两种方式将另一个节点作为群集的一部分:

  • 节点是否显示MEET消息。 Meet消息与PING消息完全一样,但是会强制接收者接受该节点作为群集的一部分。 仅当系统管理员通过以下命令请求节点时,节点才会向其他节点发送MEET消息:CLUSTER MEET ip port
  • 如果已经受信任的节点将闲(gossip )聊该节点,则该节点还将另一个节点注册为群集的一部分。 因此,如果A知道B,B知道C,则最终B会向A发送有关C的gossip 消息。发生这种情况时,A会将C注册为网络的一部分,并尝试与C连接。

这意味着只要我们在任何连接图中加入节点,它们最终将自动形成完全连接图。 这意味着群集能够自动发现其他节点,但前提是存在系统管理员强制建立的信任关系。

这种机制使群集更加健壮,但可以防止更改IP地址或其他与网络相关的事件后,不同的Redis群集意外混合。

5.重定向和重新分片

5.1.移动重定向(MOVED Redirection)

Redis客户端可以自由地向集群中的每个节点(包括复制节点)发送查询。 节点将分析查询,如果可接受(即,在查询中仅提及单个键,或提及的多个键全部位于同一哈希槽中),它将查找哪个节点负责哈希槽 一个或多个键所属的位置。

如果哈希槽由节点提供服务,则查询将被简单地处理,否则节点将检查其内部哈希槽到节点的映射,并使用MOVED错误回复客户端,如以下示例所示:

GET x
-MOVED 3999 127.0.0.1:6381

错误包括键的哈希槽(3999)和可以为查询提供服务的实例的ip:port。 客户端需要将查询重新发出到指定节点的IP地址和端口。 请注意,即使客户端在重新发出查询之前等待了很长时间,并且与此同时,群集配置已更改,如果哈希槽3999现在由另一个节点提供服务,则目标节点将再次发出MOVED错误答复。 如果联系的节点没有更新的信息,则会发生相同的情况。

因此,从群集节点的角度来看,ID是由ID标识的,我们尝试简化与客户端的接口,仅显示哈希槽与IP:port对标识的Redis节点之间的映射。

客户端不是必需的,但应尝试记住127.0.0.1:6381为哈希槽3999提供服务。 这样,一旦需要发出新命令,它就可以计算目标键的哈希槽,并有更大的机会选择正确的节点。

另一种选择是在收到MOVED重定向后,仅使用CLUSTER NODESCLUSTER SLOTS命令刷新整个客户端群集布局。 遇到重定向时,很可能重新配置了多个插槽,而不仅仅是一个插槽,因此,尽快更新客户端配置通常是最佳策略。

请注意,当群集稳定时(配置中没有正在进行的更改),最终所有客户端都将获得哈希槽映射->节点,从而使群集高效,客户端直接寻址正确的节点,而无需重定向,代理或其他单个故障点实体。

客户端还必须能够处理本文档后面介绍的-ASK重定向,否则它不是完整的Redis Cluster客户端。

5.2.集群实时重新配置

Redis Cluster支持在集群运行时添加和删除节点的功能。 添加或删除节点被抽象为相同的操作:将哈希槽从一个节点移动到另一个节点。 这意味着可以使用相同的基本机制来重新平衡群集,添加或删除节点等等。

  • 要将新节点添加到群集,请将一个空节点添加到群集,并将某些哈希槽集从现有节点移动到新节点。
  • 为了从群集中删除节点,将分配给该节点的哈希槽移至其他现有节点。
  • 为了重新平衡群集,在节点之间移动一组给定的哈希槽。

该实现的核心是能够移动哈希槽的能力。 从实际的角度来看,哈希槽只是一组键,因此Redis Cluster在重新分片期间真正要做的就是将键从一个实例移动到另一个实例。 移动哈希槽意味着将碰巧发生哈希的所有键移动到该哈希槽中。

要了解其工作原理,我们需要显示用于在Redis Cluster节点中操作插槽转换表的CLUSTER子命令。

可以使用以下子命令(在这种情况下,其他子命令无效):

前两个命令ADDSLOTS和DELSLOTS仅用于将插槽分配(或删除)到Redis节点。 分配插槽意味着告诉给定的主节点它将负责为指定的哈希插槽存储和提供内容。

分配散列槽后,它们将使用gossip协议在群集中传播,这将在后面的配置传播部分中指定。

当从头开始创建新集群时,通常使用ADDSLOTS命令为每个主节点分配所有16384个哈希槽的子集。

DELSLOTS主要用于手动修改集群配置或用于调试任务:实际上很少使用。

如果使用SETSLOT <slot> NODE格式,则SETSLOT子命令用于将插槽分配给特定的节点ID。 否则,可以在两个特殊状态MIGRATING和IMPORTING中设置插槽。 使用这两个特殊状态是为了将哈希槽从一个节点迁移到另一个节点。

为了将哈希槽从一个节点迁移到另一个节点。

  • 当将插槽设置为MIGRATING时,节点将接受与该哈希插槽有关的所有查询,但前提是所讨论的键存在,否则,将使用-ASK重定向将查询转发到作为迁移目标的节点。
  • 当将插槽设置为IMPORTING时,节点将接受与该哈希插槽有关的所有查询,但前提是请求前面带有ASKING命令。 如果客户端未给出ASKING命令,则查询将通过-MOVED重定向错误重定向到实际的哈希槽所有者,这通常会发生。

让我们通过一个哈希槽迁移示例使这一点更加清楚。 假设我们有两个Redis主节点,分别称为A和B。我们想将哈希槽8从A移到B,因此我们发出如下命令:

  • 我们发送 B: CLUSTER SETSLOT 8 IMPORTING A
  • 我们发送 A: CLUSTER SETSLOT 8 MIGRATING B

每当其他所有节点都使用属于哈希槽8的键查询客户机时,所有其他节点将继续将客户机指向节点“A”,因此发生的情况是:

  • 关于现有键的所有查询均由“A”处理。
  • 所有关于A中不存在的键的查询都由“B”处理,因为“A”会将客户端重定向到“B”。

这样,我们不再在“ A”中创建新的键。 同时,在重新分片和Redis群集配置期间使用的名为redis-trib的特殊程序会将哈希插槽8中的现有键从A迁移到B。这是使用以下命令执行的:

CLUSTER GETKEYSINSLOT slot count

上面的命令将在指定的哈希槽中返回计数键。 对于返回的每个键,redis-trib向节点“ A”发送一个MIGRATE命令,该命令将以原子方式将指定的键从A迁移到B(两个实例都在迁移键所需的时间(通常非常短的时间)内被锁定) 因此没有比赛条件)。 这是MIGRATE的工作方式:

MIGRATE target_host target_port key target_database id timeout

MIGRATE将连接到目标实例,发送键的序列化版本,并在收到OK代码后,将从其自己的数据集中删除旧键。 从外部客户端的角度来看,键在任何给定时间都存在于A或B中。

在Redis Cluster中,无需指定0以外的数据库,但是MIGRATE是通用命令,可用于不涉及Redis Cluster的其他任务。 即使移动诸如长列表之类的复杂键时,MIGRATE也已被优化为尽可能快,但是在Redis群集中,如果使用数据库的应用程序中存在延迟限制,则重新配置存在大键的群集不是明智的过程。

当迁移过程最终完成时,将SETSLOT <slot> NODE <node-id>命令发送到迁移中涉及的两个节点,以将其再次设置为正常状态。 通常将同一命令发送到所有其他节点,以避免等待新配置在群集中自然传播。

5.3.ASK重定向

在上一节中,我们简要讨论了ASK重定向。 为什么我们不能简单地使用MOVED重定向? 因为虽然MOVED表示我们认为哈希槽由其他节点永久提供,并且应针对指定节点尝试下一个查询,所以ASK表示仅将下一个查询发送到指定节点。

之所以需要这样做是因为下一个关于哈希槽8的查询可能是关于仍在A中的键的,因此我们始终希望客户端尝试A,然后在需要时尝试B。 由于只有16384个可用的哈希槽中有一个发生,因此群集上的性能下降是可以接受的。

我们需要强制该客户端行为,以便确保客户端仅在尝试了A之后才尝试节点B,如果客户端在发送查询之前发送了ASKING命令,则节点B将仅接受设置为IMPORTING的插槽的查询。

基本上,ASKING命令在客户端上设置一个一次性标志,该标志会强制节点为有关IMPORTING插槽的查询提供服务。

从客户端的角度来看,ASK重定向的完整语义如下:

  • 如果收到ASK重定向,则仅发送已重定向到指定节点的查询,而继续将后续查询发送到旧节点。
  • 使用ASKING命令启动重定向的查询。
  • 还没有更新本地客户端表以哈希位置8映射到B.

一旦哈希槽8迁移完成,A将发送MOVED消息,并且客户端可以将哈希槽8永久映射到新的IP和端口对。 请注意,如果有错误的客户端较早执行地图,这不是问题,因为它不会在发出查询之前发送ASKING命令,因此B将使用MOVED重定向错误将客户端重定向到A。

CLUSTER SETSLOT命令文档中,以相似的术语解释了插槽迁移,但是使用了不同的措辞(为了文档中的冗余)。

5.4.客户端首次连接和重定向处理

虽然有可能有一个Redis Cluster客户端实现不记住内存中的插槽配置(插槽编号和为其服务的节点的地址之间的映射),并且只能通过联系等待重定向的随机节点来工作,但这样的客户端可以效率很低。

Redis Cluster客户端应尝试足够聪明以记住插槽配置。 但是,此配置不需要最新。 由于联系错误的节点只会导致重定向,因此应该触发客户端视图的更新。

在两种不同情况下,客户端通常需要获取插槽和映射节点地址的完整列表:

  • 在启动时为了填充初始插槽配置。
  • 收到MOVED重定向时。

请注意,客户端可以通过仅更新其表中已移动的插槽来处理MOVED重定向,但是,这通常效率不高,因为经常一次修改多个插槽的配置(例如,如果将从插槽升级为主插槽,则所有插槽 由旧主节点提供的服务将被重新映射)。 通过从头开始获取插槽到节点的完整映射,对MOVED重定向做出反应要简单得多。

为了检索插槽配置,Redis Cluster提供了CLUSTER NODES命令的替代方法,该方法不需要解析,而仅向客户端提供严格需要的信息。

新命令称为CLUSTER SLOTS,它提供插槽范围的数组,并且相关的主节点和复制节点服务于指定范围。

以下是CLUSTER SLOTS的输出示例:

127.0.0.1:7000> cluster slots
1) 1) (integer) 5461
   2) (integer) 10922
   3) 1) "127.0.0.1"
      2) (integer) 7001
   4) 1) "127.0.0.1"
      2) (integer) 7004
2) 1) (integer) 0
   2) (integer) 5460
   3) 1) "127.0.0.1"
      2) (integer) 7000
   4) 1) "127.0.0.1"
      2) (integer) 7003
3) 1) (integer) 10923
   2) (integer) 16383
   3) 1) "127.0.0.1"
      2) (integer) 7002
   4) 1) "127.0.0.1"
      2) (integer) 7005

返回的数组中的每个元素的前两个子元素是该范围的start-end槽。追加元素表示地址端口对。 第一个地址端口对是为该插槽提供服务的主节点,其他地址端口对是为同一插槽提供服务的所有复制节点设备,它们均处于错误状态(即未设置FAIL标志)。

例如,输出的第一个元素说127.0.0.1:7001为从5461到10922(包括开始和结束)的插槽提供服务,并且可以缩放127.0.0.1:7004上与复制节点联系的只读负载。

如果群集配置不正确,则不能保证CLUSTER SLOTS返回覆盖整个16384插槽的范围,因此客户端应初始化插槽配置图,以目标对象填充NULL对象,如果用户尝试执行有关键的命令,则报告错误属于未分配的插槽。

如果发现未分配插槽,则在将错误返回给调用方之前,客户端应尝试再次获取插槽配置,以检查集群现在是否已正确配置。

5.5.多键操作(Multiple keys operations)

使用哈希标签,客户端可以自由使用多键操作。 例如,以下操作有效:

MSET {user:1000}.name Angela {user:1000}.surname White

当正在进行键所属的哈希槽的重新分片时,多键操作可能变得不可用。

更具体地,即使在重新分片期间,以全部存在且散列到同一插槽(源节点或目标节点)的键为目标的多键操作仍然可用。

对不存在或在重新分片期间在源节点和目标节点之间拆分的键进行的操作将生成-TRYAGAIN错误。 客户端可以在一段时间后尝试操作,或者报告错误。

一旦指定哈希槽的迁移终止,所有所有多键操作都将再次用于该哈希槽。

5.6.使用复制节点扩展读取

通常,复制节点会将客户端重定向到给定命令所涉及的哈希槽的权威主节点,但是客户端可以使用复制节点来使用READONLY命令扩展读取。

READONLY告诉Redis Cluster复制节点,客户端可以读取可能过时的数据,并且对运行写查询不感兴趣。

当连接处于只读模式时,仅当操作涉及复制节点的主节点不提供的时,群集才会向客户端发送重定向。这可能是因为:

  1. 客户端发送了有关此复制节点的主节点从未使用的哈希槽的命令。
  2. 群集已重新配置(例如重新分片),并且复制节点的不再能够为给定的哈希槽提供命令。

发生这种情况时,客户端应按照前几节中的说明更新其哈希映射。

可以使用READWRITE命令清除连接的只读状态。

6.容错能力

6.1.心跳和八卦(gossip)消息

Redis Cluster节点不断交换ping和pong数据包。这两种报文具有相同的结构,并且都携带重要的配置信息。唯一的实际区别是消息类型字段。我们将ping和pong数据包的总和称为心跳数据包

通常,节点发送ping数据包,这将触发接收者以pong数据包进行回复。但是,这不一定是正确的。节点可能仅发送pong数据包就可以向其他节点发送有关其配置的信息,而不会触发答复。例如,这对于尽快广播新配置很有用。

通常,一个节点每秒将对几个随机节点执行ping操作,以便每个节点发送的ping数据包(以及接收到的pong数据包)的总数为恒定数量,而与群集中节点的数量无关。

但是,每个节点都确保对所有未发送ping或收到pong的时间NODE_TIMEOUT超过一半的其他节点执行ping操作。在NODE_TIMEOUT流逝之前,节点还尝试将TCP链接与另一个节点重新连接,以确保仅由于当前TCP连接中存在问题,才不会认为节点不可达。

如果将NODE_TIMEOUT设置为较小的数字,并且节点数(N)非常大,则全局交换的消息数可能会很大,因为每个节点都将尝试对每隔NODE_TIMEOUT时间一半没有新鲜信息的其他节点执行ping操作。

例如,在一个节点超时设置为60秒的100个节点的群集中,每个节点将尝试每30秒发送99次ping,而每秒的ping总数量为3.3。乘以100个节点,这就是整个集群每秒330ping。

有多种方法可以减少消息数量,但是目前还没有关于Redis Cluster故障检测当前使用的带宽的报告问题,因此目前使用了显而易见的直接设计。请注意,即使在以上示例中,每秒交换的330个数据包也会平均分配给100个不同的节点,因此每个节点接收的流量都是可以接受的。

6.2.心跳包内容

Ping和Pong数据包包含所有类型的数据包(例如,请求故障转移投票的数据包)通用的标头,以及专用于Ping和Pong数据包的特殊八卦(gossip)段。

通用标头具有以下信息:

  • 节点ID,这是一个160位的伪随机字符串,在第一次创建节点时被分配,并且在Redis Cluster节点的整个生命周期中都保持不变。
  • 发送节点的currentEpochconfigEpoch字段用于装载Redis Cluster使用的分布式算法(这将在下一部分中详细说明)。如果节点是复制节点,则它configEpochconfigEpoch其主节点中最后一个已知的节点。
  • 节点标志,指示该节点是复制节点,主节点还是其他单位节点信息。
  • 发送节点服务的哈希插槽的位图,或者如果该节点是复制节点,则为其主节点服务的插槽的位图。
  • 发送方TCP基本端口(即Redis用来接受客户端命令的端口;在该端口上加上10000可获取集群总线端口)。
  • 从发件人的角度来看,群集的状态(关闭或正常)。
  • 发送节点的主节点ID(如果它是复制节点)。

ping和pong数据包还包含一个八卦(gossip)部分。本节向接收者提供发送者节点对集群中其他节点的看法。八卦(gossip)部分仅包含有关发送者已知的节点集中的一些随机节点的信息。八卦(gossip)部分提到的节点数量与群集大小成正比。

对于八卦(gossip)部分中添加的每个节点,将报告以下字段:

  • Node ID.
  • IP and port of the node.
  • Node flags.

八卦(gossip)小节允许接收节点从发送方的角度获取有关其他节点状态的信息。这对于故障检测和发现集群中的其他节点都是有用的。

6.3.故障检测

Redis群集故障检测用于识别大多数节点不再可访问主节点或复制节点的时间,然后通过将复制节点提升为主角色来做出响应。如果无法进行复制节点升级,则群集将处于错误状态,以停止接收来自客户端的查询。

如上所述,每个节点都获取与其他已知节点关联的标志的列表。有两个用于故障检测的标志称为PFAILFAILPFAIL表示可能的失败,并且是未确认的失败类型。FAIL表示节点发生故障,并且大多数主节点在固定时间内确认了此情况。

PFAIL标志:

当节点无法访问的时间超过NODE_TIMEOUT时间时,该节点将使用PFAIL标志标记另一个节点。 主节点和复制节点都可以将另一个节点标记为PFAIL,无论其类型如何。

Redis群集节点的不可访问性的概念是,我们有一个活动的ping(我们发送给它的ping,但尚未得到答复)的等待时间超过NODE_TIMEOUT。 为了使这种机制起作用,与网络往返时间相比,NODE_TIMEOUT必须很大。 为了增加正常操作期间的可靠性,一旦NODE_TIMEOUT的一半过去了,而没有回应ping,节点将尝试与群集中的其他节点重新连接。 这种机制可确保连接保持活动状态,因此断开的连接通常不会导致节点之间的错误故障报告。

FAIL标志:

仅PFAIL标志只是每个节点有关其他节点的本地信息,但是不足以触发复制节点升级。 对于要考虑关闭的节点,需要将PFAIL条件升级为FAIL条件。

如本文档的节点心跳部分中概述的那样,每个节点都会向其他每个节点发送八卦(gossip)消息,包括一些随机已知节点的状态。 每个节点最终都会为其他每个节点接收一组节点标志。 这样,每个节点都有一种向其他节点发送有关已检测到的故障情况的信号的机制。

当满足以下一组条件时,PFAIL条件将升级为FAIL条件:

  • 我们将称为A的某个节点具有另一个标记为PFAIL的节点B。
  • 节点A通过八卦(gossip)段,从集群中大多数主节点的角度收集了有关B状态的信息。
  • 大多数主节点在 NODE_TIMEOUT * FAIL_REPORT_VALIDITY_MULT 时间之内发出PFAIL或FAIL状态的信号。 (在当前实现中,有效性因子设置为2,因此这仅是NODE_TIMEOUT时间的两倍)。

如果满足以上所有条件,则节点A将:

  • 将节点标记为失败。
  • 向所有可达节点发送失败消息。

FAIL消息将强制每个接收节点将其标记为FAIL状态,无论它是否已将该节点标记为PFAIL状态。

请注意,FAIL标志通常是一种方法。 也就是说,节点可以从PFAIL变为FAIL,但是只有在以下情况下才可以清除FAIL标志:

  • 该节点已经可以访问,并且是复制节点。 在这种情况下,FAIL标志可以清除,因为复制节点不会进行故障转移。
  • 该节点已经可以访问,并且是不为任何插槽提供服务的主节点。 在这种情况下,FAIL标志可以清除,因为没有插槽的主节点实际上并没有真正参与集群,并且正在等待配置以加入集群。
  • 该节点已经可以访问并且是主节点,但是已经过去了很长时间(NDE_TIMEOUT的N倍),而没有任何可检测到的复制节点升级。 最好重新加入集群并在这种情况下继续。

值得注意的是,尽管PFAIL-> FAIL过渡使用某种形式的协议,但所使用的协议很弱:

  • 节点会在一段时间内收集其他节点的视图,因此,即使大多数主节点都需要“agree”,实际上这只是表明我们在不同时间从不同节点收集的信息,我们不确定也不需要,在一定时刻,大多数主节点都同意了。 但是,我们会丢弃过时的故障报告,因此大多数主节点会在一段时间内发出故障信号。
  • 尽管每个检测到FAIL条件的节点都将使用FAIL消息将该条件强加到群集中的其他节点上,但无法确保该消息将到达所有节点。 例如,一个节点可能检测到FAIL条件,并且由于分区的原因而无法到达任何其他节点。

但是,Redis群集故障检测具有活动性要求:最终,所有节点都应就给定节点的状态达成共识。 有两种情况可能源于大脑分裂状况。 少数节点认为该节点处于FAIL状态,或者少数节点认为该节点未处于FAIL状态。 在这两种情况下,集群最终都将具有给定节点状态的单一视图:

情况1:如果由于故障检测及其所产生的链效应,大多数主节点将节点标记为FAIL,则每个其他节点最终会将主节点标记为FAIL,因为在指定的时间范围内将报告足够的故障。

情况2:当只有少数主节点将一个节点标记为FAIL时,复制节点升级将不会发生(因为它使用了一种更为正式的算法来确保每个人最终都了解升级),并且每个节点都将按照 上面的FAIL状态清除规则(即,经过N次NODE_TIMEOUT之后没有晋级)。
FAIL标志仅用作触发以运行复制节点升级的算法的安全部分。 从理论上讲,复制节点可以独立执行操作,并在其主控无法访问时启动复制节点提升,并在主控实际上可被多数访问的情况下等待主控拒绝提供确认。 但是,PFAIL-> FAIL状态的增加的复杂性,较弱的一致性以及强制FAIL消息在群集的可到达部分中以最短的时间传播状态的方法,具有实际的优势。 由于这些机制,如果群集处于错误状态,通常所有节点将几乎同时停止接受写入。 从使用Redis Cluster的应用程序的角度来看,这是理想的功能。 还避免了由于本地问题而无法到达其主节点的复制节点发起的错误选举尝试(否则其他大多数主节点都可以到达该主节点)。

7.配置处理,传播和故障转移

7.1.集群当前时代(Cluster current epoch)

Redis Cluster使用类似于Raft算法“term”的概念。 在Redis Cluster中,该术语改为称为epoch,用于给事件提供增量版本控制。 当多个节点提供冲突的信息时,另一个节点就有可能了解哪个状态是最新的。

currentEpoch是一个64位无符号数字。

创建节点时,每个Redis群集节点(复制节点和主节点)都将currentEpoch设置为0。

每次从另一个节点接收到一个数据包时,如果发送者的epoch(集群总线消息头的一部分)大于本地节点epoch,那么currentEpoch将更新为发送者epoch。

由于这些语义,最终所有节点都同意集群中最大的currentEpoch。

当集群的状态更改并且节点寻求协议以执行某些操作时,将使用此信息。

当前,这仅在复制节点晋升期间发生,如下一节所述。 基本上,epoch是集群的逻辑时钟,它指示给定的信息以较小的epoch胜出。

7.2.配置时代(Configuration epoch)

每个主控设备总是在ping和pong数据包中宣传其configEpoch,并在其位图上宣传其服务的插槽组。

创建新节点时,主节点中的configEpoch设置为零。

复制节点选举期间将创建一个新的configEpoch。 试图替换发生故障的主节点的复制节点会增加其时期,并尝试从大多数主节点获得授权。 授权复制节点后,将创建一个新的唯一configEpoch,并且使用新的configEpoch,复制节点将成为主节点。

如以下各节所述,当不同的节点主张不同的配置时(由于网络分区和节点故障而可能发生的情况),configEpoch有助于解决冲突。

复制节点也可以在ping和pong数据包中通告configEpoch字段,但对于复制节点,该字段代表其上一次交换数据包时其主节点的configEpoch。 这允许其他实例检测复制节点何时具有需要更新的旧配置(主节点不会将投票授予具有旧配置的复制节点)。

每当configEpoch更改某个已知节点时,所有接收此信息的节点都会将它永久存储在nodes.conf文件中。 currentEpoch值也是如此。 确保在节点继续其操作之前进行更新时将这两个变量保存并同步到磁盘。

确保在故障转移期间使用简单算法生成的configEpoch值是新的,增量的和唯一的。

7.3.复制节点选举和晋升

复制节点的选举和升级是由复制节点处理的,而主节点则需要对复制节点进行投票。 从主节点中至少一个具有成为主节点的先决条件的复制节点的角度来看,当主节点处于FAIL状态时,将发生复制节点选举。

为了使复制节点自身为主,它需要开始大选,赢得它。如果主节点处于失效时所有给定的主节点的复制节点可以启动选举,但只有一个复制节点将在大选中获胜,并促进其自身为主节点。

满足以下条件时,复制节点将开始选举:

  • 复制节点的主节点处于FAIL状态。
  • 主节点提供的插槽数量非零。
  • 复制节点复制链接与主节点断开连接的时间不超过给定的时间,以确保提升后的复制节点数据是合理的。 这个时间是用户可配置的。

为了被选举,复制节点的第一步是增加其currentEpoch计数器,并请求主节点实例的投票。

复制节点通过向群集的每个主节点广播FAILOVER_AUTH_REQUEST数据包来请求投票。 然后,它等待最大时间为NODE_TIMEOUT的两倍,以使答复到达(但始终至少2秒钟)。

一旦主节点对给定的复制节点投票,并以FAILOVER_AUTH_ACK进行肯定答复,则在NODE_TIMEOUT * 2的时间内,它不能再投票给同一主节点的另一个复制节点。在此期间,它将无法答复其他授权请求。 对于同一主节点。 这不是保证安全所必需的,但对于防止多个复制节点同时被选举(即使是使用不同的configEpoch)很有用,这通常是不希望的。

在发送投票请求时,一个复制节点将丢弃任何回复的 AUTH_ACK,回复的epoch小于 currentEpoch。这样可以确保它不会计算上一次选举的选票。

一旦从从多数主节点收到的ACK,它赢得选举。 否则,如果在两次NODE_TIMEOUT(但始终至少为2秒)内未达到多数,则该选举将中止,并且将在NODE_TIMEOUT * 4(始终至少为4秒)之后再次尝试一次新的选举。

7.4.复制节点等级

一旦主节点处于FAIL状态下,从等待的短时间内试图当选之前。 延迟计算如下:

DELAY = 500 milliseconds + random delay between 0 and 500 milliseconds +
        SLAVE_RANK * 1000 milliseconds.

在我们等待失效时,固定延迟确保整个集群传播,否则该复制节点可尝试才能当选,而主节点还没有意识到故障状态,拒绝给予他们的投票。

随机延迟用于使复制节点不同步,因此他们不太可能同时开始选举。

SLAVE_RANK是此复制节点关于从主节点处理的复制数据量的等级。

当主节点失败时,复制节点交换消息以建立(最大努力)等级:复制偏移量最新的复制节点的等级为0,第二高的复制偏移量的等级为1,依此类推。通过这种方式,最新的复制节点试图在别人面前才能当选。

等级顺序没有严格执行;如果更高级别的复制节点落选,其他节点将很快尝试。

一旦从赢得选举,它获得了新的独特和增量configEpoch它比任何其他现有的主节点高。 它开始在ping和pong数据包中以自己的主节点身份进行广告宣传,为服务的插槽集提供configEpoch,它将胜过过去的插槽。

为了加快其他节点的重新配置,将pong数据包广播到群集的所有节点。 当前无法到达的节点将在从另一个节点接收到ping或pong数据包时最终进行重新配置,或者如果检测到通过心跳数据包发布的信息已过时,则将从另一个节点接收UPDATE数据包。

其他节点将检测到有一个新的主节点,该主节点与旧主节点的插槽相同,但具有更大的configEpoch,并将升级其配置。 旧的主节点(如果重新加入集群,则为故障转移的主节点)的复制节点不仅会升级配置,还将重新配置为从新的主节点复制。 下节将说明如何配置重新加入群集的节点。

7.5.主节点回复复制节点投票要求

在上一节就讨论如何让复制节点尝试才能当选。 本节从要求对给定复制节点投票的主节点的角度解释发生了什么。

主节点接收来自复制节点以FAILOVER_AUTH_REQUEST请求的形式投票请求。

要获得投票,必须满足以下条件:

  • 主节点仅对给定的时间段投票一次,拒绝对较旧的时间段进行投票:每个主节点都具有lastVoteEpoch字段,并且只要auth请求数据包中的currentEpoch不大于lastVoteEpoch,便拒绝再次投票。 当主节点对表决请求做出肯定答复时,lastVoteEpoch会相应更新,并安全地存储在磁盘上。
  • 仅当复制节点的主节点标记为“FAIL”时,主节点才投票给复制节点。
  • 小于主节点的currentEpoch的Auth请求将被忽略。 因此,主节点答复将始终具有与auth请求相同的currentEpoch。 如果同一复制节点再次请求投票,增加currentEpoch,则可以确保新主节点不能接受来自主节点的旧延迟答复。

未使用规则编号3导致的问题示例:

当前主节点的currentEpoch为5,lastVoteEpoch为1(可能在几次失败的选举之后发生)

  • 复制节点的currentEpoch是3
  • 复制节点尝试以epoch 4 (3+1) 进行选举,主节点以currentEpoch 5做出确定的答复,但答复被延迟。
  • 复制节点将尝试在以后的某个epoch  5 (4+1) 再次被选举,延迟的答复将以currentEpoch 5到达复制节点,并被视为有效。
  1. 如果 NODE_TIMEOUT * 2 的主节点的复制节点已经被投票,则主节点不会在同一主节点的复制节点上投票。这是没有严格要求,因为它是不可能的两个复制节点赢得选举在同一个epoch。 但是,实际上,它确保当一个复制节点被选出时,它有足够的时间通知其他复制节点,并避免另一个复制节点赢得新选举的可能性,从而执行不必要的第二次故障转移。
  2. 如果已经投票选出了同一主节点的复制节点,那么在达到 NODE_TIMEOUT * 2 之前,主节点不会投票给同一主节点的复制节点。这是没有严格要求,因为它是不可能的两个节点赢得选举在同一个时代(epoch)。 但是,实际上,它确保当一个复制节点被选出时,它有足够的时间通知其他复制节点,并避免另一个复制节点将赢得新的选举的可能性,从而执行不必要的第二次故障转移。
  3. 主节点不会以任何方式选择最佳的复制节点。 如果复制节点的主节点处于FAIL状态,并且主节点在当前任期中没有投票,那么将获得正面投票。最好从最容易入手的选举和其他复制节点之前赢得它,因为它通常能够在上一节中的说明应尽早开始,因为它的更高级别的投票过程。
  4. 当主节点拒绝为给定的复制节点投票时,没有否定响应,该请求将被忽略。
  5. 主节点不会投票给发送configEpoch(对于复制节点声名的插槽,configEpoch小于主节点表中的任何一个configEpoch)的复制节点。 记住,复制节点发送其主节点的configEpoch以及其主节点的插槽的位图。 这意味着,请求投票的复制节点必须为其要进行故障转移的插槽配置一个新的或等于授予投票的主节点的插槽。

7.6.分区期间配置时代(epoch)实用性的实际示例

本部分描述了该时代(epoch)的概念用于使复制节点晋升过程给分区更有抵抗力。

  • 不再是无限可能的主节点。 主节点有三个复制节点A,B,C。
  • 复制节点A赢得选举,并晋升为主节点。
  • A网络分区使A不适用于大多数群集。
  • 复制节点B赢得大选,并晋升为主节点。
  • A分区使B对大多数群集不可用。
  • 先前的分区是固定的,并且A再次可用。

此时B断开,A再次充当主节点角色(实际上UPDATE消息会立即对其进行重新配置,但在此我们假设所有UPDATE消息都丢失了)。 同时,复制节点C将尝试通过选举来故障转移B。这是发生的情况:

  1. C将尝试选举才能当选,并会成功,因为主节点(对于大多数主节点)实际上是不可用的。 它将获得一个新的增量configEpoch。
  2. A将无法声称自己是其哈希槽的主节点,因为与A发布的节点相比,其他节点已经具有与较高配置时代(epoch)(B中的一个)关联的相同哈希槽。
  3. 因此,所有节点将升级其表以将哈希槽分配给C,并且群集将继续其操作。

正如您将在下一节中看到的那样,重新加入集群的旧节点通常会尽快收到有关配置更改的通知,因为一旦对其他任何节点执行ping操作,接收方就会检测到它具有旧的信息并发送UPDATE消息。

7.7.哈希插槽配置传播

Redis群集的重要部分是用于传播有关哪个群集节点正在服务给定的哈希槽集的信息的机制。 这对于启动新集群以及在升级复制节点以为其故障主节点的插槽提供服务后升级配置的能力都至关重要。

相同的机制允许将节点无限期地分开,以明智的方式重新加入群集。

散列槽配置的传播方式有两种:

  1. 心跳(Heartbeat)消息。 ping或pong数据包的发送者始终会添加有关它(或它的主节点,如果它是复制节点)服务的哈希槽集的信息。
  2. UPDATE消息。 由于在每个心跳数据包中都有有关发件人configEpoch和服务的哈希槽集的信息,因此,如果心跳数据包的接收方发现发件人信息过时,它将发送包含新信息的数据包,从而迫使过时的节点更新其信息。

心跳(Heartbeat)或UPDATE消息的接收者使用某些简单规则,以将其哈希表映射到节点的表进行更新。 创建新的Redis Cluster节点时,其本地哈希槽表仅被初始化为NULL条目,这样每个哈希槽都不会绑定或链接到任何节点。 这看起来类似于以下内容:

0 -> NULL
1 -> NULL
2 -> NULL
...
16383 -> NULL

节点为了更新其哈希槽表而遵循的第一条规则如下:

规则1:如果未分配哈希槽(设置为NULL),并且已知节点声明该哈希槽,则将修改哈希表并将其声明的哈希槽与之关联。

因此,如果我们复制节点A收到心跳信号,声称以配置时代值为3为哈希槽1和2提供服务,则该表将被修改为:

0 -> NULL
1 -> A [3]
2 -> A [3]
...
16383 -> NULL

创建新集群时,系统管理员需要手动(使用CLUSTER ADDSLOTS命令,通过redis-trib命令行工具或通过任何其他方式)将每个主节点服务的插槽仅分配给该节点本身,并且信息将在整个群集中快速传播。

但是,该规则还不够。 我们知道哈希槽映射可能在两个事件中发生变化:

  1. 在故障转移期间,复制节点将替换其主节点。
  2. 插槽复制节点重新分片到另一个复制节点

现在,让我们集中讨论故障转移。 当复制节点故障转移其主节点时,它获得一个配置时代,该配置时代被保证大于其主节点的时代(并且通常大于先前生成的任何其他配置时代)。 例如,节点A(有复制节点B)可能会以配置时代4进行故障转移B。它将开始发送心跳数据包(第一次在群集范围内进行大规模广播),并且由于遵循以下第二条规则,接收方将进行更新他们的哈希表:

规则2:如果已经分配了哈希槽,并且已知节点正在使用configEpoch来发布它,该configEpoch大于当前与该槽关联的主节点的configEpoch,则将哈希槽重新绑定到新节点。

因此,在接收到来自B的消息后,这些消息声称将使用配置时代4来服务哈希槽1和2,接收方将以以下方式更新其表:

0 -> NULL
1 -> B [4]
2 -> B [4]
...
16383 -> NULL

活动属性(Liveness property):由于第二条规则,群集中的所有节点最终都会同意,插槽的所有者是在发布该节点的节点中configEpoch最大的插槽的所有者。

Redis Cluster中的此机制称为最后一次故障转移胜利。

在重新分片期间也会发生同样的情况。 当导入哈希槽的节点完成导入操作时,其配置时期会增加以确保更改将在整个集群中传播。

7.8.UPDATE消息,仔细查看

考虑到上一节的内容,可以更轻松地查看更新消息的工作方式。 一段时间后,节点A可能会重新加入群集。 它将发送心跳数据包,并声明其服务于配置时代为3的哈希槽1和2。所有具有更新信息的接收器将改为看到相同的哈希槽与具有更高配置时代的节点B相关联。 因此,他们将使用插槽的新配置向A发送UPDATE消息。 由于上述规则2,A将更新其配置。

7.9.节点如何重新加入集群

节点重新加入集群时,将使用相同的基本机制。 继续上面的示例,将向节点A通知B现在正在服务哈希槽1和2。假设这两个是A所服务的唯一哈希槽,则A所服务的哈希槽的数量将降至0! 因此,A将重新配置为新主节点的复制节点。

遵循的实际规则比这要复杂一些。 通常,很可能会在很长时间后重新加入A,与此同时,可能会发生由A最初服务的哈希槽由多个节点服务,例如哈希槽1可能由B服务,哈希槽2则由C服务。

因此,实际的Redis群集节点角色切换规则是:主节点将更改其配置,以复制(作为其复制节点)占用其最后一个哈希槽的节点。

在重新配置期间,最终服务的哈希槽数将降至零,并且节点将相应地重新配置。 请注意,在基本情况下,这仅意味着旧的主节点将是复制节点(故障转移后替换了它的复制节点)。 但是,一般而言,该规则涵盖所有可能的情况。

复制节点的操作完全相同:它们重新配置以复制占用其前主节点的最后一个哈希槽的节点。

7.10.复制节点迁移(Replica migration)

Redis Cluster实施了称为复制节点迁移的概念,以提高系统的可用性。 这个想法是,在具有主从设置的群集中,如果单个节点发生多个独立故障,则复制节点和主节点之间的映射是固定的,随时间的推移将受到限制。

例如,在每个主节点都有一个复制节点的群集中,只要主节点或复制节点发生故障,群集就可以继续操作,但如果两者都同时发生故障,则群集可以继续操作。 但是,有一类故障是由硬件或软件问题引起的单个节点的独立故障,这些故障可能随着时间的推移而累积。 例如:

  • 主节点A有一个复制节点A1。
  • 主节点A失败。 A1晋升为新主节点。
  • 三个小时后,A1独立失败(与A失败无关)。 由于节点A仍处于关闭状态,因此没有其他复制节点可用于升级。 群集无法继续正常操作。

如果主节点和复制节点之间的映射关系是固定的,则使群集更能抵抗上述情况的唯一方法是向每个主节点添加复制节点,但这是昂贵的,因为它需要执行更多的Redis实例,更多的内存和 等等。

一种替代方法是在群集中创建不对称性,并让群集布局随时间自动更改。 例如,群集可能具有三个主节点A,B,C。A和B每个都有一个复制节点A1和B1。 但是,主节点C是不同的,它有两个复制节点:C1和C2。

复制节点迁移是自动重新配置复制节点的过程,以便迁移到不再具有覆盖范围的主节点(没有工作的复制节点)。 使用复制节点迁移时,上述方案将变成以下情况:

  • 主节点A失败。 A1被提升。
  • C2迁移为A1的复制节点,否则将不受任何复制节点的支持。
  • 三个小时后,A1也失败了。
  • C2晋升为新的主节点,以取代A1。
  • 集群可以继续操作。

7.11.复制节点迁移算法(Replica migration algorithm)

迁移算法不使用任何形式的协议,因为Redis集群中的复制节点布局不是集群配置的一部分,需要与配置时代保持一致and/or版本化。 相反,它使用一种算法来避免在不支持主节点时,复制节点的大量迁移。 该算法保证最终(一旦群集配置稳定),每个主节点将至少由一个复制节点支持。

这就是算法的工作原理。 首先,我们需要定义在这种情况下什么是好的复制节点:从给定节点的角度来看,好的复制节点是未处于FAIL状态的复制节点。

在检测到至少有一个主节点(没有良好复制节点)的每个复制节点中,触发算法的执行。 但是,在检测到这种情况的所有复制节点中,只有一个子集应该起作用。 实际上,该子集通常是单个复制节点,除非不同的复制节点在给定时刻对其他节点的故障状态有稍微不同的看法。

代理复制节点是主节点中具有最多已连接复制节点数量的复制节点,它不处于FAIL状态并且节点ID最小。

因此,例如,如果有10个主节点,每个主节点1个复制节点,以及2个主节点,每个主节点5个复制节点,则将尝试迁移的复制节点是(在2个具有5个复制节点的主节点中)一个具有最低节点ID的主节点。如果未使用任何协议,则当集群配置不稳定时,可能会发生竞争状况,其中多个复制节点认为自己是具有较低节点ID的非故障复制节点(实际上,这种情况不太可能发生)。如果发生这种情况,结果是多个复制节点迁移到同一主节点,这是无害的。 如果争夺的发生会导致割让主节点方没有复制节点方,则一旦群集再次稳定,算法将再次重新执行,并将复制节点方迁移回原始主节点方。

最终,每个主节点将至少由一个复制节点支持。 但是,正常的行为是单个复制节点从具有多个复制节点的主节点迁移到孤立的主节点。

该算法由一个名为cluster-migration-barrier的用户可配置参数控制:在复制节点可以迁移之前,主节点必须保留好的主节点的数量。 例如,如果将此参数设置为2,则仅当复制节点的主节点仍然有两个工作的复制节点时,才可以尝试迁移。

7.12.configEpoch冲突解决算法

在故障转移期间通过复制节点升级创建新的configEpoch值时,可以确保它们是唯一的。

但是,在两个不同的事件中,以不安全的方式创建新的configEpoch值,只是增加了本地节点的本地的currentEpoch并希望同时没有冲突。 这两个事件都是由系统管理员触发的:

  1. 带有TAKEOVER选项的CLUSTER FAILOVER命令能够手动将一个复制节点提升为一个主节点,而没有大多数主节点可用。 例如,这在多数据中心设置中很有用。
  2. 出于集群性能的考虑,用于集群重新平衡的插槽迁移还会在本地节点内部生成新的配置时代。

具体来说,在手动重新分片期间,当哈希槽复制节点A迁移到节点B时,重新分片程序将强制B将其配置升级到集群中发现的最大时代加1(除非该节点是已经是配置时代最大的一个),而无需其他节点的同意。

通常,现实世界中的重新分片涉及移动数百个哈希槽(尤其是在小型集群中)。 对于重新移动的每个哈希槽,要求达成协议以在重新分片期间生成新的配置时代是无效的。

此外,它每次都需要在每个群集节点中进行fsync,以便存储新配置。 由于它的执行方式不同,因此当第一个哈希槽移动时,我们仅需要一个新的配置时代,从而使其在生产环境中效率更高。

但是,由于上述两种情况,有可能(尽管不太可能)以具有相同配置时期的多个节点结束。 如果传播速度不够快,则由系统管理员执行的重新分片操作以及同时发生的故障转移(加上很多不幸)会导致currentEpoch冲突。

此外,软件错误和文件系统损坏也可能导致具有相同配置时代的多个节点。

当服务于不同哈希槽的主节点具有相同的configEpoch时,就没有问题。 更重要的是,故障转移到主节点的复制节点具有唯一的配置时代。

也就是说,手动干预或重新分片可能会以不同方式更改集群配置。 Redis Cluster的main liveness属性要求插槽配置始终收敛,因此在每种情况下,我们实际上都希望所有主节点具有不同的configEpoch。

为了强制执行此操作,如果两个节点最终使用相同的configEpoch,则使用冲突解决算法。

  • 如果一个主节点检测到另一个主节点正在使用相同的configEpoch通告自己。
  • 并且如果该节点的字典ID比其他声称相同configEpoch的节点小。
  • 然后,它将currentEpoch加1,并将其用作新的configEpoch。

如果有任何一组具有相同configEpoch的节点,则除了具有最大Node ID的节点之外的所有节点都将继续前进,从而确保最终每个节点都将选择唯一的configEpoch,而不管发生了什么。

这种机制还可以确保在创建新集群后,所有节点都以不同的configEpoch开始(即使未实际使用),因为redis-trib确保在启动时使用CONFIG SET-CONFIG-EPOCH。 但是,如果由于某种原因而使节点配置错误,它将自动将其配置更新为其他配置时代。

7.13.节点重置

可以对节点进行软件重置(无需重新启动节点),以便在不同角色或不同群集中重复使用。

这在常规操作,测试以及云环境中很有用,在该环境中可以重新配置给定节点以加入一组不同的节点以扩大或创建新集群。

在Redis群集中,使用CLUSTER RESET命令重置节点。 该命令有两种变体:

  1. 软复位和硬复位(Soft and hard reset):如果节点是复制节点,则将其转变为主节点,并丢弃其数据集。 如果该节点是主节点且包含键,则重置操作将中止。
  2. 软复位和硬复位:释放所有插槽,并重置手动故障转移状态。
  3. 软复位和硬复位:删除了节点表中的所有其他节点,因此该节点不再知道任何其他节点。
  4. 仅硬重置(Hard reset only):currentEpoch,configEpoch和lastVoteEpoch设置为0。
  5. 仅硬重置:节点ID更改为新的随机ID。

具有非空数据集的主节点无法重置(因为通常您希望将数据重新分片到其他节点)。 但是,在合适的特殊条件下(例如,出于创建新集群的目的而完全破坏了集群),必须在执行重置之前执行FLUSHALL

7.14.从集群中删除节点

通过将所有数据重新分片到其他节点(如果它是主节点)并关闭它,实际上可以从现有群集中删除该节点。 但是,其他节点仍将记住其节点ID和地址,并尝试与其连接。

因此,当删除节点时,我们还希望从所有其他节点表中删除其条目。 这可以通过使用CLUSTER FORGET <node-id>命令来完成。

该命令执行两件事:

  1. 它复制节点表中删除具有指定节点ID的节点。
  2. 它设置了60秒的禁止时间,以防止重新添加具有相同节点ID的节点。

第二个操作是必需的,因为Redis Cluster使用八卦(gossip)来自动发现节点,因此复制节点A删除节点X可能导致节点B再次将节点X八卦(gossip)到节点A。 由于有60秒的禁止,Redis Cluster管理工具有60秒的时间才能从所有节点中删除该节点,从而防止由于自动发现而重新添加该节点。

有关详细信息,请参阅CLUSTER FORGET文档。

8.发布/订阅(Publish/Subscribe)

在Redis Cluster中,客户端可以订阅每个节点,也可以发布到每个其他节点。 群集将确保已发布的邮件根据需要进行转发。

当前的实现将简单地将每个发布的消息广播到所有其他节点,但是在某些时候将使用Bloom过滤器或其他算法对其进行优化。

9.附录

附录A:ANSI C中的CRC16参考实现

/*
 * Copyright 2001-2010 Georges Menie (www.menie.org)
 * Copyright 2010 Salvatore Sanfilippo (adapted to Redis coding style)
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the University of California, Berkeley nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/* CRC16 implementation according to CCITT standards.
 *
 * Note by @antirez: this is actually the XMODEM CRC 16 algorithm, using the
 * following parameters:
 *
 * Name                       : "XMODEM", also known as "ZMODEM", "CRC-16/ACORN"
 * Width                      : 16 bit
 * Poly                       : 1021 (That is actually x^16 + x^12 + x^5 + 1)
 * Initialization             : 0000
 * Reflect Input byte         : False
 * Reflect Output CRC         : False
 * Xor constant to output CRC : 0000
 * Output for "123456789"     : 31C3
 */

static const uint16_t crc16tab[256]= {
    0x0000,0x1021,0x2042,0x3063,0x4084,0x50a5,0x60c6,0x70e7,
    0x8108,0x9129,0xa14a,0xb16b,0xc18c,0xd1ad,0xe1ce,0xf1ef,
    0x1231,0x0210,0x3273,0x2252,0x52b5,0x4294,0x72f7,0x62d6,
    0x9339,0x8318,0xb37b,0xa35a,0xd3bd,0xc39c,0xf3ff,0xe3de,
    0x2462,0x3443,0x0420,0x1401,0x64e6,0x74c7,0x44a4,0x5485,
    0xa56a,0xb54b,0x8528,0x9509,0xe5ee,0xf5cf,0xc5ac,0xd58d,
    0x3653,0x2672,0x1611,0x0630,0x76d7,0x66f6,0x5695,0x46b4,
    0xb75b,0xa77a,0x9719,0x8738,0xf7df,0xe7fe,0xd79d,0xc7bc,
    0x48c4,0x58e5,0x6886,0x78a7,0x0840,0x1861,0x2802,0x3823,
    0xc9cc,0xd9ed,0xe98e,0xf9af,0x8948,0x9969,0xa90a,0xb92b,
    0x5af5,0x4ad4,0x7ab7,0x6a96,0x1a71,0x0a50,0x3a33,0x2a12,
    0xdbfd,0xcbdc,0xfbbf,0xeb9e,0x9b79,0x8b58,0xbb3b,0xab1a,
    0x6ca6,0x7c87,0x4ce4,0x5cc5,0x2c22,0x3c03,0x0c60,0x1c41,
    0xedae,0xfd8f,0xcdec,0xddcd,0xad2a,0xbd0b,0x8d68,0x9d49,
    0x7e97,0x6eb6,0x5ed5,0x4ef4,0x3e13,0x2e32,0x1e51,0x0e70,
    0xff9f,0xefbe,0xdfdd,0xcffc,0xbf1b,0xaf3a,0x9f59,0x8f78,
    0x9188,0x81a9,0xb1ca,0xa1eb,0xd10c,0xc12d,0xf14e,0xe16f,
    0x1080,0x00a1,0x30c2,0x20e3,0x5004,0x4025,0x7046,0x6067,
    0x83b9,0x9398,0xa3fb,0xb3da,0xc33d,0xd31c,0xe37f,0xf35e,
    0x02b1,0x1290,0x22f3,0x32d2,0x4235,0x5214,0x6277,0x7256,
    0xb5ea,0xa5cb,0x95a8,0x8589,0xf56e,0xe54f,0xd52c,0xc50d,
    0x34e2,0x24c3,0x14a0,0x0481,0x7466,0x6447,0x5424,0x4405,
    0xa7db,0xb7fa,0x8799,0x97b8,0xe75f,0xf77e,0xc71d,0xd73c,
    0x26d3,0x36f2,0x0691,0x16b0,0x6657,0x7676,0x4615,0x5634,
    0xd94c,0xc96d,0xf90e,0xe92f,0x99c8,0x89e9,0xb98a,0xa9ab,
    0x5844,0x4865,0x7806,0x6827,0x18c0,0x08e1,0x3882,0x28a3,
    0xcb7d,0xdb5c,0xeb3f,0xfb1e,0x8bf9,0x9bd8,0xabbb,0xbb9a,
    0x4a75,0x5a54,0x6a37,0x7a16,0x0af1,0x1ad0,0x2ab3,0x3a92,
    0xfd2e,0xed0f,0xdd6c,0xcd4d,0xbdaa,0xad8b,0x9de8,0x8dc9,
    0x7c26,0x6c07,0x5c64,0x4c45,0x3ca2,0x2c83,0x1ce0,0x0cc1,
    0xef1f,0xff3e,0xcf5d,0xdf7c,0xaf9b,0xbfba,0x8fd9,0x9ff8,
    0x6e17,0x7e36,0x4e55,0x5e74,0x2e93,0x3eb2,0x0ed1,0x1ef0
};

uint16_t crc16(const char *buf, int len) {
    int counter;
    uint16_t crc = 0;
    for (counter = 0; counter < len; counter++)
            crc = (crc<<8) ^ crc16tab[((crc>>8) ^ *buf++)&0x00FF];
    return crc;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

琴 韵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值