使用 Redis 集群进行扩展

使用 Redis 集群进行横向扩展

Redis 使用称为 Redis 集群的部署拓扑进行横向扩展。本主题将教你如何在生产环境中设置、测试和操作 Redis 集群。您将从最终用户的角度了解 Redis 集群的可用性和一致性特性。

如果您计划运行生产 Redis 集群部署或想更好地了解 Redis 集群的内部工作原理,请参阅 Redis 集群规范。要了解 Redis 企业版如何处理扩展,请参阅 Redis 企业版的线性扩展.

Redis 集群 101

Redis 集群提供了一种运行 Redis 安装的方式,其中数据会自动在多个 Redis 节点之间进行分片。Redis 集群还提供了一定程度的可用性,即使在分区情况下也能正常运行 - 从实际角度来看,就是当某些节点发生故障或无法通信时,系统能够继续运行。但是,如果发生更大的故障(例如,大多数主节点都不可用),集群将变得不可用。

因此,使用 Redis 集群,您将获得以下能力:

  • 自动将您的数据集拆分到多个节点之间。
  • 当部分节点发生故障或无法与集群其他部分通信时,继续运行。

Redis 集群 TCP 端口

每个 Redis 集群节点都需要两个打开的 TCP 连接:一个用于为客户端提供服务的 Redis TCP 端口,例如 6379,另一个端口称为“集群总线端口”。默认情况下,集群总线端口通过在数据端口上加 10000 来设置(例如,16379);但是,您可以在 cluster-port 配置中覆盖此设置。

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

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

  1. 客户端通信端口(通常为 6379),用于与客户端通信,并对所有需要访问集群的客户端以及其他集群节点(用于键迁移)开放。
  2. 集群总线端口必须可以从所有其他集群节点访问。

如果您没有打开两个 TCP 端口,您的集群将无法正常工作。

Redis 集群和 Docker

目前,Redis 集群不支持 NAT 环境以及一般情况下 IP 地址或 TCP 端口被重新映射的环境。

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

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

Redis 集群数据分片

Redis 集群不使用一致性哈希,而是使用一种不同的分片形式,其中每个键在概念上都是我们称为“哈希槽”的一部分。

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

Redis 集群中的每个节点都负责一部分哈希槽,例如,您可能有一个包含 3 个节点的集群,其中:

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

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

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

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

哈希标签在 Redis 集群规范中进行了说明,但其要点是,如果键中存在 {} 括号之间的子字符串,则只对字符串内部进行哈希。例如,键 user:{123}:profileuser:{123}:account 保证位于同一个哈希槽中,因为它们共享相同的哈希标签。因此,您可以在同一个多键操作中对这两个键进行操作。

Redis 集群主从模型

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

在我们的示例集群中,节点 A、B、C 如果节点 B 发生故障,集群将无法继续运行,因为我们不再有办法为 5501-11000 范围内的哈希槽提供服务。

但是,在创建集群时(或在稍后时间),我们向每个主节点添加一个副本节点,因此最终集群由作为主节点的 A、B、C 和作为副本节点的 A1、B1、C1 组成。这样,如果节点 B 发生故障,系统可以继续运行。

节点 B1 复制 B,如果 B 发生故障,集群将提升节点 B1 为新的主节点,并将继续正常运行。

但是,请注意,如果节点 B 和 B1 同时发生故障,Redis 集群将无法继续运行。

Redis 集群一致性保证

Redis 集群不保证“强一致性”。从实际角度来看,这意味着在某些情况下,Redis 集群可能会丢失系统向客户端确认的写入操作。

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

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

如您所见,B 在回复客户端之前不会等待来自 B1、B2、B3 的确认,因为这会对 Redis 造成不可接受的延迟惩罚,因此,如果您的客户端写入了一些内容,B 会确认写入操作,但在能够将写入操作发送到其副本之前崩溃,那么其中一个副本(未收到写入操作的副本)可能会被提升为主节点,从而永远丢失写入操作。

这与大多数配置为每秒将数据刷新到磁盘的数据库类似,因此,您已经能够通过过去使用传统数据库系统(不涉及分布式系统)的经验来推断这种情况。类似地,您可以通过强制数据库在回复客户端之前将数据刷新到磁盘来提高一致性,但这通常会导致性能极低。这相当于 Redis 集群中的同步复制。

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

Redis 集群支持在绝对需要时进行同步写入,这是通过 WAIT 命令实现的。这使得丢失写入操作的可能性大大降低。但是,请注意,即使使用同步复制,Redis 集群也不实现强一致性:在更复杂的故障情况下,始终有可能选择一个未收到写入操作的副本作为主节点。

还有一种值得注意的情况,Redis 集群会在其中丢失写入操作,这种情况发生在网络分区期间,其中客户端与一小部分实例(至少包含一个主节点)隔离。

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

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

Z1 仍然能够写入 B,B 会接受其写入操作。如果分区在很短时间内恢复,集群将继续正常运行。但是,如果分区持续足够长的时间以至于 B1 在分区的大多数一侧被提升为主节点,那么 Z1 在此期间发送给 B 的写入操作将丢失。

注意
Z1 能够发送给 B 的写入操作数量有一个“最大窗口”:如果经过足够的时间,分区的大多数一侧已经选举出一个副本作为主节点,那么少数一侧的每个主节点都将停止接受写入操作。

这段时间是 Redis 集群的一个非常重要的配置指令,称为“节点超时”。

节点超时时间过去后,一个主节点将被视为故障节点,并且可以被其副本之一替换。类似地,如果节点超时时间过去后,主节点仍然无法感知到大多数其他主节点,它将进入错误状态并停止接受写入操作。

Redis 集群配置参数

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

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

创建并使用 Redis 集群

要创建和使用 Redis 集群,请执行以下步骤

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

创建 Redis 集群的要求

要创建集群,首先需要在集群模式下运行一些空的 Redis 实例。

至少在 redis.conf 文件中设置以下指令

port 7000
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes

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

请注意,最小的集群必须至少包含三个主节点才能按预期工作。对于部署,我们强烈建议使用六节点集群,三个主节点和三个副本。

您可以在本地测试,方法是在任何给定目录中创建以实例端口号命名的以下目录。

例如

mkdir cluster-test
cd cluster-test
mkdir 7000 7001 7002 7003 7004 7005

在每个目录中创建一个 redis.conf 文件,从 7000 到 7005。将上面的示例用作配置文件的模板,但请确保将端口号 7000 替换为与目录名称相对应的正确端口号。

您可以按如下方式启动每个实例,每个实例都在单独的终端选项卡中运行

cd 7000
redis-server ./redis.conf

您将在日志中看到每个节点为自己分配了一个新 ID

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

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

创建 Redis 集群

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

您可以手动配置和执行各个实例,也可以使用 create-cluster 脚本。让我们来看看如何手动进行操作。

要创建集群,请运行

redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 \
127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 \
--cluster-replicas 1

此处使用的命令是create,因为我们要创建一个新集群。选项 --cluster-replicas 1 表示我们希望为每个创建的主节点创建一个副本。

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

redis-cli 将提出一个配置。通过键入yes来接受提议的配置。集群将被配置并加入,这意味着实例将被引导到彼此通信。最后,如果一切顺利,您将看到类似以下的消息

[OK] All 16384 slots covered

这意味着至少有一个主节点实例正在为 16384 个可用插槽中的每一个提供服务。

如果您不想通过手动配置和执行各个实例来创建 Redis 集群,如上所述,有一个更简单的系统(但您将学不到那么多操作细节)。

在 Redis 分发版中找到 utils/create-cluster 目录。其中包含一个名为 create-cluster 的脚本(与包含它的目录同名),这是一个简单的 bash 脚本。要启动一个包含 3 个主节点和 3 个副本的 6 节点集群,只需键入以下命令

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

redis-cli 实用程序要求您接受集群布局时,在步骤 2 中回复 yes

您现在可以与集群交互,第一个节点默认情况下将从端口 30001 开始。完成操作后,使用以下命令停止集群

  1. create-cluster stop

有关如何运行该脚本的更多信息,请阅读此目录中的 README

与集群交互

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

您也可以使用 redis-cli 命令行实用程序来测试您的 Redis 集群

$ redis-cli -c -p 7000
redis 127.0.0.1:7000> set foo bar
-> Redirected to slot [12182] located at 127.0.0.1:7002
OK
redis 127.0.0.1:7002> set hello world
-> Redirected to slot [866] located at 127.0.0.1:7000
OK
redis 127.0.0.1:7000> get foo
-> Redirected to slot [12182] located at 127.0.0.1:7002
"bar"
redis 127.0.0.1:7002> get hello
-> Redirected to slot [866] located at 127.0.0.1:7000
"world"
注意
如果您使用脚本创建了集群,则您的节点可能在不同的端口上监听,默认情况下从 30001 开始。

redis-cli 集群支持非常基础,因此它始终利用 Redis 集群节点能够将客户端重定向到正确节点这一事实。一个严肃的客户端能够做得更好,并缓存哈希插槽和节点地址之间的映射,以直接使用与正确节点的正确连接。只有在集群配置发生更改时,例如在故障转移后或系统管理员通过添加或删除节点更改集群布局后,才会刷新映射。

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

在继续展示如何操作 Redis 集群之前,例如执行故障转移或分片,我们需要创建一个示例应用程序,或者至少能够理解简单的 Redis 集群客户端交互的语义。

这样,我们就可以运行示例,同时尝试使节点发生故障或启动分片,以查看 Redis 集群在真实世界条件下的行为。在没有人向集群写入数据的情况下,查看发生的事情并不十分有用。

本节说明了redis-rb-cluster 的一些基本用法,并展示了两个示例。第一个如下,是 redis-rb-cluster 分发版中的 example.rb 文件

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

该应用程序执行一项非常简单的事情,它将 foo<number> 形式的键设置为 number,一个接一个地设置。因此,如果您运行该程序,则结果将是以下命令流

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

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

第 14 行是程序中第一行有趣的代码。它创建 Redis 集群对象,使用启动节点列表、该对象允许对不同节点建立的最大连接数以及给定操作被认为失败后的超时时间作为参数。

启动节点不需要是集群中的所有节点。重要的是至少有一个节点可以连接。还要注意,redis-rb-cluster 只要能够连接到第一个节点,就会更新此启动节点列表。您应该期望任何其他严肃的客户端都具有这种行为。

现在我们已将 Redis 集群对象实例存储在 rc 变量中,就可以像使用普通 Redis 对象实例一样使用该对象了。

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

但是请注意,它是一个 while 循环,因为我们希望即使集群停机并返回错误也尝试再次尝试。正常的应用程序不需要这么小心。

第 28 到 37 行之间的代码开始执行设置键或显示错误的主循环。

请注意循环末尾的 sleep 调用。在您的测试中,如果您希望尽可能快地向集群写入数据(相对于这是一个没有真正并行性的繁忙循环的事实,因此您将获得最佳条件下的通常 10k 次操作/秒),可以删除 sleep 调用。

通常,写入速度会降低,以便使示例应用程序更易于人类理解。

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

ruby ./example.rb
1
2
3
4
5
6
7
8
9
^C (I stopped the program here)

这不是一个非常有趣的程序,我们将在稍后使用一个更好的程序,但我们已经可以看到在分片过程中发生了什么,以及程序运行时发生了什么。

对集群进行分片

现在,我们准备尝试对集群进行分片。为此,请保持 example.rb 程序运行,这样您就可以看到程序运行时是否会产生一些影响。此外,您可能希望注释掉 sleep 调用,以便在分片过程中获得更多严重的写入负载。

分片基本上意味着将哈希插槽从一组节点移动到另一组节点。与集群创建一样,它是使用 redis-cli 实用程序完成的。

要启动分片,只需键入

redis-cli --cluster reshard 127.0.0.1:7000

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

目前,redis-cli 只能在管理员支持下进行分片,您不能只是说将 5% 的插槽从这个节点移动到另一个节点(但这在实现上相当简单)。因此它会以问题开头。第一个问题是您想进行多少分片

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

我们可以尝试对 1000 个哈希插槽进行分片,如果 example 仍在运行并且没有 sleep 调用,那么这应该已经包含相当数量的键。

然后,redis-cli 需要知道分片的目标,即接收哈希插槽的节点。我将使用第一个主节点,即 127.0.0.1:7000,但我需要指定该实例的节点 ID。这已在 redis-cli 列出的列表中打印出来,但我始终可以使用以下命令找到节点的 ID(如果需要)

$ redis-cli -p 7000 cluster nodes | grep myself
97a3a64667477371c4479320d683e4c8db5858b1 :0 myself,master - 0 0 0 connected 0-5460

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

现在,您将被问到您要从哪些节点获取这些键。我将只键入 all,以便从所有其他主节点中获取一些哈希插槽。

最终确认后,您将看到每条关于redis-cli将从一个节点移动到另一个节点的插槽的消息,并且每实际从一个节点移动到另一个节点的键都会打印一个点。

在重新分片进行期间,您应该能够看到您的示例程序不受影响地运行。 如果需要,您可以在重新分片期间多次停止并重新启动它。

在重新分片结束时,您可以使用以下命令测试集群的健康状况

redis-cli --cluster check 127.0.0.1:7000

所有插槽将像往常一样被覆盖,但这次在 127.0.0.1:7000 的主服务器将拥有更多哈希插槽,大约 6461 个。

重新分片可以自动执行,无需以交互方式手动输入参数。 这是使用以下命令行实现的

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

如果您经常重新分片,这将有助于构建一些自动化机制,但是目前 redis-cli 无法自动重新平衡集群,检查跨集群节点的键的分布并根据需要智能地移动插槽。 此功能将在未来添加。

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

更有趣的示例应用程序

我们之前编写的示例应用程序不是很好。 它以一种简单的方式写入集群,甚至不检查写入的内容是否正确。

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

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

但是,除了写入之外,应用程序还执行了另外两件事

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

这意味着此应用程序是一个简单的 **一致性检查器**,并且能够告诉您集群是否丢失了一些写入操作,或者是否接受了我们未收到确认的写入操作。 在第一种情况下,我们将看到计数器的值小于我们记住的值,而在第二种情况下,该值将更大。

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

$ ruby consistency-test.rb
925 R (0 err) | 925 W (0 err) |
5030 R (0 err) | 5030 W (0 err) |
9261 R (0 err) | 9261 W (0 err) |
13517 R (0 err) | 13517 W (0 err) |
17780 R (0 err) | 17780 W (0 err) |
22025 R (0 err) | 22025 W (0 err) |
25818 R (0 err) | 25818 W (0 err) |

该行显示执行的 **R**ead 和 **W**rite 操作数量,以及错误数量(由于系统不可用而导致的查询未被接受)。

如果发现一些不一致,则会将新行添加到输出中。 例如,如果我在程序运行时手动重置计数器,就会发生这种情况

$ redis-cli -h 127.0.0.1 -p 7000 set key_217 0
OK

(in the other tab I see...)

94774 R (0 err) | 94774 W (0 err) |
98821 R (0 err) | 98821 W (0 err) |
102886 R (0 err) | 102886 W (0 err) | 114 lost |
107046 R (0 err) | 107046 W (0 err) | 114 lost |

当我将计数器设置为 0 时,真实值为 114,因此程序报告了 114 个丢失的写入操作(INCR 命令,集群未记住)。

此程序作为测试用例更有趣,因此我们将使用它来测试 Redis 集群故障转移。

测试故障转移

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

注意
在此测试期间,您应该打开一个选项卡,运行一致性测试应用程序。

我们可以使用以下命令识别主服务器并使其崩溃

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

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

$ redis-cli -p 7002 debug segfault
Error: Server closed the connection

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

18849 R (0 err) | 18849 W (0 err) |
23151 R (0 err) | 23151 W (0 err) |
27302 R (0 err) | 27302 W (0 err) |

... many error warnings here ...

29659 R (578 err) | 29660 W (577 err) |
33749 R (578 err) | 33750 W (577 err) |
37918 R (578 err) | 37919 W (577 err) |
42077 R (578 err) | 42078 W (577 err) |

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

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

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

现在主服务器在端口 7000、7001 和 7005 上运行。 以前的主服务器,即在端口 7002 上运行的 Redis 实例,现在是 7005 的副本。

CLUSTER NODES 命令的输出可能看起来很吓人,但实际上非常简单,由以下标记组成

  • 节点 ID
  • ip:port
  • 标志:主服务器、副本、我自己、故障等
  • 如果是副本,则为主服务器的节点 ID
  • 最后一个待处理的 PING 仍在等待回复的时间。
  • 接收到的最后一个 PONG 的时间。
  • 此节点的配置纪元(请参阅集群规范)。
  • 到此节点的链接状态。
  • 提供的插槽...

手动故障转移

有时,强制执行故障转移而不会对主服务器造成任何问题很有用。 例如,要升级其中一个主节点的 Redis 进程,最好对其进行故障转移,将其转变为副本,对可用性的影响最小。

Redis 集群使用 CLUSTER FAILOVER 命令支持手动故障转移,该命令必须在要进行故障转移的主服务器的其中一个副本中执行。

手动故障转移是特殊的,与实际主服务器故障导致的故障转移相比更安全。 它们以一种避免数据丢失的方式发生,通过仅在系统确定新主服务器处理了来自旧主服务器的所有复制流时才将客户端从原始主服务器切换到新主服务器。

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

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

基本上,连接到我们要进行故障转移的主服务器的客户端将被停止。 同时,主服务器将它的复制偏移量发送到副本,该副本等待在它的一侧达到偏移量。 当复制偏移量达到时,故障转移开始,旧的主服务器会被告知配置切换。 当客户端在旧的主服务器上被解除阻塞时,它们会被重定向到新的主服务器。

注意
要将副本提升为主服务器,它必须首先被集群中大多数主服务器识别为副本。 否则,它无法赢得故障转移选举。 如果副本刚刚被添加到集群中(请参阅 添加一个新的节点作为副本),您可能需要等待一段时间才能发送 CLUSTER FAILOVER 命令,以确保集群中的主服务器已知晓新的副本。

添加新节点

添加新节点基本上是添加一个空节点,然后将一些数据移动到该节点中(如果它是新的主服务器),或者告诉它设置成已知节点的副本(如果它是副本)。

我们将从添加新的主服务器实例开始展示这两个步骤。

在这两种情况下,要执行的第一步是 **添加一个空节点**。

这很简单,只要在端口 7006(我们已经为现有的 6 个节点使用了从 7000 到 7005 的端口)启动一个新节点,并使用与其他节点相同的配置,只是端口号不同,因此您需要执行的操作来符合我们为之前的节点使用的设置

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

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

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

redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000

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

实际上,redis-cli 在这里所做的工作很少,它只是向该节点发送了一个 CLUSTER MEET 消息,这也可以手动完成。 但是 redis-cli 也会在操作之前检查集群的状态,因此建议始终通过 redis-cli 执行集群操作,即使您了解内部工作原理。

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

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

请注意,由于此节点已经连接到集群,因此它已经能够正确地重定向客户端查询,并且通常来说是集群的一部分。 但是,它与其他主服务器相比有两个特点

  • 它没有数据,因为它没有分配的哈希插槽。
  • 因为它是一个没有分配插槽的主服务器,所以它不会参与副本想要成为主服务器时的选举过程。

现在,可以使用 redis-cli 的重新分片功能将哈希插槽分配给此节点。 展示这一点基本上没有意义,因为我们已经在上一节中进行了展示,没有区别,它只是一个将空节点作为目标的重新分片操作。

将新节点添加为副本

添加新副本可以采取两种方式。 显然的一种方式是再次使用 redis-cli,但使用 --cluster-slave 选项,如下所示

redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000 --cluster-slave

请注意,这里的命令行与我们用于添加新主服务器的命令行完全相同,因此我们没有指定要将副本添加到哪个主服务器。 在这种情况下,会发生的是 redis-cli 将新节点添加到副本数量最少的几个主服务器中的一个主服务器的副本。

但是,您可以使用以下命令行指定要将新副本目标定位到哪个主服务器

redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000 --cluster-slave --cluster-master-id 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e

这样,我们就将新副本分配给特定的主服务器。

将副本手动添加到特定主服务器的另一种方式是将新节点添加为空主服务器,然后使用 CLUSTER REPLICATE 命令将其转换为副本。 如果节点被添加为副本,但您想将其移动到另一个主服务器的副本,这也有效。

例如,为了为节点 127.0.0.1:7005 添加一个副本,该节点当前正在服务 11423-16383 范围内的哈希插槽,并且具有节点 ID 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e,我需要做的就是连接到新节点(已添加为空主服务器)并发送以下命令

redis 127.0.0.1:7006> cluster replicate 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e

就是这样。 现在,我们为此组哈希插槽有了新的副本,并且集群中的所有其他节点都已经知道(经过几秒钟的配置更新时间)。 我们可以使用以下命令进行验证

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

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

删除节点

要删除副本节点,只需使用 redis-cli 的 del-node 命令即可

redis-cli --cluster del-node 127.0.0.1:7000 `<node-id>`

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

您也可以以相同的方式删除主服务器节点,**但是要删除主服务器节点,它必须为空**。 如果主服务器不是空的,您需要在重新分片数据到所有其他主服务器节点之后才能进行删除。

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

有一种特殊情况,您需要移除一个故障节点。您不应该使用 `del-node` 命令,因为它会尝试连接到所有节点,并且您将遇到“连接被拒绝”错误。相反,您可以使用 `call` 命令。

redis-cli --cluster call 127.0.0.1:7000 cluster forget `<node-id>`

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

副本迁移

在 Redis 集群中,您可以随时使用此命令重新配置一个副本以复制到不同的主节点。

CLUSTER REPLICATE <master-node-id>

但是,在一种特殊情况下,您希望副本在没有系统管理员帮助的情况下,自动从一个主节点迁移到另一个主节点。副本的自动重新配置称为*副本迁移*,它可以提高 Redis 集群的可靠性。

注意
您可以在 Redis 集群规范 中阅读有关副本迁移的详细信息,这里我们只提供有关一般概念和如何从中受益的一些信息。

您可能希望在特定条件下让集群副本从一个主节点迁移到另一个主节点的原因是,通常 Redis 集群的故障承受能力与附加到给定主节点的副本数量相同。

例如,如果每个主节点只有一个副本,那么集群无法在主节点及其副本同时发生故障的情况下继续运行,因为没有其他实例可以拥有主节点正在服务的哈希槽的副本。但是,虽然网络分割可能会同时隔离多个节点,但许多其他类型的故障,如单个节点上的硬件或软件故障,是一种非常值得注意的故障类型,不太可能同时发生,因此在每个主节点都有副本的集群中,副本可能在凌晨 4 点被杀死,而主节点在凌晨 6 点被杀死。这仍然会导致集群无法继续运行。

为了提高系统的可靠性,我们可以选择为每个主节点添加额外的副本,但这很昂贵。副本迁移允许只为少数主节点添加更多副本。因此,您有 10 个主节点,每个主节点有 1 个副本,总共 20 个实例。但是,您添加了例如 3 个实例作为某些主节点的副本,因此某些主节点将拥有超过一个副本。

使用副本迁移,如果一个主节点没有副本,那么来自拥有多个副本的主节点的一个副本将迁移到*孤立的*主节点。因此,在您在上面的示例中,副本在凌晨 4 点关闭后,另一个副本将取代它,当主节点在凌晨 5 点也发生故障时,仍然有一个副本可以被选为领导者,以便集群可以继续运行。

那么关于副本迁移您应该了解什么呢?

  • 集群将尝试从在给定时刻拥有最多副本的主节点迁移一个副本。
  • 要从副本迁移中获益,您只需在集群中的一个主节点上添加几个额外的副本即可,具体添加哪个主节点并不重要。
  • 有一个控制副本迁移功能的配置参数,名为 `cluster-migration-barrier`:您可以在 Redis 集群提供的示例 `redis.conf` 文件中阅读有关它的更多信息。

升级 Redis 集群中的节点

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

升级主节点稍微复杂一些,建议的操作步骤是

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

按照此过程,您应该逐个升级节点,直到所有节点都升级完毕。

迁移到 Redis 集群

想要迁移到 Redis 集群的用户可能只有一个主节点,或者可能已经在使用预先存在的碎片设置,其中键使用某种内部算法或由其客户端库或 Redis 代理实现的碎片算法,在 N 个节点之间进行分割。

在这两种情况下,都可以轻松迁移到 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 个主节点和 0 个副本组成的 Redis 集群。您将在稍后添加副本。确保所有节点都使用追加日志文件来进行持久化。
  5. 停止所有集群节点,用您预先存在的追加日志文件替换它们的追加日志文件,第一个节点使用 aof-1,第二个节点使用 aof-2,一直到 aof-N。
  6. 使用新的 AOF 文件重新启动 Redis 集群节点。它们会抱怨根据配置不应该存在键。
  7. 使用 `redis-cli --cluster fix` 命令修复集群,以便根据每个节点是否有权或没有权的哈希槽迁移键。
  8. 最后使用 `redis-cli --cluster check` 确保您的集群正常。
  9. 重新启动修改后的客户端,以便使用 Redis 集群感知的客户端库。

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

该命令将运行实例的所有键(从源实例中删除键)移动到指定的预先存在的 Redis 集群。但是请注意,如果您使用 Redis 2.8 实例作为源实例,则操作可能很慢,因为 2.8 没有实现迁移连接缓存,因此您可能需要在执行此类操作之前使用 Redis 3.x 版本重新启动源实例。

注意
从 Redis 5 开始,如果不是为了向后兼容,Redis 项目不再使用 slave 这个词。不幸的是,在这个命令中,slave 这个词是协议的一部分,所以我们只能在该 API 自然被弃用时才能删除这些出现。

了解更多

RATE THIS PAGE
Back to top ↑