使用 Redis 集群进行扩缩容

使用 Redis 集群进行横向扩缩容

Redis 开源版

Redis 通过一种称为 Redis 集群的部署拓扑结构实现横向扩缩容。本主题将教您如何在生产环境中设置、测试和运行 Redis 集群。您将从最终用户的角度了解 Redis 集群的可用性和一致性特性。

如果您计划运行生产环境的 Redis 集群部署,或者想更好地了解 Redis 集群的内部工作原理,请查阅Redis Cluster specification。要了解 Redis Enterprise 如何处理扩缩容,请参阅Linear Scaling with Redis Enterprise

Redis 集群 101

Redis 集群提供了一种运行 Redis 安装的方法,其中数据会自动在多个 Redis 节点之间分片。Redis 集群还在分区期间提供一定程度的可用性——实际上,能够在某些节点发生故障或无法通信时继续运行。然而,在发生较大故障(例如,大多数主节点不可用)的情况下,集群将变得不可用。

因此,通过 Redis 集群,您可以获得以下能力:

  • 自动将数据集分割到多个节点之间。
  • 在部分节点发生故障或无法与集群其余部分通信时继续运行。

Redis 集群 TCP 端口

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

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

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

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

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

Redis 集群与 Docker

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

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

为了使 Docker 与 Redis 集群兼容,您需要使用 Docker 的主机网络模式(host networking mode)。请查阅Docker documentation中的--net=host选项了解更多信息。

Redis 集群数据分片

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

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 为空,就可以将其完全从集群中移除。

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

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

哈希标签在 Redis Cluster specification 中有文档说明,但要点是如果在键中存在位于 {} 括号之间的子字符串,则只有该子字符串的内容会被用于哈希计算。例如,键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 集群不保证强一致性(strong consistency)。实际上,这意味着在某些条件下,Redis 集群可能会丢失系统已向客户端确认的写入。

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

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

如您所见,B 在回复客户端之前不会等待来自 B1、B2、B3 的确认,因为这对 Redis 来说将是巨大的延迟惩罚。因此,如果您的客户端写入了一些数据,B 确认了写入,但在能够将写入发送给其副本之前就崩溃了,那么其中一个(未收到写入的)副本可能会被提升为主节点,导致该写入永久丢失。

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

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

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

Redis 集群丢失写入的另一个值得注意的场景发生在网络分区期间,其中客户端与包括至少一个主节点在内的少数实例隔离。

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

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

Z1 仍然可以向 B 写入,B 将接受其写入。如果分区在很短时间内恢复,集群将正常继续。然而,如果分区持续足够长的时间,以至于 B1 在分区的大多数一侧被提升为主节点,则 Z1 在此期间发送到 B 的写入将丢失。

注意
Z1 能够向 B 发送的写入量存在一个最大时间窗口(maximum window):如果分区的大多数一侧已经经过足够长时间来选举一个副本作为主节点,则少数一侧的每个主节点将停止接受写入。

这段时间是 Redis 集群一个非常重要的配置指令,称为节点超时(node timeout)

节点超时时间过去后,主节点被认为是发生故障,可以由其副本之一替换。类似地,主节点在节点超时时间过去后无法感知到其他大多数主节点时,它会进入错误状态并停止接受写入。

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>:如果设置为零,副本将始终认为自身有效,因此无论主从连接断开的时间长短如何,它都会尝试对主节点进行故障转移。如果该值为正,则最大断开时间计算为节点超时(node timeout)值乘以该选项提供的因子,如果节点是副本,则如果主节点链接断开的时间超过指定时间,它将不会尝试启动故障转移。例如,如果节点超时设置为 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 集群的要求

要创建一个集群,您首先需要启动几个以集群模式(cluster mode)运行的空 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 集群实例生成,并在需要时更新。请注意,能够正常工作的最小集群(minimal cluster)必须包含至少三个主节点。对于部署,我们强烈建议使用六节点集群,包括三个主节点和三个副本。

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

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

例如:

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

在每个目录(从 7000 到 7005)内部创建一个redis.conf文件。作为配置文件的模板,只需使用上面的小示例,但请确保根据目录名称将端口号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(Node 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接受建议的配置。集群将被配置并加入(joined),这意味着实例将启动并相互通信。最后,如果一切顺利,您会看到类似这样的消息:

[OK] All 16384 slots covered

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

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

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

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

在步骤 2 中,当redis-cli工具要求您接受集群布局时,回复yes

您现在可以与集群交互,第一个节点默认将在端口 30001 启动。完成后,使用以下命令停止集群:

  1. create-cluster stop

请阅读此目录中的README文件,了解有关如何运行脚本的更多信息。

与集群交互

要连接到 Redis 集群,您需要一个集群感知(cluster-aware)的 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 集群对象,使用一组启动节点(startup nodes)作为参数,以及该对象允许连接到不同节点的最大连接数,最后是给定操作被视为失败的超时时间。

启动节点不需要是集群中的所有节点。重要的是至少有一个节点是可访问的。另请注意,redis-rb-cluster 在连接到第一个节点后会立即更新此启动节点列表。对于任何其他优秀的客户端,您都应该期待这种行为。

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

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

但是请注意,这是一个 while 循环,因为即使集群宕机并返回错误,我们也想一次又一次地尝试。普通的应用程序不需要如此小心。

第 28 行到 37 行之间开始主循环,其中设置键或显示错误。

请注意循环末尾的sleep调用。在您的测试中,如果您想尽可能快地写入集群,可以移除 sleep(当然,这相对于这是一个没有真正并行的忙循环,因此在最佳条件下您通常会获得 10k ops/second 的性能)。

通常会减慢写入速度,以便示例应用更容易被人理解。

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

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 个哈希槽,如果示例在没有 sleep 调用仍在运行,这应该已经包含相当多的键。

然后 redis-cli 需要知道重新分片的目标是什么,也就是将接收哈希槽的节点。我将使用第一个主节点,即 127.0.0.1:7000,但我需要指定该实例的节点 ID。redis-cli 已经在一个列表中打印过这个 ID,但如果需要,我总是可以使用以下命令找到节点的 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 选项指示集群管理器自动回答命令提示符的“yes”,使其可以在非交互模式下运行。请注意,此选项也可以通过设置 REDISCLI_CLUSTER_YES 环境变量来激活。

一个更有趣的示例应用

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

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

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

然而,该应用程序不仅仅是写入,还做了另外两件事

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

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

运行 consistency-test 应用程序每秒产生一行输出

$ 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**eads)和**写**(**W**rites)操作次数,以及错误次数(因系统不可用而未接受的查询)。

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

$ 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 集群的故障转移。

测试故障转移

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

注意
在此测试期间,您应该打开一个运行 consistency test 应用程序的终端选项卡。

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

$ 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

现在我们可以查看 consistency test 的输出来看看它报告了什么。

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
  • 标志:master, replica, myself, fail, ...
  • 如果是副本,则为主节点的节点 ID
  • 最后一次待处理的 PING 仍在等待回复的时间。
  • 最后一次接收到 PONG 的时间。
  • 此节点的配置 epoch(参见集群规范)。
  • 与此节点的链接状态。
  • 服务中的槽...

手动故障转移

有时强制进行故障转移而不在主节点上实际造成任何问题是很有用的。例如,要升级其中一个主节点的 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 命令将其转变为副本。如果节点已经作为副本添加,但您想将其移至另一个主节点的副本,这种方法也适用。

例如,为了给目前服务哈希槽范围 11423-16383 的节点 127.0.0.1:7005 添加一个副本,该节点的节点 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 命令,因为它会尝试连接所有节点,您将遇到“connection refused”错误。相反,您可以使用 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 脚本,但仅限于具有相同**哈希标签**(hash tag)的键,这意味着一起使用的键都包含一个相同的 {...} 子字符串。例如,以下多键操作是在相同哈希标签的上下文中定义的:SUNION {user:1000}.foo {user:1000}.bar
  3. 使用多键操作、事务或涉及多个键的 Lua 脚本,但键名没有显式的或相同的哈希标签。

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

情况 1 和 2 是支持的,所以我们将重点关注这两种情况,它们以相同的方式处理,因此文档中不再区分。

假设您已将现有数据集分割到 N 个主节点,如果之前没有分片,则 N=1。为了将您的数据集迁移到 Redis 集群,需要执行以下步骤

  1. 停止您的客户端。目前无法自动实时迁移到 Redis 集群。您可能可以在您的应用程序/环境上下文中协调实时迁移。
  2. 对所有 N 个主节点使用 BGREWRITEAOF 命令生成一个追加文件,并等待 AOF 文件完全生成。
  3. 将您的 AOF 文件(从 aof-1 到 aof-N)保存到某个位置。此时,如果您愿意,可以停止旧的实例(这很有用,因为在非虚拟化部署中,您经常需要重复使用相同的计算机)。
  4. 创建一个由 N 个主节点和零个副本组成的 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 自然废弃时才能删除这些用法。

了解更多

为本页评分
回到顶部 ↑