水平扩展与 Redict 集群

Redict 通过称为 Redict 集群的部署拓扑实现水平扩展。本主题将教您如何设置、测试和在生产中操作 Redict 集群。您将从最终用户的角度了解 Redict 集群的可用性和一致性特性。

如果您计划运行生产级别的 Redict 集群部署,或者想要更好地理解 Redict 集群的内部工作方式,请参考 Redict 集群规范

Redict 集群 101

Redict 集群提供了一种运行 Redict 安装的方式,其中数据会自动在多个 Redict 节点之间进行分片。Redict 集群还在分区期间提供一定程度的可用性——实际上,当一些节点失败或无法与集群的其他节点通信时,能够继续操作。然而,如果发生更大的故障(例如,当大多数主节点不可用时),集群将变得不可用。

因此,在 Redict 集群中,您将获得以下能力:

  • 自动将数据集分割到多个节点中。
  • 当节点的子集经历故障或无法与集群的其余部分通信时,继续操作。

Redict 集群 TCP 端口

每个 Redict 集群节点需要两个开放的 TCP 连接:一个用于服务客户端的 Redict TCP 端口,例如 6379,以及第二个端口,称为 集群总线端口。默认情况下,集群总线端口是通过在数据端口上添加 10000 来设置的(例如,16379);然而,您可以在 cluster-port 配置中覆盖此设置。

集群总线是节点之间通信的通道,使用二进制协议,这种协议更适合于节点之间的信息交换,因为带宽和处理时间都很少。节点使用集群总线进行故障检测、配置更新、故障转移授权等。客户端不应尝试与集群总线端口通信,而应使用 Redict 命令端口。然而,请确保您在防火墙中打开这两个端口,否则 Redict 集群节点将无法通信。

为了使 Redict 集群正常工作,您需要为每个节点:

  1. 客户端通信端口(通常是 6379),用于与客户端通信,并对所有需要到达集群的客户端以及使用客户端端口进行键迁移的所有其他集群节点开放。
  2. 集群总线端口必须能够从所有其他集群节点到达。

如果您没有打开这两个 TCP 端口,您的集群将不会按预期工作。

Redict 集群和 Docker

目前,Redict 集群不支持 NATted 环境,通常也不支持 IP 地址或 TCP 端口被重映射的环境。

Docker 使用一种称为 端口映射 的技术:在 Docker 容器中运行的程序可能会暴露出与程序认为正在使用的端口不同的端口。这对于在同一台服务器上同时运行多个使用相同端口的容器非常有用。

要使 Docker 与 Redict 集群兼容,您需要使用 Docker 的 主机网络模式。有关更多信息,请参阅 Docker 文档 中的 --net=host 选项。

Redict 集群数据分片

Redict 集群不使用一致性哈希,而是使用一种不同的分片形式,其中每个键都是我们所谓的 哈希槽 的一部分。

在 Redict 集群中有 16384 个哈希槽,要计算给定键的哈希槽,我们只需取键的 CRC16 模 16384。

集群中的每个节点负责一组哈希槽,例如,您可能有一个由 3 个节点组成的集群,其中:

  • 节点 A 包含从 0 到 5500 的哈希槽。
  • 节点 B 包含从 5501 到 11000 的哈希槽。
  • 节点 C 包含从 11001 到 16383 的哈希槽。

这使得添加和移除集群节点变得容易。例如,如果我想添加一个新节点 D,我需要将一些哈希槽从节点 A、B、C 移动到 D。类似地,如果我想从集群中移除节点 A,我可以将 A 服务的哈希槽移动到 B 和 C。一旦节点 A 为空,我可以完全将其从集群中移除。

将哈希槽从一个节点移动到另一个节点不需要停止任何操作;因此,添加和移除节点,或更改节点持有的哈希槽百分比,不需要停机时间。

Redict 集群支持多键操作,只要单个命令执行(或整个事务,或 Lua 脚本执行)中涉及的所有键都属于同一个哈希槽。用户可以通过使用称为 哈希标签 的功能强制多个键成为同一个哈希槽的一部分。

哈希标签在 Redict 集群规范中有文档记录,但要点是,如果键中有一个由 {} 括号内的子字符串,则仅对字符串内部的内容进行哈希。例如,键 user:{123}:profileuser:{123}:account 保证在同一个哈希槽中,因为它们共享相同的哈希标签。因此,您可以在同一个多键操作中对这两个键进行操作。

Redict 集群主-从模型

为了在一部分主节点失败或无法与大多数节点通信时保持可用性,Redict 集群使用主-从模型,其中每个哈希槽有 1(即主节点本身)到 N 个副本(N-1 个额外的副本节点)。

在我们的例子集群中,节点 A、B、C,如果节点 B 失败,集群将无法继续,因为我们不再有办法服务 5501-11000 范围内的哈希槽。

然而,在创建集群时(或稍后),我们为每个主节点添加了一个副本节点,以便最终集群由 A、B、C 这些主节点和 A1、B1、C1 这些副本节点组成。这样,如果节点 B 失败,系统可以继续。

节点 B1 复制 B,如果 B 失败,集群将提升节点 B1 为新的主节点,并继续正确运行。

然而,请注意,如果节点 B 和 B1 同时失败,Redict 集群将无法继续运行。

Redict 集群一致性保证

Redict 集群不保证 强一致性。实际上这意味着在某些条件下,Redict 集群可能会丢失系统向客户端确认过的写操作。

Redict 集群可能会丢失写操作的第一个原因是因为它使用异步复制。这意味着在写入期间会发生以下情况:

  • 您的客户端写入主节点 B。
  • 主节点 B 对您的客户端回复 OK。
  • 主节点 B 将写入传播到其副本 B1、B2 和 B3。

如您所见,B 在回复客户端之前并不等待来自 B1、B2、B3 的确认,因为这将对 Redict 造成禁止性的延迟惩罚,所以如果您的客户端写入某些内容,B 确认了写入,但在能够将其发送到其副本之前崩溃了,那么没有接收到写入的副本之一可以被提升为 master,永远丢失该写入。

这与大多数配置为每秒将数据刷新到磁盘的数据库非常相似,因此由于您对不涉及分布式系统的传统数据库系统的经验,您已经能够理解这种情况。类似地,您可以通过在客户端回复之前强制数据库将数据刷新到磁盘来提高一致性,但这通常会降低性能。在 Redict 集群的情况下,这相当于同步复制。

基本上,需要在性能和一致性之间做出权衡。

Redict 集群在绝对需要时支持同步写入,通过 WAIT 命令实现。这使得丢失写入的可能性要小得多。然而,请注意,即使使用同步复制,Redict 集群也不实现强一致性:在更复杂的故障场景下,可能无法接收写入的副本可能被选为 master。

另一个值得注意的场景是,在网络分区期间,客户端与包括至少一个主节点的少数实例隔离。

以我们的 6 节点集群为例,由 A、B、C、A1、B1、C1 组成,其中 3 个主节点和 3 个副本。还有一个客户端,我们将称之为 Z1。

在分区发生后,可能在一个分区的一侧我们有 A、C、A1、B1、C1,而在另一个分区的一侧我们有 B 和 Z1。

Z1 仍然能够向 B 写入,B 将接受其写入。如果分区在短时间内愈合,集群将正常继续。然而,如果分区持续的时间足够长,以至于在分区的大多数一侧 B1 被提升为 master,那么在此期间 Z1 发送到 B 的写入将丢失。

Z1 能够向 B 发送的写入量有一个 最大窗口:如果足够多的时间已经过去,使得分区的大多数一侧的副本 被选为主节点,少数一侧的每个主节点将停止接受写入。

这是 Z1 能够向 B 发送的写入量的一个重要配置指令,称为 节点超时

节点超时过后,一个主节点被认为是失败的,可以被其副本替换。类似地,如果在节点超时过后没有主节点能够感知到其他主节点的大多数,它将进入错误状态并停止接受写入。

Redict 集群配置参数

我们即将创建一个示例集群部署。在我们继续之前,让我们介绍 Redict 集群在 redict.conf 文件中引入的配置参数。

  • cluster-enabled <yes/no>: 如果是 yes,则在特定 Redict 实例中启用 Redict 集群支持。否则,实例将像往常一样作为独立实例启动。
  • cluster-config-file <filename>: 请注意,尽管此选项的名称如此,但这不是用户可编辑的配置文件,而是 Redict 集群节点自动持久化集群配置(即状态)的文件,每次有更改时都会更新,以便在启动时能够重新读取。该文件列出了诸如集群中的其他节点、它们的状态、持久变量等。
  • cluster-node-timeout <milliseconds>: Redict 集群节点不可用的最大时间,而不被视为失败。如果主节点在指定的时间内无法到达,它将被其副本故障转移。此参数还控制 Redict 集群中的其他重要事项。值得注意的是,每个无法在指定时间内到达大多数主节点的节点,将停止接受查询。
  • cluster-slave-validity-factor <factor>: 如果设置为零,副本将始终认为自己有效,因此将始终尝试对主节点进行故障转移,而不管主节点和副本之间的连接断开的时间长度如何。如果该值为正数,则计算最大断开连接时间为 节点超时 值乘以此选项提供的因子,如果节点是副本,则如果主节点连接断开的时间超过指定的时间,则不会尝试启动故障转移。例如,如果节点超时设置为 5 秒,有效性因子设置为 10,那么与主节点断开连接超过 50 秒的副本将不会尝试故障转移其主节点。请注意,任何非零值都可能导致在没有能够故障转移的副本的情况下,Redict 集群在主节点故障后不可用。在这种情况下,只有在原始主节点重新加入集群时,集群才会恢复可用性。
  • cluster-migration-barrier <count>: 主节点将保持连接的最小副本数量,以便另一个副本迁移到不再被任何副本覆盖的主节点。有关副本迁移的更多信息,请参见本教程中关于副本迁移的适当部分。
  • cluster-require-full-coverage <yes/no>: 如果设置为 yes,如默认设置,如果某个百分比的键空间没有被任何节点覆盖,集群将停止接受写入。如果选项设置为 no,即使只能处理子集键的请求,集群仍将服务查询。
  • cluster-allow-reads-when-down <yes/no>: 如果设置为 no,如默认设置,Redict 集群中的节点将在集群被标记为失败时停止服务所有流量,无论是当节点无法到达主节点的大多数,还是当未达到完全覆盖时。这防止从不知道集群变化的节点读取可能不一致的数据。此选项可以设置为 yes,以允许在故障状态下从节点读取,这对于希望优先读取可用性但仍然希望防止不一致写入的应用程序很有用。它还可以用于在使用只有一个或两个分片的 Redict 集群时,因为它允许节点在主节点故障但无法自动故障转移时继续服务写入。

创建和使用 Redict 集群

要创建和使用 Redict 集群,请按照以下步骤操作:

但首先,熟悉创建集群的要求。

创建 Redict 集群的要求

要创建集群,您首先需要运行几个空的 Redict 实例,这些实例在 集群模式 下。

至少,在 redict.conf 文件中设置以下指令:

  1. port 7000
  2. cluster-enabled yes
  3. cluster-config-file nodes.conf
  4. cluster-node-timeout 5000
  5. appendonly yes

要启用集群模式,请将 cluster-enabled 指令设置为 yes。每个实例还包含一个文件的路径,该文件存储此节点的配置,默认情况下是 nodes.conf。这个文件从不由人类触摸;它只是在启动时由 Redict 集群实例生成,并在需要时更新。

请注意,最小集群 至少包含三个主节点。对于部署,我们强烈推荐一个六节点集群,其中有三个主节点和三个副本。

您可以通过在任何给定目录内创建以下目录来本地测试,这些目录以您将运行的实例的端口号命名。

例如:

  1. mkdir cluster-test
  2. cd cluster-test
  3. mkdir 7000 7001 7002 7003 7004 7005

在每个目录中创建一个 redict.conf 文件,从 7000 到 7005。对于配置文件,只需使用上面的小示例,但确保将端口号 7000 替换为根据目录名称正确的端口号。

您可以按照以下方式启动每个实例,每个实例在单独的终端标签页中运行:

  1. cd 7000
  2. redict-server ./redict.conf

您将从日志中看到,每个节点都为自己分配了一个新的 ID:

  1. [82462] 26 Nov 11:56:55.329 * No cluster configuration found, I'm 97a3a64667477371c4479320d683e4c8db5858b1

此 ID 将由此特定实例永久使用,以便该实例在集群上下文中具有唯一名称。每个节点都使用这些 ID 记住每个其他节点,而不是通过 IP 或端口。IP 地址和端口可能会更改,但节点的唯一标识符在其生命周期内永远不会更改。我们称此标识符为 节点 ID

创建 Redict 集群

现在我们已经运行了一些实例,您需要通过向节点写入一些有意义的配置来创建您的集群。

您可以手动配置和执行单个实例,或者使用 create-cluster 脚本。让我们先看看如何手动操作。

要创建集群,请运行:

  1. redict-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 \
  2. 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 \
  3. --cluster-replicas 1

这里使用的命令是 create,因为我们想要创建一个新的集群。选项 --cluster-replicas 1 表示我们想要为每个创建的主节点都有一个副本。

其他参数是要用于创建新集群的实例的地址列表。

redict-cli 将提出一个配置。通过输入 yes 接受所提出的配置。集群将被配置并 加入,这意味着实例将被引导相互通信。最后,如果一切顺利,您将看到类似于以下的消息:

  1. [OK] All 16384 slots covered

这意味着至少有一个主实例在服务每个可用的 16384 个槽。

如果您不想通过手动配置和执行单个实例来创建 Redict 集群,如上所述,有一个更简单的系统(但您将不会了解相同数量的操作细节)。

在 Redict 分发中找到 utils/create-cluster 目录。目录中有一个名为 create-cluster 的脚本,它是一个简单的 bash 脚本。要启动一个具有 3 个主节点和 3 个副本的 6 节点集群,只需输入以下命令:

  1. create-cluster start
  2. create-cluster create

在第 2 步中,当 redict-cli 实用程序要求您接受集群布局时,请回答 yes

您现在可以与集群交互,第一个节点默认从端口 30001 开始监听。当您完成后,可以使用以下命令停止集群:

  1. create-cluster stop

请阅读该目录中的 README 以获取有关如何运行脚本的更多信息。

与集群交互

要连接到 Redict 集群,您需要一个集群感知的 Redict 客户端。请参阅您选择的客户端文档,以确定其集群支持。

您还可以使用 redict-cli 命令行实用程序测试您的 Redict 集群:

  1. $ redict-cli -c -p 7000
  2. redict 127.0.0.1:7000> set foo bar
  3. -> Redirected to slot [12182] located at 127.0.0.1:7002
  4. OK
  5. redict 127.0.0.1:7002> set hello world
  6. -> Redirected to slot [866] located at 127.0.0.1:7000
  7. OK
  8. redict 127.0.0.1:7000> get foo
  9. -> Redirected to slot [12182] located at 127.0.0.1:7002
  10. "bar"
  11. redict 127.0.0.1:7002> get hello
  12. -> Redirected to slot [866] located at 127.0.0.1:7000
  13. "world"

如果您使用脚本创建了集群,您的节点可能会在不同的端口上监听,默认从 30001 开始。

redict-cli 的集群支持非常基础,因此它总是利用 Redict 集群节点能够将客户端重定向到正确的节点这一事实。一个认真的客户端能够做得更好,并且可以缓存哈希槽与节点地址之间的映射,直接使用正确的连接到正确的节点。该映射仅在集群配置发生变化时才更新,例如在故障转移后或系统管理员通过添加或删除节点更改集群布局后。

使用 redict-rb-cluster 编写示例应用程序

在展示如何操作 Redict 集群、执行故障转移或重新划分哈希槽之前,我们需要创建一些示例应用程序,或者至少能够理解简单的 Redict 集群客户端交互的语义。

这样我们可以运行一个示例,并同时尝试使节点故障,或启动重新划分哈希槽,以查看 Redict 集群在真实世界条件下的行为。如果没有人正在向集群写入,那么看到发生什么就没有太多帮助。

本节解释了 redict-rb-cluster 的一些基本用法,展示了两个示例。第一个是以下示例,是 redict-rb-cluster 分布中的 example.rb 文件:

  1. 1 require './cluster'
  2. 2
  3. 3 if ARGV.length != 2
  4. 4 startup_nodes = [
  5. 5 {:host => "127.0.0.1", :port => 7000},
  6. 6 {:host => "127.0.0.1", :port => 7001}
  7. 7 ]
  8. 8 else
  9. 9 startup_nodes = [
  10. 10 {:host => ARGV[0], :port => ARGV[1].to_i}
  11. 11 ]
  12. 12 end
  13. 13
  14. 14 rc = RedisCluster.new(startup_nodes,32,:timeout => 0.1)
  15. 15
  16. 16 last = false
  17. 17
  18. 18 while not last
  19. 19 begin
  20. 20 last = rc.get("__last__")
  21. 21 last = 0 if !last
  22. 22 rescue => e
  23. 23 puts "error #{e.to_s}"
  24. 24 sleep 1
  25. 25 end
  26. 26 end
  27. 27
  28. 28 ((last.to_i+1)..1000000000).each{|x|
  29. 29 begin
  30. 30 rc.set("foo#{x}",x)
  31. 31 puts rc.get("foo#{x}")
  32. 32 rc.set("__last__",x)
  33. 33 rescue => e
  34. 34 puts "error #{e.to_s}"
  35. 35 end
  36. 36 sleep 0.1
  37. 37 }

该应用程序执行了一个非常简单的操作,它按顺序设置形式为 foo<number> 的键到 number。所以如果您运行程序,结果将是以下命令流:

  • SET foo0 0
  • SET foo1 1
  • SET foo2 2
  • 等等…

程序看起来比它应该的要复杂,因为它被设计为在屏幕上显示错误而不是用异常退出,所以与集群执行的每个操作都被 begin rescue 块包装。

第 14 行 是程序中第一个有趣的行。它创建了 Redict 集群对象,使用参数列表作为启动节点,这个对象允许的最大连接数对不同节点,以及超时时间。

启动节点不需要是集群的所有节点。重要的是至少有一个节点是可到达的。同样请注意,redict-rb-cluster 一旦能够与第一个节点连接,就会立即更新启动节点列表。您应该期望这种行为与其他任何认真的客户端一样。

现在,既然我们有了 Redict 集群对象实例存储在 rc 变量中,我们就可以使用该对象,就像它是一个普通的 Redict 对象实例一样。

这正是在 第 18 到 26 行 中发生的事情:当我们重启示例时,我们不想从 foo0 开始,所以我们在 Redict 本身中存储了计数器。上面的代码设计为读取此计数器,或者如果计数器不存在,则将其分配为零。

然而请注意,这是一个 while 循环,因为我们希望即使集群关闭并返回错误,也要不断尝试。正常的应用程序不需要这么小心。

第 28 到 37 行 启动了主要循环,其中设置键或显示错误。

请注意循环末尾的 sleep 调用。在您的测试中,如果您想以尽可能快的速度向集群写入(相对于这是一个没有真实并行性的忙循环的事实),可以删除睡眠。通常为了示例应用程序更容易被人类理解,写入速度较慢。

启动应用程序会产生以下输出:

  1. ruby ./example.rb
  2. 1
  3. 2
  4. 3
  5. 4
  6. 5
  7. 6
  8. 7
  9. 8
  10. 9
  11. ^C (我在这里停止了程序)

这不是一个非常有趣的程序,我们很快就会使用一个更好的程序,但我们已经可以看到在程序运行时重新划分哈希槽会发生什么。

重新划分集群

现在我们准备尝试对集群进行重新划分。要做到这一点,请保持 example.rb 程序运行,以便您可以看到对运行中的程序的影响。此外,您可能希望注释掉 sleep 调用,以便在重新划分哈希槽期间有更多的写入负载。

重新划分哈希槽基本上意味着将一组节点上的哈希槽移动到另一组节点上。像集群创建一样,它使用 redict-cli 实用程序完成。

要开始重新划分哈希槽,只需输入:

  1. redict-cli --cluster reshard 127.0.0.1:7000

您只需要指定一个节点,redict-cli 将自动找到其他节点。

目前 redict-cli 只能以管理员支持的方式重新划分哈希槽,您不能只是说将 5% 的哈希槽从这个节点移动到另一个节点(但这很容易实现)。所以它从问题开始。第一个问题是您想做多少重新划分:

  1. How many slots do you want to move (from 1 to 16384)?

我们可以尝试重新划分 1000 个哈希槽,这应该已经包含了一个非平凡的键数量,如果示例程序仍然在没有 sleep 调用的情况下运行。

然后 redict-cli 需要知道重新划分的目标是什么,即哪个节点将接收哈希槽。我将使用第一个主节点,即 127.0.0.1:7000,但我需要指定实例的节点 ID。这已经在 redict-cli 打印的列表中了,但如果我需要,我可以随时使用以下命令找到节点的 ID:

  1. $ redict-cli -p 7000 cluster nodes | grep myself
  2. 97a3a64667477371c4479320d683e4c8db5858b1 :0 myself,master - 0 0 0 connected 0-5460

好的,所以我的目标节点是 97a3a64667477371c4479320d683e4c8db5858b1。

现在,他们会问您想从哪个节点拿走那些键。我将只输入 all,以便从所有其他主节点中拿走一些哈希槽。

在最终确认后,您将看到 redict-cli 要移动的每个槽的消息,并在每个实际键从一边移动到另一边时打印一个点。

在重新划分哈希槽进行中时,您应该能够看到您的示例程序在运行中不受影响。如果您愿意,可以在重新划分哈希槽期间多次停止并启动它。

重新划分哈希槽结束时,您可以使用以下命令测试集群的健康状况:

  1. redict-cli --cluster check 127.0.0.1:7000

所有槽都将像往常一样被覆盖,但这次 127.0.0.1:7000 上的主节点将拥有更多的哈希槽,大约在 6461 左右。

重新划分哈希槽可以自动执行,无需手动输入参数。这可以通过使用以下命令行实现:

  1. redict-cli --cluster reshard <host>:<port> --cluster-from <node-id> --cluster-to <node-id> --cluster-slots <number of slots> --cluster-yes

这允许您在可能经常重新划分哈希槽的情况下构建一些自动化,但目前没有 redict-cli 的方法可以自动重新平衡集群,检查集群节点的键分布,并智能地按需移动槽。这个特性将在未来添加。

--cluster-yes 选项指示集群管理器自动回答命令的提示,允许它以非交互模式运行。请注意,此选项也可以通过设置 REDISCLI_CLUSTER_YES 环境变量来激活。

更有趣的示例应用程序

我们早期编写的示例应用程序并不很好。它以简单的方式向集群写入,甚至没有检查写入的内容是否正确。

从我们的角度来看,接收写入的集群可以对每个操作都将键 foo 写入 42,我们根本不会注意到。

因此,在 redict-rb-cluster 存储库中,有一个名为 consistency-test.rb 的更有趣的应用程序。它使用一组计数器,默认为 1000,并发送 INCR 命令以增加计数器。

然而,与仅仅写入不同,应用程序还做了两个额外的事情:

  • 当使用 INCR 更新计数器时,应用程序会记住写入。
  • 它还在每次写入前读取一个随机计数器,并检查值是否是我们期望的值,将其与内存中的值进行比较。

这意味着这个应用程序是一个简单的 一致性检查器,能够告诉您集群是否丢失了一些写入,或者是否接受了我们没有收到确认的写入。在第一种情况下,我们将看到一个计数器的值比我们记忆中的值小,而在第二种情况下,值会更大。

运行一致性测试应用程序每秒产生一行输出:

  1. $ ruby consistency-test.rb
  2. 925 R (0 err) | 925 W (0 err) |
  3. 5030 R (0 err) | 5030 W (0 err) |
  4. 9261 R (0 err) | 9261 W (0 err) |
  5. 13517 R (0 err) | 13517 W (0 err) |
  6. 17780 R (0 err) | 17780 W (0 err) |
  7. 22025 R (0 err) | 22025 W (0 err) |
  8. 25818 R (0 err) | 25818 W (0 err) |

该行显示了执行的 R eads(读取)和 W rites(写入)的数量,以及由于系统不可用而未被接受的查询数量的错误数量。

如果发现某些不一致,输出中会添加新行。例如,如果在程序运行时我手动重置了一个计数器:

  1. $ redict-cli -h 127.0.0.1 -p 7000 set key_217 0
  2. OK
  3. (在另一个标签页中我看到...)
  4. 94774 R (0 err) | 94774 W (0 err) |
  5. 98821 R (0 err) | 98821 W (0 err) |
  6. 102886 R (0 err) | 102886 W (0 err) | 114 lost |
  7. 107046 R (0 err) | 107046 W (0 err) | 114 lost |

当我将计数器设置为 0 时,实际值是 114,所以程序报告了 114 个丢失的写入(未被集群记住的 INCR 命令)。

这个程序作为测试用例更有趣,所以我们将使用它来测试 Redict 集群的故障转移。

测试故障转移

要触发故障转移,我们可以做的最简单的事情(也是在分布式系统中可能发生的语义上最简单的故障)是使一个单独的进程崩溃,在我们的例子中是一个单独的主节点。

在进行此测试时,您应该打开一个标签页,运行一致性测试应用程序。

我们可以通过以下命令识别一个主节点并使其崩溃:

  1. $ redict-cli -p 7000 cluster nodes | grep master
  2. 3e3a6cb0d9a9a87168e266b0a0b24026c0aae3f0 127.0.0.1:7001 master - 0 1385482984082 0 connected 5960-10921
  3. 2938205e12de373867bf38f1ca29d31d0ddb3e46 127.0.0.1:7002 master - 0 1385482983582 0 connected 11423-16383
  4. 97a3a64667477371c4479320d683e4c8db5858b1 :0 myself,master - 0 0 0 connected 0-5959 10922-11422

好的,所以 7000、7001 和 7002 是主节点。让我们使用 DEBUG SEGFAULT 命令使节点 7002 崩溃:

  1. $ redict-cli -p 7002 debug segfault
  2. Error: Server closed the connection

现在我们可以查看一致性测试的输出,看看它报告了什么。

  1. 18849 R (0 err) | 18849 W (0 err) |
  2. 23151 R (0 err) | 23151 W (0 err) |
  3. 27302 R (0 err) | 27302 W (0 err) |
  4. ... many error warnings here ...
  5. 29659 R (578 err) | 29660 W (577 err) |
  6. 33749 R (578 err) | 33750 W (577 err) |
  7. 37918 R (578 err) | 37919 W (577 err) |
  8. 42077 R (578 err) | 42078 W (577 err) |

正如您所见,在故障转移期间,系统无法接受 578 个读取和 577 个写入,但是数据库中没有创建不一致性。这听起来可能出乎意料,因为在本教程的第一部分中我们指出,由于 Redict 集群使用异步复制,因此在故障转移期间可能会丢失写入。我们没有说的是,这种情况发生的可能性不大,因为 Redict 几乎同时向客户端发送回复和向副本复制命令,因此丢失数据的可能性非常小。然而,难以触发这一事实并不意味着它是不可能的,所以这并不改变 Redict 集群提供的一致性保证。

我们现在可以检查故障转移后集群的设置(请注意,在同时我重新启动了崩溃的实例,以便它作为副本重新加入集群):

  1. $ redict-cli -p 7000 cluster nodes
  2. 3fc783611028b1707fd65345e763befb36454d73 127.0.0.1:7004 slave 3e3a6cb0d9a9a87168e266b0a0b24026c0aae3f0 0 1385503418521 0 connected
  3. a211e242fc6b22a9427fed61285e85892fa04e08 127.0.0.1:7003 slave 97a3a64667477371c4479320d683e4c8db5858b1 0 1385503419023 0 connected
  4. 97a3a64667477371c4479320d683e4c8db5858b1 :0 myself,master - 0 0 0 connected 0-5959 10922-11422
  5. 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 127.0.0.1:7005 master - 0 1385503419023 3 connected 11423-16383
  6. 3e3a6cb0d9a9a87168e266b0a0b24026c0aae3f0 127.0.0.1:7001 master - 0 1385503417005 0 connected 5960-10921
  7. 2938205e12de373867bf38f1ca29d31d0ddb3e46 127.0.0.1:7002 slave 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 0 1385503418016 3 connected

现在主节点在端口 7000、7001 和 7005 上运行。之前是主节点的 Redict 实例,即在端口 7002 上运行的实例,现在是 7005 的副本。

CLUSTER NODES 命令的输出可能看起来很吓人,但它实际上相当简单,由以下令牌组成:

  • 节点 ID
  • ip:port
  • 标志:master, replica, myself, fail, …
  • 如果是副本,为主节点的节点 ID
  • 最后一次挂起的 PING 仍在等待回复的时间。
  • 最后一次收到的 PONG 的时间。
  • 此节点的配置纪元(见集群规范)。
  • 此节点链接的状态。
  • 服务的槽位…

手动故障转移

有时,强制进行故障转移而不实际导致主节点上出现问题是有用的。例如,为了升级其中一个主节点上的 Redict 进程,最好在对可用性影响最小的情况下将其故障转移到一个副本。

手动故障转移通过在您想要故障转移的主节点的一个副本上执行 CLUSTER FAILOVER 命令来支持 Redict 集群。

与由实际主节点故障引起的故障转移相比,手动故障转移是特殊的,并且更安全。它们以一种避免数据丢失的方式发生,只有当系统确定新主节点已处理完旧主节点的所有复制流时,才会将客户端从原始主节点切换到新主节点。

当您执行手动故障转移时,这是您在副本日志中看到的内容:

  1. # Manual failover user request accepted.
  2. # Received replication offset for paused master manual failover: 347540
  3. # All master replication stream processed, manual failover can start.
  4. # Start of election delayed for 0 milliseconds (rank #0, offset 347540).
  5. # Starting a failover election for epoch 7545.
  6. # Failover election won: I'm the new master.

基本上,连接到我们要故障转移的主节点的客户端将被停止。同时,主节点将其复制偏移量发送给副本,副本等待在自己的一侧达到偏移量。当达到复制偏移量时,故障转移开始,旧主节点被告知配置切换。当客户端在旧主节点上被解封时,它们被重定向到新主节点。

要将副本提升为主节点,它首先必须被集群中的大多数主节点知道作为副本。否则,它无法赢得故障转移选举。如果副本刚刚被添加到集群中(见添加新节点作为副本),在发送 CLUSTER FAILOVER 命令之前,您可能需要等待一段时间,以确保集群中的主节点知道新副本。

添加新节点

添加新节点基本上是添加一个空节点的过程,然后移动一些数据到其中,如果是新主节点,或者告诉它设置为已知节点的副本,如果是副本。

我们将展示两者,从添加新主节点实例开始。

在这两种情况下,首先执行的步骤是添加一个空节点

这就像在端口 7006 上启动一个新节点(我们已经使用 7000 到 7005 为我们现有的 6 个节点使用了端口),使用与其他节点相同的配置,除了端口号,所以您应该做的是:

  • 在终端应用程序中创建一个新的标签页。
  • 进入 cluster-test 目录。
  • 创建一个名为 7006 的目录。
  • 在其中创建一个 redict.conf 文件,与用于其他节点的文件类似,但使用 7006 作为端口号。
  • 最后使用 ../redict-server ./redict.conf 启动服务器。

此时服务器应该正在运行。

现在我们可以使用redict-cli像往常一样将节点添加到现有集群中。

  1. redict-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000

如您所见,我使用了add-node命令,将新节点的地址作为第一个参数,将集群中随机现有节点的地址作为第二个参数。

在实际操作中,redict-cli 这里帮了我们很少的忙,它只是向节点发送了一个 CLUSTER MEET 消息,这也是可以手动完成的。然而,redict-cli 在操作之前还会检查集群的状态,因此即使您知道内部工作原理,最好总是通过 redict-cli 执行集群操作。

现在我们可以连接到新节点,看看它是否真的加入了集群:

  1. redict 127.0.0.1:7006> cluster nodes
  2. 3e3a6cb0d9a9a87168e266b0a0b24026c0aae3f0 127.0.0.1:7001 master - 0 1385543178575 0 connected 5960-10921
  3. 3fc783611028b1707fd65345e763befb36454d73 127.0.0.1:7004 slave 3e3a6cb0d9a9a87168e266b0a0b24026c0aae3f0 0 1385543179583 0 connected
  4. f093c80dde814da99c5cf72a7dd01590792b783b :0 myself,master - 0 0 0 connected
  5. 2938205e12de373867bf38f1ca29d31d0ddb3e46 127.0.0.1:7002 slave 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 0 1385543178072 3 connected
  6. a211e242fc6b22a9427fed61285e85892fa04e08 127.0.0.1:7003 slave 97a3a64667477371c4479320d683e4c8db5858b1 0 1385543178575 0 connected
  7. 97a3a64667477371c4479320d683e4c8db5858b1 127.0.0.1:7000 master - 0 1385543179080 0 connected 0-5959 10922-11422
  8. 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 127.0.0.1:7005 master - 0 1385543177568 3 connected 11423-16383

请注意,由于这个节点已经连接到集群,它已经能够正确地重定向客户端查询,并且通常已经是集群的一部分。然而,与其它主节点相比,这个节点有两个特点:

  • 它不持有数据,因为它没有分配的哈希槽。
  • 因为它是一个没有分配槽的主节点,所以它不参与选举过程,当一个副本想要成为主节点时。

现在可以通过使用 redict-cli 的重新划分哈希槽功能来为这个节点分配哈希槽。这基本上和我们之前做的没有区别,只是重新划分的目标是空节点。

添加新节点作为副本

添加新副本可以通过两种方式完成。显而易见的方式是再次使用 redict-cli,但使用 –cluster-slave 选项,如下所示:

  1. redict-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000 --cluster-slave

请注意,这里的命令行与我们用来添加新主节点的命令行完全相同,所以我们没有指定要将副本添加到哪个主节点。在这种情况下,redict-cli 将随机选择主节点中的一个,并将新节点作为该主节点的副本添加。

但是,您可以使用以下命令行精确指定要为目标主节点添加新副本:

  1. redict-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000 --cluster-slave --cluster-master-id 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e

这样我们就为特定主节点分配了新的副本。

将副本添加到特定主节点的更手动的方法是将新节点作为空主节点添加,然后使用 CLUSTER REPLICATE 命令将其设置为已知节点的副本。如果节点被添加为副本,但您想将其移动为不同主节点的副本,这种方法也有效。

例如,为了添加一个副本来复制当前服务哈希槽范围 11423-16383 的节点 127.0.0.1:7005,该节点的节点 ID 为 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e,我需要做的就是连接到新节点(已经作为空主节点添加)并发送命令:

  1. redict 127.0.0.1:7006> cluster replicate 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e

就是这样。现在我们为这组哈希槽有了新的副本,所有集群中的其他节点在几秒钟后(需要更新它们的配置)都已经知道。我们可以使用以下命令验证:

  1. $ redict-cli -p 7000 cluster nodes | grep slave | grep 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e
  2. f093c80dde814da99c5cf72a7dd01590792b783b 127.0.0.1:7006 slave 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 0 1385543617702 3 connected
  3. 2938205e12de373867bf38f1ca29d31d0ddb3e46 127.0.0.1:7002 slave 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 0 1385543617198 3 connected

节点 3c3a0c… 现在有了两个副本,分别运行在端口 7002(现有的)和 7006(新的)。

移除节点

要移除副本节点,只需使用 redict-cli 的 del-node 命令:

  1. redict-cli --cluster del-node 127.0.0.1:7000 `<node-id>`

第一个参数只是集群中的一个随机节点,第二个参数是您要移除的节点的 ID。

您也可以以相同的方式移除主节点,但是为了移除主节点,它必须是空的。如果主节点不为空,您需要在移除之前将数据重新划分到所有其他主节点。

移除主节点的另一种替代方法是对其执行手动故障转移,将其变为副本,然后在它变成新主节点的副本后移除该节点。显然,当您想要减少集群中主节点的实际数量时,这并没有帮助,在这种情况下,需要重新划分哈希槽。

您想要移除失败节点的特殊场景时,不应使用 del-node 命令,因为它会尝试连接到所有节点,如果给定节点不可用,您将遇到“连接拒绝”错误。相反,您可以使用 call 命令:

  1. redict-cli --cluster call 127.0.0.1:7000 cluster forget `<node-id>`

此命令将在每个节点上执行 CLUSTER FORGET 命令。

副本迁移

在 Redict 集群中,您可以随时使用以下命令重新配置副本以复制不同的主节点:

  1. CLUSTER REPLICATE <master-node-id>

但是有一种特殊场景,您可能希望副本在不需要系统管理员帮助的情况下自动从一台主节点迁移到另一台主节点。副本的自动重新配置称为 副本迁移,并且可以提高 Redict 集群的可靠性。

您可以在 Redict 集群规范 中阅读有关副本迁移的详细信息,这里我们只提供一些关于总体思路的信息以及您应该做什么以从中受益。

您可能希望让您的集群副本在某些条件下从一台主节点迁移到另一台主节点的原因是因为,通常,Redict 集群对故障的抵抗力与给定主节点附加的副本数量一样多。

例如,每个主节点只有一个副本的集群不能在主节点和其副本同时失败时继续操作,因为没有任何其他实例可以拥有主节点正在服务的哈希槽的副本。然而,当网分割同时孤立了多个节点时,许多其他类型的故障,如影响单个节点的硬件或软件故障,是不太可能同时发生的故障类别,因此可能在您的集群中,每个主节点都有一个副本,副本在凌晨 4 点被杀死,主节点在凌晨 6 点被杀死。这仍然会导致无法继续操作的集群。

为了提高系统的可靠性,我们有向每个主节点添加额外副本的选项,但这很昂贵。副本迁移允许您只向少数主节点添加更多副本。所以您有 10 个主节点,每个主节点有 1 个副本,总共 20 个实例。然而,您添加了 3 个额外的实例作为一些主节点的副本,这样某些主节点将有不止一个副本。

副本迁移发生的事情是,如果一个主节点没有副本,那么具有多个副本的主节点上的副本将迁移到 孤立 的主节点。所以在我们上面做的例子中,如果您的副本在凌晨 4 点下降,另一个副本将取代它的位置,当主节点也在凌晨 5 点失败时,仍然有一个副本可以被选举,以便集群可以继续操作。

那么您应该了解有关副本迁移的简短信息是什么?

  • 集群将尝试从给定时刻副本数量最多的主节点迁移一个副本。
  • 要从副本迁移中受益,您只需向集群中的一个主节点添加几个更多的副本,它不重要是哪个主节点。
  • 有一个控制副本迁移功能的配置参数,称为 cluster-migration-barrier:您可以在 Redict 集群提供的示例 redict.conf 文件中阅读更多关于它的信息。

在 Redict 集群中升级节点

升级副本节点很容易,因为您只需要停止节点,并使用更新版本的 Redict 重新启动它。如果客户端使用副本节点进行读取扩展,它们应该能够在不同的副本不可用时连接到另一个副本。

升级主节点有点更复杂,建议的程序是:

  1. 使用 CLUSTER FAILOVER 触发主节点到其一个副本的手动故障转移。(见本主题中的 手动故障转移。)
  2. 等待主节点变成副本。
  3. 最后像升级副本一样升级节点。
  4. 如果您希望主节点是您刚刚升级的节点,触发一个新的手动故障转移,将更新的节点变回主节点。

按照这个程序,您应该一个接一个地升级节点,直到所有节点都升级完毕。

迁移到 Redict 集群

希望迁移到 Redict 集群的用户可能只有一个主节点,或者可能已经使用现有的分片设置,其中键通过一些内部算法或客户端库或 Redict 代理实现的分片算法分布在 N 个节点上。

在这两种情况下,都可以轻松迁移到 Redict 集群,但最重要的细节是应用程序是否使用多键操作,以及如何使用。有三种不同的情况:

  1. 不使用多键操作、事务或涉及多个键的 Lua 脚本。键独立访问(即使通过事务或 Lua 脚本将多个命令,关于同一键,组合在一起)。
  2. 使用涉及多个键的多键操作、事务或 Lua 脚本,但只与具有相同 哈希标签 的键一起使用,这意味着一起使用的键都有相同的 {...} 子字符串。例如,以下多键操作定义在相同的哈希标签上下文中:SUNION {user:1000}.foo {user:1000}.bar
  3. 使用涉及多个键的操作、事务或 Lua 脚本,且键没有显式或相同的哈希标签。

Redict 集群不处理第三种情况:应用程序需要修改,以便不使用多键操作或只在相同的哈希标签上下文中使用它们。

情况 1 和 2 是被覆盖的,所以我们将关注这两个案例,它们以相同的方式处理,因此在文档中不会进行区分。

假设您有预先存在的数据集分布在 N 个主节点中,其中 N=1 如果您没有预先存在的分片,迁移数据集到 Redict 集群需要以下步骤:

  1. 停止您的客户端。目前不可能自动进行实时迁移到 Redict 集群。您可能能够在应用程序/环境中编排实时迁移。
  2. 使用 BGREWRITEAOF 命令为所有 N 个主节点生成一个仅附加文件,并等待 AOF 文件完全生成。
  3. 将您的 AOF 文件从 aof-1 到 aof-N 保存在某个地方。此时,如果您愿意,可以停止旧实例(这在非虚拟化部署中很有用,因为您通常需要重用相同的计算机)。
  4. 创建一个由 N 个主节点和 0 个副本组成的 Redict 集群。您稍后会添加副本。确保所有节点都使用仅附加文件进行持久化。
  5. 停止所有集群节点,用您预先存在的仅附加文件替换它们的 AOF 文件,第一个节点使用 aof-1,第二个节点使用 aof-2,依此类推,直到 aof-N。
  6. 使用新的 AOF 文件重启您的 Redict 集群节点。它们会抱怨有不应该在那里的键,根据它们的配置。
  7. 使用 redict-cli --cluster fix 命令修复集群,以便根据每个节点是否权威,将键迁移到相应的哈希槽。
  8. 最后使用 redict-cli --cluster check 确保您的集群没问题。
  9. 重新启动修改为使用 Redict 集群感知客户端库的客户端。

从外部实例导入数据到 Redict 集群的另一种方法是使用 redict-cli --cluster import 命令。

该命令将移动运行实例的所有键(从源实例中删除键)到指定的预先存在的 Redict 集群。

Redict 项目不再使用“slave”这个词汇。不幸的是,在这个命令中,“slave”这个词是协议的一部分,所以我们只能在这个 API 自然淘汰后才能移除这些用法。

添加新节点作为副本

添加新副本可以通过两种方式完成。显而易见的方式是再次使用 redict-cli,但使用 –cluster-slave 选项,如下所示:

  1. redict-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000 --cluster-slave

请注意,这里的命令行与我们用来添加新主节点的命令行完全相同,所以我们没有指定要将副本添加到哪个主节点。在这种情况下,redict-cli 将随机选择主节点中的一个,并将新节点作为该主节点的副本添加。

但是,您可以使用以下命令行精确指定要为目标主节点添加新副本:

  1. redict-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000 --cluster-slave --cluster-master-id 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e

这样我们就为特定主节点分配了新的副本。

将副本添加到特定主节点的更手动的方法是将新节点作为空主节点添加,然后使用 CLUSTER REPLICATE 命令将其设置为已知节点的副本。如果节点被添加为副本,但您想将其移动为不同主节点的副本,这种方法也有效。

例如,为了添加一个副本来复制当前服务哈希槽范围 11423-16383 的节点 127.0.0.1:7005,该节点的节点 ID 为 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e,我需要做的就是连接到新节点(已经作为空主节点添加)并发送命令:

  1. redict 127.0.0.1:7006> cluster replicate 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e

就是这样。现在我们为这组哈希槽有了新的副本,所有集群中的其他节点在几秒钟后(需要更新它们的配置)都已经知道。我们可以使用以下命令验证:

  1. $ redict-cli -p 7000 cluster nodes | grep slave | grep 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e
  2. f093c80dde814da99c5cf72a7dd01590792b783b 127.0.0.1:7006 slave 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 0 1385543617702 3 connected
  3. 2938205e12de373867bf38f1ca29d31d0ddb3e46 127.0.0.1:7002 slave 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 0 1385543617198 3 connected

节点 3c3a0c… 现在有了两个副本,分别运行在端口 7002(现有的)和 7006(新的)。

移除节点

要移除副本节点,只需使用 redict-cli 的 del-node 命令:

  1. redict-cli --cluster del-node 127.0.0.1:7000 `<node-id>`

第一个参数只是集群中的一个随机节点,第二个参数是您要移除的节点的 ID。

您也可以以相同的方式移除主节点,但是为了移除主节点,它必须是空的。如果主节点不为空,您需要在移除之前将数据重新划分到所有其他主节点。

移除主节点的另一种替代方法是对其执行手动故障转移,将其变为副本,然后在它变成新主节点的副本后移除该节点。显然,这并不帮助当您想要减少集群中主节点的实际数量时,这种情况下,需要重新划分哈希槽。

您想要移除失败节点的特殊场景时,不应使用 del-node 命令,因为它会尝试连接到所有节点,如果给定节点不可用,您将遇到“连接拒绝”错误。相反,您可以使用 call 命令:

  1. redict-cli --cluster call 127.0.0.1:7000 cluster forget `<node-id>`

此命令将在每个节点上执行 CLUSTER FORGET 命令。

副本迁移

在 Redict 集群中,您可以随时使用以下命令重新配置副本以复制不同的主节点:

  1. CLUSTER REPLICATE <master-node-id>

但是有一种特殊场景,您可能希望副本在不需要系统管理员帮助的情况下自动从一台主节点迁移到另一台主节点。副本的自动重新配置称为 副本迁移,并且可以提高 Redict 集群的可靠性。

您可以通过 Redict 集群规范 阅读有关副本迁移的详细信息,这里我们只提供一些关于总体思路的信息以及您应该做什么以从中受益。

副本迁移的主要原因是,通常 Redict 集群对故障的抵抗力与给定主节点附加的副本数量一样多。

例如,每个主节点只有一个副本的集群不能在主节点和其副本同时失败时继续操作,因为没有其他实例可以拥有主节点正在服务的哈希槽的副本。然而,网分割可能同时孤立了多个节点,但许多其他类型的故障,如影响单个节点的硬件或软件故障,是不太可能同时发生的故障类别,所以可能在您的集群中,每个主节点都有一个副本,副本在凌晨 4 点被杀死,主节点在凌晨 6 点被杀死。这仍然会导致无法继续操作的集群。

为了提高系统的可靠性,我们有向每个主节点添加额外副本的选项,但这很昂贵。副本迁移允许您只向少数主节点添加更多副本。所以您有 10 个主节点,每个主节点有 1 个副本,总共 20 个实例。然而,您添加了 3 个额外的实例作为一些主节点的副本,这样某些主节点将有不止一个副本。

副本迁移的工作方式是,如果一个主节点没有副本,那么具有多个副本的主节点上的副本将迁移到 孤立 的主节点。所以在上面的例子中,如果您的副本在凌晨 4 点下降,另一个副本将取代它的位置,当主节点也在凌晨 5 点失败时,仍然有一个副本可以被选举,以便集群可以继续操作。

关于副本迁移,您应该了解的简短信息是什么?

  • 集群将尝试从给定时刻副本数量最多的主节点迁移一个副本。
  • 要从副本迁移中受益,您只需向集群中的一个主节点添加几个更多的副本,它不重要是哪个主节点。
  • 有一个配置参数控制副本迁移功能,称为 cluster-migration-barrier:您可以在 Redict 集群提供的示例 redict.conf 文件中阅读更多关于它的信息。

在 Redict 集群中升级节点

升级副本节点很容易,因为您只需要停止节点,并使用更新版本的 Redict 重新启动它。如果客户端使用副本节点进行读取扩展,它们应该能够在不同的副本不可用时连接到另一个副本。

升级主节点有点更复杂,建议的程序是:

  1. 使用 CLUSTER FAILOVER 触发主节点到其一个副本的手动故障转移。(见本主题中的 手动故障转移。)
  2. 等待主节点变成副本。
  3. 最后像升级副本一样升级节点。
  4. 如果您希望主节点是您刚刚升级的节点,触发一个新的手动故障转移,将更新的节点变回主节点。

按照这个程序,您应该一个接一个地升级节点,直到所有节点都升级完毕。

迁移到 Redict 集群

想要迁移到 Redict 集群的用户可能只有一个主节点,或者可能已经使用现有的分片设置,其中键通过一些内部算法或客户端库或 Redict 代理实现的分片算法分布在 N 个节点上。

在这两种情况下,都可以轻松迁移到 Redict 集群,但最重要的细节是应用程序是否使用多键操作,以及如何使用。有三种不同的情况:

  1. 不使用多键操作、事务或涉及多个键的 Lua 脚本。键独立访问(即使通过事务或 Lua 脚本将多个命令,关于同一键,组合在一起)。
  2. 使用涉及多个键的多键操作、事务或 Lua 脚本,但只与具有相同哈希标签的键一起使用,这意味着一起使用的键都有相同的 {...} 子字符串。例如,以下多键操作定义在相同的哈希标签上下文中:SUNION {user:1000}.foo {user:1000}.bar
  3. 使用涉及多个键的操作、事务或 Lua 脚本,且键没有显式或相同的哈希标签。

Redict 集群不处理第三种情况:应用程序需要修改,以便不使用多键操作或只在相同的哈希标签上下文中使用它们。

情况 1 和 2 是被覆盖的,所以我们将关注这两个案例,它们以相同的方式处理,因此在文档中不会进行区分。

假设您有预先存在的数据集分布在 N 个主节点中,其中 N=1 如果您没有预先存在的分片,迁移数据集到 Redict 集群需要以下步骤:

  1. 停止您的客户端。目前不可能自动进行实时迁移到 Redict 集群。您可能能够在应用程序/环境中编排实时迁移。
  2. 使用 BGREWRITEAOF 命令为所有 N 个主节点生成一个仅附加文件,并等待 AOF 文件完全生成。
  3. 将您的 AOF 文件从 aof-1 到 aof-N 保存在某个地方。此时,如果您愿意,可以停止旧实例(这在非虚拟化部署中很有用,因为您通常需要重用相同的计算机)。
  4. 创建一个由 N 个主节点和 0 个副本组成的 Redict 集群。您稍后会添加副本。确保所有节点都使用仅附加文件进行持久化。
  5. 停止所有集群节点,用您预先存在的仅附加文件替换它们的 AOF 文件,第一个节点使用 aof-1,第二个节点使用 aof-2,依此类推,直到 aof-N。
  6. 使用新的 AOF 文件重启您的 Redict 集群节点。它们会抱怨有不应该在那里的键,根据它们的配置。
  7. 使用 redict-cli --cluster fix 命令修复集群,以便根据每个节点是否权威,将键迁移到相应的哈希槽。
  8. 最后使用 redict-cli --cluster check 确保您的集群没问题。
  9. 重新启动修改为使用 Redict 集群感知客户端库的客户端。

从外部实例导入数据到 Redict 集群的另一种方法是使用 redict-cli --cluster import 命令。

该命令将移动运行实例的所有键(从源实例中删除键)到指定的预先存在的 Redict 集群。

Redict 项目不再使用“slave”这个词汇。不幸的是,在这个命令中,“slave”这个词是协议的一部分,所以我们只能在这个 API 自然淘汰后才能移除这些用法。

添加新节点 #

添加新节点基本上是添加一个空节点的过程,然后移动一些数据到其中,如果是新主节点,或者告诉它设置为已知节点的副本,如果是副本。

我们将会展示两种情况,从添加新主节点实例开始。

在这两种情况下,首先执行的步骤是添加一个空节点

这就像在端口 7006 上启动一个新节点(我们已经使用 7000 到 7005 为我们现有的 6 个节点使用了端口),使用与其他节点相同的配置,除了端口号,所以您应该做的是:

  • 在终端应用程序中创建一个新的标签页。
  • 进入 cluster-test 目录。
  • 创建一个名为 7006 的目录。
  • 在其中创建一个 redict.conf 文件,与用于其他节点的文件类似,但使用 7006 作为端口号。
  • 最后使用 ../redict-server ./redict.conf 启动服务器。

此时服务器应该正在运行。

现在我们可以使用redict-cli像往常一样将节点添加到现有集群中。

  1. redict-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000

如您所见,我使用了add-node命令,将新节点的地址作为第一个参数,将集群中随机现有节点的地址作为第二个参数。

在实际操作中,redict-cli 这里帮了我们很少的忙,它只是向节点发送了一个 CLUSTER MEET 消息,这也是可以手动完成的。然而,redict-cli 在操作之前还会检查集群的状态,所以即使您知道内部工作原理,最好总是通过 redict-cli 执行集群操作。

现在我们可以连接到新节点,看看它是否真的加入了集群:

  1. redict 127.0.0.1:7006> cluster nodes
  2. 3e3a6cb0d9a9a87168e266b0a0b24026c0aae3f0 127.0.0.1:7001 master - 0 1385543178575 0 connected 5960-10921
  3. 3fc783611028b1707fd65345e763befb36454d73 127.0.0.1:7004 slave 3e3a6cb0d9a9a87168e266b0a0b24026c0aae3f0 0 1385543179583 0 connected
  4. f093c80dde814da99c5cf72a7dd01590792b783b :0 myself,master - 0 0 0 connected
  5. 2938205e12de373867bf38f1ca29d31d0ddb3e46 127.0.0.1:7002 slave 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 0 1385543178072 3 connected
  6. a211e242fc6b22a9427fed61285e85892fa04e08 127.0.0.1:7003 slave 97a3a64667477371c4479320d683e4c8db5858b1 0 1385543178575 0 connected
  7. 97a3a64667477371c4479320d683e4c8db5858b1 127.0.0.1:7000 master - 0 1385543179080 0 connected 0-5959 10922-11422
  8. 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 127.0.0.1:7005 master - 0 1385543177568 3 connected 11423-16383

请注意,由于这个节点已经连接到集群,它已经能够正确地重定向客户端查询,并且通常已经是集群的一部分。然而,与其它主节点相比,这个节点有两个特点:

  • 它不持有数据,因为它没有分配的哈希槽。
  • 因为它是一个没有分配槽的主节点,所以它不参与选举过程,当一个副本想要成为主节点时。

现在可以通过使用 redict-cli 的重新划分哈希槽功能来为这个节点分配哈希槽。这基本上和我们之前做的没有区别,只是重新划分的目标是空节点。

添加新节点作为副本

添加新副本可以通过两种方式完成。显而易见的方式是再次使用 redict-cli,但使用 –cluster-slave 选项,如下所示:

  1. redict-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000 --cluster-slave

请注意,这里的命令行与我们用来添加新主节点的命令行完全相同,所以我们没有指定要将副本添加到哪个主节点。在这种情况下,redict-cli 将随机选择主节点中的一个,并将新节点作为该主节点的副本添加。

但是,您可以使用以下命令行精确指定要为目标主节点添加新副本:

  1. redict-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000 --cluster-slave --cluster-master-id 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e

这样我们就为特定主节点分配了新的副本。

将副本添加到特定主节点的更手动的方法是将新节点作为空主节点添加,然后使用 CLUSTER REPLICATE 命令将其设置为已知节点的副本。如果节点被添加为副本,但您想将其移动为不同主节点的副本,这种方法也有效。

例如,为了添加一个副本来复制当前服务哈希槽范围 11423-16383 的节点 127.0.0.1:7005,该节点的节点 ID 为 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e,我需要做的就是连接到新节点(已经作为空主节点添加)并发送命令:

  1. redict 127.0.0.1:7006> cluster replicate 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e

就是这样。现在我们为这组哈希槽有了新的副本,所有集群中的其他节点在几秒钟后(需要更新它们的配置)都已经知道。我们可以使用以下命令验证:

  1. $ redict-cli -p 7000 cluster nodes | grep slave | grep 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e
  2. f093c80dde814da99c5cf72a7dd01590792b783b 127.0.0.1:7006 slave 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 0 1385543617702 3 connected
  3. 2938205e12de373867bf38f1ca29d31d0ddb3e46 127.0.0.1:7002 slave 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 0 1385543617198 3 connected

节点 3c3a0c… 现在有了两个副本,分别运行在端口 7002(现有的)和 7006(新的)。

移除节点 #

要移除副本节点,只需使用 redict-cli 的 del-node 命令:

  1. redict-cli --cluster del-node 127.0.0.1:7000 `<node-id>`

第一个参数只是集群中的一个随机节点,第二个参数是您要移除的节点的 ID。

您也可以以相同的方式移除主节点,但是为了移除主节点,它必须是空的。如果主节点不为空,您需要在移除之前将数据重新划分到所有其他主节点。

移除主节点的另一种替代方法是对其执行手动故障转移,将其变为副本,然后在它变成新主节点的副本后移除该节点。显然,这并不帮助当您想要减少集群中主节点的实际数量时,这种情况下,需要重新划分哈希槽。

您想要移除失败节点的特殊场景时,不应使用 del-node 命令,因为它会尝试连接到所有节点,如果给定节点不可用,您将遇到“连接拒绝”错误。相反,您可以使用 call 命令:

  1. redict-cli --cluster call 127.0.0.1:7000 cluster forget `<node-id>`

此命令将在每个节点上执行 CLUSTER FORGET 命令。

副本迁移 #

在 Redict 集群中,您可以随时使用以下命令重新配置副本以复制不同的主节点:

  1. CLUSTER REPLICATE <master-node-id>

但是有一种特殊场景,您可能希望副本在不需要系统管理员帮助的情况下自动从一台主节点迁移到另一台主节点。副本的自动重新配置称为 副本迁移,并且可以提高 Redict 集群的可靠性。

您可以通过 Redict 集群规范 阅读有关副本迁移的详细信息,这里我们只提供一些关于总体思路的信息以及您应该做什么以从中受益。

副本迁移的主要原因是,通常 Redict 集群对故障的抵抗力与给定主节点附加的副本数量一样多。

例如,每个主节点只有一个副本的集群不能在主节点和其副本同时失败时继续操作,因为没有其他实例可以拥有主节点正在服务的哈希槽的副本。然而,当网分割同时孤立了多个节点时,许多其他类型的故障,如影响单个节点的硬件或软件故障,是不太可能同时发生的故障类别,所以可能在您的集群中,每个主节点都有一个副本,副本在凌晨 4 点被杀死,主节点在凌晨 6 点被杀死。这仍然会导致无法继续操作的集群。

为了提高系统的可靠性,我们有向每个主节点添加额外副本的选项,但这很昂贵。副本迁移允许您只向少数主节点添加更多副本。所以您有 10 个主节点,每个主节点有 1 个副本,总共 20 个实例。然而,您添加了 3 个额外的实例作为一些主节点的副本,这样某些主节点将有不止一个副本。

副本迁移的工作方式是,如果一个主节点没有副本,那么具有多个副本的主节点上的副本将迁移到 孤立 的主节点。所以在上面的例子中,如果您的副本在凌晨 4 点下降,另一个副本将取代它的位置,当主节点也在凌晨 5 点失败时,仍然有一个副本可以被选举,以便集群可以继续操作。

关于副本迁移,您应该了解的简短信息是什么?

  • 集群将尝试从给定时刻副本数量最多的主节点迁移一个副本。
  • 要从副本迁移中受益,您只需向集群中的一个主节点添加几个更多的副本,它不重要是哪个主节点。
  • 有一个配置参数控制副本迁移功能,称为 cluster-migration-barrier:您可以在 Redict 集群提供的示例 redict.conf 文件中阅读更多关于它的信息。

在 Redict 集群中升级节点 #

升级副本节点很容易,因为您只需要停止节点,并使用更新版本的 Redict 重新启动它。如果客户端使用副本节点进行读取扩展,它们应该能够在不同的副本不可用时连接到另一个副本。

升级主节点有点更复杂,建议的程序是:

  1. 使用 CLUSTER FAILOVER 触发主节点到其一个副本的手动故障转移。(见本主题中的 手动故障转移。)
  2. 等待主节点变成副本。
  3. 最后像升级副本一样升级节点。
  4. 如果您希望主节点是您刚刚升级的节点,触发一个新的手动故障转移,将更新的节点变回主节点。

按照这个程序,您应该一个接一个地升级节点,直到所有节点都升级完毕。

迁移到 Redict 集群 #

愿意迁移到Redis集群的用户可能只有一个主节点,或者可能已经在使用预先存在的分片设置,其中键被分配到N个节点上,使用某些内部算法或由其客户端库或Redis代理实现的分片算法。

在这两种情况下,都可以轻松迁移到Redis集群,但最重要的细节是应用程序是否使用了多键操作,以及如何使用。有三种不同的情况:

  1. 不使用多键操作、事务或涉及多个键的Lua脚本。键是独立访问的(即使通过事务或Lua脚本将多个命令组合在一起,也是关于同一个键)。
  2. 使用涉及多个键的多键操作、事务或Lua脚本,但仅限于具有相同哈希标签的键,这意味着一起使用的键都有一个{...}子字符串,恰好是相同的。例如,以下多键操作定义在相同的哈希标签上下文中:SUNION {user:1000}.foo {user:1000}.bar
  3. 使用涉及多个键的多键操作、事务或Lua脚本,且键名没有明确或相同的哈希标签。

第三种情况Redis集群不处理:应用程序需要修改,以不使用多键操作,或仅在相同的哈希标签上下文中使用它们。

案例1和2被覆盖了,所以我们将专注于这两种情况,它们以相同的方式处理,因此在文档中不会进行区分。

假设您已经将现有的数据集分成了N个主节点,如果N=1则表示您没有预先存在的分片,迁移数据集到Redis集群需要以下步骤:

  1. 停止您的客户端。目前不可能自动实时迁移到Redis集群。您可能能够在应用程序/环境的上下文中编排实时迁移。
  2. 使用BGREWRITEAOF命令为所有N个主节点生成一个仅追加文件,并等待AOF文件完全生成。
  3. 将您的AOF文件从aof-1保存到aof-N的某个地方。此时,如果您愿意,可以停止您的旧实例(这很有用,因为在非虚拟化部署中,您通常需要重用相同的计算机)。
  4. 创建一个由N个主节点和零个副本组成的Redis集群。您稍后将添加副本。确保所有节点都使用仅追加文件进行持久化。
  5. 停止所有集群节点,用您预先存在的仅追加文件替换它们的仅追加文件,第一个节点用aof-1,第二个节点用aof-2,依此类推,直到aof-N。
  6. 使用新的AOF文件重新启动您的Redis集群节点。它们会抱怨有些键根据它们的配置不应该在那里。
  7. 使用redict-cli --cluster fix命令修复集群,以便根据每个节点是否权威,将键迁移到哈希槽中。
  8. 最后使用redict-cli --cluster check命令确保您的集群没有问题。
  9. 重新启动修改为使用Redis集群感知客户端库的客户端。

还有一种将数据从外部实例导入到Redis集群的替代方法,即使用redict-cli --cluster import命令。

该命令将移动一个正在运行实例的所有键(从源实例中删除键)到指定的预先存在的Redis集群。

Redis项目不再使用“slave”这个词。不幸的是,在该命令中,“slave”这个词是协议的一部分,所以我们只能在这个API自然被淘汰时才能移除这些情况。