Redis集群规范译文

原文地址:https://redis.io/topics/cluster-spec

Redis集群规范

欢迎来到Redis集群规范。在这里您可以找到有关Redis Cluster的算法和设计原理的信息。该文档正在进行中,因为它与Redis的实际实施不断同步。

设计的主要特性和基本原理

Redis集群目标

Redis集群是Redis的一个分布式实现,具有以下目标,按照设计中的重要性排序:

  • 高性能和线性可扩展性高达1000个节点。没有代理,使用异步复制,并且没有对值执行合并操作。
  • 可接受的写入安全程度:系统尝试(以尽力而为的方式)保留源自与大多数主节点连接的客户端的所有写入。通常会有小窗口,可能会丢失已确认的写入内容。当客户端处于少数分区时,丢失确认写入的窗口会更大。
  • 可用性:Redis集群能够在大部分主节点可到达的分区中存活,并且每个不再可访问的主节点至少有一个可到达的从节点。此外,使用副本迁移,任何slave不再复制的master将从多个slave所覆盖的master接收。

本文档中描述的内容在Redis 3.0或更高版本中实现。

实施子集

Redis集群实现了Redis非分布式版本中可用的所有单key命令。执行复杂多key操作(如Set类型联合或交集)的命令也可以实现,只要这些key都属于同一个节点即可。

Redis集群实现了一个称为hash标记的概念,可用于强制某些key存储在同一节点中。但是,在手动重新绑定期间,多key操作可能在一段时间内不可用,而单key操作始终可用。

Redis集群不支持多个数据库,如Redis的独立版本。只有数据库0,并且不允许SELECT命令。

Redis集群协议中的客户端和服务器角色

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

为了执行他们的任务,所有的集群节点都使用TCP总线和称为Redis集群总线的二进制协议进行连接。每个节点都使用集群总线连接到集群中的每个其他节点。节点使用gossip协议来传播关于集群的信息,以便发现新节点,发送ping分组以确保所有其他节点都正常工作,并发送需要的集群消息以表示特定条件。集群总线还用于在集群中传播发布/订阅消息,并在用户请求时协调手动故障转移(手动故障转移是不是由Redis集群故障检测器启动的故障转移,而是由系统管理员直接进行)。

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

写安全

Redis集群使用节点之间的异步复制,并且最后的故障转移获得隐式合并功能。这意味着最后选择的主数据集最终会替换所有其他副本。在分区期间总有一段时间可能会丢失写入。然而,这些窗口与连接到大多数master的客户端以及连接到少数master的客户端的情况非常不同。

与在少数方面执行的写操作相比,Redis Cluster更难以保留由连接到大多数master的客户端执行的写入操作。以下是在故障期间会导致大多数分区中收到的确认写入丢失的情况示例:

  1. 写操作可能会到达master,但master可能能够回复客户端,写操作可能不会通过主节点和slave节点之间使用的异步复制传播到slave。如果master在没有写到达slave的情况下死掉,那么如果master在一段足够长的时间内写操作无法到达其升级的一个slave,写入将永远丢失。在主节点发生突然故障时,通常很难观察到这种情况,因为主节点尝试在几乎同一时间回复客户端(确认写入)和slave(传播写入)。然而这是一个真实世界的故障模式。
  2. 写丢失的另一理论上可能的故障模式如下:
    • 由于分区,master无法访问。
    • 它的一个slave失败了。
    • 一段时间后,它可能会再次可达。
    • 具有过期路由表的客户端可能会在旧主节点被集群转换为slave节点(新主节点的)之前写入旧主节点。

第二种故障模式不太可能发生,因为主节点无法与大多数其他master进行足够时间的通信故障切换,将不再接受写入操作,并且当分区被修复时,写入操作仍会在少量时间内被拒绝 以允许其他节点通知配置更改。此故障模式还要求客户端的路由表尚未更新。

以分区的少数为目标的写入有一个更大的窗口丢失。例如,Redis集群在少数主节点和至少一个或多个客户端的分区上失去了不平凡的写入次数,因为发送给master的所有写入操作可能会在master在多数端发生故障时丢失。

具体而言,对于master进行故障切换,大多数master至少需要NODE_TIMEOUT才能访问它,因此如果在此之前分区是固定的,则不会丢失写入操作。当分区的持续时间超过NODE_TIMEOUT时,所有在少数端执行的写入操作可能会丢失。然而,Redis集群的少数端将在NODE_TIMEOUT时间过去后立即开始拒绝写入,而不与多数端联系,所以最大化窗口后,少数端不再可用。因此,在那段时间之后,没有任何写入被接受或丢失。

可用性

Redis Cluster在分区的少数端不可用。在分区的多数端,假设至少大多数master和每个无法访问的master都有一个slave,则在NODE_TIMEOUT时间加上slave被选举并故障切换其master所需的几秒钟后,集群将再次可用(故障转移通常在1或2秒内执行)。

这意味着Redis集群可以在集群中的少数几个节点出现故障的情况下继续运行,但它不适用于需要大量网络分区时可用性的应用程序。

在由N个主节点组成的集群的示例中,每个节点具有单个slave节点,只要单个节点被划分开来,集群的多数侧将保持可用,并且将保持可用的概率为1-(1 /(N * 2-1))当两个节点分开时(在第一个节点失败后,我们总共剩下N * 2-1个节点,并且没有副本失败的唯一主节点的概率为1 /(N *2-1))。

例如,在每个节点有5个节点和一个slave节点的集群中,有一个1 /(5 * 2-1)= 11.11%的概率,即在将两个节点从大多数节点分开之后,集群将不再可用。

由于Redis Cluster功能(称为副本迁移),集群可用性在许多真实世界的情况下得到了改善,因为副本迁移到孤立的master(master不再有副本)。因此,在每次成功的故障事件中,集群都可以重新配置slave以便更好地抵御下一次故障。

性能

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

最终客户端获得集群的最新展现,以及哪个节点服务于哪个key子集,因此在正常操作期间,客户端直接连接正确的节点以发送给定的命令。

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

另外,由于多key命令仅限于接近的key,所以除了重新分片之外,数据不会在节点之间移动。

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

非常高的性能和可扩展性,同时保持弱但合理形式的数据安全性和可用性是Redis Cluster的主要目标。

为什么要避免合并操作

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

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

Redis集群主要组件的概述

key分配模型

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

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

(用于将key映射到hash槽的基本算法如下所示(请阅读本规则的hash标记异常的下一段落):

HASH_SLOT = CRC16(key) mod 16384

CRC16规定如下:

  • Name: XMODEM (also known as ZMODEM or CRC-16/ACORN)
  • 宽度:16位
  • Poly:1021(实际上是 x 16 + x 12 + x 5 + 1
  • 初始化:0000
  • 反射输入字节:False
  • 反射输出CRC:False
  • 异或常数输出CRC:0000
  • 输出为“123456789”:31C3

16个CRC16输出位中有14个被使用(这就是为什么上述公式中存在模16384操作的原因)。

在我们的测试中,CRC16在16384槽中均匀分配不同类型的key方面表现出色。

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

key hash标记

计算用于实现hash标记的hash槽有一个例外。哈希标记是确保多个key分配在同一个哈希槽中的一种方法。这用于在Redis集群中实现多key操作。

为了实现hash标签,在某些条件下,key的hash槽以稍微不同的方式计算。如果key包含“{…}”模式,则只有{和}之间的子字符串被hash才能获得hash槽。

然而,由于可能有多次出现{or},所以通过以下规则可以很好地指定算法:

  • 如果key包含{字符。
  • 并且如果有一个}在{字符右面。
  • 并且如果第一次出现{和第一次出现}之间有一个或多个字符。

然后,不是对key进行哈希处理,而是仅对第一次出现的{和第一次出现的}之间进行哈希处理。

例如:

  • 两个key{user1000}.following和{user1000} .followers将哈希到相同的哈希槽,因为只有子字符串user1000将被哈希以计算哈希槽。
  • 对于keyfoo {} {bar},整个key将像平常一样被hash,因为在第一次出现{后面是}右侧没有中间的字符。
  • 对于关keyfoo {{bar}} zap,子字符串{bar将被hash,因为它是第一次出现{和其右侧第一次出现}之间的子字符串。
  • 对于foo {bar} {zap}key,子字符串将被hash,因为该算法在{和}的第一个有效或无效(无字节内部)匹配处停止。
  • 从该算法得出的结论是,如果key以{}开始,则它总是被hash。当使用二进制数据作为key名时,这很有用。

添加hash标记异常,以下是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;
}

集群节点属性

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

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

节点ID不是与每个节点相关的唯一信息,而是始终全局一致的唯一信息。每个节点还有以下一组相关信息。一些信息是关于此特定节点的集群配置详细信息,并且最终在集群中保持一致。其他一些信息,比如最后一次节点被ping的时间,对每个节点而言都是本地的。

有关所有节点字段的详细说明,请参阅CLUSTER NODES文档。

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的特定部分时予以介绍。

集群总线

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

节点到节点之间的通信独占地使用集群总线和集群总线协议:由不同类型和大小的帧组成的二进制协议。集群总线二进制协议没有公开记录,因为它不适用于外部软件设备与使用此协议的Redis集群节点通信。但是,通过阅读Redis集群源代码中的cluster.h和cluster.c文件,您可以获得关于集群总线协议的更多详细信息。

集群拓扑

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

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

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

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

节点握手

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

一个节点只能通过两种方式接受另一个节点作为集群的一部分:

  • 如果节点出现MEET消息。meet消息与PING消息完全相同,但强制接收者接受节点作为集群的一部分。只有系统管理员通过以下命令请求时,节点才会将MEET消息发送到其他节点:
    CLUSTER MEET ip port
  • 如果已经被信任的节点将gossip这个其他节点,节点也将注册另一个节点作为集群的一部分。因此,如果A知道B,B知道C,那么B最终会向A发送关于C的gossip消息。发生这种情况时,A将C注册为网络的一部分,并尝试与C连接。

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

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

重定向和重新分区

MOVED重定向

Redis客户端可以自由地向集群中的每个节点(包括从节点)发送查询。节点将分析查询,如果它是可接受的(即查询中只提到了一个key,或者提到的多个key都是相同的哈希槽),它将查找哪个节点负责哈希槽一个或多个key所属的位置。

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

GET x
-MOVED 3999 127.0.0.1:6381

该错误包括key的hash槽(3999)和可以用于查询的实例的ip:port。客户端需要将查询重新发布到指定节点的IP地址和端口。请注意,即使客户端在重新发布查询之前等待了很长时间,并且在此期间集群配置发生了变化,如果hash槽3999现在由另一个节点提供服务,目标节点将再次以MOVED错误进行回复。如果联系的节点没有更新信息,则会发生同样的情况。

因此,从集群节点的角度来看,我们试图简化与客户端的接口,只是公开由IP:端口对标识的哈希槽和Redis节点之间的映射。

因此,从集群节点的角度来看,我们试图简化与客户端的接口,只是公开由IP:端口对标识的哈希槽和Redis节点之间的映射。

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

另一种方法是在收到MOVED重定向时,使用CLUSTER NODES或CLUSTER SLOTS命令刷新整个客户端集群布局。遇到重定向时,可能会重新配置多个槽而不是一个槽,因此,尽快更新客户端配置通常是最佳策略。

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

客户端还必须能够处理本文后面所述的ASK重定向,否则它不是完整的Redis集群客户端。

集群实时重新配置

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

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

实现的核心是能够移动哈希槽。从实际的角度来看,hash槽只是一组key,所以Redis Cluster在重新分解过程中真正做的是将key从实例移动到另一个实例。移动hash槽意味着将发生hash的所有key移动到该hash槽中。

为了理解这是如何工作的,我们需要显示用于操作Redis集群节点中的槽转换表的CLUSTER子命令。

以下子命令可用(其他情况下在此情况下无效):

  • CLUSTER ADDSLOTS slot1 [slot2] … [slotN]
  • CLUSTER DELSLOTS slot1 [slot2] … [slotN]
  • CLUSTER SETSLOT slot NODE node
  • CLUSTER SETSLOT slot MIGRATING node
  • CLUSTER SETSLOT slot IMPORTING node

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

hash槽被分配后,它们将使用gossip协议在集群中传播,如稍后在配置传播部分中指定的。

ADDSLOTS命令通常用于从头开始创建新集群,以便为每个主节点分配可用的所有16384hash槽的子集。

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

如果使用SETSLOT <槽> NODE表单,则SETSLOT子命令用于将槽分配给特定的节点ID。否则,槽可以设置为两种特殊状态MIGRATING和IMPORTING。这两个特殊状态用于将hash槽从一个节点迁移到另一个节点。

  • 当一个槽设置为MIGRATING时,节点将接受所有关于此hash槽的查询,但前提是存在问题的key,否则将使用-ASK重定向将查询转发到作为迁移目标的节点。
  • 当一个槽设置为IMPORTING时,节点将接受所有关于此hash槽的查询,但前提是请求前面有一个ASKING命令。如果ASKING命令不是由客户端提供的,那么查询将通过-MOVED重定向错误重定向到真正的哈希槽所有者,正如通常发生的那样。

让我们用hash槽迁移的例子来说明这一点。假设我们有两个Redis主节点,称为A和B.我们希望将hash槽8从A移动到B,所以我们发出这样的命令:

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

每次使用属于hash槽8的key查询时,所有其他节点将继续将客户端指向节点“A”,所以会发生的情况是:

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

这样我们不再在“A”中创建新的key。同时,在重新配置和Redis集群配置过程中使用的一个称为redis-trib的特殊程序会将hash槽8中的现有key从A迁移到B.这是使用以下命令执行的:

CLUSTER GETKEYSINSLOT slot count

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

MIGRATE target_host target_port key target_database id timeout

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

在Redis集群中,不需要指定非0的数据库,但MIGRATE是一个常规命令,可用于不涉及Redis集群的其他任务。即使在移动复杂key(如长列表)时,MIGRATE的优化速度也会尽可能快,但在Redis集群中,如果在使用数据库的应用程序中存在延迟限制,则重新配置集群中存在大key的集群不被视为明智的过程。

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

ASK重定向

在上一节中,我们简要介绍了ASK重定向。为什么我们不能简单地使用MOVED重定向? 因为虽然MOVED意味着我们认为hash槽由一个不同节点永久服务,并且应该针对指定节点尝试下一个查询,但ASK意味着只发送下一个查询到指定节点。

这是必要的,因为下一个关于hash槽8的查询可能是关于一个仍然在A中的key,所以我们总是希望客户端尝试A然后根据需要尝试B. 由于这种情况只发生在16384个可用的哈希槽中,集群的性能可以接受。

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

基本上,ASKING命令在客户端上设置一次性标记,强制节点提供关于IMPORTING槽的查询。

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

  • 如果收到ASK重定向,只发送重定向到指定节点的查询,但继续向旧节点发送后续查询。
  • 用ASKING命令启动重定向的查询。
  • 尚未更新本地客户端表以将hash槽8映射到B.

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

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

客户首次连接并处理重定向

尽管Redis集群客户端实现可能不记住槽配置(槽编号与服务节点的地址之间的映射),并且只能通过联系等待重定向的随机节点来工作,但这样的客户端可能非常低效。

Redis集群客户端应该尽量聪明地记住槽配置。但是,此配置不需要是最新的。由于联系错误的节点只会导致重定向,应该触发客户端视图的更新。

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

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

请注意,客户端可以通过更新其表中移动的槽来处理MOVED重定向,但这通常效率不高,因为通常多个槽的配置一次性被修改(例如,如果slave被升级为master,由旧master担任的所有槽将被重新映射)。通过从头开始将完整的槽映射到节点,对MOVED重定向作出反应要简单得多。

为了检索槽配置,Redis集群为不需要解析的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

返回数组的每个元素的前两个子元素都是该槽范围的开始和结束。其他元素表示地址端口对。第一个地址 - 端口对是服务于该槽的master,并且附加的地址 - 端口对是服务于同一槽且不处于错误状态(即未设置FAIL标记)的所有slave。

例如,输出的第一个元素表示从5461到10922(包括开始和结束)的槽由127.0.0.1:7001提供服务,并且可以在127.0.0.1:7004处扩展与slave联系的只读负载。

如果集群配置错误,CLUSTER SLOTS不能保证返回覆盖整个16384槽的范围,因此客户端应该初始化槽配置映射,以便用NULL对象填充目标节点,并在用户尝试执行有关属于未分配槽的key的命令时报告错误。

在发现某个槽未分配时,在向调用方返回错误之前,客户端应尝试再次获取槽配置,以检查集群现在是否已正确配置。

多key操作

使用hash标记,客户端可以自由使用多key操作。例如,以下操作是有效的:

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

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

更具体地说,即使在重新分片期间,针对所有存在且仍然在同一节点(源节点或目的节点)中的多key操作仍然可用。

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

只要指定hash槽的迁移终止,所有多key操作就可以再次用于该hash槽。

使用从节点扩展读取

通常,slave节点会将客户端重定向到授权master以获取给定命令中涉及的hash槽,但客户端可以使用slave来使用READONLY命令来扩展读取。

READONLY告诉Redis集群从节点客户端正在读取可能过时的数据,并且对运行写查询不感兴趣。

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

  1. 客户端发送了一个关于hash槽的命令,hash槽从来没有被这个slave的master所服务。
  2. 集群已重新配置(例如重新分片),并且slave不再能够为给定hash槽提供命令。

发生这种情况时,客户端应按照前面部分所述更新其hash图。

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

容错

心跳和gossip消息

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

通常节点发送ping数据包,这会触发接收方用pong数据包进行回复。但是,这不一定是真的。节点只需发送pong数据包就可以向其他节点发送有关其配置的信息,而不会触发回复。例如,为了尽快广播新配置,这很有用。

通常一个节点每秒会ping几个随机节点,这样每个节点发送的ping数据包总数(和接收的pong数据包)的数量是恒定的,与集群中节点的数量无关。

但是,每个节点都确保能够ping每个其他未发送ping或收到pong的节点超过NODE_TIMEOUT时间的一半。在NODE_TIMEOUT过去之前,节点还会尝试与另一个节点重新连接TCP链路,以确保节点不被认为不可达,只是因为当前TCP连接有问题。

如果将NODE_TIMEOUT设置为小数字并且节点数量(N)非常大,则全局交换的消息数目可以相当大,因为每个节点都会尝试在每隔NODE_TIMEOUT时间一半的时间内ping每个没有刷新信息其他节点。

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

有许多方法可以降低消息的数量,但是目前Redis集群故障检测使用的带宽没有报告问题,因此目前使用明显而直接的设计。请注意,即使在上述示例中,交换的每秒330个数据包在100个不同的节点之间均匀分配,因此每个节点接收的流量都可以接受。

心跳包内容

Ping和pong数据包包含一个对所有类型的数据包(例如请求故障转移投票的数据包)都通用的头文件,以及特定的Ping和Pong数据包特殊的Gossip Section。

常见的标题包含以下信息:

  • 节点ID,一个160位伪随机字符串,在首次创建节点时分配,并且在Redis集群节点的所有生命周期中保持不变。
  • 发送节点的currentEpoch和configEpoch字段,用于装载Redis集群使用的分布式算法(在接下来的部分中对此进行了详细说明)。如果节点是slave节点,则configEpoch是其主节点的最后一个已知配置节点。
  • 节点标记,指示节点是否是从节点,主节点和其他单比特节点信息。
  • 由发送节点服务的hash位的位图,或者如果节点是从节点,则由其master提供的槽的位图。
  • 发送端TCP基本端口(即,Redis用来接受客户端命令的端口;为此获取集群总线端口时加10000)。
  • 从发送端的角度看集群的状态(下线或ok)。
  • 发送节点的主节点ID,如果它是slave节点。

Ping和pong也包含gossip部分。这部分向接收方提供发送方节点对集群中其他节点的视图。gossip部分仅包含关于发送者已知的一组节点中的几个随机节点的信息。gossip部分提到的节点数量与集群大小成正比。

对于在gossip部分添加的每个节点,都将报告以下字段:

  • 节点ID。
  • 节点的IP和端口。
  • 节点标记。

gossip部分允许接收节点从发送者的角度获得有关其他节点状态的信息。这对故障检测和发现集群中的其他节点都很有用。

故障检测

Redis集群故障检测用于识别大多数节点不再可访问主节点或从节点,然后通过提升从节点为主节点角色来做出应答。当从节点提升不可行时,集群处于错误状态以停止接收来自客户端的查询。

如前所述,每个节点都会获取与其他已知节点关联的标记列表。有两个标记用于故障检测,称为PFAIL和FAIL。PFAIL表示可能的故障,并且是未确认的故障类型。FAIL意味着一个节点失败,并且这种情况在一定时间内被大多数master证实。

PFAIL 标记:
当节点超过NODE_TIMEOUT时间不可达时,节点用PFAIL标记标记另一个节点。主节点和从节点都可以将另一个节点标记为PFAIL,而不管其类型。

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

FAIL 标记:
PFAIL标记本身就是每个节点关于其他节点的本地信息,但它不足以触发从节点提升。要认为一个节点下线,PFAIL条件需要升级到FAIL条件。

包括几个随机已知节点的状态。每个节点最终都会为每个其他节点接收一组节点标记。这样每个节点都有一个机制来向其他节点发送关于它们检测到的故障状态的信号。

满足以下条件时,PFAIL条件升级为FAIL条件:

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

如果以上所有条件均为真,则节点A将:

  • 将该节点标记为FAIL。
  • 向所有可到达的节点发送FAIL消息。

FAIL消息将强制每个接收节点将节点标记为失败状态,无论它是否已经标记了处于PFAIL状态的节点。

请注意,FAIL标记大多是单向的。也就是说,节点可以从PFAIL变为FAIL,但FAIL标记只能在下列情况下被清除:

  • 该节点已经可以访问并且是从节点。在这种情况下,可以清除FAIL标记,因为从节点不会故障转移。
  • 该节点已经可以访问,并且是不提供任何槽的master。在这种情况下,FAIL标记可以被清除,因为没有槽的master并不真正参与集群,并且正在等待配置以加入集群。
  • 节点已经可以访问并且是主节点,但是很长一段时间(NODE_TIMEOUT的N倍)已经过去,没有任何可检测到的从节点升级。重新加入集群并继续进行这种情况会更好。

值得注意的是,虽然PFAIL-> FAIL转换使用了一种协议形式,但使用的协议很薄弱:

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

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

情况1:如果大多数master已经将节点标记为FAIL,由于故障检测和它产生的连锁效应,每个其他节点最终将master标记为FAIL,因为在指定的时间窗口中将报告足够的故障。

情况2:当只有少数master将节点标记为FAIL时,从节点提升将不会发生(因为它使用更正式的算法,以确保每个人都最终知道提升),每个节点都将根据上面的FAIL状态清除规则清除FAIL状态(即在NODE_TIMEOUT过去N次后没有升级)。

FAIL标记仅用作触发器来运行slave升级算法的安全部分。从理论上讲,slave可以独立行事,并在master无法到达时开始slave晋升,如果master实际上可以到达多数节点,等待master拒绝提供确认。然而,增加PFAIL→FAIL状态的复杂性,弱协议和FAIL消息强迫在可达部分集群中在最短时间内传播状态具有实际优势。由于这些机制,如果集群处于错误状态,通常所有节点都将在大约同一时间停止接受写入。从使用Redis集群的应用程序角度来看,这是一个令人满意的功能。也避免了由于本地问题而无法到达master的slave发起的错误选举尝试(其他master节点的大部分可以访问master)。

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

集群当前epoch

Redis Cluster使用类似于Raft算法“周期”的概念。在Redis集群中,该周期被称为epoch,它用于为事件提供增量版本控制。当多个节点提供冲突信息时,其他节点就有可能了解哪个状态是最新的。

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

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

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

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

当集群状态改变并且节点为了执行一些动作而寻求一致时,使用该信息。

目前这只发生在slave升级期间,如下一节所述。基本上,这个epoch对于集群来说是一个逻辑时钟,并且规定给定的信息胜过一个更小的epoch。

配置epoch

每个master总是通过ping和pong数据包通告它的configEpoch以及广告其所服务的一组槽的位图。

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

在slave选举期间创建一个新的configEpoch。slave试图取代失败的master增加他们的epoch,并试图获得大多数master的授权。当slave被授权时,会创建一个新的唯一configEpoch,并使用新的configEpoch将该slave变成master。

正如接下来的部分所解释的,当不同的节点声明不同的配置(由于网络分区和节点故障可能发生的情况)时,configEpoch有助于解决冲突。

从节点还通告ping和pong包中的configEpoch字段,但在slave节点中,该字段表示自上次交换数据包时其主节点的configEpoch。这允许其他实例检测slave是否具有需要更新的旧配置(主节点不会使用旧配置向slave提供投票)。

每当某个已知节点的configEpoch发生更改时,它将由接收此信息的所有节点永久存储在nodes.conf文件中。currentEpoch值也会发生同样的情况。在节点继续运行之前,这两个变量保证被保存并且在更新之前fsync-ed到磁盘。

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

slave选举和晋升

slave选举和推广由slave节点处理,在主节点的帮助下投票给slave进行提升。master处于FAIL状态时,从至少有一个slave的角度来看,slave选举会发生,这些slave有先决条件以成为master。

为了让slave提升自己掌握,它需要开始选举并赢得胜利。如果master处于失败状态,给定master的所有slave都可以开始选举,但是只有一个slave会赢得选举并将自己提升为master。

当满足以下条件时,slave开始选举:

  • slave的master处于FAIL状态。
  • master正在服务一个非零数量的槽。
  • slave复制链接与master断开的时间不超过给定时间,以确保升级的slave数据合理新鲜。这个时间是用户可配置的。

为了被选中,slave的第一步是增加它的当前epoch计数器,并请求主实例的投票。

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

一旦master投票给定了一个给定的slave,并积极回复FAILOVER_AUTH_ACK,它就不能再投票给同一个master的另一个slave一段时间NODE_TIMEOUT * 2。在这段时间内,它不能回复其他授权请求为同一个master。这并不需要保证安全性,但可用于防止多个slave在大约同一时间被选中(即使具有不同的configEpoch),这通常是不需要的。

slave丢弃任何AUTH_ACK回复,并且在投票请求被发送时具有小于currentEpoch的epoch。这可以确保它不会为上次选举投票。

一旦slave从大多数master收到确认,它赢得选举。否则,如果在两次NODE_TIMEOUT(但总是至少2秒)内没有达到大多数,则会中止选举,并在NODE_TIMEOUT * 4(总是至少4秒)后再次尝试新选举。

slave等级

一旦master处于FAIL状态,一个slave等待一段时间后才能当选。该延迟计算如下:

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

固定延迟确保我们等待失败状态在集群中传播,否则slave可能会尝试选举,而master仍然不知道失败状态,拒绝授予他们的投票。

随机延迟被用来解除slave同步,所以他们不可能同时开始选举。

SLAVE_RANK是这个slave关于它从master处理的复制数据量的等级。

当master失败以建立(尽力而为)等级时,slave交换消息:具有最新复制偏移量的slave处于等级0,次等级最高的处于等级1,依此类推。通过这种方式,最新更新的slave试图在其他人之前获得当选。

排名顺序没有严格执行; 如果一个更高级别的slave不能当选,其他人会尽快尝试。

一旦slave赢得选举,它就会获得一个新的唯一的增量的configEpoch,它比任何其他现有的master都要高。它开始在ping和pong包中宣传自己是master,提供一组服务的槽,并配置一个configEpoch,以赢得过去。

为了加速其他节点的重新配置,pong分组被广播到集群的所有节点。目前无法访问的节点最终会在从另一个节点收到ping或pong数据包时重新配置,或者如果检测到它通过心跳数据包发布的信息过期,它将从另一个节点收到UPDATE数据包。

其他节点将检测到有一个新的master为旧master提供相同的槽,但具有更大的configEpoch,并且将升级其配置。旧master的slave(或重新加入集群的故障切换master)不仅会升级配置,还会重新配置为从新master进行复制。下一节将介绍如何配置重新加入集群的节点。

master回复slave投票请求

在前一节中,讨论了slave如何尝试当选。本部分解释了从要求为给定slave投票的master的角度发生的情况。

master以来自slave的FAILOVER_AUTH_REQUEST请求的形式接收投票请求。

为了获得投票,需要满足以下条件:

  1. master只给一个给定epoch投票一次,并拒绝为较早的epoch投票:每个master都有一个lastVoteEpoch字段,只要认证请求数据包中的currentEpoch不大于lastVoteEpoch,就会拒绝投票。当master积极响应投票请求时,lastVoteEpoch会相应更新,并安全地存储在磁盘上。
  2. 只有当slave的master被标记为FAIL时,master才会为slave投票。
  3. currentEpoch小于master currentEpoch的Auth请求将被忽略。正因为如此,master响应将始终与auth请求具有相同的currentEpoch。如果同一slave再次要求投票,递增currentEpoch,则保证新的投票不能接受来自master的旧延迟答复。

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

master currentEpoch是5,lastVoteEpoch是1(这可能发生在几次失败的选举之后)

  • slave currentEpoch是3。
  • slave试图以epoch 4(3 + 1)当选,主持人答复currentEpoch 5 ok,但答复延迟。
  • 随后,slave将尝试再次选举,epoch为5(4 + 1),延迟回复以currentEpoch 5到达slave,并被接受为有效。
    1. 如果该master的slave已经被投票,则在NODE_TIMEOUT * 2过去之前,master不会投票给同一master的slave。这并非严格要求,因为两个slave不可能在同一epoch赢得选举。然而,从实际角度来说,它确保了当一个slave被选中时,它有足够的时间通知其他slave,并避免另一个slave赢得新选举的可能性,从而执行不必要的第二次故障切换。
    2. master不会以任何方式选择最佳slave。如果slave的master处于失败状态,并且master未在当前任期投票,则可以获得正面投票。最好的slave最有可能开始选举并在其他slave之前赢得选举,因为它通常能够提前开始投票过程,因为它的级别较高,如前一节所述。
    3. 当一个master拒绝为一个给定的slave投票时,没有负面的回应,这个请求就被忽略了。
    4. master不会投票给slave发送configEpoch,该设置小于master所声明的槽在主表中的任何configEpoch。请记住,slave发送其master的configEpoch,以及由其master提供的槽的位图。这意味着请求投票的slave必须为其希望进行故障转移的槽配置更新或等于授予投票的master的配置。

分区期间配置epoch有用性的实例

本节说明如何使用epoch概念使slave升级过程更能抵抗分区。

  • master不再无限期地可达。master有三个slave A,B,C。
  • slave A赢得选举并晋升为master。
  • 网络分区使A不可用于集群中的大多数。
  • slave B赢得选举并被提升为master。
  • 分区使B不可用于集群中的大多数。
  • 前一个分区固定了,A再次可用。

此时B已关闭,并且A再次可用于master(实际上UPDATE消息会立即重新配置它,但在此我们假设所有UPDATE消息均已丢失)。与此同时,slave C会尝试当选,以便让B失败。这就是发生的事情:

  1. C将尝试获得当选并取得成功,因为对于大多数master来说,其master实际上是失败的。它将获得一个新的增量configEpoch。
  2. A将无法声称自己是hash槽的master,因为其他节点已经具有与由A发布的更高配置epoch相关联的相同hash槽(B的一个)。
  3. 所以,所有的节点都会升级他们的表来为C分配哈希槽,并且集群将继续运行。

正如你在接下来的章节中看到的那样,重新加入集群的旧节点通常会尽快得到关于配置更改的通知,因为只要它ping任何其他节点,接收器就会检测到它有过时的信息,并将发送一个UPDATE消息。

hash槽配置传播

Redis集群的一个重要组成部分是用于传播有关哪个集群节点正在服务给定的一组hash槽的信息的机制。这对于全新集群的启动以及升级slave服务器以便为失败master的槽提供服务后升级配置至关重要。

同样的机制允许无限期地划分节点以合理的方式重新加入集群。

hash槽配置有两种传播方式:

  1. 心跳消息。ping或pong数据包的发送者总是添加关于它(或其master,如果它是slave)服务的hash槽集合的信息。
  2. UPDATE消息。由于在每个心跳数据包中都有关于发送者configEpoch和hash槽的信息,如果心跳数据包的接收者发现发送者信息已过期,它将发送一个包含新信息的数据包,迫使旧的节点更新其信息。

心跳或UPDATE消息的接收者使用某些简单的规则来更新其哈希槽到节点的表格映射。当创建一个新的Redis集群节点时,其本地哈希槽表简单地初始化为NULL条目,以便每个哈希槽没有绑定或链接到任何节点。这看起来类似于以下内容:

0 -> NULL
1 -> NULL
2 -> NULL

16383 -> NULL

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

规则1:如果一个hash槽未被分配(设置为NULL),并且一个已知的节点声明它,我将修改我的hash槽表并将所声称的hash槽关联到它。

因此,如果我们收到来自节点A的心跳,声称要为配置epoch值为3的hash槽1和2提供服务,则该表将被修改为:

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

16383 -> NULL

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

但是这个规则是不够的。我们知道哈希槽映射可以在两个事件期间改变:

  1. slave在故障转移期间替换其master。
  2. 一个槽从一个节点重新分配到另一个节点。

现在让我们专注于故障转移。当一个slave故障切换到它的master时,它会获得一个配置epoch,该epoch的保证大于它的master(并且通常比之前生成的任何其他配置epoch)要大。例如,作为A的slave节点的节点B可以故障切换B,其配置epoch为4。它将开始发送心跳包(首次全集群广播),并且由于接下来的第二个规则,接收机将更新他们的哈希槽表:

规则2:如果已经分配了一个hash槽,并且一个已知节点使用比当前与该槽相关联的master的configEpoch大的configEpoch进行广告,我会将hash槽重新绑定到新节点。

因此,在接收到来自B的声明服务于配置epoch为4的hash槽1和2的消息之后,接收器将按照以下方式更新其表格:

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

16383 -> NULL

活性属性:由于第二条规则,集群中的所有节点最终都会同意,在广告节点中,槽的拥有者是具有最大configEpoch的节点。

Redis集群中的这种机制称为last failover wins。

重新分片时也是如此。当导入哈希槽的节点完成导入操作时,其配置epoch会增加以确保更改将在整个集群中传播。

UPDATE消息,仔细看看

考虑到前面的部分,更容易看到更新消息的工作方式。节点A可能会在一段时间后重新加入集群。它将发送心跳包,并声称它服务于配置epoch为3的hash槽1和2。所有具有更新信息的接收器将相反地看到相同的hash槽与具有较高配置epoch的节点B相关联。正因为如此,他们会使用槽的新配置向A发送UPDATE消息。由于上述规则2,A将更新其配置。

节点如何重新加入集群

节点重新加入集群时使用相同的基本机制。继续上面的例子,节点A将被通知hash槽1和2现在由B服务。假设这两个是由A服务的唯一hash槽,则由A服务的hash槽的计数将下降到0! 所以A会重新成为新master的slave。

遵循的实际规则比这更复杂一点。一般情况下,可能发生A在很长时间后重新加入,同时可能发生由A最初服务的hash槽由多个节点服务,例如,hash槽1可以由B服务,而hash槽2由C。

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

在重新配置期间,最终所服务的hash槽的数量将降至零,并且节点将相应地重新配置。请注意,在基本情况下,这意味着旧的master将成为故障转移后替换它的slave的slave。然而,在一般形式中,该规则涵盖了所有可能的情况。

slave方式完全相同:它们重新配置为复制偷走其以前主节点的最后一个hash槽的节点。

副本迁移

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

例如,在每个master都有一个slave的集群中,master或slave发生故障,集群可以继续运行,但如果两者都发生故障,则集群不可以继续运行。然而,有一类故障是单个节点因硬件或软件问题导致的独立故障,可能会随着时间的推移而积累。例如:

  • master A有一个slave A1。
  • master A失败。A1被提升为新master。
  • 三个小时后,A1以独立的方式失败(与A的失败无关)。由于节点A仍然关闭,因此没有其他slave可用于升级。集群无法继续正常操作。

如果master和slave之间的映射是固定的,则使集群对上述方案更具抵抗性的唯一方法是向每个master添加slave,但这样做成本高昂,因为它需要执行更多的Redis实例,更多的内存等等。

另一种方法是在集群中创建非对称性,并让集群布局随时间自动更改。例如,集群可能有三个master A,B,C。A和B分别有一个slave,A1和B1。然而,主C不同,并有两个slave:C1和C2。

副本迁移是为了迁移到不再覆盖的主节点(无工作从节点)而自动重新配置从节点的过程。通过副本迁移,上述情况变为以下情况:

  • master A失败。A1被提升。
  • C2作为A1的slave迁移,否则不会由任何slave支持。
  • 三个小时后A1也失败了。
  • C2被提升为新master以取代A1。
  • 集群可以继续操作。

副本迁移算法

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

这是算法的工作原理。要开始,我们需要定义在这种情况下什么是好slave:从给定节点的角度来看,好的slave不是处于失败状态的slave。

算法的执行在每个从机中被触发,它检测到至少有一个主机没有好的从机。然而,在所有检测到这种情况的slave中,只有一个子集应该起作用。这个子集实际上通常是一个单独的slave设备,除非不同的slave在给定时刻对其他节点的故障状态有略微不同的观点。

代理slave是master中具有最大数量slave的slave,即不处于FAIL状态且节点ID最小的slave。

例如,如果每个master有10个master,每个slave有10个master,每个master有5个slave,则将尝试迁移的slave是 - 具有5个slave的2个master - 具有最低节点ID的master。鉴于没有使用协议,有可能当集群配置不稳定时,竞争状况发生在多个slave相信自己是具有较低节点ID的非故障slave(这在实践中不太可能发生)。如果发生这种情况,结果是多个slave迁移到同一个master,这是无害的。如果竞赛发生的方式会使得master没有slave,那么一旦集群再次稳定,算法将再次执行,并将slave迁移回原来的master。

最终,每个master都会得到至少一个slave的支持。然而,正常的行为是单个slave从具有多个slave的主设备迁移到孤立主设备。

该算法由用户可配置的参数(称为cluster-migration-barrier)控制:在slave迁移之前,主设备必须保留的良好slave的数量。例如,如果此参数设置为2,则slave只能在master保留两个工作slave的情况下尝试迁移。

configEpoch冲突解决算法

当在故障转移期间通过slave升级创建新的configEpoch值时,它们保证是唯一的。

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

  1. 使用TAKEOVER选项的CLUSTER FAILOVER命令可以手动将从节点升级为主节点,而无需大多数主节点可用。例如,在多数据中心设置中这很有用。
  2. 用于集群重新平衡的槽迁移还会在本地节点内生成新的配置epoch,而不会出于性能原因达成一致。

特别是,在手动重新分片期间,当哈希槽从节点A迁移到节点B时,重新分片程序将强制B将其配置升级到集群中发现的最大epoch,再加上1(除非节点是已经是配置epoch最大的那个),而不需要其他节点的同意。通常情况下,真实世界的重新分片包括移动数百个哈希槽(特别是在小型集群中)。要求协议在重新分片期间生成新的配置epoch,对于移动的每个hash槽,效率不高。此外,每次在每个集群节点中都需要fsync才能存储新配置。由于它的执行方式不同,当第一个hash槽移动时,我们只需要一个新的配置epoch,使其在生产环境中更加高效。

然而,由于上述两种情况,可能(尽管不太可能)以具有相同配置epoch的多个节点结束。系统管理员执行的重新分片操作以及同时发生的故障切换(加上大量的厄运),如果传播速度不够快,可能会导致currentEpoch冲突。

此外,软件缺陷和文件系统损坏也可能会导致具有相同配置epoch的多个节点。

当服务于不同哈希槽的主设备具有相同的configEpoch时,不存在任何问题。更重要的是,slave失败的master有唯一的配置epoch。

也就是说,手动干预或重新分片可能会以不同方式更改集群配置。Redis集群的主活性属性要求槽配置总是收敛,所以在任何情况下,我们都希望所有主节点都有不同的configEpoch。

为了执行此操作,在两个节点以相同的configEpoch结束的情况下使用冲突解决算法。

  • 如果主节点检测到另一个主节点正在使用相同的configEpoch宣告自己。
  • 以及如果节点具有字典上较小的节点ID,则与声明相同configEpoch节点的另一节点相比。
  • 那么它将currentEpoch递增1,并将其用作新的configEpoch。

如果有任何一组节点具有相同的configEpoch,那么除了具有最大节点ID的节点之外的所有节点都将向前移动,从而保证每个节点最终都会选择一个唯一的configEpoch,而不管发生了什么。

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

节点重置

节点可以软件重置(无需重新启动它们)以便在不同的角色或不同的集群中重用。这在正常操作,测试和云环境中非常有用,在这种环境中,给定节点可以重新配置以加入不同的节点组以放大或创建新的集群。

在Redis中,集群节点使用CLUSTER RESET命令重置。该命令以两种方式提供:

  • CLUSTER RESET SOFT
  • CLUSTER RESET HARD

该命令必须直接发送到节点进行重置。如果不提供重置类型,则执行软重置。

以下是由重置执行的操作列表:

  1. 软重置和硬重置:如果节点是从节点,则它将变成主节点,并丢弃其数据集。如果节点是主设备且包含key,则重置操作将中止。
  2. 软重置和硬重置:所有槽都被释放,手动故障切换状态被重置。
  3. 软重置和硬重置:节点表中的所有其他节点都被删除,因此节点不再知道任何其他节点。
  4. 仅硬重置:currentEpoch,configEpoch和lastVoteEpoch设置为0。
  5. 仅硬重置:将节点ID更改为新的随机ID。

具有非空数据集的主节点无法重置(因为通常需要将数据重新存储到其他节点)。但是,在特殊情况下(例如,在创建新集群的时候完全破坏集群),FLUSHALL必须在执行重置之前执行。

从集群中删除节点

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

出于这个原因,当一个节点被删除时,我们还想从其他所有节点表中删除它的条目。这是通过使用CLUSTER FORGET <node-id>命令完成的。

该命令执行两件事:

  1. 它从节点表中删除具有指定节点ID的节点。
  2. 它设置了一个60秒的禁止,防止具有相同节点ID的节点被重新添加。

第二个操作是必需的,因为Redis集群使用gossip以自动发现节点,所以从节点A移除节点X可能导致节点B再次将节点X gossiping到A。由于60秒禁止,Redis集群管理工具需要60秒才能从所有节点中删除节点,从而防止由于自动发现而重新添加节点。

更多信息可在CLUSTER FORGET文档中找到。

发布/订阅

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

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

附录

附录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;
}

猜你喜欢

转载自blog.csdn.net/heroqiang/article/details/80280344