Redis集群教程

>>强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

本文档是对Redis Cluster的简要介绍,其中没有使用难以理解的分布式系统概念。它提供了有关如何设置集群,测试和操作集群的说明,而没有涉及Redis集群规范中涵盖的细节,而只是从用户的角度描述了系统的行为。

但是,本教程尝试从最终用户的角度提供有关Redis Cluster的可用性和一致性特征的信息,并以一种易于理解的方式进行陈述。

请注意,本教程需要Redis 3.0或更高版本。

如果您打算运行认真的Redis Cluster部署,则建议您阅读更为正式的规范,即使并非严格要求也是如此。但是,最好从本文档开始,与Redis Cluster一起玩一些时间,然后再阅读规范。

Redis集群101

Redis Cluster提供了一种运行Redis安装的方法,在该安装中,数据会在多个Redis节点之间自动分片。

Redis Cluster在分区期间还提供了一定程度的可用性,这实际上就是在某些节点出现故障或无法通信时继续操作的能力。但是,如果发生较大故障(例如,大多数主服务器不可用时),群集将停止运行。

因此,实际上,Redis Cluster能带来什么?

自动在多个节点之间拆分数据集的能力。
当一部分节点出现故障或无法与其余群集通信时,可以继续操作。

Redis群集TCP端口

每个Redis群集节点都需要打开两个TCP连接。用于服务客户端的常规Redis TCP端口,例如6379,加上通过将10000添加到数据端口而获得的端口,因此在示例中为16379。

第二个高端口用于群集总线,即使用二进制协议的节点到节点通信通道。节点将群集总线用于故障检测,配置更新,故障转移授权等。客户端永远不要尝试与群集总线端口进行通信,而应始终与普通的Redis命令端口进行通信,但是请确保您同时打开防火墙中的两个端口,否则Redis群集节点将无法进行通信。

命令端口和集群总线端口偏移是固定的,并且始终为10000。

请注意,对于每个节点,要使Redis群集正常工作,您需要:

普通客户端通信端口(通常为6379)用于与客户端通信,以向需要访问群集的所有客户端以及所有其他群集节点(使用客户端端口进行密钥迁移)开放。
群集总线端口(客户端端口+ 10000)必须可以从所有其他群集节点访问。
如果您没有同时打开两个TCP端口,则您的群集将无法正常工作。

集群总线使用不同的二进制协议进行节点到节点的数据交换,它更适合于使用很少的带宽和处理时间在节点之间交换信息。

Redis集群和Docker

当前,Redis Cluster不支持NATted环境以及在重新映射IP地址或TCP端口的常规环境中。

Docker使用一种称为端口映射的技术:与该程序认为正在使用的端口相比,在Docker容器内运行的程序可能会使用不同的端口公开。为了在同一服务器上同时使用同一端口运行多个容器,这很有用。

为了使Docker与Redis Cluster兼容,您需要使用Docker的主机联网模式。请检查Docker文档中的--net = host选项以获取更多信息。

Redis集群数据分片

Redis Cluster不使用一致的哈希,而是使用一种不同形式的分片,其中每个键从概念上讲都是我们称为哈希槽的一部分。

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 Cluster就支持多种键操作。用户可以通过使用称为哈希标签的概念来强制多个密钥成为同一哈希槽的一部分。

哈希标签记录在Redis Cluster规范中,但是要点是,如果密钥的{}括号之间有一个子字符串,则仅对字符串中的内容进行哈希处理,例如,此{foo}键和另一个{foo} key保证在同一哈希槽中,并且可以在以多个key作为参数的命令中一起使用。

Redis 集群主从模型

为了在主节点子集出现故障或无法与大多数节点通信时保持可用,Redis Cluster使用主从模型,其中每个哈希槽具有从1(主节点本身)到N个副本(N -1个其他从属节点)。

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

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

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

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

Redis群集一致性保证

Redis Cluster无法保证强一致性。实际上,这意味着在某些情况下,Redis Cluster可能会丢失系统已确认给客户端的写入。

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

  • 您的客户端写入主节点B。

  • 主节点B向您的客户端答复“确定”。

  • 主节点B将写操作传播到其从节点B1,B2和B3。

如您所见,B在回复客户端之前不会等待B1,B2,B3的确认,因为这会对Redis造成极高的延迟影响,因此,如果您的客户端写了一些东西,B会确认写,但是在崩溃之前崩溃由于能够将写操作发送到其从属设备,因此一个从属设备(未接收到写操作)可以提升为主设备,从而永远丢失写操作。

这与配置为每秒将数据刷新到磁盘的大多数数据库所发生的情况非常相似,因此由于过去使用不涉及分布式系统的传统数据库系统的经验,您已经可以对此进行推理。同样,您可以通过强制数据库在答复客户端之前将数据刷新到磁盘来提高一致性,但这通常会导致性能过低。在Redis Cluster的情况下,这相当于同步复制。

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

Redis Cluster在绝对需要时支持通过WAIT命令实现的同步写入。这使丢失写入的可能性大大降低。但是,请注意,即使使用同步复制,Redis Cluster也无法实现强一致性:在更复杂的故障情况下,总是有可能将无法接收写入的从设备选为主服务器。

还有另一种值得注意的情况,Redis Cluster将丢失写操作,这种情况发生在网络分区期间,在该分区中,客户端与少数实例(至少包括主实例)隔离。

以我们的6个节点集群为例,该集群由A,B,C,A1,B1,C1组成,具有3个主节点和3个从节点。还有一个客户,我们将其称为Z1。

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

Z1仍然能够写入B,它将接受其写入。如果分区在很短的时间内恢复正常,则群集将继续正常运行。但是,如果分区持续的时间足以使B1升级到该分区的多数端上的Master,则Z1同时发送给B的写操作将丢失。

请注意,Z1将能够发送给B的写入量有一个最大的窗口:如果已经有足够的时间让分区的多数派选举出一个从属主机,则少数派的每个主节点都将停止。接受写。

此时间量是Redis Cluster的一个非常重要的配置指令,称为节点超时。

节点超时过去之后,将主节点视为发生故障,并且可以用其副本之一替换该主节点。类似地,在没有主节点能够感知其他大多数主节点的节点超时之后,它进入错误状态并停止接受写操作。

Redis集群配置参数

我们将创建一个示例集群部署。在继续之前,让我们在Redis.conf文件中介绍Redis Cluster引入的配置参数。当您继续阅读时,有些会很明显,有些会更清晰。

  • cluster-enabled <yes/no>:如果yes,请在特定的Redis实例中启用Redis Cluster支持。否则,该实例将像往常一样作为独立实例启动。

  • cluster-config-file : 请注意,尽管有此选项的名称,但它不是用户可编辑的配置文件,而是Redis Cluster节点每次发生更改时都会自动保留集群配置(基本上是状态)的文件,以便能够 在启动时重新阅读。该文件列出了诸如群集中其他节点之类的内容,它们的状态,持久变量等等。通常,由于收到某些消息,此文件将被重写并刷新到磁盘上。

  • cluster-node-timeout : Redis群集节点不可用的最长时间(不视为失败)。如果主节点无法访问的时间超过指定的时间量,则其主节点将对其进行故障转移。此参数控制Redis Cluster中的其他重要事项。值得注意的是,在指定的时间内无法到达大多数主节点的每个节点都将停止接受查询。

  • cluster-slave-validity-factor :如果设置为零,则从站将始终认为自己有效,因此将始终尝试对主机进行故障转移,而不管主机和从站之间的链路保持断开状态的时间长短。如果该值为正,则将最大断开时间计算为节点超时值乘以此选项提供的因子,如果该节点是从节点,则如果主链接断开了更多连接,它将不会尝试启动故障转移。超过指定的时间。例如,如果节点超时设置为5秒,而有效性因子设置为10,则与主服务器断开连接的从服务器超过50秒将不会尝试对其主服务器进行故障转移。请注意,如果没有从属设备可以对其进行故障转移,则任何不为零的值都可能导致Redis Cluster在主服务器发生故障后不可用。在这种情况下,只有当原始主服务器重新加入集群后,集群才会恢复可用。

  • cluster-migration-barrier : 一个主站将保持连接的最小数量的从站,以便另一个从站迁移到不再受任何从站覆盖的主站。有关更多信息,请参见本教程中有关副本迁移的相应部分。

  • cluster-require-full-coverage <yes/no>: 如果将其设置为“yes”(默认情况下为默认值),则在任何节点未覆盖一定比例的键空间的情况下,群集将停止接受写入。如果该选项设置为no,即使仅可以处理有关键子集的请求,群集仍将提供查询。

  • cluster-allow-reads-when-down <yes/no>: 如果将其设置为no(默认情况下为no),则当Redis群集中的节点被标记为失败时,或者当节点无法达到法定主机数量或未达到全覆盖时,该节点将停止为所有流量提供服务。遇见。这样可以防止从不知道群集更改的节点读取可能不一致的数据。可以将此选项设置为yes,以允许在失败状态期间从节点进行读取,这对于希望优先考虑读取可用性但仍希望防止写入不一致的应用程序很有用。当仅使用一个或两个分片的Redis Cluster时,也可以使用它,因为它允许节点在主服务器发生故障但无法进行自动故障转移时继续为写入提供服务。

创建和使用Redis集群

注意:要手动部署Redis群集,了解其某些操作方面非常重要。但是,如果要尽快建立集群并运行(尽快),请跳过本节和下一节,直接转到使用create-cluster脚本创建Redis集群。

要创建集群,我们需要做的第一件事就是让一些空的Redis实例在集群模式下运行。基本上,这意味着不要使用普通的Redis实例创建群集,因为需要配置特殊模式,以便Redis实例将启用特定于群集的功能和命令。

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

如您所见,启用集群模式的只是启用集群的指令。每个实例还包含该节点的配置存储位置的文件路径,默认情况下为nodes.conf。该文件永远不会被人类触及。它仅在启动时由Redis Cluster实例生成,并在需要时进行更新。

请注意,按预期工作的最小群集至少需要包含三个主节点。对于您的第一个测试,强烈建议启动一个由三个主节点和三个从节点组成的六个节点群集。

为此,输入一个新目录,并创建以下目录,该目录以我们将在任何给定目录中运行的实例的端口号命名。

就像是:

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

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

现在,将您的redis-server可执行文件(从GitHub不稳定分支中的最新资源编译而来)复制到cluster-test目录中,最后在您喜欢的终端应用程序中打开6个终端选项卡。
像这样启动每个实例,每个选项卡一个:

cd 7000
../redis-server ./redis.conf

从每个实例的日志中可以看到,由于不存在nodes.conf文件,因此每个节点都会为其分配一个新的ID。

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

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

创建集群

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

如果您使用的是Redis 5或更高版本,这很容易实现,这是因为redis-cli中嵌入了Redis Cluster命令行实用程序,我们可以使用它来创建新集群,检查或重新分片现有集群等等。向前。

对于Redis版本3或4,有一个称为redis-trib.rb的旧工具,它非常相似。您可以在Redis源代码分发的src目录中找到它。您需要安装redis gem才能运行redis-trib。

gem install redis

第一个示例,即集群创建,将在Redis 5中使用redis-cli以及在Redis 3和4中使用redis-trib来显示。但是,接下来的所有示例都将仅使用redis-cli,因为您可以看到 语法非常相似,您可以通过使用redis-trib.rb help来获取有关旧语法的信息,从而将一个命令行简单地更改为另一命令行。重要:请注意,如果需要,可以对Redis 4集群使用Redis 5 redis-cli。

要使用redis-cli为Redis 5创建集群,只需键入:

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

将redis-trib.rb用于Redis 4或3:

./redis-trib.rb create --replicas 1 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

此处使用的命令是create,因为我们要创建一个新集群。选项--cluster-replicas 1表示我们希望每个创建的主服务器都具有一个从服务器。其他参数是我要用于创建新集群的实例的地址列表。

显然,满足我们要求的唯一设置是创建具有3个主设备和3个从设备的集群。

Redis-cli将为您提出配置。输入是,接受建议的配置。集群将被配置并加入,这意味着实例将被引导成彼此对话。最后,如果一切顺利,您将看到类似的消息:

[OK] All 16384 slots covered

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

使用create-cluster脚本创建Redis集群
如果您不想如上所述通过手动配置和执行单个实例来创建Redis集群,则可以使用一个简单得多的系统(但您不会学到相同数量的操作细节)。

只需检查Redis发行版中的utils / create-cluster目录。内部有一个名为create-cluster的脚本(名称与包含在其中的目录相同),它是一个简单的bash脚本。为了启动具有3个主节点和3个从节点的6节点群集,只需键入以下命令:

create-cluster start
create-cluster create

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

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

create-cluster stop.

请阅读该目录下的自述文件,以获取有关如何运行脚本的更多信息。

玩集群

在此阶段,Redis Cluster的问题之一是缺少客户端库的实现。我知道以下实现:

  • redis-rb-cluster是我(@antirez)编写的Ruby实现,可作为其他语言的参考。它是原始redis-rb的简单包装,实现了最小语义以有效地与集群通信。

  • edis-py-cluster redis-rb-cluster到Python的端口。支持大多数redis-py功能。正在积极发展中。

  • 流行的Predis支持Redis Cluster,该支持最近已更新并且正在积极开发中。

  • 使用最频繁的Java客户端Jedis最近添加了对Redis Cluster的支持,请参阅项目README中的Jedis Cluster部分。

  • StackExchange.Redis提供对C#的支持(并且应该可以与大多数.NET语言,VB,F#等配合使用)

  • thunk-redis提供对Node.js和io.js的支持,它是基于thunk / promise的redis客户端,具有管道和集群。

  • redis-go-cluster是使用Redigo库客户端作为基本客户端的Go语言Redis集群的实现。通过结果聚合实现MGET / MSET。

  • ioredis是流行的Node.js客户端,为Redis Cluster提供了强大的支持。

  • 当使用-c开关启动时,redis-cli实用程序将实现基本的集群支持。

测试Redis Cluster的一种简单方法是尝试上述任何客户端,或者只是redis-cli命令行实用程序。以下是使用后者进行交互的示例:

$ 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:7000> get hello
-> Redirected to slot [866] located at 127.0.0.1:7000
"world"

注意:如果使用脚本创建集群,则节点可能会监听不同的端口,默认情况下从30001开始。

redis-cli集群支持非常基础,因此它始终使用以下事实:Redis Cluster节点能够将客户端重定向到正确的节点。认真的客户可以做得更好,并且可以在哈希槽和节点地址之间缓存映射,以直接使用与正确节点的正确连接。仅在集群配置中发生某些更改时(例如,在故障转移之后或系统管理员通过添加或删除节点来更改集群布局之后),才刷新映射。

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

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

通过这种方式,我们可以运行一个示例,同时尝试使节点发生故障或开始重新分片,以了解Redis Cluster在实际条件下的行为。看到没有人向集群写入数据时发生的情况并不是很有帮助。

本节说明了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 形式的键设置为数字,一个接一个。因此,如果您运行该程序,结果将是以下命令流:

  • SET foo0 0

  • SET foo1 1

  • SET foo2 2

  • And so forth...

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

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

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

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

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

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

28和37之间的行开始主循环,在该循环中设置了键或显示错误。

注意循环结束时的sleep调用。在测试中,如果您想尽可能快地写入集群,则可以消除睡眠(相对于事实上这是一个繁忙的循环而没有真正的并行性的事实,因此,通常情况下,您将获得每秒10k个操作)最好的条件)。

通常,为了使示例应用程序更易于被人类遵循,写入速度会减慢。

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

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个哈希槽,如果该示例仍在运行时没有睡眠调用,则该哈希槽应已包含少量的密钥。

然后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无法自动重新平衡集群,无法检查集群节点上的密钥分布并根据需要智能地移动插槽。将来会添加此功能。

一个更有趣的示例应用程序

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

从我们的角度来看,接收写操作的集群总是可以将密钥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) |

该行显示执行的读取和写入的次数,以及错误的数目(由于系统不可用,因此由于错误而无法接受查询)。

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

$ 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 Cluster故障转移。

测试故障转移

注意:在此测试期间,应打开一个标签,并运行一致性测试应用程序。

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

我们可以使用以下命令来识别主机并将其崩溃:

$ 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 Cluster在故障转移期间会丢失写操作,因为它使用异步复制。我们没有说的是,这不太可能发生,因为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命令的输出可能看起来令人生畏,但实际上非常简单,并且由以下标记组成:

  • 节点编号

  • ip:端口

  • 标志:主,从属,我自己,失败,...

  • 如果是从属节点,则为主节点的节点ID

  • 最后一个未决PING的时间仍在等待答复。

  • 最后收到的PONG时间。

  • 此节点的配置时期(请参阅集群规范)。

  • 到该节点的链接状态。

  • 插槽已投放...

手动故障转移

有时,强制故障转移而实际上不会在主服务器上引起任何问题很有用。例如,为了升级主节点之一的Redis进程,最好对其进行故障转移,以将其转变为对可用性的影响最小的从节点。

Redis Cluster使用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.

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

添加一个新节点

添加新节点基本上是以下过程:添加一个空节点,然后将一些数据移入该节点(如果它是新的主节点),或者告诉它设置为已知节点的副本(如果它是从节点的话)。

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

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

这就像在端口7006中启动一个新节点(现有的6个节点已经使用7000到7005)一样简单,除了端口号以外,其他节点都使用相同的配置,因此您应该按顺序进行操作 符合我们先前节点使用的设置:

  • 在终端应用程序中创建一个新选项卡。

  • 输入集群测试目录。

  • 创建一个名为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。

您也可以用相同的方法删除主节点,但是要删除主节点,它必须为空。如果主节点不为空,则需要先将数据从其重新分片到所有其他主节点。

删除主节点的另一种方法是在其从节点之一上对其执行手动故障转移,并在该节点成为新主节点的从节点之后将其删除。显然,这在您要减少群集中的主节点的实际数量时无济于事,在这种情况下,需要重新分片。

副本迁移

在Redis Cluster中,可以使用以下命令随时重新配置从属服务器以与其他主服务器进行复制:

CLUSTER REPLICATE <master-node-id>

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

注意:您可以在Redis集群规范中阅读副本迁移的详细信息,这里我们仅提供一些有关一般想法以及您应该从中受益的信息。

在某些情况下,您可能希望让群集副本从一个主副本移动到另一个主副本的原因是,Redis群集通常具有与附加到给定主副本的副本数量相同的耐故障性。

例如,如果一个主节点及其副本同时失败,则每个主节点都有一个副本的群集将无法继续操作,这仅仅是因为没有其他实例拥有该主节点服务的哈希槽的副本。但是,尽管网络拆分可能会同时隔离多个节点,但许多其他类型的故障(例如单个节点本地的硬件或软件故障)是非常值得注意的一类故障,不太可能同时发生时间,因此在您的每个主节点都有一个从节点的集群中,有可能在凌晨4点杀死该从节点,而在凌晨6点杀死该主节点。这仍然会导致群集无法运行。

为了提高系统的可靠性,我们可以选择向每个主数据库添加其他副本,但这很昂贵。副本迁移允许将更多从服务器添加到仅几个主服务器中。因此,您有10个主机,每个主机1个从机,总共有20个实例。但是,例如,您增加了3个实例作为您的某些主服务器的从服务器,因此某些主服务器将具有多个从服务器。

使用副本迁移时,发生的情况是,如果一个主服务器不包含从服务器,则具有多个从服务器的主服务器的副本将迁移到孤立的主服务器。因此,当您的从属服务器在上述示例中的凌晨4点关闭之后,另一个从属服务器将取代它,而当主服务器在凌晨5点也发生故障时,仍然可以选择一个从属服务器,以便集群可以继续操作。

简而言之,您应该了解副本迁移吗?

  • 群集将尝试在给定时刻从具有最大副本数的主副本迁移副本。

  • 为了从副本迁移中受益,您只需将更多副本添加到群集中的单个主数据库中,哪个主数据库都没有关系。

  • 有一个配置参数可控制副本迁移功能,称为集群迁移屏障:您可以在Redis Cluster随附的示例redis.conf文件中了解有关此功能的更多信息。

升级Redis集群中的节点

升级从节点很容易,因为您只需要停止节点并使用更新版本的Redis重新启动它即可。如果存在使用从属节点扩展读取的客户端,则在给定的从属节点不可用时,它们应该能够重新连接到其他从属节点。

升级母版要复杂一些,建议的过程是:

  1. 使用CLUSTER FAILOVER触发主服务器到其从服务器之一的手动故障转移(请参阅本文档的“手动故障转移”部分)。

  2. 等待主机变成从机。

  3. 最后,像对从属服务器一样升级节点。

  4. 如果要将主节点作为刚刚升级的节点,请触发新的手动故障转移,以将升级后的节点转回为主节点。

按照此过程,您应该先升级一个节点,然后再升级所有节点。

迁移到Redis集群

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

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

  1. 不使用多键操作或事务或涉及多个键的Lua脚本。键是独立访问的(即使通过事务或将多个命令(大约是同一键)组合在一起的Lua脚本访问)。

  2. 使用了涉及多个密钥的多个密钥操作,事务或Lua脚本,但仅对具有相同哈希标签的密钥使用,这意味着一起使用的所有密钥都有一个恰好是相同的{...}子字符串。例如,以下多个键操作是在同一哈希标记的上下文中定义的:SUNION {user:1000} .foo {user:1000} .bar。

  3. 涉及多个键的多个键操作,事务或Lua脚本与不具有显式或相同哈希标签的键名称一起使用。

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

讨论了案例1和2,因此我们将重点讨论这两种情况,它们以相同的方式处理,因此在文档中将没有区别。

假设您已将现有数据集划分为N个主数据,如果没有现有分片,则N = 1,如果要将现有数据集迁移到Redis集群,则需要执行以下步骤:

  1. 停止您的客户。当前无法自动实时迁移到Redis Cluster。您可能能够在应用程序/环境的上下文中编排实时迁移。

  2. 使用BGREWRITEAOF命令为所有N个母版生成一个仅附加文件,并等待AOF文件完全生成。

  3. 将AOF文件从aof-1保存到aof-N。此时,您可以根据需要停止旧实例(这很有用,因为在非虚拟化部署中,您经常需要重用同一台计算机)。

  4. 创建由N个主节点和零个从节点组成的Redis集群。稍后将添加奴隶。确保所有节点都使用仅附加文件来保持持久性。

  5. 停止所有群集节点,将它们的仅附加文件替换为您现有的仅附加文件,第一个节点aof-1,第二个节点aof-2,直到aof-N。

  6. 使用新的AOF文件重新启动Redis Cluster节点。他们会抱怨有些钥匙根据其配置不应该放在那儿。

  7. 使用redis-cli --cluster fix命令修复集群,以便根据每个节点是否具有权威性的哈希槽迁移密钥。

  8. 最后使用redis-cli --cluster检查,以确保您的集群正常。

  9. 重新启动您的客户端,使其修改为使用支持Redis Cluster的客户端库。

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

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

原文始发于微信公众号(Jast):Redis集群教程