使用Redis集群进行扩展

使用Redis集群进行水平扩展

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

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

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

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

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

哈希标签在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 Cluster 不保证强一致性。在实际应用中,这意味着在某些情况下,Redis Cluster 可能会丢失已被系统确认给客户端的写入操作。

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

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

正如你所看到的,B在回复客户端之前不会等待B1、B2、B3的确认,因为这对Redis来说将是一个不可接受的延迟惩罚。因此,如果你的客户端写入了一些内容,B会确认写入,但在能够将写入发送到其副本之前崩溃,其中一个副本(未收到写入)可以被提升为主节点,导致写入永久丢失。

这与大多数配置为每秒将数据刷新到磁盘的数据库非常相似,因此这是一个您已经能够推理的场景,因为过去使用不涉及分布式系统的传统数据库系统的经验。同样,您可以通过强制数据库在回复客户端之前将数据刷新到磁盘来提高一致性,但这通常会导致性能低得令人无法接受。在Redis集群的情况下,这相当于同步复制。

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

Redis Cluster 在绝对需要时支持同步写入,通过 WAIT 命令实现。这使得丢失写入的可能性大大降低。然而,请注意,即使在使用同步复制时,Redis Cluster 也不实现强一致性:在更复杂的故障场景下,总是有可能选出一个未能接收写入的副本作为主节点。

还有一个值得注意的场景,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 Cluster在redis.conf文件中引入的配置参数。

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

在每个从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地址和端口可能会更改,但节点的唯一标识符在节点的整个生命周期内永远不会更改。我们简单地称此标识符为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 来接受提议的配置。 集群将被配置并 加入,这意味着实例将被引导以相互通信。最后,如果一切顺利,您将看到类似这样的消息:

[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

在步骤2中,当redis-cli工具要求您接受集群布局时,回复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 展示了两个 例子。 第一个如下,是 example.rb 文件在 redis-rb-cluster 发行版中:

   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,一个接一个。因此,如果你运行程序,结果将是以下命令流:

  • 设置 foo0 0
  • 设置 foo1 1
  • 设置 foo2 2
  • 依此类推...

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

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

启动节点不需要是集群的所有节点。重要的是至少有一个节点是可访问的。还要注意,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个哈希槽,如果示例仍在运行而没有调用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) |

该行显示了执行的Reads和Writes的数量,以及错误的数量(由于系统不可用而未被接受的查询)。

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

$ 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上。之前作为主节点的Redis实例,即运行在端口7002上的实例,现在成为了7005的副本。

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

  • 节点ID
  • ip:端口
  • flags: master, replica, myself, fail, ...
  • 如果是副本,主节点的节点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命令将其转换为副本。如果节点是作为副本添加的,但您希望将其移动为不同主节点的副本,这也适用。

例如,为了为当前服务于哈希槽范围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命令,因为它会尝试连接到所有节点,您会遇到“连接被拒绝”的错误。 相反,您可以使用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 Cluster提供的示例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个主节点和零个副本组成的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 ↑