Redis集群规范
欢迎来到Resis群规范。在这里,您可以找到有关 Redis 集群算法和设计原理的信息。本文档是一项正在进行中的工作,因为它与 Redis 的实际实现持续同步。
设计的主要属性和基本原理
Redis集群目标
Redis 集群是 Redis 的分布式实现,具有以下目标,按设计的重要性顺序排列:
- 高达 1000 个节点的高性能和线性可扩展性。没有代理,使用异步复制,并且对值不执行合并操作。
- 可接受的写入安全程度:系统尝试(尽最大努力)保留来自与大多数主节点相连的客户端的所有写入。通常有小窗口,其中确认的写入可能会丢失。当客户端在少数分区中时,丢失已确认写入的 Windows 更大。
- 可用性:Redis Cluster 能够在大多数主节点可访问且每个主节点至少有一个可访问从属节点无法到达的分区中生存。此外,使用副本迁移,不再由任何从属复制的母版将从由多个从属覆盖的主服务器接收一个。
本文档中描述的内容在 Redis 3.0 或更高版本中实现。
实现子集
Redis Cluster 实现非分布式 Redis 版本中可用的所有单个键命令。执行复杂的多键操作(如 Set 类型联合或交集)的命令将实现,只要键全部哈希到同一插槽。
Redis Cluster 实现了一个称为哈希标记的概念,可用于强制将某些键存储在同一个哈希槽中。但是,在手动重新分片期间,多键操作可能在一段时间不可用,而单个密钥操作始终可用。
Redis 集群不支持多个数据库,如独立版本的 Redis。只有数据库 0,不允许使用SELECT命令。
Redis 集群协议中的客户端和服务器角色
在 Redis 集群节点中,节点负责存储数据并拍摄集群的状态,包括将键映射到正确的节点。集群节点还能够自动发现其他节点、检测非工作节点,并根据需要将从属节点提升为主节点,以便在发生故障时继续运行。
为了执行其任务,所有集群节点都使用 TCP 总线和二进制协议(称为Redis 集群总线)进行连接。每个节点都使用集群总线连接到集群中的所有其他节点。节点使用八卦协议传播有关集群的信息,以便发现新节点,发送 ping 数据包以确保所有其他节点正常工作,并发送发出特定条件信号所需的集群消息。集群总线还用于在整个集群中传播 Pub/Sub 消息,并在用户请求时协调手动故障转移(手动故障转移是故障转移,不是由 Redis 集群故障检测器启动,而是由系统管理员直接启动)。
由于集群节点无法代理请求,因此客户端可能会使用重定向错误和 重定向重定向到其他节点。理论上,客户端可以自由地向集群中的所有节点发送请求,如果需要,则重定向,因此客户端不需要保持集群的状态。但是,能够在键和节点之间缓存映射的客户端可以以合理的方式提高性能。-MOVED``-ASK
写入安全性
Redis 集群在节点之间使用异步复制,上次故障转移赢得隐式合并函数。这意味着最后一个选择的主数据集最终将替换所有其他副本。始终有一个时间窗口,在分区期间可能会丢失写入。但是,这些窗口在连接到大多数主机的客户端和连接到少数主机的客户端的情况下是非常不同的。
Redis Cluster 更加努力地保留由连接到大多数主机的客户端执行的写入,而在少数端执行写入。以下是导致在失败期间在多数分区中接收的确认写入丢失的方案示例:
- 写入可能到达主节点,但尽管主服务器可能能够回复客户端,但写入可能不会通过主节点和从节点之间使用的异步复制传播到从节点。如果主服务器在写入到从节点的情况下死亡,则如果主服务器在足够长时间内无法到达其一个从属服务器,则写入将永远丢失。在主节点完全突然发生故障的情况下,通常很难观察到这一点,因为主节点尝试同时回复客户端(确认写入)和从属服务器(传播写入)。然而,这是一个现实世界的失败模式。
- 另一个理论上可能导致写入丢失的故障模式如下:
- 由于分区,主服务器无法访问。
- 它被它的一个从节点故障。
- 一段时间后,它可能再次到达。
- 具有过期路由表的客户端在集群将其转换为从属(新主机)之前可以写入旧主机。
第二个故障模式不太可能发生,因为主节点无法与大多数其他主机通信,无法在足够的时间进行故障处理,因此将不再接受写入,并且当分区是固定的时,仍拒绝写入,以允许其他节点通知有关配置更改。此故障模式还要求客户端的路由表尚未更新。
以分区的少数侧为目标的写入具有较大的窗口,在其中要丢失。例如,Redis Cluster 在分区上丢失了非平凡的写入数,其中存在少数主服务器和至少一个或多个客户端,因为如果主服务器在多数端发生故障,则发送到主服务器的所有写入可能会丢失。
具体来说,要使主服务器故障过,至少大多数主服务器都无法访问它,因此,如果分区在该时间之前是固定的,则不会丢失写入。当分区持续超过 时,在少数端执行的所有写入操作到该点时可能会丢失。但是,Redis 集群的少数端将在时间过去后立即开始拒绝写入,而不与大多数人联系,因此有一个最大窗口,在此之后,少数方将不再可用。因此,在此之后,不会接受或丢失任何写入。NODE_TIMEOUT``NODE_TIMEOUT``NODE_TIMEOUT
可用性
Redis 集群在分区的少数端不可用。在分区的大多数方面假设每个无法访问的主主机至少有大多数主服务器和从节点,集群在一次又一次地变为可用,再加上从节点当选和故障转移其主服务器所需的更多秒(故障转移通常在 1 或 2 秒内执行)。NODE_TIMEOUT
这意味着 Redis Cluster 旨在生存集群中几个节点的故障,但它不适合在发生大型网络拆分时需要可用性的应用程序。
在由 N 个主节点组成的集群示例中,每个节点都有一个从属节点,只要将单个节点分区出去,集群的大多数侧将保持可用状态,并且将保持可用,当两个节点分区离开时(在第一个节点发生故障后,我们总共只剩下节点,并且唯一没有副本的主服务器失败的概率为 。1-(1/(N*2-1))``N*2-1``1/(N*2-1))
例如,在具有 5 个节点和每个节点的单个从属节点的集群中,在将两个节点从多数节点分区后,集群将不再可用。1/(5*2-1) = 11.11%
由于 Redis 集群功能称为副本迁移,因此,由于副本迁移到孤立主机(母版不再具有副本),因此在许多真实情况下,集群可用性得到了改进。因此,在每一个成功的失败事件中,集群可能会重新配置从属布局,以便更好地抵御下一次失败。
性能
在 Redis 集群节点中,节点不会将命令代理到负责给定密钥的右侧节点,而是将客户端重定向到为密钥空间的给定部分提供服务的右侧节点。
最终客户端获取集群的最新表示形式,以及哪个节点提供哪个键子集,因此在正常操作期间,客户端直接与正确的节点联系,以便发送给定的命令。
由于使用异步复制,节点不会等待其他节点的写入确认(如果没有使用 WAIT 命令显式请求)。
此外,由于多键命令仅限于近键,因此除非重新分片,否则数据永远不会在节点之间移动。
正常操作的处理方式与单个 Redis 实例的处理方式完全一样。这意味着,在具有 N 个主节点的 Redis 集群中,您可以期望与单个 Redis 实例乘以 N 的性能与设计线性缩放时相同的性能。同时,查询通常在一次往返中执行,因为客户端通常保留与节点的持久连接,因此延迟数字与单个独立 Redis 节点大小写相同。
Redis 集群的主要目标是实现高性能和可扩展性,同时保持弱但合理的数据安全和可用性形式。
为什么避免合并操作
Redis 集群设计避免了多个节点中相同键值对的冲突版本,例如在 Redis 数据模型中,这并不总是可取的。Redis 中的值通常非常大;通常查看包含数百万个元素的列表或排序集。此外,数据类型在语义上也很复杂。传输和合并这些类型的值可能是一个主要瓶颈和/或可能需要应用程序端逻辑的不平凡参与、存储元数据的额外内存等等。
这里没有严格的技术限制。CRD 或同步复制的状态计算机可以对类似于 Redis 的复杂数据类型进行建模。但是,此类系统的实际运行时行为不会类似于 Redis 集群。Redis 集群旨在涵盖非集群 Redis 版本的确切用例。
Redis 集群主要组件概述
键分布模型
密钥空间被拆分为 16384 个插槽,有效地为 16384 个主节点的集群大小设置上限(但建议的节点最大大小在 + 1000 个节点之间)。
集群中的每个主节点处理 16384 个哈希槽的子集。当没有进行集群重新配置时(即哈希槽从一个节点移动到另一个节点),集群是稳定的。当集群稳定时,单个节点将充当单个哈希槽(但是,服务节点可以具有一个或多个从属服务器,在发生网络拆分或失败时替换它,并且可用于扩展可接受读取陈旧数据的读取操作)。
用于将键映射到哈希槽的基本算法如下(阅读下一段有关此规则的哈希标记异常):
HASH_SLOT = CRC16(key) mod 16384
CRC16 的具体规定如下:
- 名称:XMODEM(也称为 ZMODEM 或 CRC-16/ACORN)
- 宽度: 16 位
- 多边形: 1021 (这实际上是 x\ x\ x+ 1)
- 初始化: 0000
- 反射输入字节:错误
- 反射输出 CRC:错误
- Xor 常数输出 CRC: 0000
- 输出”123456789”: 31C3
使用 16 个 CRC16 输出位中的 14 个(这就是为什么在上面的公式中存在 modulo 16384 操作的原因)。
在我们的测试中,CRC16 在 16384 插槽中均匀分布不同类型的密钥方面表现非常良好。
注意:本文档附录 A 提供了 CRC16 算法的参考实现。
键哈希标记
用于实现哈希标记的哈希槽的计算存在异常。哈希标记是确保在同一哈希槽中分配多个键的一种方式。这用于在 Redis 集群中实现多键操作。
为了实现哈希标记,在某些情况下,密钥的哈希槽计算方式略有不同。如果密钥包含”[…]”模式,则只有 和 之间的子字符串进行哈希处理,以便获取哈希槽。但是,由于 以下规则可能多次出现 或 算法指定得很清楚:{``}``{``}
- 如果键包含字符。
{
- 如果右边有一个字符
}``{
- 并且 如果 第一次出现和第一次出现之间有一个或多个字符。
{``}
然后,只有第一次出现和以下第一次出现之间的哈希,而不是散列键。{``}
例子:
- 两个键和将哈希到相同的哈希槽,因为只有子字符串将散列,以计算哈希槽。
{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;
}
集群节点属性
每个节点在集群中都有一个唯一的名称。节点名称是 160 位随机数的十六进制表示形式,首次启动节点时获得(通常使用 /dev/urandom)。节点将将其 ID 保存在节点配置文件中,并且将永久使用相同的 ID,或者至少只要系统管理员不删除节点配置文件,或者通过 CLUSTER RESET 命令请求硬重置。
节点 ID 用于标识整个集群中的每个节点。给定节点无需更改节点 ID 即可更改其 IP 地址。集群还能够检测 IP/端口中的更改,并使用在集群总线上运行的八卦协议重新配置。
节点 ID 不是与每个节点关联的唯一信息,而是始终全局一致的唯一信息。每个节点还具有以下关联信息集。某些信息有关此特定节点的集群配置详细信息,并且最终在集群中保持一致。其他一些信息(如上次 ping 节点时)是每个节点的本地信息。
每个节点维护有关它在集群中感知的其他节点的以下信息:节点的节点 ID、IP 和端口、一组标志、如果节点被标记为 时节点被标记为 、上次 ping 节点和上次收到 pong 时,节点的当前配置时间(在此规范稍后说明)、链接状态以及最终的哈希槽集。slave
所有节点字段的详细说明在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 集群的特定部分,将介绍上述字段的详细信息。
集群总线
每个 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 集群节点形成完整网格,节点使用八卦协议和配置更新机制,以避免在正常情况下在节点之间交换太多消息,因此交换的消息数不是指数。
节点握手
节点始终接受集群总线端口上的连接,甚至在收到 ping 时甚至回复 ping,即使 ping 节点不受信任。但是,如果发送节点不被视为集群的一部分,则接收节点将丢弃所有其他数据包。
节点将接受另一个节点作为集群的一部分,仅以两种方式:
- 如果节点显示消息。meet 消息与PING消息完全一样,但强制接收方接受节点作为集群的一部分。只有当系统管理员通过以下命令请求时,节点才会向其他节点发送消息:
MEET``MEET
集群满足 ip 端口 - 如果已受信任的节点将有关此其他节点的八卦,节点还将将另一个节点注册为集群的一部分。所以,如果 A 知道 B,B 知道 C,最终 B 会向 A 发送关于 C 的八卦消息。发生这种情况时,A 将注册 C 作为网络的一部分,并尝试与 C 连接。
这意味着,只要我们在任何连接的图形中加入节点,它们最终都会自动形成完全连接的图形。这意味着集群能够自动发现其他节点,但只有当存在系统管理员强制的受信任关系时。
此机制使集群更加健壮,但可防止不同的 Redis 集群在 IP 地址更改或其他网络相关事件更改后意外混合。
重定向和重新分片
移动重定向
Redis 客户端可以自由地向集群中的每个节点(包括从属节点)发送查询。该节点将分析查询,如果该查询可以接受(即查询中只提到一个键,或者提到的多个键都位于同一哈希槽中),则节点将查找哪个节点负责键或键所属的哈希槽。
如果由节点提供哈希槽,则只需处理查询,否则该节点将检查其内部哈希槽到节点映射,并将回复具有移动错误的客户端,如下例所示:
GET x
-MOVED 3999 127.0.0.1:6381
该错误包括密钥 (3999) 的哈希槽和可为查询服务的实例的 ip:port。客户端需要将查询重新发出到指定的节点的 IP 地址和端口。请注意,即使客户端在重新提交查询之前等待很长时间,并且同时更改了集群配置,如果哈希槽 3999 现在由另一个节点提供,目标节点将再次回复移动错误。如果已联系节点没有更新的信息,也会发生同样情况。
因此,虽然从集群节点的角度来看,集群节点由 ID 标识,但我们尝试简化与客户端的接口,只需公开由 IP:port 对标识的哈希槽和 Redis 节点之间的映射。
客户端不需要,但应尝试记住哈希槽 3999 由 127.0.0.1:6381 提供。这样,一旦需要发出新命令,就可以计算目标键的哈希槽,并有更大的机会选择正确的节点。
另一种选择是,在收到移动重定向时,使用 CLUSTER节点或CLUSTER SLOTS命令刷新整个客户端集群布局。遇到重定向时,很可能重新配置了多个插槽,而不仅仅是一个插槽,因此尽快更新客户端配置通常是最好的策略。
请注意,当集群稳定(配置中没有持续更改)时,最终所有客户端都将获得哈希槽映射 -> 节点,使集群高效,客户端直接寻址正确的节点,而无需重定向、代理或其他单点故障实体。
客户端还必须能够处理本文档稍后介绍的 -ASK重定向,否则它不是完整的 Redis 集群客户端。
集群实时重新配置
Redis 集群支持在集群运行时添加和删除节点的能力。添加或删除节点将抽象为同一操作:将哈希槽从一个节点移动到另一个节点。这意味着使用相同的基本机制来重新平衡集群、添加或删除节点等。
- 要向集群添加新节点,将空节点添加到集群中,并将一些哈希槽集从现有节点移动到新节点。
- 要从集群中删除节点,分配给该节点的哈希槽将移动到其他现有节点。
- 为了重新平衡集群,在节点之间移动一组给定的哈希槽。
实现的核心是移动哈希槽的能力。从实际的角度来看,哈希槽只是一组键,因此 Redis Cluster 在重新分片期间真正执行的是将密钥从实例移动到另一个实例。移动哈希槽意味着将碰巧哈希的所有键移动到此哈希槽中。
为了了解其工作原理,我们需要显示用于操作 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
前两个命令 和 仅用于将(或删除)插槽分配给 Redis 节点。分配槽意味着告诉给定的主节点,它将负责存储和提供指定哈希槽的内容。ADDSLOTS``DELSLOTS
分配哈希槽后,它们将使用八卦协议在集群中传播,如配置传播部分稍后所述。
当从头开始创建新集群来为每个主节点分配所有可用的 16384 个哈希槽的子集时,通常使用该命令。ADDSLOTS
主要用于手动修改集群配置或调试任务:实际上很少使用。DELSLOTS
如果使用窗体,子命令用于将槽分配给特定的节点 ID。否则,插槽可以设置为两个特殊状态 和 。这两个特殊状态用于将哈希槽从一个节点迁移到另一个节点。SETSLOT``SETSLOT <slot> NODE``MIGRATING``IMPORTING
- 当插槽设置为”迁移”时,节点将接受有关此哈希槽的所有查询,但仅当存在有关密钥时,否则使用重定向将查询转发到作为迁移目标的节点。
-ASK
- 当插槽设置为导入时,节点将接受有关此哈希槽的所有查询,但仅在请求前面有命令时。如果客户端未提供该命令,则查询通过重定向错误重定向到真正的哈希槽所有者,就像通常发生的情况一样。
ASKING``ASKING``-MOVED
让我们用哈希槽迁移示例来明确这一点。假设我们有两个 Redis 主节点,称为 A 和 B。我们希望将哈希槽 8 从 A 移动到 B,因此我们发出这样的命令:
- 我们发送 B: Cluster 集洛特 8 导入 A
- 我们发送 A: 集群集洛特 8 迁移 B
所有其他节点在每次使用属于哈希槽 8 的键查询时都将继续将客户端指向节点”A”,因此将发生以下情况:
- 有关现有密钥的所有查询都由”A”处理。
- 关于 A 中不存在的密钥的所有查询都由”B”处理,因为”A”会将客户端重定向到”B”。
这样,我们不再在”A”中创建新密钥。同时,在重新分片和 Redis 集群配置期间使用的特殊程序将从 A 迁移哈希槽 8 中的现有密钥。这是使用以下命令执行的:redis-trib
CLUSTER GETKEYSINSLOT slot count
上述命令将返回指定哈希槽中的键。对于返回的每个密钥,发送节点”A”一个迁移命令,该命令会以原子方式将指定的密钥从 A 迁移到 B(两个实例在迁移密钥所需的时间(通常非常小的时间)中锁定,因此没有比赛条件)。这是迁移的工作原理:count``redis-trib
MIGRATE target_host target_port key target_database id timeout
MIGRATE将连接到目标实例,发送密钥的序列化版本,一旦收到 OK 代码,将删除其自身数据集中的旧密钥。从外部客户端的角度来看,密钥存在于 A 或 B 中。在任何给定时间。
在 Redis 集群中,无需指定 0 以外的数据库,但”迁移”是可用于不涉及 Redis 集群的其他任务的通用命令。即使移动复杂键(如长列表),迁移也得到优化,即使移动了长列表,但在 Redis 集群中,如果应用程序中使用数据库存在延迟限制,则重新配置存在大键的集群不被认为是一个明智的过程。
当迁移过程最终完成时,该命令将发送到迁移中涉及的两个节点,以便再次将插槽设置为正常状态。通常将相同的命令发送到所有其他节点,以避免等待新配置在集群中的自然传播。SETSLOT <slot> NODE <node-id>
询问重定向
在上一节中,我们简要地讨论了 ASK 重定向。为什么我们不能简单地使用移动重定向?因为虽然 MOVED 意味着我们认为哈希槽由其他节点永久提供,并且应针对指定的节点尝试下一个查询,但 ASK 意味着仅将下一个查询发送到指定的节点。
这是必要的,因为关于哈希槽 8 的下一个查询可能是关于仍在 A 中的密钥,因此我们总是希望客户端尝试 A,然后根据需要尝试 B。由于这仅适用于 16384 个可用哈希插槽中的一个,因此集群上的性能命中是可以接受的。
我们需要强制该客户端行为,因此,若要确保客户端仅在尝试 A 后尝试节点 B,节点 B 将仅接受设置为导入的插槽的查询,如果客户端在发送查询之前发送询问命令。
基本上,询问命令在客户端上设置一个一次标志,强制节点为有关导入槽的查询服务。
从客户端的角度来看,ASK 重定向的完整语义如下:
- 如果收到 ASK 重定向,则仅发送重定向到指定节点的查询,但继续向旧节点发送后续查询。
- 使用”询问”命令启动重定向查询。
- 尚未更新本地客户端表以将哈希槽 8 映射到 B。
一旦哈希槽 8 迁移完成,A 将发送移动消息,客户端可以永久地将哈希槽 8 映射到新的 IP 和端口对。请注意,如果错误客户端之前执行映射,这不是问题,因为它在发出查询之前不会发送询问命令,因此 B 会使用移动重定向错误将客户端重定向到 A。
在 CLUSTER SETSLOT 命令文档中,插槽迁移用类似术语进行解释,但措辞不同(为了文档中的冗余)。
客户端第一次连接和处理重定向
虽然内存中可能具有不记得插槽配置(插槽编号和提供该配置的节点的地址之间的映射)的 Redis 集群客户端实现,并且只能通过联系等待重定向的随机节点来工作,但此类客户端将非常低效。
Redis 集群客户端应尽量智能以记住插槽配置。但是,此配置不需要是最新的。由于联系错误的节点只会导致重定向,因此应触发客户端视图的更新。
客户端通常需要在两种不同的情况下获取完整的插槽列表和映射的节点地址:
- 在启动时,以填充初始插槽配置。
- 收到重定向时。
MOVED
请注意,客户端可以通过只更新其表中移动的插槽来处理重定向,但是这通常效率不高,因为通常一次修改多个插槽的配置(例如,如果从属服务器提升为主插槽,则将重新映射旧主机提供的所有插槽)。通过从头开始获取插槽的完整映射到节点,对重定向做出反应要简单得多。MOVED``MOVED
为了检索插槽配置,Redis Cluster 提供了不需要解析的CLUSTER NODES命令的替代方法,并且仅向客户端提供严格需要的信息。
新命令称为CLUSTER SLOTS,提供插槽范围数组,以及提供指定范围的关联的主节点和从节点。
以下是 CLUSTER 插槽输出的示例:
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 标志)的所有从属服务器。
例如,输出的第一个元素表示,127.0.0.1:7001 提供从 5461 到 10922(包括开始和结束)的插槽,并且可以缩放在 127.0.0.1:7004 接触从属的只读负载。
如果集群配置错误,不保证返回覆盖完整 16384 插槽的范围,因此客户端应初始化填充目标节点的插槽配置映射,并报告错误,如果用户尝试执行有关属于未分配插槽的键的命令。
在发现插槽未分配时将错误返回给调用方之前,客户端应尝试再次获取插槽配置,以检查集群现在配置正确。
多个键操作
使用哈希标记,客户端可以自由使用多键操作。例如,以下操作有效:
MSET {user:1000}.name Angela {user:1000}.surname White
当密钥所属的哈希槽的重新分片进行时,多键操作可能会变得不可用。
更具体地说,即使在重新分片期间,目标键的多键操作仍然可用,所有键都存在,并且所有仍哈希到同一插槽(源节点或目标节点)。
对不存在或在重新分片期间在源节点和目标节点之间拆分的密钥的操作将生成错误。客户端可以在一段时间后尝试该操作,或报告错误。-TRYAGAIN
一旦指定的哈希槽的迁移终止,所有多键操作将再次可用于该哈希槽。
使用从属节点缩放读取
通常,从节点会将客户端重定向到给定命令中涉及的哈希槽的权威主节点,但是客户端可以使用从属节点来使用READONLY 命令缩放读取。
READONLY告诉 Redis 集群从属节点,客户端可以读取可能陈旧的数据,并且对运行写入查询不感兴趣。
当连接处于只读模式时,集群将仅在操作涉及从从主节点未提供密钥时才向客户端发送重定向。这可能是因为:
- 客户端发送了有关此从节点主从未提供过的哈希槽的命令。
- 集群已重新配置(例如重新分片),并且从属服务器不再能够为给定的哈希槽提供命令。
发生这种情况时,客户端应更新其哈希位映射,如前几节所述。
可以使用 READWRITE 命令清除连接的只读状态。
容错
心跳和八卦消息
Redis 集群节点持续交换 ping 和 pong 数据包。这两种数据包具有相同的结构,并且都带有重要的配置信息。唯一的实际差异是消息类型字段。我们将ping和pong数据包的总和称为心跳数据包。
通常节点发送 ping 数据包,这将触发接收器使用 pong 数据包进行回复。然而,这并不一定是真的。节点只需发送 pong 数据包即可向其他节点发送有关其配置的信息,而无需触发回复。例如,这很有用,以便尽快广播新配置。
通常,节点将每秒钟 ping 几个随机节点,以便每个节点发送的 ping 数据包总数(以及接收的 pong 数据包)是恒定的数量,而不管集群中的节点数如何。
但是,每个节点请确保 ping 未发送 ping 或收到 pong 超过一半时间的其他节点。在经过之前,节点还尝试将 TCP 链路重新连接到另一个节点,以确保节点不会仅仅因为当前 TCP 连接中出现问题而被认为无法访问。NODE_TIMEOUT``NODE_TIMEOUT
如果全局交换的消息数设置为小数字且节点数 (N) 非常大,则全局交换的消息数可以很大,因为每个节点将尝试每半时间没有新信息的所有其他节点。NODE_TIMEOUT``NODE_TIMEOUT
例如,在节点超时设置为 60 秒的 100 个节点集群中,每个节点将尝试每 30 秒发送 99 ping,总 ping 量为 3.3/秒。乘以 100 个节点,这是整个集群中 330 ping/秒。
有一种方法可以减少消息数,但是 Redis 集群故障检测当前使用的带宽没有报告问题,因此目前使用明显和直接的设计。请注意,即使在上述示例中,交换的 330 个数据包/秒也均匀地划分为 100 个不同的节点,因此每个节点接收的流量是可以接受的。
检测信号数据包内容
Ping 和 pong 数据包包含所有类型的数据包都通用的标头(例如,请求故障转移投票的数据包),以及特定于 Ping 和 Pong 数据包的特殊 Gossip 部分。
公共标头具有以下信息:
- 节点 ID 是一个 160 位伪随机字符串,在首次创建节点时分配该字符串,在 Redis 集群节点的所有生命周期中保持不变。
- 用于装载 Redis 集群使用的分布式算法的发送节点的 和 字段(下一节将对此进行详细说明)。如果节点是从属,是其主节点的最后已知节点。
currentEpoch``configEpoch``configEpoch``configEpoch
- 节点标志,指示节点是否为从属节点、主节点和其他单位节点信息。
- 发送节点提供哈希槽的位图,或者如果节点是从属,则由其主节点提供该位槽的位图。
- 发送方 TCP 基端口(也就是说,Redis 用于接受客户端命令的端口;向其添加 10000 个端口以获取集群总线端口)。
- 从发送方的角度来看集群的状态(向下或确定)。
- 发送节点的主节点 ID(如果是从属节点)。
乒乓球和乒乓球包也包含八卦部分。本节向接收方提供发送方节点对集群中其他节点的看法视图。八卦部分仅包含发送方已知的节点集中几个随机节点的信息。八卦部分中提及的节点数与集群大小成正比。
对于八卦部分中添加的每个节点,将报告以下字段:
- 节点 ID。
- 节点的 IP 和端口。
- 节点标志。
八卦部分允许接收节点从发送方的视角获取有关其他节点状态的信息。这对于故障检测和发现集群中的其他节点都很有用。
故障检测
Redis 集群故障检测用于识别主节点或从节点何时不再被大多数节点访问,然后通过将从节点提升为主节点角色来响应。当无法进行从属升级时,集群将处于错误状态,以停止接收来自客户端的查询。
如前所述,每个节点都会使用与其他已知节点关联的标志列表。有两个用于故障检测的标志,这些标志被调用 和 。 表示可能失败,并且是一种未确认的故障类型。 表示节点出现故障,并且此条件在固定范围内由大多数主机确认。PFAIL``FAIL``PFAIL``FAIL
PfaIL 标志:
当节点在超过一段时间时无法访问时,节点会用标志标记另一个节点。主节点和从节点都可以将另一个节点标记为 ,而不考虑其类型。PFAIL``NODE_TIMEOUT``PFAIL
Redis 集群节点的不可访问性的概念是,我们有一个活动ping(我们发送的 ping,我们尚未获得答复)等待的时间超过。要使此机制正常工作,必须与网络往返时间相比较大。为了在正常操作期间增加可靠性,节点将尝试在一半经过时与集群中的其他节点重新连接,而无需回复 ping。此机制可确保连接保持活动状态,因此断开的连接通常不会导致节点之间的错误故障报告。NODE_TIMEOUT``NODE_TIMEOUT``NODE_TIMEOUT
失败标志:
标志只是每个节点与其他节点的本地信息,但不足以触发从属升级。要将节点视为向下,需要将条件升级为条件。PFAIL``PFAIL``FAIL
如本文档的节点检测信号部分所述,每个节点向所有其他节点发送八卦消息,包括几个随机已知节点的状态。每个节点最终接收一组其他节点的节点标志。这样,每个节点都有一个机制,向其他节点发出它们检测到的故障条件的信号。
满足以下条件集时,条件将升级为条件:PFAIL``FAIL
- 某些节点(我们称之为 A)将另一个节点 B 标记为 。
PFAIL
- 节点 A 通过八卦部分从集群中大多数主机的角度来看,收集了有关 B 状态的信息。
- 大多数主机在一小时内发出 或 条件信号。(当前实现中的有效性因子设置为 2,因此这只是时间的两倍)。
PFAIL``FAIL``NODE_TIMEOUT * FAIL_REPORT_VALIDITY_MULT``NODE_TIMEOUT
如果上述所有条件都为 true,则节点 A 将:
- 将节点标记为 。
FAIL
- 向所有可访问节点发送消息。
FAIL
该消息将强制每个接收节点将节点标记为状态,无论该节点是否已将节点标记为状态。FAIL``FAIL``PFAIL
请注意,FAIL 标志主要是一种方法。也就是说,节点可以从 转到 ,但只能在以下情况下清除标志:PFAIL``FAIL``FAIL
- 该节点已可访问,并且是从属节点。在这种情况下,可以清除标志,因为从属服务器没有故障。
FAIL
- 该节点已可访问,并且是主节点,不为任何插槽服务。在这种情况下,可以清除标志,因为没有插槽的母版并不真正参与集群,并且正在等待配置以加入集群。
FAIL
- 该节点已可访问,并且是主节点,但时间很长 (N 倍 ) 没有任何可检测到的从属升级。最好重新加入集群并在这种情况下继续。
NODE_TIMEOUT
需要注意的是,虽然 -> 过渡使用一种协议形式,但使用的协议是弱的:PFAIL``FAIL
- 节点在一段时间内收集其他节点的视图,因此即使大多数主节点需要”同意”,实际上这只是我们在不同的时间从不同节点收集的状态,我们不确定,也要求,在给定时刻,大多数主节点都同意。但是,我们丢弃的是旧的故障报告,因此大多数主机在一个时间范围内发出故障信号。
- 虽然每个检测到该条件的节点都将使用消息强制集群中的其他节点存在该条件,但无法确保消息将到达所有节点。例如,节点可能检测到该条件,并且由于分区无法到达任何其他节点。
FAIL``FAIL``FAIL
但是,Redis 集群故障检测具有实时性要求:最终所有节点都应同意给定节点的状态。有两种情况可能源于大脑分裂。要么是少数节点认为节点处于状态,要么是少数节点认为节点未处于状态。在这两种情况下,最终集群将具有给定节点状态的单个视图:FAIL``FAIL
案例 1:如果大多数主机由于故障检测和它生成的链效应而将节点标记为 ,则所有其他节点最终将将主节点标记为 ,因为在指定的时间窗口中将报告足够的故障。FAIL``FAIL
案例 2:当只有少数主机将节点标记为 时,从属升级不会发生(因为它使用更正式的算法,确保每个人都知道升级最终),并且每个节点将清除状态,根据上述状态清除规则(即 N 倍之后没有升级)。FAIL``FAIL``FAIL``NODE_TIMEOUT
FAIL
标志仅用作触发器,用于运行从属升级算法的安全部分。从理论上讲,从节点可以独立行动,在主人无法接触到时开始从节点提升,并等待主人拒绝提供承认,如果主人实际上是可以达到的多数。然而,状态的复杂性、弱协议以及强制在集群可到达部分的最短时间内传播状态的消息具有实际优势。由于这些机制,如果集群处于错误状态,通常所有节点将大约同时停止接受写入。从使用 Redis 集群的应用程序的角度来看,这是一个理想的功能。此外,还避免了由因本地问题而无法到达主节点的从节点发起的错误选择尝试(否则大多数其他主节点都可以到达主节点)。PFAIL -> FAIL``FAIL
配置处理、传播和故障转移
集群当前纪元
Redis 集群使用的概念类似于 Raft 算法”术语”。在 Redis 集群中,术语改为纪元,用于为事件提供增量版本控制。当多个节点提供冲突信息时,另一个节点可以了解哪个状态是最新的。
是 64 位未签名的数字。currentEpoch
在节点创建时,每个 Redis 集群节点(从属节点和主节点)都设置为 0。currentEpoch
每次从另一个节点接收数据包时,如果发送方(集群总线消息头的一部分)的纪元大于本地节点纪元,则 将 更新到发送方纪元。currentEpoch
由于这些语义,最终所有节点都将同意集群中最大的节点。currentEpoch
当集群的状态更改,节点寻求协议以执行某些操作时,将使用此信息。
目前,这仅在从属升级期间发生,如下一节所述。基本上,纪元是集群的逻辑时钟,并规定给定信息胜于一个时代较小的时间。
配置纪元
每个主级总是在乒乓球和乒乓球包中宣传其,同时在位图中宣传它服务中的插槽集。configEpoch
创建新节点时,在母版中设置为零。configEpoch
在从节点选举期间创建一个新。试图取代失败的主人的从节点增加了他们的纪元, 并试图从大多数大师获得授权。当从属授权时,将创建一个新的唯一,并且从属使用 新的 转换为主。configEpoch``configEpoch``configEpoch
如下一节所述,当不同的节点声称配置不同时,有助于解决冲突(由于网络分区和节点故障而可能发生这种情况)。configEpoch
从节点还在 ping 和 pong 数据包中通告该字段,但在从节点的情况下,该字段表示其主节点的 ,因为它们上次交换数据包时。这允许其他实例检测从属服务器何时具有需要更新的旧配置(主节点不会向具有旧配置的从属节点授予投票)。configEpoch``configEpoch
每次更改某些已知节点时,接收此信息的所有节点都会将该节点永久存储在节点中。值也发生同样情况。保证在节点继续其操作之前更新这两个变量并保存到磁盘。configEpoch``currentEpoch``fsync-ed
在故障转移期间使用简单算法生成的值保证是新的、增量的和唯一的。configEpoch
从节点选举和晋升
从属选举和升级由从属节点处理,由投票支持从属进行升级的主节点帮助处理。当主服务器处于状态时,就会发生从至少一个从节点的角度来看,该从节点具有成为主服务器的先决条件。FAIL
为了让从节点自我提升,它需要开始选举并赢得选举。给定主人的所有从节点都可以开始选举,如果主人处于状态,但是只有一个从节点将赢得选举,并提升自己为主人。FAIL
当满足以下条件时,从属人开始选举:
- 从节点的主人处于状态。
FAIL
- 主主机提供非零数量的插槽。
- 从属复制链接与主服务器断开连接的时间不超过给定时间,以确保提升的从节点数据相当新鲜。这一次是用户可配置的。
为了当选,从属服务器的第一步是增加其计数器,并请求主实例的投票。currentEpoch
从节点通过将数据包广播到集群的每个主节点来请求投票。然后,它等待回复到达的最大时间为 2 倍(但始终至少 2 秒)。FAILOVER_AUTH_REQUEST``NODE_TIMEOUT
一旦主人投票给一个给定的从节点,积极回答,它不能再投票给同一主人的另一个从节点一段时间。在此期间,它将无法回复同一主主机的其他授权请求。这不需要保证安全,但有助于防止多个从节点当选(即使具有不同的)在同一时间,这通常是不需要的。FAILOVER_AUTH_ACK``NODE_TIMEOUT * 2``configEpoch
从属放弃任何具有小于发送投票请求时的时间数的答复。这确保了它不计算上一次选举所需的选票。AUTH_ACK``currentEpoch
一旦从节点从大多数主人那里获得 AKS, 它就赢得了选举。否则,如果在两次期间内未达到多数票(但始终至少 2 秒),则选举将中止,并在之后(且始终至少 4 秒)再次尝试新的选举。NODE_TIMEOUT``NODE_TIMEOUT * 4
从属排名
一旦主服务器处于状态,从节点要等待很短的时间,然后才试图当选。该延迟的计算方式如下:FAIL
DELAY = 500 milliseconds + random delay between 0 and 500 milliseconds +
SLAVE_RANK * 1000 milliseconds.
固定的延迟确保我们等待状态在整个集群中传播,否则从属人可能会尝试当选,而主权者仍然不知道该州,拒绝授予他们的选票。FAIL``FAIL
随机延迟用于取消同步从属服务器,因此它们不太可能同时开始选举。
是此从属服务器的排名,它从主服务器处理的复制数据量。当主服务器出现故障以建立(尽最大努力)排名时交换消息:具有更新的复制偏移量的从属服务器位于排名 0,第二个最更新的级别为 1,等等。这样,最新的从节点们会试图在别人之前当选。SLAVE_RANK
等级顺序不严格执行;如果一个高级从节点不能当选, 其他人很快就会尝试。
一旦从节点赢得选举,它获得一个新的独特和增量,这是高于任何其他现有的主。它开始宣传自己作为主在乒乓球和乒乓球包,提供一套服务插槽,将赢得过去的。configEpoch``configEpoch
为了加快其他节点的重新配置,将 PONG 数据包广播到集群的所有节点。当前无法访问的节点最终将重新配置,当它们从另一个节点接收 ping 或 pong 数据包时,或者如果检测到通过检测信号数据包发布的信息过期,则从另一个节点接收数据包。UPDATE
其他节点将检测是否有新主机提供与旧主机相同的插槽,但具有更大的,并将升级其配置。旧主机的从属服务器(或已故障的主机(如果重新加入集群)将不仅升级配置,还将重新配置以从新主服务器进行复制。下一节将介绍如何配置重新加入集群的节点。configEpoch
主人回复从节点投票请求
在上一节中,讨论了从节点如何试图当选。本节从请求投票支持给定从属的主机的角度来看说明发生的情况。
大师以从节点的请求形式收到投票请求。FAILOVER_AUTH_REQUEST
要进行表决,必须满足以下条件:
- 主级只为给定纪元只投票一次,并拒绝为旧纪元投票:每个大师都有最后一个VoteEpoch字段,只要身份验证请求数据包中的 不大于最后一个VoteEpoch,主级将拒绝再次投票。当主服务器对投票请求做出积极答复时,最后一个VoteEpoch将相应地更新,并安全地存储在磁盘上。
currentEpoch
- 只有当从主服务器标记为 时,主服务器才能投票支持从节点。
FAIL
- 将忽略小于主的 Auth 请求。因此,主答复将始终具有与身份验证请求相同的项。如果同一从节点再次要求投票,增加 ,则保证新投票的接受来自主的旧延迟答复。
currentEpoch``currentEpoch``currentEpoch``currentEpoch
不使用规则 3 引起的问题示例:
大师是 5, 最后一票是 1 (这可能发生在几个失败的选举后)currentEpoch
- 从节点是3。
currentEpoch
- 从节点试图用纪元 4 (3+1) 当选, 主回答与确定与 5, 但答复被延迟。
currentEpoch
- 从节点将尝试再次当选,在以后的时间,与纪元5(4+1),延迟的答复到达从5,并接受为有效。
currentEpoch
- 如果大师的从节点已经投了赞成票, 大师们以前不会投票给同一大师的从节点。这不是严格要求,因为两个从节点不可能在同一时期赢得选举。然而,实际上,它确保当从节点当选时,它有足够的时间通知其他从节点,并避免另一个从节点将赢得新选举的可能性,执行不必要的第二次故障转移。
NODE_TIMEOUT * 2
- 大师们不尽任何努力去选择最好的从节点。如果从节点的主人处于状态,而主主在当前任期内没有投票,则给予正票。最好的从节点是最有可能开始选举,并在其他从节点之前赢得选举,因为它通常能够更早地开始投票过程,因为它的排名更高,如上一节所述。
FAIL
- 当主服务器拒绝为给定的从节点投票时,没有负面响应,则请求被忽略。
- 大师不会投票支持从属人发送的比主表中任何由从节点声称的插槽都少的。请记住,从服务器发送其主服务器的 ,以及其主服务器提供插槽的位图。这意味着请求投票的从节点必须具有要进行故障转移的插槽的配置,该槽的更新或等于授予投票的主服务器之一。
configEpoch``configEpoch``configEpoch
分区期间配置时代有用性的实际示例
本节说明如何使用时代概念使从属升级过程对分区的抵抗力更强。
- 主服务器不再可以无限期地到达。主人有三个从节点 A ,B ,C 。
- 从节点 A 赢得选举,被提升为主人。
- 网络分区使 A 对大多数集群不可用。
- 从节点 B 赢得选举,被提升为主人。
- 分区使 B 对大多数集群不可用。
- 上一个分区是固定的,A 再次可用。
此时 B 已关闭,A 将再次具有主角色(实际上消息会立即重新配置它,但在这里我们假设所有消息都丢失了)。同时,从节点C将试图当选,以失败超过B。发生的情况是:UPDATE``UPDATE
- C 将尝试当选, 并将成功, 因为对于大多数大师来说, 它的主人实际上是下来的。它将获得一个新的增量。
configEpoch
- A 将无法声明其哈希槽的主节点,因为与其他节点的哈希槽已具有与较高的配置纪元(B 的一个)关联的相同哈希槽,而 A 发布的哈希值时代则相同。
- 因此,所有节点都将升级其表以将哈希槽分配给 C,集群将继续其操作。
如下部分所述,重新加入集群的陈旧节点通常会尽快收到有关配置更改的通报,因为一旦 ping 任何其他节点,接收方就会检测到它有陈旧信息,并将发送消息。UPDATE
哈希插槽配置传播
Redis 集群的一个重要部分是用于传播有关哪个集群节点为一组给定哈希槽服务的信息的机制。这对于新集群的启动和在从服务器升级为其故障主集群的插槽服务后升级配置的能力都至关重要。
同一机制允许无限期地分区的节点以合理的方式重新加入集群。
传播哈希槽配置有两种方式:
- 检测信号消息。ping 或 pong 数据包的发送者始终添加有关它(或其主数据包主(如果是从)的哈希插槽集的信息。
UPDATE
消息。由于在每个检测信号数据包中都有有关发送方和一组已服务哈希插槽的信息,因此,如果检测信号数据包的接收方发现发送方信息已过时,它将发送包含新信息的数据包,从而强制陈旧节点更新其信息。configEpoch
检测信号或消息的接收者使用某些简单的规则来更新其表映射哈希槽到节点。创建新的 Redis 集群节点时,其本地哈希槽表将简单地初始化为条目,以便每个哈希槽不会绑定或链接到任何节点。这类似于以下内容:UPDATE``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 ADDS ADDSLOTS 命令,通过 redis-trib 命令行工具,或任何其他方式)每个主节点仅向节点本身提供插槽,信息将快速传播到集群中。
但是,此规则是不够的。我们知道哈希槽映射在两个事件期间可能会更改:
- 从服务器在故障转移期间替换其主服务器。
- 插槽从节点重新分片到其他节点。
现在,让我们关注故障转移。当从节点故障超过其主服务器时,它获取的配置纪元保证大于其主服务器(通常大于之前生成的任何配置纪元)。例如,作为 A 从属节点的节点 B 可能会以 4 的配置纪元进行故障转移 B。它将开始发送检测信号数据包(第一次大规模广播集群范围),并且由于以下第二条规则,接收方将更新其哈希槽表:
规则 2:如果已分配哈希槽,并且已知节点使用大于当前与该槽关联的主节点进行广告宣传,我将散列槽重新绑定到新节点。configEpoch``configEpoch
因此,在收到来自 B 的消息,声称为配置纪元为 4 的哈希槽 1 和 2 服务的消息后,接收方将按照以下方式更新其表:
0 -> NULL
1 -> B [4]
2 -> B [4]
...
16383 -> NULL
Liveness 属性:由于第二条规则,最终集群中的所有节点都会同意插槽的所有者是其中广告槽中功能最大的节点。configEpoch
Redis 集群中的此机制称为上次故障转移获胜。
在重新分片过程中也发生同样情况。当导入哈希槽的节点完成导入操作时,其配置纪元将递增,以确保更改将在整个集群中传播。
更新消息,仔细看看
在讨论上一节时,可以更轻松地查看更新消息的工作方式。节点 A 可能在一段时间后重新加入集群。它将发送检测信号数据包,声称它提供哈希插槽 1 和 2 的配置纪元为 3。所有具有更新信息的接收器都将看到相同的哈希槽与具有较高配置纪元的节点 B 关联。因此,他们将向 A 发送一条消息,其中包含插槽的新配置。由于上面的规则2,将更新其配置。UPDATE
节点如何重新加入集群
当节点重新加入集群时,使用相同的基本机制。继续上面的示例,节点 A 将收到通知,哈希槽 1 和 2 现在由 B 提供。 假设这两个插槽是 A 提供的唯一哈希槽,A 提供哈希槽的计数将下降到 0!因此,A将重新配置为新的主从。
遵循的实际规则比这复杂一些。通常,A 在很多时间后重新加入,在此期间,A 最初由多个节点提供哈希槽,例如哈希槽 1 可能由 B 提供,哈希槽 2 由 C 提供。
因此,实际的 Redis 集群节点角色切换规则是:主节点将更改其配置以复制(成为)窃取其最后一个哈希槽的节点。
在重新配置期间,最终服务哈希槽的数量将降至零,节点将相应地重新配置。请注意,在基本情况下,这仅仅意味着旧主服务器将是从属服务器,在故障转移后替换它。但是,在一般形式中,该规则涵盖所有可能的情况。
从节点这样做完全相同:它们重新配置以复制窃取其前主主机的最后一个哈希槽的节点。
副本迁移
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。
- 集群可以继续操作。
副本迁移算法
迁移算法不使用任何形式的协议,因为 Redis 集群中的从属布局不是需要与配置纪元保持一致和/或版本化的集群配置的一部分。相反,它使用算法来避免在主服务器未得到支持时大量迁移从节点。该算法保证最终(一旦集群配置稳定)每个主服务器将至少由一个从节点支持。
这就是算法的工作原理。首先,我们需要定义什么是好从属在此上下文中:好的从属是从属,而不是从给定节点的角度来看处于状态。FAIL
算法的执行在每个从节点中触发,这些从节点检测到至少有一个主服务器没有良好的从节点。但是,在检测到此情况的所有从属中,只有子集应采取行动。此子集实际上通常是单个从属,除非不同的从属在给定时刻对其他节点的故障状态略有不同。
代理从属服务器是主服务器中的最大附加从属,该从属服务器不在 FAIL 状态且具有最小的节点 ID。
因此,例如,如果有 10 个主服务器,每个主服务器有 1 个从属,而每个主服务器有 5 个从节点,则尝试迁移的从属服务器是 - 在具有 5 个从属的 2 个主机中,一个节点 ID 最低。由于未使用协议,则当集群配置不稳定时,可能会出现一种竞争条件,其中多个从节点认为自己是节点 ID 较低的非失败从节点(在实践中不太可能发生这种情况)。如果发生这种情况,结果是多个从节点迁移到同一主服务器,这是无害的。如果比赛发生的方式将使让让主没有从节点,只要集群再次稳定,算法将再次执行,并将从属迁移回原始主服务器。
最终,每个主将至少由一个从节点支持。但是,正常行为是单个从属从具有多个从属服务器迁移到孤立的从节点。
该算法由用户可配置的参数控制,该参数称为:主服务器必须留下好从节点的数量,才能让从节点迁移出去。例如,如果此参数设置为 2,则仅在其主数据库仍保留两个工作从节点时,才能尝试迁移从属服务器。cluster-migration-barrier
配置一些冲突解决算法
当在故障转移期间通过从属升级创建新值时,它们保证是唯一的。configEpoch
但是,有两个不同的事件,其中以不安全的方式创建新的配置Epoch值,只是增加本地节点的本地值,并希望同时没有冲突。这两个事件都是系统管理员触发的:currentEpoch
- 带选项的CLUSTER FAILOVER 命令能够手动将从属节点提升为主节点,而无需大多数主节点可用。这很有用,例如,在多数据中心设置中。
TAKEOVER
- 用于集群再平衡的插槽迁移也会在本地节点内生成新的配置纪元,而由于性能原因,不会达成一致。
具体地说,在手动重新分片期间,当哈希槽从节点 A 迁移到节点 B 时,重新分片程序将强制 B 将其配置升级到集群中发现的最大纪元,外加 1(除非该节点已经是配置时代最大的一个),而无需其他节点同意。通常,真实世界的重新分片涉及移动几百个哈希槽(尤其是在小型集群中)。对于每个移动的哈希槽,要求协议在重新分片期间生成新的配置纪元是低效的。此外,为了存储新配置,它每次都需要在每个集群节点中设置 fsync。由于它的表现方式,我们只需要一个新的配置纪元,当第一个哈希槽被移动,使其在生产环境中效率更高。
但是,由于上述两种情况,可能(尽管不太可能)以多个节点具有相同的配置纪元结束。系统管理员执行的重新分片操作以及同时发生的故障转移(加上很多运气不好)如果传播速度不够快,可能会导致冲突。currentEpoch
此外,软件错误和文件系统损坏也可能导致多个节点具有相同的配置纪元。
当为不同的哈希插槽提供主具有相同的时,则没有任何问题。更重要的是,在主服务器故障的从节点具有唯一的配置纪元。configEpoch
也就是说,手动干预或重新分片可能会以不同的方式更改集群配置。Redis 集群主实时属性要求插槽配置始终收敛,因此在任何情况下,我们都希望所有主节点都具有不同的 。configEpoch
为了实施此措施,在两个节点最终具有相同的 时,使用冲突解决算法。configEpoch
- 如果主节点检测到另一个主节点,将用相同的 方式进行广告宣传。
configEpoch
- 并且如果节点的字典尺寸较小,而声称相同的其他节点。
configEpoch
- 然后,它将其递增为 1,并使用它作为新的 。
currentEpoch``configEpoch
如果有任何一组节点具有相同的,所有节点,但一个最大的节点ID将向前推进,保证,最终,每个节点将选择一个唯一的配置Epoch,无论发生了什么。configEpoch
此机制还保证在创建新集群后,所有节点都以不同的集群开始(即使实际上未使用),因为请确保在启动时使用。但是,如果由于某种原因,节点配置错误,它会自动将其配置更新到不同的配置纪元。configEpoch``redis-trib``CONFIG SET-CONFIG-EPOCH
节点重置
节点可以进行软件重置(无需重新启动),以便在其他角色或不同集群中重用。这在正常操作、测试以及可以重新配置给定节点以加入一组不同的节点以放大或创建新集群的云环境中非常有用。
在 Redis 集群节点中,使用 CLUSTER重置命令重置。该命令提供两种变体:
CLUSTER RESET SOFT
CLUSTER RESET HARD
必须将命令直接发送到节点以重置。如果未提供重置类型,则执行软重置。
以下是重置执行的操作列表:
- 软重置和硬重置:如果节点是从属节点,则将其转换为主节点,并丢弃其数据集。如果节点是主节点并包含键,将中止重置操作。
- 软和硬重置:释放所有插槽,并重置手动故障转移状态。
- 软重置和硬重置:节点表中的所有其他节点都已删除,因此节点不再知道任何其他节点。
- 仅硬重置: , 和 设置为 0 。
currentEpoch``configEpoch``lastVoteEpoch
- 仅硬重置:节点 ID 更改为新的随机 ID。
无法重置具有非空数据集的主节点(因为通常您希望将数据重新分片到其他节点)。但是,在适当情况下(例如,当集群完全被销毁以创建新集群时),在继续重置之前必须执行 FLUSHALL。
从集群中删除节点
通过将节点的所有数据重新碎片到其他节点(如果是主节点)并关闭它,可以实际从现有集群中删除节点。但是,其他节点仍将记住其节点 ID 和地址,并将尝试与它连接。
因此,当删除节点时,我们还要将其从所有其他节点表中删除其条目。这是通过使用 命令完成的。CLUSTER FORGET <node-id>
该命令执行两项操作:
- 它将具有指定节点 ID 的节点从节点表中删除。
- 它设置了 60 秒的禁止,以防止重新添加具有相同节点 ID 的节点。
需要第二个操作,因为 Redis Cluster 使用八卦来自动发现节点,因此从节点 A 中删除节点 X 可能会导致节点 B 再次对节点 X 到 A 进行八卦。由于 60 秒的禁令,Redis 集群管理工具有 60 秒的时间从所有节点中删除节点,从而防止由于自动发现而重新添加节点。
有关更多信息,请参阅“集群忘记”文档。
发布/订阅
在 Redis 集群客户端中可以订阅每个节点,还可以发布到每个其他节点。集群将确保所有已发布的消息根据需要转发。
当前实现将简单地将每个已发布的消息广播到所有其他节点,但在某些时候,这将使用 Bloom 筛选器或其他算法进行优化。
附录
附录 A:CRC16 在 ANSI C 中的参考实施
/*
* 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;
}