Redis 集群规格
Redis 集群的详细规格
Redis 堆栈 | Redis 社区版 |
---|
欢迎使用 Redis 集群规范。在这里您可以找到信息 了解 Redis Cluster 的算法和设计原理。本文档是一部作品 正在进行中,因为它与实际实施持续同步 的 Redis。
设计的主要特性和基本原理
Redis 集群目标
Redis Cluster 是 Redis 的分布式实现,其目标如下(按设计中的重要性排序):
- 高性能和线性可扩展性,最高可达 1000 个节点。没有代理,使用异步复制,并且不对值执行合并作。
- 可接受的写入安全程度:系统尝试(以最大努力的方式)保留来自与大多数主节点连接的客户端的所有写入。通常,在一些小窗口中,已确认的写入可能会丢失。当客户端位于少数分区中时,丢失已确认写入的窗口会更大。
- 可用性:Redis 集群能够在大多数主节点可访问的分区中幸存下来,并且不再可访问的每个主节点至少有一个可访问的副本。此外,使用副本迁移,不再被任何副本复制的 master 将从被多个副本覆盖的 master 接收一个。
本文档中描述的内容是在 Redis 3.0 或更高版本中实现的。
已实现的子集
Redis Cluster 实现了 Redis 的非分布式版本。执行复杂多键的命令 对于以下情况,实现 Set Unions 和 Intersections 等作 作中涉及的所有 key 哈希到同一个 slot 中。
Redis Cluster 实现了一个称为哈希标签的概念,可以使用该概念 强制某些 key 存储在同一个哈希槽中。但是,在 手动重新分片,多键作可能在一段时间内不可用 而单键作始终可用。
Redis Cluster 不像独立版本那样支持多个数据库
的 Redis。我们只支持数据库0
;这SELECT
命令。
Redis 集群协议中的客户端和服务器角色
在 Redis Cluster 中,节点负责保存数据, 并获取集群的状态,包括将键映射到正确的节点。 集群节点还能够自动发现其他节点,检测不工作 nodes 节点,并在需要时按顺序将 replica 节点提升为 master 节点 在发生故障时继续运行。
为了执行其任务,所有集群节点都使用 TCP 总线和二进制协议(称为 Redis 集群总线)。 每个节点都使用集群连接到集群中的每个其他节点 总线。节点使用 gossip 协议来传播有关集群的信息 为了发现新节点,要发送 ping 数据包以确保所有 其他节点工作正常,并且要发送集群消息需要 信号特定条件。集群总线还用于 在集群中传播 Pub/Sub 消息并手动编排 用户请求时的故障转移(手动故障转移是指 不是由 Redis 集群故障检测器启动的,而是由 系统管理员)。
由于群集节点无法代理请求,因此客户端可能会被重定向
使用重定向错误的其他节点-MOVED
和-ASK
.
理论上,客户端可以自由地向集群中的所有节点发送请求,
,因此客户端不需要持有
集群的状态。但是,能够在
key 和 nodes 可以以合理的方式提高性能。
写入安全
Redis Cluster 使用节点之间的异步复制,最后一次故障转移赢得隐式合并功能。这意味着最后选择的主数据集最终会替换所有其他副本。在分区期间,总有一个时间窗口可能会丢失写入。但是,对于连接到大多数 master 的 Client 端和连接到少数 master 的 Client 端,这些窗口非常不同。
与在少数端执行的写入相比,Redis Cluster 会更加努力地保留由连接到大多数主节点的客户端执行的写入。 以下是导致丢失已确认 故障期间在 majority 分区中接收的写入:
-
写入可以到达 master,但是虽然 master 可能能够回复 client,但写入可能不会通过 master 节点和 replica 节点之间使用的异步复制传播到副本。如果 master 在写入未到达副本的情况下宕机,则如果 master 在足够长的时间内无法访问以致其其中一个副本被提升,则写入将永远丢失。在 master 节点完全突然失败的情况下,这通常很难观察到,因为 master 尝试几乎同时回复 client(确认写入)和副本(传播写入)。但是,这是实际的故障模式。
-
另一种理论上可能的写入丢失故障模式如下:
- 由于分区,无法访问 master。
- 它由其副本之一进行故障转移。
- 一段时间后,它可能会再次访问。
- 路由表过期的 Client 端可以在集群将其转换为(新 master)的副本之前写入旧 master。
第二种故障模式不太可能发生,因为无法与大多数其他主节点通信足够时间以进行故障转移的 master 节点将不再接受写入,并且当分区固定时,写入仍会被拒绝一小段时间,以允许其他节点通知配置更改。此故障模式还要求客户端的路由表尚未更新。
针对分区的少数端的写入具有更大的窗口,可能会丢失。例如,Redis Cluster 在具有少数 master 和至少一个或多个客户端的分区上丢失了大量写入作,因为如果 master 在多数端进行故障转移,则发送到 masters 的所有写入都可能会丢失。
具体来说,对于要进行故障转移的 master ,大多数 master 必须至少在一段时间内无法访问它NODE_TIMEOUT
,因此,如果分区在该时间之前已修复,则不会丢失任何写入作。当分区持续时间超过NODE_TIMEOUT
,则到该点为止在少数端执行的所有写入都可能丢失。但是,Redis 集群的少数端将在NODE_TIMEOUT
没有与多数人联系的时间已经过去了,因此有一个最大的窗口,在此之后,少数人将不再可用。因此,在该时间之后,不会接受或丢失任何写入作。
可用性
Redis Cluster 在分区的少数侧不可用。在分区的多数端,假设至少有大多数 master,并且每个无法访问的 master 都有一个副本,则集群在NODE_TIMEOUT
时间加上选择副本并故障转移其主服务器所需的几秒钟(故障转移通常在 1 秒或 2 秒内执行)。
这意味着 Redis Cluster 旨在承受集群中几个节点的故障,但对于在发生大型网络拆分时需要可用性的应用程序,它不是一个合适的解决方案。
在由 N 个主节点组成的集群示例中,每个节点都有一个副本,只要单个节点被分区出去,集群的多数端将保持可用,并且保持可用的可能性为1-(1/(N*2-1))
当两个节点被分区掉时(在第一个节点发生故障后,我们只剩下N*2-1
nodes 的总数,唯一没有副本的 master 失败的概率为1/(N*2-1))
.
例如,在具有 5 个节点且每个节点只有一个副本的集群中,有一个1/(5*2-1) = 11.11%
在将两个节点从多数节点分区出去后,集群将不再可用。
得益于名为 replicas migration 的 Redis 集群功能,集群 在许多实际场景中,可用性得到了提高,因为 副本迁移到孤立的 Master(不再具有 Replica 的 Master)。 因此,在每次成功的失败事件中,集群可能会重新配置副本 layout 才能更好地抵御下一次失败。
性能
在 Redis Cluster 中,节点不会将命令代理到负责给定密钥的正确节点,而是将客户端重定向到为给定部分的密钥空间提供服务的正确节点。
最终,客户端会获得集群的最新表示形式,以及哪个节点为哪些密钥子集提供服务,因此在正常作期间,客户端会直接联系正确的节点以发送给定的命令。
由于使用了异步复制,节点不会等待其他节点的写入确认(如果未使用WAIT
命令)。
此外,由于多键命令仅限于 near 键,因此除非重新分片,否则数据永远不会在节点之间移动。
正常作的处理方式与单个 Redis 实例完全相同。这意味着,在具有 N 个主节点的 Redis 集群中,随着设计线性扩展,您可以获得与单个 Redis 实例乘以 N 相同的性能。同时,查询通常在单次往返中执行,因为客户端通常与节点保持持久连接,因此延迟数据也与单个独立 Redis 节点的情况相同。
非常高的性能和可扩展性,同时保留 weak but 合理形式的数据安全性和可用性是 Redis 集群。
为什么避免合并作
Redis 集群设计避免了多个节点中同一键值对的冲突版本,因为在 Redis 数据模型的情况下,这并不总是可取的。Redis 中的值通常非常大;经常会看到包含数百万个元素的列表或排序集。此外,数据类型在语义上也很复杂。传输和合并这些类型的值可能是一个主要瓶颈,并且/或者可能需要应用程序端逻辑的重要参与、额外的内存来存储元数据等。
这里没有严格的技术限制。CRDT 或同步复制 状态机可以对类似于 Redis 的复杂数据类型进行建模。但是, 此类系统的实际运行时行为与 Redis Cluster 不相似。 Redis Cluster 旨在涵盖 非集群 Redis 版本。
Redis Cluster 主要组件概述
密钥分发模型
集群的密钥空间被拆分为 16384 个槽,有效地设置了上限 对于 16384 个主节点的集群大小(但是,建议的最大大小为 nodes 大约为 ~ 1000 个节点)。
集群中的每个主节点都处理 16384 个哈希槽的子集。 当 中没有集群重新配置时,集群是稳定的 进度(即哈希槽从一个节点移动到另一个节点的位置)。 当集群稳定时,单个节点将提供单个哈希槽 (但是,服务节点可以有一个或多个副本,这些副本将在网络分裂或失败的情况下替换它, 这可用于扩展读取作,其中读取过时的数据是可以接受的)。
用于将键映射到哈希槽的基本算法如下 (请阅读下一段,了解此规则的哈希标签例外):
HASH_SLOT = CRC16(key) mod 16384
CRC16 指定如下:
- 名称:XMODEM(也称为 ZMODEM 或 CRC-16/ACORN)
- 宽度:16 位
- 多边形:1021(实际上是 x^16 + x^12 + x^5 + 1)
- 初始化:0000
- 反射输入字节:False
- 反射输出 CRC:False
- 输出 CRC 的 Xor 常数:0000
- “123456789”的输出:31C3
使用了 16 个 CRC16 输出位中的 14 个(这就是为什么有 上式中的 16384 模运算)。
在我们的测试中,CRC16 在分配不同类型的 键均匀地分布在 16384 插槽中。
注意:本文档的附录 A 中提供了所用 CRC16 算法的参考实现。
哈希标签
按顺序使用的哈希槽的计算有一个例外 实现哈希标签。哈希标签是确保多个 key 分配在同一个哈希槽中。这是为了实现 Redis 集群中的多键作。
要实现哈希标签,密钥的哈希槽是在
在某些情况下,方式略有不同。
如果键包含 “{...}” 模式,则仅对 和 之间的子字符串进行哈希处理,以获取哈希槽。但是,由于
可能存在多次出现或算法为
由以下规则指定:{
}
{
}
- 如果键包含字符。
{
- 如果 的右侧有一个字符。
}
{
- AND 如果 的第一次出现和第一次出现之间有一个或多个字符。
{
}
然后,不对键进行哈希处理,而是仅对第一次出现的 of 和接下来的第一次出现之间的内容进行哈希处理。{
}
例子:
- 两个键
{user1000}.following
和{user1000}.followers
将哈希到同一个哈希槽,因为只有子字符串user1000
将进行哈希处理以计算哈希槽。 - 对于密钥
foo{}{bar}
整个键将像往常一样进行哈希处理,因为第一次出现的 of 后跟在右侧,中间没有字符。{
}
- 对于密钥
foo{{bar}}zap
子字符串{bar
将被哈希处理,因为它是其右侧第一次出现的 of 和第一次出现的 of 之间的子字符串。{
}
- 对于密钥
foo{bar}{zap}
子字符串bar
将进行哈希处理,因为算法在 AND 的第一个有效或无效(内部没有字节)匹配项处停止。{
}
- 从算法中得出的是,如果 key 以 开头,则保证它作为一个整体进行哈希处理。这在使用二进制数据作为键名称时非常有用。
{}
glob 样式模式
接受 glob 样式模式的命令,包括KEYS
,SCAN
和SORT
)针对表示单个插槽的模式进行了优化。
这意味着,如果所有可以匹配 pattern 的 key 都必须属于特定 slot,则仅搜索此 slot 匹配 pattern 的 key。
模式槽优化是在 Redis 8.0 中引入的。
当模式满足以下条件时,优化开始:
- 该模式包含一个井号标签,
- 井号标签前没有通配符或转义字符,并且
- 大括号中的 hashtag 不包含任何通配符或转义字符。
例如SCAN 0 MATCH {abc}*
可以成功识别 hashtag 并仅扫描abc
.
然而,模式*{abc}
,{a*c}
或{a\*bc}
无法识别主题标签,因此需要扫描所有插槽。
Hash slot 示例代码
添加哈希标签异常,以下是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/port 中,并使用在集群上运行的 gossip 协议进行重新配置 总线。
节点 ID 不是与每个节点关联的唯一信息,而是 唯一一个始终全局一致的。每个节点还具有 以下一组相关的信息。一些信息是关于 cluster configuration details 的 cluster configuration 详细信息,并且最终是 在整个集群中保持一致。一些其他信息,例如上次 节点被 ping作,而是每个节点的本地节点。
每个节点都维护有关其他节点的以下信息
aware of :节点的 ID、IP 和端口,一组
flags,如果节点被标记为replica
上次
节点已 ping作,上次收到 pong 时,节点的当前配置 epoch(在本规范后面解释),
链路状态,最后是提供的哈希槽集。
所有节点字段的详细说明在CLUSTER NODES
文档。
这CLUSTER NODES
命令可以发送到集群中的任何节点,并根据查询节点拥有的集群本地视图提供集群的状态和每个节点的信息。
以下是CLUSTER NODES
命令发送到 master
节点。
$ 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、配置 epoch、链路状态、插槽。一旦我们讨论 Redis Cluster 的特定部分,就会介绍有关上述字段的详细信息。
群集总线
每个 Redis 集群节点都有一个额外的 TCP 端口,用于接收 来自其他 Redis 集群节点的传入连接。此端口将通过向数据端口添加 10000 来派生,也可以使用 cluster-port 配置指定。
示例 1:
如果 Redis 节点正在端口 6379 上侦听客户端连接, 并且您没有在 redis.conf 中添加 cluster-port 参数, Cluster 总线端口 16379 将被打开。
示例 2:
如果 Redis 节点正在端口 6379 上侦听客户端连接, 并在 redis.conf 中设置 cluster-port 20000, Cluster 总线端口 20000 将被打开。
节点到节点通信仅使用 Cluster 总线和
Cluster 总线协议:由帧组成的二进制协议
不同类型和大小的。Cluster 总线二进制协议不是
公开记录,因为它不适用于外部软件设备
与使用此协议的 Redis 集群节点通信。但是,您可以
通过阅读cluster.h
和cluster.c
文件。
集群拓扑
Redis 集群是一个全网状网络,其中每个节点都使用 TCP 连接与其他每个节点连接。
在包含 N 个节点的集群中,每个节点都有 N-1 个传出 TCP 连接和 N-1 个传入连接。
这些 TCP 连接始终保持活动状态,而不是按需创建。 当节点期望响应集群总线中的 ping 时,在等待足够长的时间将节点标记为无法访问之前,它将尝试 通过从头开始重新连接来刷新与节点的连接。
Redis Cluster 节点形成一个完整的网格,而节点使用 gossip 协议和 一种配置更新机制,以避免交换太多 messages 在正常情况下,因此消息的数量 exchange 不是指数的。
节点握手
节点始终接受集群总线端口上的连接,甚至回复 收到 ping 值,即使 ping 节点不受信任。 但是,如果 发送节点不被视为集群的一部分。
一个节点将仅通过两种方式接受另一个节点作为集群的一部分:
-
如果节点自身显示
MEET
消息 (CLUSTER MEET
命令)。Meet 消息恰好是 就像PING
消息,但强制接收方接受节点作为 集群。节点将发送MEET
仅当系统管理员通过以下命令请求时,才会向其他节点发送消息:集群 MEET IP 端口
-
如果已经受信任的节点将八卦有关其他节点的八卦,则节点还将另一个节点注册为集群的一部分。因此,如果 A 认识 B,B 也知道 C,最终 B 会向 A 发送关于 C 的八卦消息。发生这种情况时,A 会将 C 注册为网络的一部分,并尝试与 C 连接。
这意味着,只要我们将任何连通图中的节点连接起来,它们最终就会自动形成一个完全连通的图。这意味着集群能够自动发现其他节点,但前提是存在系统管理员强制的信任关系。
此机制使集群更加健壮,但可以防止不同的 Redis 集群在更改 IP 地址或其他网络相关事件后意外混合。
重定向和重新分片
MOVED 重定向
Redis 客户端可以自由地向集群中的每个节点发送查询,包括 副本节点。节点将分析查询,以及是否可以接受 (即,查询中只提到一个键,或者多个键 提到的都指向同一个哈希槽),它会查找什么 node 负责 key 或 key 所属的 hash slot。
如果哈希槽由节点提供,则仅处理查询,否则 节点将检查其内部哈希槽到 Node Map,并回复 发送到客户端,并显示 MOVED 错误,如以下示例所示:
GET x
-MOVED 3999 127.0.0.1:6381
该错误包括密钥的哈希槽 (3999) 和可提供查询的实例的 endpoint:port。
客户端需要向指定节点的终端节点地址和端口重新发出查询。
端点可以是 IP 地址、主机名,也可以是空的(例如-MOVED 3999 :6380
).
空终端节点表示服务器节点具有未知终端节点,客户端应将下一个请求发送到与当前请求相同的终端节点,但使用提供的端口。
请注意,即使客户端在重新发出查询之前等待了很长时间, 同时,集群配置发生了变化,目标节点 如果哈希槽 3999 现在由 另一个节点。如果联系的节点没有更新的信息,也会发生同样的情况。
因此,虽然从集群节点的角度来看,节点是由 我们尝试简化与客户端的接口,只需公开一个地图 在哈希槽和由 endpoint:port 对标识的 Redis 节点之间。
客户端不是必需的,但应该尝试记住该哈希槽 3999 由 127.0.0.1:6381 提供服务。这样一旦有新命令需要 可以计算目标 key 的哈希槽,并具有 选择正确节点的机会更大。
另一种方法是只刷新整个客户端集群布局
使用CLUSTER SHARDS
或已弃用的CLUSTER SLOTS
命令
当收到 MOVED 重定向时。遇到重定向时,它会
可能重新配置了多个插槽,而不仅仅是一个,因此更新
尽快进行客户端配置通常是最佳策略。
请注意,当 Cluster 稳定时(配置中没有持续的更改), 最终,所有客户端都将获得 hash slots 的 map >节点,使 集群高效,客户端直接寻址正确的节点 没有重定向、代理或其他单点故障实体。
客户端还必须能够处理所描述的 -ASK 重定向 ,否则它不是一个完整的 Redis Cluster 客户端。
实时重配置
Redis 集群支持在集群中添加和删除节点 正在运行。添加或删除节点被抽象为相同的 operation:将 Hash slot 从一个节点移动到另一个节点。这意味着 相同的基本机制可以用于重新平衡集群,请添加 或删除节点,依此类推。
- 要向集群添加新节点,需要向集群中添加一个空节点,并将一组哈希槽从现有节点移动到新节点。
- 要从集群中删除节点,分配给该节点的哈希槽将移动到其他现有节点。
- 为了重新平衡集群,在节点之间移动一组给定的哈希槽。
实现的核心是移动哈希槽的能力。 从实际角度来看,哈希 slot 只是一组键,因此 Redis Cluster 在重新分片期间真正做的是将 key 从 一个实例到另一个实例。移动哈希槽意味着移动所有键 碰巧将 hash 放入这个 hash slot 中。
要了解其工作原理,我们需要显示CLUSTER
子命令
用于作 Redis Cluster 节点中的 slots 转换表。
以下子命令可用(其中包括在这种情况下无用的子命令):
CLUSTER ADDSLOTS
插槽 1 [插槽 2] ...[插槽 N]CLUSTER DELSLOTS
插槽 1 [插槽 2] ...[插槽 N]CLUSTER ADDSLOTSRANGE
start-slot1 end-slot1 [start-slot2 end-slot2] ...[起始槽 N 结束槽 N]CLUSTER DELSLOTSRANGE
start-slot1 end-slot1 [start-slot2 end-slot2] ...[起始槽 N 结束槽 N]CLUSTER SETSLOT
slot NODE 节点CLUSTER SETSLOT
slot MIGRATING 节点CLUSTER SETSLOT
slot IMPORTING 节点
前四个命令ADDSLOTS
,DELSLOTS
,ADDSLOTSRANGE
和DELSLOTSRANGE
,仅用于分配
(或删除)槽添加到 Redis 节点。分配插槽意味着告诉给定的
主节点,它将负责存储和提供其内容
指定的哈希槽。
分配哈希槽后,它们将在整个集群中传播 使用 gossip 协议,稍后在 配置传播 部分指定。
这ADDSLOTS
和ADDSLOTSRANGE
创建新集群时通常使用命令
从头开始为每个 Master 节点分配所有 16384 哈希的子集
有空位。
这DELSLOTS
和DELSLOTSRANGE
主要用于手动修改集群配置
或用于调试任务:在实践中很少使用。
这SETSLOT
子命令用于将槽分配给特定的节点 ID,如果
这SETSLOT <slot> NODE
表单。否则,可以在
两种特殊状态MIGRATING
和IMPORTING
.这两个特殊状态
用于将哈希槽从一个节点迁移到另一个节点。
- 当槽设置为 MIGRATING 时,节点将接受满足以下条件的所有查询
是关于这个哈希槽的,但前提是有问题的 key
存在,否则使用
-ASK
重定向到 node 作为迁移的目标。 - 当槽设置为 IMPORT 时,节点将接受所有满足以下条件的查询
是关于这个哈希槽的,但前提是请求是
前面有一个
ASKING
命令。如果ASKING
未给出命令 由客户端,查询通过 一个-MOVED
重定向错误,这通常会发生。
让我们通过一个哈希槽迁移的例子来更清楚地说明这一点。 假设我们有两个 Redis 主节点,分别称为 A 和 B。 我们想将哈希槽 8 从 A 移动到 B,因此我们发出如下命令:
- 我们发送 B: CLUSTER SETSLOT 8 导入 A
- 我们发送 A:集群 SETSLOT 8 迁移 B
所有其他节点将继续每次将客户端指向节点 “A” 它们使用属于哈希槽 8 的键进行查询,那么会发生什么 那是:
- 有关现有键的所有查询都由 “A” 处理。
- 有关 A 中不存在的键的所有查询都由 “B” 处理,因为 “A” 会将客户端重定向到 “B”。
这样我们就不再在 “A” 中创建新键。
与此同时,redis-cli
在重新分片期间使用
Redis 集群配置将迁移
哈希槽 8 从 A 到 B。
这是使用以下命令执行的:
CLUSTER GETKEYSINSLOT slot count
上述命令将返回count
键。
对于返回的键,redis-cli
向节点 “A” 发送 aMIGRATE
命令,则
将以原子方式将指定的 key 从 A 迁移到 B(两个实例
在迁移密钥所需的时间(通常非常短的时间)内被锁定,因此
没有竞争条件)。就是这样MIGRATE
工程:
MIGRATE target_host target_port "" target_database id timeout KEYS key1 key2 ...
MIGRATE
将连接到目标实例,发送
密钥,一旦收到 OK 代码,就会从其自己的数据集中获取旧密钥
将被删除。从外部客户端的角度来看,存在一个密钥
在任何给定时间在 A 或 B 中。
在 Redis Cluster 中,不需要指定 0 以外的数据库,但MIGRATE
是可用于其他任务的通用命令,而不是
涉及 Redis 集群。MIGRATE
经过优化,即使在移动复杂时也能尽可能快
键(例如长列表),但在 Redis Cluster 中重新配置
如果存在大键的集群,则不被视为明智的过程,如果
使用数据库的应用程序中存在延迟约束。
迁移过程最终完成后,SETSLOT <slot> NODE <node-id>
命令发送到迁移中涉及的两个节点,以便
再次将插槽设置为其正常状态。相同的命令通常是
发送到所有其他节点,以避免等待自然
新配置在集群中的传播。
ASK 重定向
在上一节中,我们简要讨论了 ASK 重定向。为什么不能 我们简单地使用 MOVED 重定向?因为虽然 MOVED 意味着 我们认为 hash slot 永久由不同的节点提供服务,并且 接下来,应针对指定的节点尝试查询。ASK 的意思是 仅将下一个查询发送到指定的节点。
这是必需的,因为关于哈希槽 8 的下一个查询可能是关于 key 仍然在 A 中,因此我们总是希望客户端尝试 A 和 如果需要,然后是 B。由于这仅发生在 16384 个哈希槽中的一个 available,则集群的性能影响是可接受的。
我们需要强制客户端行为,因此要确保 客户端只会在尝试 A 后尝试节点 B,节点 B 只会尝试 接受设置为 IMPORTING 的槽的查询(如果客户端发送 ASKING 命令。
基本上,ASKING 命令在客户端上设置一个一次性标志,该标志强制 一个节点,用于提供有关 IMPORTING 槽的查询。
从客户端的角度来看,ASK 重定向的完整语义如下:
- 如果收到 ASK 重定向,则仅发送重定向到指定节点的查询,但继续向旧节点发送后续查询。
- 使用 ASKING 命令启动重定向的查询。
- 暂时不要更新本地客户端表以将哈希槽 8 映射到 B。
哈希槽 8 迁移完成后,A 将发送一条 MOVED 消息,并且 客户端可以将哈希槽 8 永久映射到新的终端节点和端口对。 请注意,如果 buggy 客户端更早地执行映射,则不会 一个问题,因为它不会在发出查询之前发送 ASKING 命令, 因此 B 将使用 MOVED 重定向错误将客户端重定向到 A。
插槽迁移的解释相似,但措辞不同
(为了文档中的冗余)在CLUSTER SETSLOT
命令文档。
客户端连接和重定向处理
为了提高效率,Redis 集群客户端维护当前槽的映射 配置。但是,此配置不需要是最新的。 当联系错误的节点导致重定向时,客户端 可以相应地更新其内部插槽映射。
客户端通常需要获取 slot 和 mapped node 的完整列表 地址:
- 启动时,填充初始插槽配置
- 当客户端收到
MOVED
重定向
请注意,客户端可以处理MOVED
重定向,只需更新
移动了 slot 的表;然而,这通常效率不高,因为通常
多个 slot 的配置将一次修改。例如,如果
replica 被提升为 master,则旧 master 提供的所有 slot 都将
被重新映射)。对MOVED
重定向方式
从头开始获取 slots 到 nodes 的完整 Map。
客户端可以发出CLUSTER SLOTS
命令检索 slot 的数组
范围以及提供指定范围的关联主节点和副本节点。
以下是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 插槽。其他元素表示 address-port 对。第一个地址-端口对是为插槽提供服务的主服务器, 其他地址-端口对是为同一插槽提供服务的副本。副本 将仅在未处于错误条件时(即未设置其 FAIL 标志时)列出。
上面输出中的第一个元素表示从 5461 到 10922 的插槽 (包括开始和结束)由 127.0.0.1:7001 提供,并且是可能的 扩展只读负载,联系位于 127.0.0.1:7004 的副本。
CLUSTER SLOTS
不保证返回涵盖完整
16384 插槽,因此客户端应初始化
slots 配置映射使用 NULL 对象填充目标节点,以及
如果用户尝试执行有关键的命令,则报错
属于未分配的槽。
在向调用方返回错误之前,当找到 slot 时 未分配,则客户端应尝试获取 slots 配置 以检查集群现在是否已正确配置。
多键作
使用哈希标签,客户端可以自由使用多键作。 例如,以下作有效:
MSET {user:1000}.name Angela {user:1000}.surname White
当 密钥所属的哈希槽正在进行中。
更具体地说,即使在重新分片期间,以 键都存在并且仍然散列到同一个槽(源或 目标节点)仍然可用。
对不存在的键的作,或者在重新分片期间对 - split 的键进行作
在源节点和目标节点之间,将生成一个-TRYAGAIN
错误。
客户端可以在一段时间后尝试该作,也可以报告错误。
一旦指定哈希槽的迁移终止,所有 多键作再次可用于该哈希槽。
使用副本节点扩展读取
通常,副本节点会将客户端重定向到权威 master 节点
给定命令中涉及的哈希槽,但客户端可以使用 replicas
为了使用READONLY
命令。
READONLY
告诉 Redis 集群副本节点客户端读取正常
可能是过时的数据,并且对运行写入查询不感兴趣。
当连接处于只读模式时,集群将发送重定向 仅当作涉及未提供的密钥时,才向客户端发送 通过副本的主节点。这可能是因为:
- 客户端发送了一个关于此副本的主服务器从未提供的哈希槽的命令。
- 集群已重新配置(例如重新分片),并且副本不再能够为给定的哈希槽提供命令。
发生这种情况时,客户端应更新其哈希槽映射,如 前面的部分。
可以使用READWRITE
命令。
容错
心率和 gossip 消息
Redis 集群节点不断交换 ping 和 pong 数据包。这两种类型的数据包具有相同的结构,并且都携带重要的配置信息。唯一的实际区别是 message type 字段。我们将 ping 和 pong 数据包的总和称为检测信号数据包。
通常节点会发送 ping 数据包,这些数据包将触发接收器以 pong 数据包进行回复。然而,这并不一定是真的。节点可以只发送 pong 数据包以向其他节点发送有关其配置的信息,而不会触发回复。这非常有用,例如,为了尽快广播新配置。
通常,一个节点每秒会 ping 几个随机节点,因此无论集群中的节点数量如何,每个节点发送的 ping 数据包(和接收的 pong 数据包)的总数都是恒定的。
但是,每个节点都会确保 ping 未发送 ping 或收到 pong 的时间超过一半的所有其他节点NODE_TIMEOUT
时间。以前NODE_TIMEOUT
已过,节点还会尝试将 TCP 链接与另一个节点重新连接,以确保不会仅仅因为当前 TCP 连接存在问题而认为节点无法访问。
如果满足以下条件,则全局交换的消息数量可能相当大NODE_TIMEOUT
设置为一个小数字,并且节点数 (N) 非常大,因为每个节点都会尝试每隔一半 ping 一次它们没有新信息的所有其他节点NODE_TIMEOUT
时间。
例如,在节点超时设置为 60 秒的 100 节点集群中,每个节点将尝试每 30 秒发送 99 个 ping,ping 总数为每秒 3.3 个。乘以 100 个节点,即整个集群中每秒 330 个 ping。
有一些方法可以减少消息的数量,但是没有 报告的 Redis 集群当前使用的带宽问题 检测,所以现在使用 Obvious 和 Direct 的设计。请注意,即使 在上面的示例中,每秒交换的 330 个数据包是均匀的 划分为 100 个不同的节点,因此每个节点接收的流量 是可以接受的。
检测信号包内容
Ping 和 Pong 数据包包含所有类型数据包(例如请求故障转移投票的数据包)通用的报头,以及特定于 Ping 和 Pong 数据包的特殊 gossip 部分。
common header 包含以下信息:
- 节点 ID,一个 160 位伪随机字符串,在首次创建节点时分配,并在 Redis 集群节点的整个生命周期内保持不变。
- 这
currentEpoch
和configEpoch
用于挂载 Redis Cluster 使用的分布式算法的发送节点的字段(这将在下一节中详细解释)。如果节点是副本, 则configEpoch
是已知的最后一个configEpoch
它的主人。 - 节点标志,指示节点是副本节点、主节点和其他单位节点信息。
- 发送节点提供的哈希槽的位图,或者如果节点是副本,则为其主节点提供的槽的位图。
- 发送方 TCP 基本端口,即 Redis 用于接受客户端命令的端口。
- 集群端口,即 Redis 用于节点到节点通信的端口。
- 从发送方的角度来看,集群的状态(down 或 ok)。
- 发送节点的主节点 ID(如果是副本)。
Ping 和 pong 数据包还包含一个 gossip 部分。此部分为接收方提供了发送方节点对集群中其他节点的看法的视图。gossip 部分仅包含有关发送者已知的节点集中的几个随机节点的信息。gossip 部分中提到的节点数与群集大小成正比。
对于在 gossip 部分中添加的每个节点,将报告以下字段:
- 节点 ID。
- 节点的 IP 和端口。
- Node 标志。
Gossip 部分允许接收节点从发送者的角度获取有关其他节点状态的信息。这对于故障检测和发现集群中的其他节点都很有用。
故障检测
Redis 集群故障检测用于识别大多数节点何时无法访问主节点或副本节点,然后通过将副本提升为主节点角色来做出响应。当无法进行副本提升时,集群将处于错误状态,以停止接收来自客户端的查询。
如前所述,每个节点都采用与其他已知节点关联的标志列表。有两个用于故障检测的标志,称为PFAIL
和FAIL
.PFAIL
表示 Possible failure,并且是未确认的失败类型。FAIL
表示节点出现故障,并且大多数 Master 在固定时间内确认了此情况。
PFAIL 标志:
一个节点使用PFAIL
标志NODE_TIMEOUT
时间。主节点和副本节点都可以将另一个节点标记为PFAIL
,无论其类型如何。
Redis 集群节点的不可访问性概念是,我们有一个活动的 ping(我们发送的 ping,但我们尚未收到回复)等待的时间超过NODE_TIMEOUT
.要使此机制正常工作,NODE_TIMEOUT
必须与网络往返时间相比较大。为了在正常作期间增加可靠性,节点将在NODE_TIMEOUT
已过,未回复 ping。此机制可确保连接保持活动状态,因此断开的连接通常不会导致节点之间出现错误的故障报告。
FAIL 标志:
这PFAIL
单独的标志只是每个节点具有的有关其他节点的本地信息,但不足以触发副本提升。对于要被视为关闭的节点,PFAIL
条件需要升级到FAIL
条件。
如本文档的节点心跳部分所述,每个节点都会向其他每个节点发送 gossip 消息,包括几个随机已知节点的状态。每个节点最终都会收到每个其他节点的一组节点标志。这样,每个节点都有一个机制,可以向其他节点发出有关它们检测到的故障情况的信号。
一个PFAIL
condition 升级为FAIL
condition(条件)时满足以下条件:
- 某个节点(我们称为 A)将另一个节点 B 标记为
PFAIL
. - 节点 A 通过 gossip 部分从集群中大多数 master 的角度收集有关 B 状态的信息。
- 大多数 master 都发出了
PFAIL
或FAIL
条件NODE_TIMEOUT * FAIL_REPORT_VALIDITY_MULT
时间。(在当前实现中,有效性因子设置为 2,因此这只是NODE_TIMEOUT
时间)。
如果满足上述所有条件,则节点 A 将:
- 将节点标记为
FAIL
. - 发送
FAIL
消息(而不是FAIL
条件)添加到所有可访问的节点。
这FAIL
message 将强制每个接收节点在FAIL
state 中,无论它是否已在PFAIL
州。
请注意,FAIL 标志大多是单向的。也就是说,节点可以从PFAIL
自FAIL
,但FAIL
只有在以下情况下才能清除 flag:
- 该节点已可访问,并且是副本。在这种情况下,
FAIL
标志可以清除,因为副本不会进行故障转移。 - 该节点已经可以访问,并且是不为任何 slot 提供服务的主节点。在这种情况下,
FAIL
标志可以清除,因为没有插槽的 Master 并没有真正参与集群,并且正在等待配置以加入集群。 - 该节点已经可访问并且是主节点,但时间很长(N 乘以
NODE_TIMEOUT
) 已过,没有任何可检测到的副本提升。在这种情况下,最好重新加入集群并继续。
需要注意的是,虽然PFAIL
-> FAIL
transition 使用一种形式的协议,使用的协议是弱的:
- 节点在一段时间内收集其他节点的视图,因此即使大多数主节点需要“同意”,实际上这只是我们在不同时间从不同节点收集的状态,我们不确定,也不需要,在给定的时刻大多数主节点都同意。但是,我们丢弃了旧的失败报告,因此大多数 master 在一段时间内都发出了失败的信号。
- 虽然每个节点都检测到
FAIL
condition 将使用FAIL
消息,则无法确保消息会到达所有节点。例如,节点可能会检测到FAIL
条件,并且由于分区无法访问任何其他节点。
但是,Redis 集群故障检测有一个活动性要求:最终所有节点都应该就给定节点的状态达成一致。有两种情况可能源于脑裂状况。要么是少数节点认为该节点位于FAIL
state 或少数节点认为该节点未处于FAIL
州。在这两种情况下,集群最终都将拥有给定节点状态的单一视图:
情况 1:如果大多数 master 已将节点标记为FAIL
,由于故障检测和它产生的连锁效应,其他每个节点最终都会将 master 标记为FAIL
,因为在指定的时间窗口内将报告足够多的失败。
情况 2:当只有少数 master 将节点标记为FAIL
,副本提升不会发生(因为它使用更正式的算法,确保每个人最终都知道提升),并且每个节点都将清除FAIL
state 根据FAIL
状态清算规则(即在 N 倍NODE_TIMEOUT
已过去)。
这FAIL
标志仅用作运行算法的 safe 部分的触发器用于副本提升。理论上,副本可以独立行动并在无法访问其 master 时启动副本提升,如果大多数 master 实际上都可以访问,则等待 master 拒绝提供确认。然而,增加的复杂性PFAIL -> FAIL
state、弱协议和FAIL
消息强制在最短的时间内将状态传播到集群的可到达部分,具有实际优势。由于这些机制,如果集群处于错误状态,通常所有节点将几乎同时停止接受写入。从使用 Redis Cluster 的应用程序的角度来看,这是一个理想的功能。此外,还可以避免由于本地问题而无法访问其 master 的副本发起的错误选举尝试(否则大多数其他 master 节点都可以访问 master)。
配置处理、传播和故障转移
群集当前纪元
Redis Cluster 使用类似于 Raft 算法“term”的概念。在 Redis Cluster 中,该术语称为 epoch,用于为事件提供增量版本控制。当多个节点提供冲突的信息时,另一个节点可以了解哪个状态是最新的。
这currentEpoch
是一个 64 位无符号数字。
在创建节点时,每个 Redis 集群节点(包括副本节点和管理节点)都会设置currentEpoch
设置为 0。
每次从另一个节点收到数据包时,如果发送方的纪元(集群总线消息报头的一部分)大于本地节点纪元,则currentEpoch
更新为发送方纪元。
由于这些语义,最终所有节点都将同意currentEpoch
在集群中。
当集群的状态发生更改并且节点寻求协议以执行某些作时,将使用此信息。
目前,这仅在副本提升期间发生,如下一节所述。基本上,纪元是集群的逻辑时钟,并指示给定的信息胜过具有较小纪元的信息。
配置 epoch
每个大师总是宣传其configEpoch
在 ping 和 pong 数据包中,以及一个 bitmap 来通告它所服务的槽集。
这configEpoch
在创建新节点时,在 masters 中设置为零。
新的configEpoch
在副本选举期间创建。尝试替换的副本
失败的 master 增加其 epoch 并尝试从
大多数大师。当副本获得授权时,一个新的唯一configEpoch
,并且副本会变成一个主节点,使用新的configEpoch
.
如下一节所述,configEpoch
当不同的节点声明不同的配置时,有助于解决冲突(由于网络分区和节点故障而可能发生的情况)。
副本节点还会公布configEpoch
字段,但在副本的情况下,该字段表示configEpoch
截至他们上次交换数据包时,它的主人。这允许其他实例检测副本何时具有需要更新的旧配置(主节点不会向具有旧配置的副本授予投票)。
每次configEpoch
更改,则所有接收此信息的节点都会将其永久存储在 nodes.conf 文件中。同样的情况也适用于currentEpoch
价值。这两个变量保证被保存,并且fsync-ed
更新到磁盘。
这configEpoch
在故障转移期间使用简单算法生成的值
保证是新的、增量的和唯一的。
副本选举和提升
副本选择和提升由副本节点处理,并借助主节点投票支持副本进行提升。
当 master 处于FAIL
state 的 state 中,至少有一个副本具有成为 Master 的先决条件。
为了让复制品将自己提升为主,它需要开始选举并赢得选举。如果 master 在FAIL
state 的 Controller,但是只有一个 Replica 会赢得选举并将自己提升为 Master。
当满足以下条件时,副本将启动选举:
- 副本的 master 位于
FAIL
州。 - 主服务器提供的插槽数量不为零。
- 副本复制链接与主节点断开连接的时间不超过给定的时间,以确保提升的副本的数据是合理的新鲜的。此时间是用户可配置的。
为了当选,副本的第一步是增加其currentEpoch
counter 和从 master 实例请求投票。
副本通过广播FAILOVER_AUTH_REQUEST
数据包发送到集群的每个主节点。然后,它等待的最大时间是NODE_TIMEOUT
以便回复到达(但始终至少持续 2 秒)。
一旦 master 对给定副本进行了投票,就会用FAILOVER_AUTH_ACK
,则在NODE_TIMEOUT * 2
.在此期间,它将无法回复同一 master 的其他授权请求。这不是为了保证安全性所必需的,但对于防止选择多个副本很有用(即使使用不同的configEpoch
),这通常是不需要的。
副本会丢弃任何AUTH_ACK
回复的 epoch 小于currentEpoch
在发送投票请求时。这可确保它不会计算用于上一次选举的选票。
一旦副本收到来自大多数 master 的 ACK,它就会赢得选举。
否则,如果在两次期限内未达到多数NODE_TIMEOUT
(但总是至少 2 秒),则选举将中止,之后将再次尝试新的选举NODE_TIMEOUT * 4
(并且总是至少 4 秒)。
副本等级
一旦 master 进入FAIL
state,则副本会等待一小段时间,然后尝试当选。该延迟的计算方式如下:
DELAY = 500 milliseconds + random delay between 0 and 500 milliseconds +
REPLICA_RANK * 1000 milliseconds.
固定的延迟确保我们等待FAIL
state 在集群中传播,否则副本可能会尝试在主节点仍然不知道FAIL
州,拒绝授予他们的投票。
随机延迟用于取消同步副本,因此它们不太可能同时启动选举。
这REPLICA_RANK
是此副本相对于它已从主服务器处理的复制数据量的排名。
当 master 发生故障时,副本会交换消息,以建立 (尽力而为) 排名:
具有最新复制偏移量的副本位于排名 0,第二个最新的副本位于排名 1,依此类推。
通过这种方式,最新的副本会尝试在其他副本之前被选举。
排名顺序没有严格执行;如果更高等级的副本无法 当选后,其他人将很快尝试。
一旦副本赢得选举,它将获得一个新的唯一和增量configEpoch
这比任何其他现有的 master 都要高。它开始在 ping 和 pong 数据包中将自己宣传为 master,为一组服务槽提供configEpoch
那将战胜过去。
为了加快其他节点的重新配置,将 pong 数据包广播到集群的所有节点。当前无法访问的节点最终会在收到来自另一个节点的 ping 或 pong 数据包时重新配置,或者会收到UPDATE
如果检测到它通过心跳数据包发布的信息已过期,则从另一个节点收到数据包。
其他节点将检测到有一个新的 master 为旧 master 提供的相同 slot 提供服务,但具有更大的configEpoch
,并将升级其配置。旧 master (或故障转移的 master ,如果它重新加入集群)的副本不仅会升级配置,还会重新配置为从新 master 复制。下一节将介绍如何配置重新加入集群的节点。
主服务器回复副本投票请求
在上一节中,我们讨论了副本如何尝试被选举。本节从请求为给定副本投票的 master 的角度解释了会发生什么情况。
Masters 以FAILOVER_AUTH_REQUEST
来自副本的请求。
要授予投票权,需要满足以下条件:
- 主节点只对给定的 epoch 进行一次投票,并拒绝对较旧的 epoch 进行投票:每个主节点都有一个 lastVoteEpoch 字段,只要
currentEpoch
在身份验证请求数据包中不大于 lastVoteEpoch。当 master 对投票请求做出肯定的回复时,lastVoteEpoch 会相应地更新,并安全地存储在磁盘上。 - 仅当副本的 master 被标记为
FAIL
. - Auth 请求替换为
currentEpoch
这小于 mastercurrentEpoch
被忽略。因此,主回复将始终具有相同的currentEpoch
作为 auth 请求。如果同一副本再次要求投票,则递增currentEpoch
,则保证 Master 的旧延迟回复不能被接受为新的投票。
未使用规则 3 导致的问题示例:
主人currentEpoch
为 5,则 lastVoteEpoch 为 1(这可能会在几次选举失败后发生)
- 复制品
currentEpoch
是 3. - 副本尝试使用纪元 4 (3+1) 进行选举,主副本回复 ok 和
currentEpoch
5,但回复会延迟。 - 副本将尝试再次被选举,稍后,使用 epoch 5 (4+1),延迟回复到达副本
currentEpoch
5 的 ID 和 5 的 ID 的 5 中,并被接受为有效。
- 母版之前不会投票支持同一母版的副本
NODE_TIMEOUT * 2
如果已经投票选出该 master 的副本,则已过去。这并不是严格要求的,因为两个副本不可能在同一 epoch 中赢得选举。但是,实际上,它确保当选择副本时,它有足够的时间通知其他副本,并避免另一个副本赢得新的选举,从而执行不必要的第二次故障转移。 - Masters 不会以任何方式努力选择最佳副本。如果副本的 master 位于
FAIL
state 和 Master 在当前任期内没有投票,则授予赞成票。最佳副本最有可能在其他副本之前开始选举并赢得选举,因为它通常能够更早地开始投票过程,因为它的排名更高,如上一节所述。 - 当 master 拒绝为给定的副本投票时,没有否定响应,该请求被简单地忽略。
- 主服务器不会投票支持发送
configEpoch
这比任何configEpoch
在 master 表中,了解副本声明的槽。请记住,副本会发送configEpoch
的 master,以及其 master 提供的插槽的位图。这意味着请求投票的副本必须具有它想要故障转移的插槽的配置,该配置较新或等于授予投票的主服务器。
分区期间配置 epoch 有用性的实际示例
本节说明了如何使用 epoch 概念使副本提升过程对分区更具抵抗力。
- 主服务器不再可无限期地访问。主服务器有三个副本 A、B、C。
- 副本 A 赢得选举并提升为主服务器。
- 网络分区使 A 对集群的大部分不可用。
- 副本 B 赢得选举并被提升为主服务器。
- 分区使 B 对集群的大部分不可用。
- 前一个分区是固定的,A 再次可用。
此时 B 已关闭,而 A 再次可用,具有 master 角色(实际上UPDATE
消息会立即重新配置它,但这里我们假设所有UPDATE
消息丢失)。同时,副本 C 将尝试被选举,以便对 B 进行故障转移。这是发生的事情:
- C 将尝试当选并会成功,因为对于大多数 master 来说,它的 master 实际上是宕机的。它将获得一个新的增量
configEpoch
. - A 将无法声称自己是其哈希槽的主节点,因为与 A 发布的节点相比,其他节点已经拥有与更高配置 epoch(B 的 epoch)关联的相同哈希槽。
- 因此,所有节点都将升级其表以将哈希槽分配给 C,并且集群将继续其作。
正如您将在下一节中看到的那样,一个过时的节点重新加入集群
通常会尽快收到有关配置更改的通知
因为一旦它 ping 到任何其他节点,接收器就会检测到它
具有过时的信息,并将发送UPDATE
消息。
哈希槽配置传播
Redis Cluster 的一个重要部分是用于传播有关哪个集群节点正在为一组给定哈希槽提供服务的信息的机制。这对于启动新集群以及在提升副本以服务其失败的主节点的插槽后升级配置的能力都至关重要。
相同的机制允许将节点分区出无限量的 是时候以明智的方式重新加入集群了。
哈希槽配置的传播方式有两种:
- 检测信号消息。ping 或 pong 数据包的发送方总是添加有关它(或其主服务器,如果是副本)服务的哈希槽集的信息。
UPDATE
消息。因为在每个心跳数据包中都有有关发送方的信息configEpoch
和一组哈希槽,如果心跳数据包的接收者发现发送者信息过时,它将发送一个包含新信息的数据包,迫使过时的节点更新其信息。
检测信号的接收方或UPDATE
message 在
更新其表映射哈希槽到节点。创建新的 Redis Cluster 节点时,其本地哈希槽表只需初始化为NULL
条目,以便每个哈希槽都不会绑定或链接到任何节点。这看起来类似于以下内容:
0 -> NULL
1 -> NULL
2 -> NULL
...
16383 -> NULL
节点更新其哈希槽表时,遵循的第一条规则如下:
规则 1:如果哈希槽未分配(设置为NULL
),并且已知节点声明了它,我将修改我的哈希槽表并将声明的哈希槽与它相关联。
因此,如果我们从节点 A 收到一个心跳,声称为哈希槽 1 和 2 提供服务,并且配置 epoch 值为 3,则该表将被修改为:
0 -> NULL
1 -> A [3]
2 -> A [3]
...
16383 -> NULL
创建新集群时,系统管理员需要手动分配(使用CLUSTER ADDSLOTS
命令,通过 redis-cli 命令行工具,或通过任何其他方式)每个主节点提供的插槽仅提供给节点本身,并且信息将在整个集群中快速传播。
然而,这条规则还不够。我们知道 hash slot 映射可以改变 在两个事件中:
- 副本在故障转移期间替换其主副本。
- 插槽从一个节点重新分片到另一个节点。
现在,让我们专注于故障转移。当副本对其 master 进行故障转移时,它会获取 一个 configuration epoch,保证大于其 master 的 epoch 中(通常大于任何其他配置 epoch 之前生成)。例如,节点 B 是 A 的副本,可能会进行故障转移 A 的配置纪元为 4。它将开始发送心跳数据包 (第一次群播集群范围)并且由于以下原因 第二条规则,接收者将更新他们的 Hash slot 表:
规则 2:如果已分配哈希槽,并且已知节点正在使用configEpoch
大于configEpoch
中,我将哈希 slot 重新绑定到新节点。
因此,在收到来自 B 的消息后,这些消息声称提供配置 epoch 为 4 的哈希槽 1 和 2,接收方将按以下方式更新其表:
0 -> NULL
1 -> B [4]
2 -> B [4]
...
16383 -> NULL
liveness 属性:由于第二条规则,最终集群中的所有节点都会同意 slot 的所有者是 slot 的所有者configEpoch
在通告它的节点中。
Redis Cluster 中的这种机制称为 last failover wins。
在重新分片期间也会发生同样的情况。当节点导入哈希槽完成时 import作中,其 configuration epoch 会递增,以确保 更改将传播到整个集群中。
UPDATE 消息,仔细观察
记住上一节,更容易看到更新消息
工作。节点 A 可能会在一段时间后重新加入集群。它将发送心跳
数据包,其中它声称它为具有配置 epoch 的哈希槽 1 和 2 提供服务
共 3 页。所有具有更新信息的接收器都将看到
相同的哈希槽与具有更高配置的节点 B 相关联
时代。因此,他们将发送一个UPDATE
message 替换为新的
插槽的配置。A 将由于上述规则 2 而更新其配置。
节点如何重新加入集群
当节点重新加入集群时,使用相同的基本机制。 继续上面的示例,节点 A 将收到通知 哈希槽 1 和 2 现在由 B 提供服务。假设这两个是 唯一由 A 提供的哈希槽,A 提供的哈希槽数量将 降到 0!因此,A 将重新配置为新 master 的副本。
实际遵循的规则比这要复杂一些。一般来说,它可能会 发生 A 在很多时间后重新加入,同时可能会发生 最初由 A 提供的哈希槽由多个节点提供,例如 哈希槽 1 可以由 B 提供,哈希槽 2 可以由 C 提供。
因此,实际的 Redis 集群节点角色切换规则是:主节点将更改其配置,以复制(成为)窃取其最后一个哈希槽的节点(成为其副本)。
在重新配置期间,最终提供的哈希槽数量将下降到零,节点将相应地重新配置。请注意,在基本情况下,这只是意味着旧的主服务器将是故障转移后替换它的副本的副本。但是,在一般形式中,该规则涵盖所有可能的情况。
副本的作用完全相同:它们重新配置以复制 偷走了它前主人的最后一个哈希槽。
副本迁移
Redis Cluster 实现了一个称为副本迁移的概念,以便 提高系统的可用性。这个想法是,在 主副本设置(如果副本和主副本之间的映射是固定的) 如果单个 节点发生。
例如,在每个 master 都有一个副本的集群中,集群 只要主服务器或副本服务器发生故障,就可以继续作,但不能 如果两者同时失败。但是,有一类失败是 由硬件或软件问题引起的单个节点的独立故障 这会随着时间的推移而积累。例如:
- Master A 有一个副本 A1。
- Master A 失败。A1 中提升为新主控。
- 3 小时后,A1 以独立的方式失败(与 A 的失败无关)。由于节点 A 仍处于关闭状态,因此没有其他副本可用于升级。集群无法继续正常作。
如果 master 和 replica 之间的 map 是固定的,则唯一的方法是制作集群 然而,更抵制上述情况的是向每个 master 添加副本 这成本很高,因为它需要执行更多的 Redis 实例,更多 memory 等。
另一种方法是在集群中创建一个不对称性,然后让集群 布局会随时间自动更改。例如,集群可能有三个 大师 A、B、C。A 和 B 各有一个副本,即 A1 和 B1。然而,大师 C 是不同的,有两个副本:C1 和 C2。
副本迁移是副本自动重新配置的过程 为了迁移到不再覆盖的 Master(No Working replicas) 的 MirrorS 副本)。通过副本迁移,上述场景将变为 以后:
- Master A 失败。A1 被提升。
- C2 作为 A1 的副本迁移,否则没有任何副本支持。
- 三个小时后,A1 也失败了。
- C2 被提升为新的 master 以取代 A1。
- 集群可以继续作。
副本迁移算法
迁移算法不使用任何形式的协议,因为副本 布局不是需要的集群配置的一部分 与配置 epochs 保持一致和/或版本控制。相反,它使用 算法来避免在没有 master 支持时对副本进行大规模迁移。 该算法保证最终(一旦集群配置为 stable)每个 master 都将由至少一个 replica 提供支持。
这就是算法的工作原理。首先,我们需要定义在这种情况下什么是好副本:一个好的副本是不在FAIL
州
从给定节点的角度来看。
算法的执行在每个检测到 至少有一个 master 没有好的 replicas。然而,在所有 replica 检测到此条件时,只有 subset 应起作用。此子集为 实际上,除非在给定时刻具有不同的副本,否则通常是单个副本 其他节点的故障状态视图略有不同。
acting replica 是 master 中数量最多的 replica 的附加副本,该副本不处于 FAIL 状态且具有最小的节点 ID。
因此,例如,如果有 10 个 master,每个 master有 1 个副本,而 2 个 masters 具有 每个 5 个副本,将尝试迁移的副本是 - 在 2 个 master 中 具有 5 个副本 - 节点 ID 最低的副本。鉴于没有达成协议 使用时,有可能在集群配置不稳定时, 当多个副本认为自己是 具有较低节点 ID 的非失败副本(这种情况不太可能发生 在实践中)。如果发生这种情况,结果是多个副本迁移到 同一个主帅,无害。如果比赛以某种方式进行,那将离开 没有副本的 ceding master,一旦集群再次稳定 该算法将再次重新执行,并将副本迁移回 原始母版。
最终,每个 master 都将由至少一个副本提供支持。然而 正常行为是单个副本从 master 迁移 多个副本分配给孤立的主服务器。
该算法由名为cluster-migration-barrier
:主服务器的良好副本数
必须保留 ,副本才能迁移走。例如,如果此
parameter 设置为 2,则仅当 master 保留时,副本才能尝试迁移
具有两个工作副本。
configEpoch 冲突解决算法
当 newconfigEpoch
值是通过副本提升创建的
failovers,则保证它们是唯一的。
但是,有两个不同的事件,其中新的 configEpoch 值为
以不安全的方式创建,只是递增本地的currentEpoch
之
本地节点,希望同时没有冲突。
这两个事件都是系统管理员触发的:
CLUSTER FAILOVER
command 替换为TAKEOVER
选项能够手动将副本节点提升为主节点,而无需大多数主节点可用。例如,这在多数据中心设置中非常有用。- 迁移用于集群再平衡的槽还会在本地节点内生成新的配置纪元,而无需出于性能原因达成协议。
具体来说,在手动重新分片期间,当哈希槽从 将节点 A 升级到节点 B,则重新分片程序将强制 B 升级 它的配置设置为集群中发现的最大 epoch, 加 1(除非该节点已经是配置最大的节点 epoch),而无需其他节点的同意。 通常,实际的重新分片涉及移动数百个哈希槽 (尤其是在小集群中)。要求协议生成新的 对于移动的每个哈希槽,重新分片期间的配置 epochs 为 低 效。此外,它需要在每个集群节点中都有一个 fsync 每次都要存储新的配置。因为它的方式 相反,我们只需要在移动第一个哈希槽时使用一个新的配置 epoch, 使其在生产环境中更加高效。
但是,由于上述两种情况,有可能(尽管不太可能)结束
多个节点具有相同的配置 epoch。重新分片作
由系统管理员执行,并且故障转移在同一时间进行
时间(加上很多坏运气)可能会导致currentEpoch
collisions (如果
它们的传播速度不够快。
此外,软件错误和文件系统损坏也可能是原因 添加到具有相同配置 epoch 的多个节点。
当提供不同哈希槽的 master 具有相同的configEpoch
那里
没有问题。更重要的是,通过 master 进行故障转移的副本具有
唯一配置 epoch。
也就是说,手动干预或重新分片可能会更改集群
配置。Redis Cluster main 活动性属性
要求槽配置始终收敛,因此在任何情况下
我们真的希望所有 Master 节点都有不同的configEpoch
.
为了强制执行这一点,在
事件中,两个节点以相同的configEpoch
.
- 如果一个主节点检测到另一个主节点正在使用
一样
configEpoch
. - 并且如果该节点的节点 ID 在字典顺序上小于声明相同 ID 的另一个节点
configEpoch
. - 然后,它递增其
currentEpoch
by 1,并将其用作新的configEpoch
.
如果存在任何具有相同configEpoch
,除了节点 ID 最大的节点之外,所有节点都将向前移动,从而保证最终,无论发生了什么,每个节点都将选择一个唯一的 configEpoch。
此机制还保证在创建新集群后,所有
节点以不同的configEpoch
(即使这实际上并非如此
used),因为redis-cli
确保使用CLUSTER SET-CONFIG-EPOCH
在启动时。
但是,如果由于某种原因节点配置错误,它将更新
其配置会自动设置为不同的配置 epoch。
节点重置
节点可以进行软件重置(无需重新启动)以便重复使用 在不同的角色或不同的集群中。这在正常情况下很有用 作、测试中以及给定节点可以 重新配置以加入一组不同的节点,以扩大或创建新的 簇。
在 Redis 中,集群节点使用CLUSTER RESET
命令。这
命令提供两种变体:
CLUSTER RESET SOFT
CLUSTER RESET HARD
该命令必须直接发送到要重置的节点。如果没有重置类型为 前提是执行软重置。
以下是 reset 执行的作列表:
- 软重置和硬重置:如果节点是副本,则将其转换为 master,并丢弃其数据集。如果节点是主节点并且包含密钥,则 reset作将中止。
- 软重置和硬重置:释放所有插槽,并重置手动故障转移状态。
- 软重置和硬重置:nodes 表中的所有其他节点都将被删除,因此该节点不再知道任何其他节点。
- 仅限硬重置:
currentEpoch
,configEpoch
和lastVoteEpoch
设置为 0。 - 仅硬重置:Node ID 更改为新的随机 ID。
具有非空数据集的主节点无法重置(因为通常您希望将数据重新分片到其他节点)。但是,在适当的情况下(例如,当集群被完全销毁以创建新集群时),FLUSHALL
必须在继续重置之前执行。
从集群中删除节点
实际上,可以通过以下方式从现有集群中删除节点 将其所有数据重新分片到其他节点(如果它是主节点)和 关闭它。但是,其他节点仍会记住其节点 ID 和地址,并将尝试与其连接。
因此,当一个节点被删除时,我们也希望删除它的条目
从所有其他节点表中。这是通过使用CLUSTER FORGET <node-id>
命令。
该命令执行两项作:
- 它将从 nodes 表中删除具有指定节点 ID 的节点。
- 它设置了 60 秒的禁令,以防止重新添加具有相同节点 ID 的节点。
第二个作是必需的,因为 Redis Cluster 使用 gossip 来自动发现节点,因此从节点 A 中删除节点 X,可能会导致节点 B 再次将节点 X 八卦到节点 A。由于 60 秒的禁令,Redis 集群管理工具有 60 秒的时间从所有节点中删除节点,从而防止由于自动发现而重新添加节点。
更多信息可在CLUSTER FORGET
文档。
发布/订阅
在 Redis 集群中,客户端可以订阅每个节点,还可以 发布到每个其他节点。集群将确保已发布的 根据需要转发消息。
客户端可以将 SUBSCRIBE 发送到任何节点,也可以将 PUBLISH 发送到任何节点。 它只会将每条发布的消息广播到所有其他节点。
Redis 7.0 及更高版本具有分片发布/订阅功能,其中分片通道通过与向槽分配键的相同算法分配给槽。 必须将分片消息发送到拥有分片通道哈希到的槽的节点。 集群确保将发布的分片消息转发到分片中的所有节点,因此客户端可以通过连接到负责槽的主服务器或其任何副本来订阅分片通道。
附录
附录 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;
}