Redis集群入门(官方文档)

本文是本人翻译自Redis官方文档,地址: https://redis.io/topics/cluster-tutorial

Redis集群入门

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

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

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

如果您打算真正的运行一个Redis集群部署,则建议您阅读更正式的规范,虽然并非一
定要这么做。 但是,最好从本文档开始,与Redis集群一起简单的动动手,然后再阅读规范。

Redis集群101

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

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

实际上,Redis集群能给你带来什么?

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

Redis集群TCP端口

每个Redis集群节点都需要打开两个TCP连接。用于服务客户端的常规Redis
TCP端口,例如6379,再加上将数据端口加10000的端口,比如在示例中为16379。

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

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

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

  1. 用于与客户端通信的常规通信端口(通常为6379),向那些需要访问集群的所有客户端以及所有其他集群节点(使用客户端端口进行key迁移)开放。
  2. 集群总线端口(客户端端口+ 10000)必须可以从所有其他集群节点访问。

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

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

Redis集群与Docker

当前,Redis集群不支持NATted环境以及需要重新映射IP地址或TCP端口的常规环境。

Docker使用一种称为端口映射的技术:与此程序本身指定的端口相比,它在Docker容器内运行的时候可能会使用不同的端口对外暴露。这对于在同一服务器上同时使
用同一端口运行多个容器非常有用。

为了使Docker与Redis集群兼容,您需要使用Docker的主机联网模式(host networking mode)。

请检查 Docker文档 中的--net=host选项以获取更多信息。

Redis集群数据分片

Redis集群不使用一致性哈希,而是使用一种不同形式的分片,从概念上讲每个key
都是我们称为hash槽的一部分。

Redis集群中有16384个hash槽,要计算给定key的hash槽,我们只需对key的CRC16
值用16384取模。

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

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

这样可以轻松添加和删除集群中的节点。例如,如果我想添加一个新节点D,则需要
将一些hash槽从节点A,B,C移到D。类似地,如果我想从集群中删除节点A,则只需
移动A所服务的hash槽到B和C。当节点A为空时,我可以将其从集群中完全删除。

因为将hash槽从一个节点移动到另一个节点不需要停止操作,所以添加删除节点
或更改节点服务的hash槽的百分比不需要停机。

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

hash标签记录在Redis集群规范中,注意,如果key的{}中的括号之间有一个
子字符串,则仅对字符串中的内容进行hash处理,例如,一个叫{foo}的key和另一
个叫{foo} 的 key保证在同一hash槽中,并且可以在以多个key作为参数的命令中一起使用。

Redis集群主备模式

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

在一个包含节点A,B,C的集群中,如果节点B失败,则集群将无法继续,因为我们
不能为 5501-11000 范围内的hash槽提供服务。

但是,在创建集群(或稍后)时,我们向每个主节点添加一个备份节点,以便最终
集群由作为主节点的A,B,C和作为备份节点的A1,B1,C1组成
,如果节点B发生故障,系统将能够持续运行。

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

但是请注意,如果节点B和B1同时失败,则Redis集群无法继续运行。

Redis集群的一致性保证

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

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

  • 您的客户端向B主节点写入数据。
  • B主节点向您的客户端答复“确定”。
  • 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实例中将会启用集群支持。否则,该实例将像往常一样作为独立实例启动。
  • cluster-config-file <filename> : 请注意,尽管有此选项,但它是不允许用户可编辑的配置文件,而是Redis集
    群节点在每次有变更时(基本上是状态)都会自动持久保存的集群配置文件,以便能够在启动时重新读取它。
    该文件列出了诸如集群中其他节点的内容,状态,持久变量等等之类的东西。通常,在收到某些消息时,此文件将被
    重写并刷新到磁盘上。
  • cluster-node-timeout <毫秒> :Redis集群节点在被认为故障前的最长间隔时间。如果无法访问主节点的时间超过指定的时间长度,则它的备份节点将对
    其进行故障转移。此参数也控制Redis集群中的其他重要事情。值得注意的是,在指定的时间内无法连通大多数主节点
    的每个节点都将停止接受查询请求。
  • cluster-slave-validity-factor <factor>:如果设置为零,则备份节点将始终尝试对主节点进行故障转移,而不管主节点和备份节点之间的链接断开
    状态的时间长短。如果该值为正,则将最大断开时间计算为节点超时时间乘以此选项提供的因子,如果该节点是备份
    节点,并且主链接断开的时间超过了指定的时间,它将不会尝试启动故障转移。例如,如果节点超时设置为5秒,而有
    效性因子设置为10,则备份节点与主节点断开连接超过50秒将不会尝试对其主节点进行故障转移。请注意,如果没有
    备份节点可以对其进行故障转移,则任何不为零的值都可能导致Redis集群在主节点发生故障后不可用。
    在这种情况下,只有当原始主节点重新加入集群后,集群才会返回可用状态。
  • cluster-migration-barrier <count>:主节点需要保持连接的备份节点的最小数量,以便另一个备份节点迁移到一个没有任何备份节点覆盖的主
    节点。有关更多信息,请参见本教程中有关副本迁移的相应部分。
  • cluster-require-full-coverage <yes / no>:如果设置为yes,默认情况下,如果某个节点未覆盖一定比例的key空间,集群将停止接受写入。如果该选项设
    置为no,即使此节点仅能处理有关key的部分子集的请求,集群仍将提供查询。

创建和使用一个Redis集群

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

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

以下是最小的Redis集群配置文件:

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

启用集群模式的只需要直接打开cluster-enabled命令。每个实例还包含该节点配置存储位置的文件路径,默认情况
下为nodes.conf。 该文件不会被人接触。 它只是由Redis集群实例在启动时生成,并在需要时进行更新。

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

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

就像是:

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集群命令行实用程序,我们可以使用它
来创建新集群,检查或重新分片现有集群等。

对于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 4或者3 请使用redis-trib.rb工具:

./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将为您提供配置。 键入yes,将接受建议的配置。集群将被配置并加入,这意味着实例将被启动然后彼此
之间可以对话。 最后,如果一切顺利,您将看到如下消息:

[OK] All 16384 slots covered

意思就是说至少有一个主节点实例服务于16384个槽位中某一个.

使用create-cluster脚本创建一个Redis集群

如果您不想如上所述通过手动配置和执行单个实例来创建Redis集群,则可以使用更简单的系统(但是您将不会学到
同样多的操作细节)。

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

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

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

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

create-cluster stop.

关于如何运行这个脚本的更多信息,请阅读目录里的README。

集群操作

到目前为止,Redis集群的问题之一是缺少客户端库的实现。

据我所知有以下实现:

  • redis-rb-cluster 是我(@antirez)编写的Ruby实现,可作为其他语言的参考。它是原始redis-rb的简单包装,实现了最小语义以有效地与集群通信。
  • redis-py-cluster redis-rb-cluster的Python实现。支持大多数redis-py功能。正在积极发展中。
  • 流行的Predis支持Redis集群,该支持最近已更新并且正在积极开发中。
  • 使用最广泛的Java客户端,Jedis最近添加了对Redis集群的支持,请参阅项目README中的Jedis集群部分。
  • StackExchange.Redis提供对C#的支持(并且应与大多数.NET语言,VB,F#等兼容)
  • thunk-redis提供对Node.js和io.js的支持,它是基于thunk/promise的redis客户端,具有管道和集群功能。
    redis-go-cluster是Go语言的
    Redis集群的实现,它使用了Redigo library client作为基本客户端,通过结果聚合实现了MGET/MSET。
  • ioredis是流行的Node.js客户端,为Redis集群提供了强大的支持。
  • 当使用-c开关启动时,redis-cli程序实现了基本的集群支持。

测试Redis集群的一种简单方法是尝试上述任何客户端,或者仅尝试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集群节点能够将客户
端重定向到正确的节点。一个严格的客户端可以做得更好,并且可以在hash槽和节
点地址之间缓存映射,以便直接使用与节点的正确连接。
仅在集群配置中发生某些更改时(例如,在故障转移之后或系统管理员通过添加或删除节点来更改集群布局之后),才会刷新映射。

使用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>形式的key的值设置为数字,并且是一个接一个的递增。
因此,如果您运行该程序,其结果将和以下命令是一样的效果:

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

该程序看起来比较复杂,因为它需要在屏幕上显示错误而不是异常退出,因此,对集群执行的每个操作都应该由
错误处理包装。

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

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

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

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

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

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

注意循环结束时的sleep调用。在测试中,如果您想尽可能快地写入集群,则可以删除sleep(相对来说,这只是一个
很繁忙的循环操作,它并没有真正的并行,因此,在最好的条件下,您通常将获得每秒10k个操作))。

通常,为了使示例程序更容易被人看懂,写入速度会减慢。

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

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

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

集群重新分片

现在,我们准备尝试集群重新分片。 为此,请保持example.rb程序运行,以便您查看对程序的运行是否有影响。
另外,您可能想注释一下sleep调用,以便在重新分片期间发生一些更严重的写入负载。

重新分片基本上意味着将hash槽从一组节点移动到另一组节点,并且像集群创建一样,它使用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个hash槽,如果该示例仍在运行且没有sleep调用,则该hash槽应已包含少量的key。

然后redis-cli需要知道重新分片的目标是什么,也就是将接收hash槽的节点。 我将使用第一个主节点,即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。

现在,它会问你要从哪些节点获取这些key。我只输入all,以便从所有其他主节点获取一些hash槽。

最终确认后,您会看到一条消息,表明redis-cli将要从一个节点移动到另一个节点,并且将从一侧移动到另一侧的每个实际的key都会打印出来。

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

重新分片结束时,可以使用以下命令测试集群的运行状况:

redis-cli --cluster check 127.0.0.1:7000

所有插槽都会被覆盖到,但是这次127.0.0.1:7000的主节点将具有更多的hash插槽
,大约为6461。

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

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

从我们的角度来看,接收写操作的集群可以始终在每个操作里将名为foo的key写到42这个hash槽里,而我们根本不会注意到。

因此,在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 集群故障转移。

测试故障转移

注意:在此测试过程中,你应打开一个tab标签页并在上面运行一致性测试应用程序。

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

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

$ 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都是主节点,我们把7002这台机器用DEBUG SEGFAULT
命令使其崩溃。

$ 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:端口
  • 标志位: 主节点,备份节点, myself, 失败状态, ...
  • 如果自己是备份节点,则是其主节点的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.

基本上,连接到我们将要进行故障转移的主节点的客户端都已停止。
同时,主节点将其复制偏移发送到备份节点,备份节点会在它这边等待偏移接收
完毕。 当复制偏移量完成时,故障转移开始,并且将向旧的主节点通知配置切换
。 当客户端在旧的主节点上解锁时,它们将被重定向到新的主节点。

添加新节点

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

从添加新的主节点开始,我们两者都会展示。

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

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

  • 在你的终端应用上开启一个新的tab。
  • 输入 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

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

  • 由于没有分配的hash槽,因此不保存任何数据。
  • 因为它是没有分配插槽的主机,所以当备份节点要成为主节点时,它不会参与选
    举过程。

现在可以使用redis-cli的重新分片功能将hash槽分配给该节点。像上一节中已经
展示的那样,这里我就不展示了,他们的操作没有区别,只是将空节点作为目标进
行重新分片。

添加一个节点作为副本(备份节点)

添加一个备份节点可以通过2种方式完成。最常用的是用 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添加副本,此节点当前服务的hash槽正在11423-
16383范围内,节点ID为3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e,我要做的
就是连接到新节点(已经作为空的主节点添加到集群)并在新节点上发送命令:

redis 127.0.0.1:7006> cluster replicate 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e

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

$ 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...现在有了2个备份节点,运行在7002端口(已存在)和7006端口(新加入的)

移除节点

为了移除一个备份节点,只需要在redis-cli上使用del-node 命令:

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

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

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

删除主节点的另一种方法是在其一个备份节点上对其执行手动故障转移,并在该节
点成为新主节点的备份节点之后删除该节点。显然,这在您想要减少集群中的主节
点的实际数量时没什么用,在这种情况下,需要重新分片。

副本迁移

在Redis集群里里任何时间你都可以重新配置一个备份节点使其作为另一个主节点
的从属节点,使用下列命令:

CLUSTER REPLICATE <master-node-id>

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

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

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

例如,如果一个主节点及其副本同时失败,则每个主节点都有一个副本的集群将无
法继续工作,这仅仅是因为没有其他实例拥有与该主节点服务的相同的hash槽的副
本。但是,尽管网络断裂可能会同时隔离多个节点,但是许多其他类型的故障(例
如单个节点本地的硬件或软件故障)是非常值得注意的一类故障,这类故障不太可能同时发生,因此在每个主节点都有一个备份节点的集群中,如果该备份节点在凌
晨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个节点之间分配key的分片算法。

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

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

  • 使用多key操作,事务或Lua脚本,但仅针对具有相同hash tag的key,这意味着这些一起使用的所有key都有一个恰好是相同的{...}子字符串
    。 例如,以下多个key操作是在同一hash标签的上下文中定义的:
    SUNION {user:1000}.foo {user:1000}.bar。

  • 使用多key操作,事务或Lua脚本,但与没有显式指定或相同hash标签的key一起使用。

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

上面说到了案例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集群节点。他们会提示有些key根据其配置不应该出现在这里。
  7. 请使用redis-cli --cluster fix命令来修复集群,以便根据每个节点是否具有hash槽的权限来迁移key。
  8. 最后使用redis-cli --cluster检查,以确保您的集群正常。
  9. 重新启动您的客户端,并修改为使用支持Redis集群的客户端库。

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270