Redis学习之Redis Cluster规范(四)

1,764 阅读1小时+

Redis集群规范

Redis集群目标

Redis Cluster是Redis的分布式实现,具有以下目标,按设计重要性排序:

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

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

实施子集

Redis Cluster实现Redis的非分布式版本中可用的所有单个键命令。执行复杂的多键操作(如Set类型联合或交叉)的命令也可以实现,只要这些键都散列到同一个槽。

Redis Cluster实现了一个称为hash tags的概念,可用于强制某些key存储在同一个哈希槽中。但是,在手动重新分片期间,多键操作可能会在一段时间内不可用,而单键操作始终可用。

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

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

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

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

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

写安全

Redis Cluster使用节点之间的异步复制,上次故障转移获胜者拥有隐式合并功能。这意味着最后选出的主数据集最终将替换所有其他副本。在分区期间总会有一个时间窗口,可能会丢失写入。然而,在连接到大多数主设备的客户端和连接到少数主设备的客户端的情况之间,这些窗口是非常不同的。

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

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

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

针对分区的少数端的写入有一个更大的窗口可以丢失。例如,Redis Cluster在有少数主设备和至少一个或多个客户端的分区上丢失了很多的写入次数,如果多数主设备在故障转移时发送到主设备的所有写入可能都会丢失。

具体来说,对于要进行故障转移的主服务器,至少NODE_TIMEOUT期间必须由大多数主服务器无法访问,因此如果在该时间之前修复了分区,则不会丢失任何写入。当分区持续时间超过NODE_TIMEOUT时,在少数端执行的所有写操作可能会丢失。然而,Redis集群的少数派一方将在没有与大多数人接触的情况下时间超过NODE_TIMEOUT时开始拒绝写入,因此有一个最大窗口,此后少数群体将不再可用。因此,在此之后不接受或丢失写入。

可用性

Redis Cluster在分区的少数端不可用。在分区的多数端,假设每个无法访问的主服务器至少有大多数主服务器和从服务器,则服务器会在NODE_TIMEOUT一段时间后再次可用,再次需要几秒钟以便从服务器获得选举并故障转移其主服务器,通常在1或2秒内执行)。

这意味着Redis Cluster旨在拯救集群中几个节点的故障,但对于需要在大型网络分裂时需要可用性的应用程序而言,它不是合适的解决方案。

在由N个主节点组成的集群的示例中,每个节点具有单个从节点,只要单个节点被分区,集群的大多数端将保持可用,并且在两个节点被分区时,将保持可用的概率为1-(1/(N*2-1))(在第一个节点失败后,我们总共留下了N*2-1节点,并且唯一没有副本的主机失败的概率是1/(N*2-1))

例如,在每个节点具有5个节点和每个结点都有个slave的集群中,有1/(5*2-1) = 11.11%可能性,在两个节点与多数节点分开后,集群将不再可用。

由于Redis Cluster功能称为replicas migration,因此复制副本迁移到孤立主服务器(主服务器不再具有副本)这一事实可以改善许多真实场景中的集群可用性。因此,在每个成功的故障事件中,集群可以重新配置从设备布局,以便更好地抵抗下一个故障。

性能

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

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

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

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

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

Redis Cluster的主要目标是提供极高的性能和可扩展性,同时保留弱的但合理的数据安全性和可用性。

为什么避免合并操作

Redis集群设计避免了多个节点中相同键值对的冲突版本,就像Redis数据模型的情况一样,这并不总是令人满意的。Redis中的值通常非常大; 通常会看到包含数百万个元素的列表或排序集。数据类型在语义上也很复杂。转移和合并这些值可能是主要瓶颈,and/or可能需要应用程序端逻辑的non-trivial参与,存储元数据的附加存储器等等。

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

key分发模型

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

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

用于将键映射到散列槽的基本算法如下(读取此规则的散列标记异常的下一段):

HASH_SLOT = CRC16(key) mod 16384

CRC16规定如下:

  • Name:XMODEM(也称为ZMODEM或CRC-16 / ACORN)
  • Width:16位
  • Poly:1021(实际上是x^16 + x^12 + x^5 + 1)
  • Initialization:0000
  • Reflect Input byte:False
  • Reflect Output CRC:错误
  • Xor constant to output CRC:0000
  • Output for "123456789":31C3

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

在我们的测试中,CRC16在16384个插槽中均匀分配不同类型的key时表现非常出色。

:所用CRC16算法的参考实现可在本文档的附录A中找到。

键哈希标签

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

为了实现散列标签,在某些条件下以稍微不同的方式计算key的散列槽。如果key包含一个“{...}”模式仅是{and}之间的子串 ,以获得散列slot被散列。但是,由于可能存在多次出现{or},以下规则很好地指定了算法:

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

如果满足条件三,不是对key进行散列,而是仅对第一次出现{和第一次出现}之间的内容进行散列。

例子:

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

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

集群节点属性

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

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

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

每个节点都维护有关集群中知道的其他节点的以下信息:节点ID,节点的IP和端口,一组标志,标记为节点的主节点slave,上次节点被ping后的时间戳,最近接收到pong的节点的时间戳, configuration epoch(在本说明书后面解释),链路状态以及最后服务的散列slots集合。

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,configuration epoch,链路状态,slots。一旦我们谈到Redis Cluster的特定部分,我们将详细介绍上述领域的详细信息。

集群总线

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

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

集群拓扑

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

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

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

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

节点握手

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

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

  • 如果节点为自己显示MEET消息。MEET消息与PING消息几乎完全相同,但强制接收者接受节点作为集群的一部分。仅当系统管理员通过以下命令请求时,节点才会将MEET消息发送到其他节点:

    CLUSTER MEET ip port

  • 如果已经信任的节点谈及另一个节点,则节点还将另一个节点注册为集群的一部分。因此,如果A知道B,并且B知道C,则最终B将向A发送关于C的gossip消息。当发生这种情况时,A将注册C作为网络的一部分,并将尝试与C连接。

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

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

MOVED重定向

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

如果节点为哈希槽提供服务,则只处理查询,否则节点将检查其内部哈希槽到节点映射,并将回复具有MOVED错误的客户端,如下例所示:

GET x
-MOVED 3999 127.0.0.1:6381

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

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

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

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

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

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

集群实时重配置

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

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

实现的核心是移动哈希槽的能力。从实际的角度来看,哈希槽只是一组key,因此Redis Cluster在重新分片期间的确实做的是将key从一个实例移动到另一个实例。移动哈希槽意味着将哈希的合适的所有key移动到此哈希槽中。

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

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

前两个命令ADDSLOTSDELSLOTS,仅用于将插槽分配(或删除)到Redis节点。分配时隙意味着告诉给定主节点它将负责存储和提供指定散列槽的内容。

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

ADDSLOTS当从头开始创建新集群时,通常会使用该命令,以便为每个主节点分配所有可用的16384个散列插槽的子集。

DELSLOTS主要用于集群配置的人工修改或用于调试任务:在实践中很少使用。

SETSLOT如果使用SETSLOT <slot> NODE表单,子命令用于将槽分配给特定节点ID 。否则,插槽可以在两种特殊状态进行设置MIGRATINGIMPORTING。使用这两个特殊状态是为了将散列槽从一个节点迁移到另一个节点。

  • 当插槽设置为MIGRATING时,节点将接受与此散列插槽有关的所有查询,但仅当存在有问题的key时,否则使用-ASK重定向将查询转发到作为迁移目标的节点。
  • 当一个插槽设置为IMPORTING时,该节点将接受与该散列插槽有关的所有查询,但前提是该请求前面有一个ASKING命令。如果ASKING客户端未给出该命令,则查询将通过重定向错误-MOVED,重定向到真正的哈希槽所有者。

让我们通过哈希槽迁移的例子来说明这一点。假设我们有两个Redis主节点,称为A和B。我们想将散列槽8从A移动到B,所以我们发出如下命令:

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

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

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

这样我们就不再在“A”中创建新key了。与此同时,redis-trib在重新分片和Redis集群配置期间使用的特殊程序将把散列槽8中的现有key从A迁移到B.这是使用以下命令执行的:

CLUSTER GETKEYSINSLOT slot count

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

MIGRATE target_host target_port key target_database id timeout

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

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

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

ASK重定向

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

这是必需的,因为关于散列槽8的下一个查询可以是关于仍在A中的key,因此我们总是希望客户端尝试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 命令文档中使用不同的措辞(为了文档中的冗余)。

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

虽然有可能让Redis集群客户端实现不记得内存中的插槽配置(插槽号和节点的地址之间的映射),并且只能通过联系等待重定向的随机节点来工作,这样的客户端将是非常低效的。

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

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

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

请注意,客户端可以MOVED通过仅更新其表中移动的插槽来处理重定向,但这通常效率不高,因为通常会立即修改多个插槽的配置(例如,如果将从属设备提升为主服务器,则所有服务旧master的插槽将重新映射)。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

返回数组的每个元素的前两个子元素是范围的起始端插槽。附加元素表示地址 - 端口对。第一个地址 - 端口对是服务于插槽的主设备,附加的地址端口对是服务于相同插槽的所有从设备,它们不处于错误状态(即未设置FAIL标志)。

例如,输出的第一个元素表示从5461到10922(包括起始和结束)的插槽由127.0.0.1:7001提供服务,并且可以看到只读slave的信息:127.0.0.1:7004。

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

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

多键操作

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

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

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

更具体地说,即使在重新分片期间,仍然可以获得所有存在且仍然散列到相同slot(源节点或目的地节点)的多键操作。

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

一旦指定的散列槽的迁移终止,所有多键操作再次可用于该散列槽。

使用从节点缩放读取

通常,从节点会将客户端重定向到给定命令中涉及的散列槽的权威主节点,但是客户端可以使用从属节点来使用READONLY命令扩展读取。

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

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

  1. 客户端发送了一个关于从未由该从属服务器的主服务器提供服务的散列槽的命令。
  2. 集群被重新配置(例如重新配置),并且从属设备不再能够为给定的哈希槽提供命令。

发生这种情况时,客户端应更新其散列图映射,如前面部分所述。

可以使用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 Cluster故障检测当前使用的带宽没有报告问题,因此目前使用了明显且直接的设计。注意,即使在上面的例子中,每秒交换的330个数据包在100个不同的节点之间均匀分配,因此每个节点接收的流量是可接受的。

心跳包内容

Ping和pong数据包包含所有类型数据包通用的标头(例如,请求故障转移投票的数据包),以及特定于Ping和Pong数据包的特殊Gossip部分。

公共标头具有以下信息:

  • 节点ID,一个160位伪随机字符串,在第一次创建节点时分配,并在Redis集群节点的所有生命周期内保持不变。
  • 发送节点的currentEpochconfigEpoch字段,用于挂载Redis Cluster使用的分布式算法(这将在下一节中详细介绍)。如果节点是从属节点,则它configEpochconfigEpoch其主节点的最后一个节点。
  • 节点标志,指示节点是否是从设备,主设备和其他单比特节点信息。
  • 由发送节点服务的散列槽的位图,或者如果节点是从属节点,则是其主节点服务的槽的位图。
  • 发送方TCP基本端口(即Redis用于接受客户端命令的端口;向此添加10000以获取集群总线端口)。
  • 从发送方的角度来看集群的状态(down或ok)。
  • 发送节点的主节点ID(如果它是从属节点)。

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

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

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

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

故障检测

Redis集群故障检测用于识别大多数节点无法再访问主节点或从节点,然后通过将从属设备提升为主节点来进行响应。当无法进行从属提升时,集群将处于错误状态以停止接收来自客户端的查询。

如前所述,每个节点都采用与其他已知节点相关联的标志列表。有两个标志用于故障检测,被称为PFAILFAILPFAIL表示可能的故障,并且是未确认的故障类型。FAIL意味着节点出现故障,并且大多数master在固定的时间内确认了这一情况。

PFAIL标志:

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

Redis集群节点的不可达性概念是我们有一个活动的ping(我们发送的ping,我们还没有得到回复)等待的时间超过NODE_TIMEOUT。对于这种工作机制,NODE_TIMEOUT与网络往返时间相比必须很大。为了在正常操作期间增加可靠性,节点将尝试在NODE_TIMEOUT已经过去一半的情况下与集群中的其他节点重新连接,而不会回复ping。此机制可确保连接保持活动状态,因此断开的连接通常不会造成节点之间错误的故障报告。

Fail标志:

PFAIL标志就是本地信息对其他节点信息的看法,但它不足以触发一个slave节点称为主节点。对于要被视为关闭的节点,需要将PFAIL条件升级到FAIL条件。

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

假如PFAIL条件升级为FAIL条件时,下面的一组条件将满足:

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

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

  • 将节点标记为FAIL
  • FAIL向所有可访问节点发送消息。

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

请注意,FAIL标志主要是单向的。也就是说,一个节点可以去从PFAILFAIL,但FAIL标志只能在下列情况下被清除:

  • 该节点已经可以访问并且是从属节点。在这种情况下,FAIL可以清除标志,因为slave未进行故障转移。
  • 该节点已经可以访问,并且是不为任何插槽提供服务的主节点。在这种情况下,FAIL可以清除标志,因为没有插槽的主服务器不会真正参与集群,并且正在等待配置以加入集群。
  • 该节点已经可以访问并且是主节点,但是很长时间(N次NODE_TIMEOUT)已经过去而没有任何可检测的从属促销。它最好重新加入集群并在这种情况下继续。

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

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

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

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

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

该FAIL标志仅用作触发器来运行slave变成master的算法的安全部分。理论上,从属设备可以在其主设备无法访问时独立启动slave promotion,并等待主设备拒绝提供确认(如果主设备实际上可由多数人访问)。然而,由于PFAIL -> FAIL状态复杂性的增加,薄弱的协议,以及FAIL强制在集群的可到达部分中以最短的时间传播状态的消息具有实际优点。由于这些机制,如果集群处于错误状态,通常所有节点将几乎同时停止接受写入。从使用Redis Cluster的应用程序的角度来看,这是一个理想的功能。还避免了由于本地问题而无法到达其主设备的从设备发起的错误选举尝试(主设备可由大多数其他主节点到达)。

集群Current epoch

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

currentEpoch是一个64位无符号数。

在创建节点时,每个Redis Cluster节点(从属节点和主节点)都将其currentEpoch设置为0。

每当从另一节点接收到分组时,如果发送方的epoch(集群总线消息报头的一部分)大于本地节点epoch,currentEpoch则更新为发送方时期。

由于这些语义,最终所有节点都将采用集群中的最好节点的configEpoch

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

目前,这只发生在slave promotion期间,如下一节所述。基本上,epoch是集群的逻辑时钟,并且要求给定的信息将具有较小epoch的集群统一。

Configuration epoch

每个master总是在ping和pong数据包中公布configEpoch,以及一个位图公布其服务的插槽集。

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

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

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

从节点还在ping和pong数据包中通告该configEpoch字段,但是在slave的情况下,该configEpoch字段表示它们最后一次交换数据包时的主节点的配置。这允许其他实例检测从属设备何时具有需要更新的旧配置(主节点不会授予具有旧配置的slave的投票权限)。

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

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

slave选举和晋升

slave节点选举和晋升由slave节点处理,在主节点的投票支持下实现slave promotion。当主机FAIL处于从至少一个具有先决条件以成为主设备的从设备的角度处于状态时,发生从设备选举。

为了让slave能够自我提升,它需要开始选举并赢得选举。如果master处于FAIL状态,那么给定master的所有slave都可以开始选举,但是只有一个slave会赢得选举并促使自己掌握节点。

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

  • slave的master处于FAIL状态。
  • 主机正在提供非零数量的插槽。
  • 从复制链接与主服务器断开连接的时间不超过给定的时间,以确保提升的从属数据是最新的。此时间是用户可配置的。

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

通过将FAILOVER_AUTH_REQUEST分组广播到集群的每个主节点,slave请求投票。然后它等待两倍的回复最大时间NODE_TIMEOUT到达(但总是至少2秒)。

一旦master投票给一个给定的slave,回答FAILOVER_AUTH_ACKNODE_TIMEOUT * 2段时间内它就不能再投票给同一个master的另一个slave。在此期间,它将无法回复同一主机的其他授权请求。这不是保证安全所必需的,但对于防止多个slaveconfigEpoch在大约同一时间(通常是不同的)被选中(通常不需要)是有用的。

从属服务器在收到epoch时间小于发送投票请求时的currentEpoch时丢弃任何AUTH_ACK回复。这确保它不计算用于先前选举的投票。

一旦slave接收到来自大多数master的ACK,它就赢得了选举。否则,如果在两倍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.

固定延迟确保我们等待FAIL状态在集群中传播,否则slave可能会在其他masters仍然不知道此masterFAIL状态时尝试当选,拒绝投票给它们

随机延迟用于使slaves去同步,因此它们不太可能同时开始选举。

SLAVE_RANK是slave关于它从master处理的复制数据量的级别。当主设备发生故障时,从设备交换消息以建立(best effort)排名:具有最新复制偏移的从设备为等级0,第二高的等级为1,依此类推。通过这种方式,最新的slave试图在其他人之前当选。

排名顺序没有严格执行; 如果更高级别的slave未能当选,其他人将很快尝试。

一旦slave赢得选举,它将获得一个新的唯一和增量的configEpoch,高于任何其他现有master。它开始在ping和pong数据包中宣传自己作为master的角色,提供一组服务的插槽与configEpoch

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

其他节点将检测到有一个新主服务器为旧主服务器提供服务但具有更好的configEpoch以及具有相同插槽服务,并将升级其配置。旧主服务器的从服务器(如果它重新加入集群,则是故障转移主服务器)不仅会升级配置,还会重新配置以从新主服务器进行复制。如何配置重新加入集群的节点将在下一节中介绍。

master回复slave投票请求

在上一节中,讨论了slave如何试图当选。本节解释了从请求为给定从属者投票的master的角度发生的事情。

master们以FAILOVER_AUTH_REQUEST要求形式收到slave的投票请求。

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

  1. 主设备只对给定epoch投票一次,并拒绝给旧的epoch投票:每个主设备都有一个lastVoteEpoch字段,只要auth请求包中的currentEpoch值不大于lastVoteEpoch,它就会拒绝再次投票。当master对投票请求作出肯定回复时,lastVoteEpoch会相应更新,并安全地存储在磁盘上。
  2. 只有当slave的master被标记为FAIL时,masters才会投票给slave。
  3. auth请求的currentEpoch小于master的currentEpoch将被忽略。因此,master回复的currentEpoch将始终与auth请求相同。如果同一个slave再次要求投票,增加currentEpoch,可以保证不能接受来自master的旧延迟回复用于新投票。

不使用规则3导致的问题示例:

mastercurrentEpoch是5,lastVoteEpoch是1(这可能发生在选举失败后)

  • slavecurrentEpoch是3。
  • slave试图用epoch 4(3 + 1)当选,master用currentEpoch5 回答确定,但回复延迟了。
  • slave将尝试再次当选,在晚些时候,使用epoch 5(4 + 1),延迟回复到达slavecurrentEpoch5,并被接受为有效。
  1. 如果该master的slave已经被投票,则master不会在NODE_TIMEOUT * 2过去之前投票给同一master的slave。这不是严格要求的,因为两个slave不可能在同一时期赢得选举。但是,实际上它确保当一个slave被选中时,它有足够的时间通知其他slave,并避免另一个slave赢得新选举的可能性,执行不必要的第二次故障转移。
  2. master们不会以任何方式选择最好的slave。如果slave的master处于FAIL,并且master没有在当前任期内投票,则给予正面投票。最好的slave是最有可能开始选举并在其他slave选举之前赢得它,因为它通常能够提前开始投票过程,因为它的*排名更高,*如上一节所述。
  3. 当master拒绝为给定的slave投票时没有否定回应,该请求就会被忽略。
  4. 对于slave声称的插槽,master不投票给slave发送的configEpoch数量小于master表中的任何一个configEpoch。请记住,从属设备发送其主设备configEpoch,以及主设备提供的插槽位图。这意味着请求投票的slave必须具有其想要故障转移的插槽的配置,该配置新于或等于授予投票的主设备。

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

本节说明了如何使用epoch概念使slave promotion过程对分区更具抵抗力。

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

此时B已关闭且A再次具有master的角色(实际上UPDATE消息会立即重新配置它,但在这里我们假设所有UPDATE消息都丢失了)。与此同时,slaveC将尝试当选,以便将B故障转移。这就是:

  1. C将尝试当选并将成功,因为对于大多数master来说,它的master实际上已经失败了。它将获得一个新的增量configEpoch
  2. A将因为其散列槽失去master身份,因为与A发布的节点相比,其他节点已经具有与更高配置epoch(B是其中的一个)相关联的相同散列槽。
  3. 因此,所有节点都将升级其表以将散列槽分配给C,并且集群将继续其操作。

正如您将在下一节中看到的,重新加入集群的陈旧节点通常会尽快收到有关配置更改的通知,因为只要它ping任何其他节点,接收方就会检测到它有陈旧信息并将发送一个UPDATE信息。

散列槽配置传播

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

相同的机制允许以无限长的时间划分的节点以合理的方式重新加入集群。

哈希槽配置有两种传播方式:

  1. 心跳消息。ping或pong数据包的发送方总是添加有关它(或其主节点,如果它是从节点)服务的散列插槽集的信息。
  2. UPDATE消息。由于在每个心跳包中都有关于所服务的发送方configEpoch和一组哈希slot的信息,如果心跳包的接收方发现发送方信息是陈旧的,它将发送包含新信息的包,迫使过时节点更新其信息。

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

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

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

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

因此,如果我们从节点A接收到声称为configEpoch值为3的散列插槽1和2提供服务的心跳,则该表将被修改为:

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

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

但是这条规则还不够。我们知道散列槽映射可以在两个事件期间发生变化:

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

现在让我们关注故障转移。当从设备故障转移其主设备时,它获得configEpoch,该configEpoch保证大于其主设备之一(并且通常大于先前生成的任何其他配置时期)。例如,作为A的从属节点B,可以使用configEpoch 4来故障转移B。它将开始发送心跳包(第一次在集群范围内进行大规模广播),并且由于以下第二规则,接收器将更新他们的哈希槽表:

规则2:如果已经分配了一个散列槽,并且一个已知节点正在使用configEpoch大于当前与该槽相关联的主节点的configEpoch通告它,我将把散列槽重新绑定到新节点。

因此,在接收到来自B的消息声称服务于配置时期为4的散列插槽1和2之后,接收器将按以下方式更新其表:

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

活动属性:由于第二个规则,最终集群中的所有节点都会同意插槽的所有者是公布它的节点中具有最大configEpoch的插槽的所有者。

Redis集群中的此机制称为last failover wins

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

更新消息,仔细看看

考虑到上一节,更容易了解更新消息的工作原理。节点A可能在一段时间后重新加入集群。它将发送心跳包,声称它服务于散列槽1和2,配置时期为3.所有具有更新信息的接收器将看到相同的散列槽与具有更高配置时期的节点B相关联。因此,他们将使用插槽的新配置向A 发送UPDATE消息。由于上面的规则2,A将更新其配置 。

节点如何重新加入集群

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

遵循的实际规则比这复杂一点。通常,可能会发生A在很多时间之后重新加入,同时可能发生最初由A服务的哈希时隙由多个节点服务,例如,哈希slot 1可以由B服务,而哈希时隙2由C提供。

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

在重新配置期间,最终服务的散列槽的数量将下降到零,并且节点将相应地重新配置。请注意,在基本情况下,这只意味着旧主服务器将成为在故障转移后替换它的从服务器的从服务器。但是,在一般形式中,规则涵盖所有可能的情况。

从属设备完全相同:它们重新配置以复制其前主设备的最后一个哈希槽的节点。

副本迁移

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

例如,在每个主服务器都有一个从服务器的集群中,只要主服务器或从服务器发生故障,集群就可以继续运行,但如果两者都失败,则集群可以继续运行。然而,存在一类故障,这些故障是由于可能随时间累积的硬件或软件问题引起的单个节点的独立故障。例如:

  • Master A有一个slaveA 1。
  • A master失败了。A1被提升为新的master。
  • 三小时后,A1以独立的方式失败(与A的失败无关)。由于节点A仍处于关闭状态,因此没有其他从站可用于升级。集群无法继续正常运行。

如果主服务器和从服务器之间的映射是固定的,那么使集群更能抵抗上述情况的唯一方法是向每个主服务器添加从服务器,但这样做成本很高,因为它需要执行更多Redis实例,更多内存和等等。

另一种方法是在集群中创建不对称,让集群布局随着时间的推移自动更改。例如,集群可以具有三个主设备A,B,C。A和B各自具有单个从设备A1和B1。然而,主设备C是不同的并且具有两个从设备:C1和C2。

副本迁移是自动重新配置从站以便迁移到不再覆盖的主站(无工作从站)的过程。使用副本迁移,上面提到的场景变为:

  • Amaster失败了。A1升级。
  • C2作为A1的从属进行迁移,否则不会被任何从属服务器支持。
  • 三小时后,A1也失败了。
  • C2被提升为新的master以取代A1。
  • 集群可以继续操作。

副本迁移算法

迁移算法不使用任何形式的协议,因为Redis集群中的从属布局不是需要与configEpoch一致和/或版本化的集群配置的一部分。相反,当没有支持主服务器时,它使用算法来避免从服务器的大规模迁移。该算法最终确保(一旦集群配置稳定),每个主设备将由至少一个从设备支持。

这就是算法的工作原理。首先,我们需要在此上下文中定义什么是 好的从属:从给定节点的角度来看,良好的从属是没有FAIL从属状态的从属。

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

活跃slave是具有最大可达master连接的从站数,不是在FAIL状态,并具有最小的ID节点的slave。

因此,例如,如果有10个主服务器,每个服务器有1个从服务器,2个主服务器各有5个从服务器,那么将尝试迁移的从服务器是 - 在具有5个从服务器的2个主服务器中 - 具有最低节点ID的从服务器。鉴于没有使用协议,当集群配置不稳定时,可能会出现竞争条件,其中多个从属设备认为自己是具有较低节点ID的非故障从设备(在实践中不太可能发生这种情况) )。如果发生这种情况,结果是多个从属服务器迁移到同一个主服务器,这是无害的。如果比赛发生的方式会使分出的master没有slave,但在最终每个master都将得到至少一个slave的支持。但是,正常行为是单个从设备从具有多个从设备的主设备迁移到孤立主设备。

该算法由用户可配置的参数控制,该参数称为 cluster-migration-barrier:在从属设备迁移之前必须留下主设备的良好从设备的数量。例如,如果此参数设置为2,则只有在其主服务器保留两个工作从服务器时,服务器才能尝试迁移。

configEpoch冲突解决算法

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

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

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

具体来说,在手动重新分片期间,当散列槽从节点A迁移到节点B时,重新分片程序将强制B将其配置升级到集群中发现的最大的时期加1(除非节点是已经是具有最大configEpoch的那个),而不需要来自其他节点的协议。通常,真实世界的重新分片涉及移动数百个散列槽(特别是在小簇中)。对于每个移动的散列槽,要求每次在重新分片期间生成新configEpoch的协议是低效的。此外,每次在resharding期间,都要让每个集群节点中用fsync来存储新配置。因为它的执行方式,我们可以用更好的方法代替,我们只需要在第一个hash slot被移动时,使用新的configEpoch ,这更有效。

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

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

当服务于不同散列槽的主服务器具有相同的configEpoch时,没有问题。更重要的是,slave failing over master具有唯一的configEpoch。

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

为了强制执行此操作,在两个节点以相同的configEpoch方式结束时使用冲突解决算法

  • 如果主节点检测到另一个主节点正在使用相同的configEpoch
  • 如果与声称相同的configEpoch节点ID相比,节点具有按字典顺序更小的节点ID。
  • 然后它将其增加currentEpoch1,并将其用作新的configEpoch

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

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

节点重置

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

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

  • CLUSTER RESET SOFT
  • CLUSTER RESET HARD

必须将命令直接发送到节点才能重置。如果未提供复位类型,则执行软复位。

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

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

无法重置具有非空数据集的主节点(因为通常您希望将数据重新硬化到其他节点)。但是,在适当的特殊条件下(例如,当为了创建新集群而完全销毁集群时),必须在继续复位之前执行FLUSHALL

从集群中删除节点

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

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

该命令有两个作用:

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

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

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

Redis Cluster开发运维常见问题

1)Pub/Sub广播

问题:publish在集群每个节点广播:加重带宽。

解决:单独使用一套Redis Sentinel。

2)集群倾斜

数据倾斜:内存不均。

1:节点和槽分配不均。

2:不同槽对应键值数量差异较大。

3:包含bigkey。

4:内存相关配置不一致。

请求倾斜:热点。

热点key:重要的key或者bigkey。

优化:

1、避免bigkey

2、热键不要用hash_tag

3、当一致性不高时,可以用本地缓存+MQ

3)读写分离

只读连接:集群模式的从节点不接受任何读写请求。

1:重定向到负责槽的主节点。

2:readonly命令可以读:连接级别的命令。

读写分离:更加复杂。

1:同样的问题:复制延迟、读取过期数据、从节点故障。

2:修改客户端:cluster slaves {nodeId}

4)数据迁移

在线迁移的一些工具:

1:唯品会:redis-migrate-tool。

2:豌豆荚:redis-port。

5)集群限制

1:key批量操作支持有限:例如mget、mset必须在一个slot。

2:key事务和Lua支持有限:操作的key必须在一个节点。

3:key是数据分区的最小粒度:不支持bigkey分区。

4:不支持多个数据库:集群模式下只有一个db 0。

5:复制只支持一层:不支持树形复制结构。

结论

1:Redis Cluster:满足容量和性能的扩展性,很多业务“不需要”。

2:很多场景Redis Sentinel已经足够好。

集群总结

1)Redis cluster数据分区规则采用虚拟槽方式(16384个槽),每个结点负责一部分槽和相关数据,实现数据和请求的负载均衡。

2)搭建集群划分四个步骤:准备节点、节点握手、分配槽、复制。

3)集群伸缩通过在节点之间移动槽和相关数据实现。

4)使用smart客户端操作集群达到通信效率最大化,客户端内部负责计算维护键->槽->节点的映射,用于快速定位到目标节点。

5)集群自动故障转移过程分为故障发现和节点恢复。节点下线分为主观下线和客观下线,当超过半数主节点认为故障节点为主观下线时标记它为客观下线状态。从节点负责对客观下线的主节点触发故障恢复流程,保证集群的可用性。