Redis cluster tutorial

这篇文档是对Redis Cluster的简单介绍,不会使用复杂的分布式系统概念来去理解。它只是提供了从用户角度来如果如何搭建集群,测试以及使用的方法,没有完全覆盖Redis Cluster specification内容。

所以本教程试图提供给最终用户一个简单的关于集群和一致性特征的描述.

注意,本教程必须使用Redis 3.0版本或者更高的版本。

如果你计划部署一个重要的Redis Cluster,推荐阅读正式的规范文档,即使载不严格的要求下。不管怎样从这篇文档开始是一个很好的主意,玩一会儿Redis Cluster后,在去阅读规范。

Redis Cluster 101

Redis Cluster 提供了一种Redis安装方式,能够让数据自动的分片到多个Redis节点。

Redis Cluster 也在某种程度上通过分区来提供可用性,在实际环境中当某个节点宕机或者不可达的情况下继续处理命令。
在发生较大故障的时候集群会停止操作(例如大部分的主节点不可用的情况下)。
所以你会得到一个什么样的Redis Cluster?

  • 自动分割数据到不同的节点上的能力。
  • 整个集群的部分节点失败或者不可达的情况下能够继续处理命令的能力。

Redis Cluster TCP ports

每个Redis Cluster节点必须有两个TCP链接被打开。正常情况下的Redis TCP端口使用的是6379来服务客户端,在这个数据端口的基础上加10000就可以获得集群使用的TCP端口,比如16379。第二个大的端口用于Cluster总线,节点和节点之间的通信采用二进制协议。这个Cluster总线用于对节点进行故障检测,配置更新,故障操作授权等等。Clients不要尝试和Cluster总线端口进行通信,应该使用Redis命令端口,在你的防火墙里面要打开这2个端口,否则其他Redis Cluster节点将不能够进行连接。

这个命令端口和cluster总线的端口的偏移量是固定的10000.
如果要让Redis集群环境很好的工作,每个节点都需要注意下面2点:

  1. 用于正常通讯的端口(通常是6379)和集群环境的所有节点都能够正确的到达(使用这个端口进行键的迁移)。
  2. 这个集群总线的端口(客户端端口+10000)也必须和其他所有的集群节点互通。

如果你没有打开这2个TCP端口,那你的集群环境将不能够工作。
集群总线使用的是二进制协议,作为节点到节点的数据交换,更适合用较小的带宽和处理时间来交换节点之间的信息。

Redis Cluster and Docker

目前Redis Cluster 不支持NATted环境和常规环境中ip地址和端口的映射。

Docker使用了一种端口映射技术:程序会在Docker容器内部运行,可能会暴露一个不同的端口来给程序使用。在同一台服务器,同一时间内可以运行相同端口的多个容器,这是很有用的。
如果要在Doocker里面兼容Redis Cluster你需要采用host networking模式。请检查–net=host选项,更多的信息请阅读Docker文档

Redis Cluster data sharding

Redis Cluster 没有使用一致性哈希,而是使用的另一种分片方式 hash slot

Redis集群里有16384个哈希槽,并且会计算出给定key的哈希槽是什么,我们是通过CRC16对16384取模来决定。
在集群里的每个节点负责一部分哈希槽的子集,比如你有3个节点的集群,那么:

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

这样允许你很容易的在集群里添加并且删除节点。如果我想新增一个新的节点D,我只需要从节点A,B,C中移动某些哈希槽到D节点。同样如果我想从集群里面删除节点A,我只需把服务于A的哈希槽移动到B和C节点上。当节点A为空的时候我就可以把它从集群环境中完全的删除。

移动哈希槽从一个节点到另一个节点不需要停止操作,添加和删除节点,改变节点持有哈希槽的百分比,也不需要停机。

Redis集群支持多个key操作作为一个命令被执行(整个交易,或者Lua脚本的执行)在同一个哈希槽里。通过使用散列就可以强制多个key在同一个哈希槽里面。

散列标记在Redis集群规范里面提到,大致意思就是如果key里面有{}子串,就只会对这个花括号内的字符串进行哈希计算,比如一个{foo}key 和另一个{foo}key就保证在同一个哈希槽,这样就可以在同一个命令里面使用多个key作为参数使用。

Redis Cluster master-slave model

为了保证在部分主节点出现故障或者大部分节点无法通信的情况下仍然可用。Redis集群使用了主从模式,每个哈希槽从1(master自己)到N个副本(N-1个从节点)。

在我们的例子里集群拥有A,B,C三个节点,如果节点B失败了这个集群就不能够继续工作,在这个哈希槽的5501-11000范围内我们无法提供服务了。

所以我们在集群创建以后(或者过一段时间)我们要为每个主节点添加一个从节点,最终的集群组合是这样的:A,B,C是主节点,A1,B1,C1是他们的从节点,如果节点B失败了这个系统还是可以继续提供服务。

节点B1复制节点B,节点B失败后,集群将会推选节点B1作为新的主节点并且继续提供服务。

不过当节点B和B1都失败后,集群就不能够提供服务了。

Redis Cluster consistency guarantees

Redis集群不能够保证强一致性。这意味着在实际环境中集群在特定的条件下可能会丢失写操作。

为什么集群会丢失写主要原因是因为集群使用的是异步复制。写的流程如下:

  • 你的客户端向主节点B进行写操作。
  • 主节点B回复了你的客户端OK。
  • 主节点B将写的内容传播给其他从节点B1,B2和B3。

正如你所看到的节点B并没有等待B1,B2,B3的确认就回复了客户端,因为这里考虑了性能问题,所以你的客户端在往节点B写入后,节点B会确认写操作,但是在它往其他从节点发送内容的时候崩掉了,这些从节点并没有接收到写的内容,此时有一个从节点被提升成了主节点,这个写的内容会被永久丢失。

这非常类似的和大多数数据库配置为每秒刷新数据到磁盘一样,你可以在响应客户端之前强制将数据刷新到磁盘,但是这会影响性能。这也是Redis集群中基于同步复制的一种方案。

基本上是在性能和一致性之前进行权衡。

Redis Cluster在某些必要的情况下也能够支持同步写,通过实施WAIT命令,可以降低丢失写操作的可能性,要注意即使使用了同步复制
Redis集群也不能够保证强一致性:在更复杂的情况下,一个不接受写操作的从节点被提升为主节点的可能性总是存在。

这里还有另一种情况能够导致Redis集群丢失写操作,这发生在一个网络分区中,其中的一个客户端和一些少数的实例(至少包含一个主节点)被隔离在一起。
以我们的6个节点组成的集群环境为例子,A,B,C,A1,B1,C1,三个主节点和三个从节点。还有一个客户端,我们称为Z1。
发生网络分区,那么集群可能会分为两方,一方包含节点 A 、C 、A1 、B1 和 C1 ,另一方则包含节点 B 和客户端 Z1。
Z1仍然能够往主节点B中进行写操作, 如果网络分区发生时间较短,那么集群将会继续正常的运作,如果分区的时间足够让大部分的一方将B1选举为新的master节点,那么Z1写入B中得数据将会丢失掉。
注意, 在网络分裂出现期间, 客户端 Z1 可以向主节点 B 发送写命令的最大时间是有限制的, 这一时间限制称为节点超时时间(node timeout), 是 Redis 集群的一个重要的配置选项。
节点超时后,主节点被认为失败了,并且能够被它的副本代替。主节点不能够感应其他的主节点,它将进入错误的状态并且停止写的接收。

Redis Cluster configuration parameters

我们创建一个集群部署的例子。在这之前,让我们从redis.conf文件里了解一下Redis集群的配置参数。有些参数是很容易理解的,有些参数会随着你的继续阅读而越来越清晰。

  • cluster-enabled <yes/no> : 在指定的Redis实例中如果设置的是yes就会激活Redis集群。否则实例会作为一个独立实例来运行。
  • cluster-config-file :注意虽然有这个名字的选项,但不是给用户编辑的配置文件,这个文件会随着集群环境的变化而自动改变,以便在启动时重新读取它。这个文件列出了其他节点的信息,他们的状态,变量等等。这个文件会因为某些消息的接收而重新将结果写到磁盘。
  • cluster-node-timeout :这个是Redis集群节点不认为是失败的最大时间,如果在指定的时间内不能够到达其他节点,它将被其他从节点代替。这个参数的控制在集群里很重要。尤其是,每个节点不能够在指定的时间内到达大多数主节点,将停止接收查询操作。
  • cluster-slave-validity-factor :如果设置为0,则从节点总是会尝试去对一个主节点进行故障转移,不管从节点和主节点断开的时间长度。如果值为正数,就会计算节点超时时间乘以这个因数得到最大的断开时间,如果这个节点是从节点,并且和主节点断开的连接时间超过了这个最大断开连接时间,就不会去尝试故障转移。举个例子:假设节点超时设置的是5秒,并且validity-factor设置的是10,一个从节点和主节点断开的时间超过了50秒就不会进行故障转移。注意任何一个非0的值都可能导致在主节点失败后没有从节点去进行故障转移而使得Redis集群不可用。这种情况下只有在原始的集群节点重新加入后才可以继续使用集群环境。
  • cluster-migration-barrier :主节点保持连接从节点的最小值。查看关于副本迁移的内容 获取更多的信息。
  • cluster-require-full-coverage <yes/no>: 默认情况下设置为yes,集群全部的slot有节点负责,集群状态才为ok,才能提供服务。如果设置为no,可以在slot没有全部分配的时候提供服务。不建议打开该配置,这样会造成分区的时候,小分区的master一直在接受写请求,而造成很长时间数据不一致。

Creating and using a Redis Cluster

注意:通过手动部署一个Redis集群环境也是很重要的一个学习环节。当然如果你想快速的获得一个可以运行的集群环境,可以跳过这一章节,直接通过使用create-cluster脚本来创建一个Redis集群环境。
创建一个集群环境,首先要有几个空实例运行在集群模式下。意味着集群的创建不使用正常的Redis实例,需要在激活了集群特性的基础上被创建。
以下是集群配置文件需要的最小配置:

1
2
3
4
5
port 7000
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes

我们看到通过cluster-enabled指令就可以激活集群模式。每个实例还包含了一个路径来存储这个配置文件,默认名字为nodes.conf。它会在Redis集群实例启动和每一次更新的时候需要。

要让集群正常运作至少需要三个主节点,不过在刚开始试用集群功能时, 强烈建议使用六个节点: 其中三个为主节点, 而其余三个则是各个主节点的从节点。
首先, 让我们进入一个新目录, 并创建六个以端口号为名字的子目录, 稍后我们在将每个目录中运行一个 Redis 实例。像这样:

1
2
3
mkdir cluster-test
cd cluster-test
mkdir 7000 7001 7002 7003 7004 7005

在文件夹 7000 至 7005 中, 各创建一个 redis.conf 文件, 文件的内容可以使用上面的示例配置文件, 但记得将配置中的端口号从 7000 改为与文件夹名字相同的号码。
从 Redis Github 页面 的 unstable 分支中取出最新的 Redis 源码, 编译出可执行文件 redis-server , 并将文件复制到 cluster-test 文件夹, 然后使用类似以下命令, 在每个标签页中打开一个实例:

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

实例打印的日志显示, 因为 nodes.conf 文件不存在, 所以每个节点都为它自身指定了一个新的 ID。

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

这个ID将会永久的被这个实例使用以便这个实例在集群的上下文中具有唯一的名称。每个节点都会通过这个IDs来记住,而不是通过IP或者端口。IP地址和端口也许会改变,但这个唯一的节点身份不会改变,我们称为Node ID

Creating the cluster

现在我们已经有一些实例在运行,我们需要对这些节点写入一些有用的配置来创建我们的集群。
通过使用 Redis 集群命令行工具 redis-trib可以很容易帮助我们完成,这是一个Ruby写的可执行程序,能够发送特殊的命令来创建新的集群环境,检查或者重新分片一个已经存在的集群,等等。
这个redis-trib工具可以在src目录下面找到。你需要安装redis gem来运行redis-trib。

1
gem install redis

简单的创建集群:

1
2
./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

这个命令在这里用于创建一个新的集群, 选项–replicas 1 表示我们希望为集群中的每个主节点创建一个从节点。
其他的参数则是我想创建新集群环境的实例地址列表。
我们的要求是创建一个集群环境有3个主节点和3个从节点。
Redis-trib 会提供一份配置给你参考。如果你接收这份配置输入yes。 redis-trib 就会将这份配置应用到集群当中,让各个节点开始互相通讯,最后可以得到如下信息:

1
[OK] All 16384 slots covered

这表示集群中的 16384 个槽都有至少一个主节点在处理, 集群运作正常。

Creating a Redis Cluster using the create-cluster script

如果你不想手动的去创建一个Redis集群环境,这里有一个非常简单的系统(但是你就不能够了解到更多的细节)。
在Redis分发目录里检查utils/create-cluster目录,这里有一个脚本文件create-cluster,这是一个简单的bash脚本。启动6个节点的集群环境,三主三从只需要执行以下流程的命令:

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

在第二步回答yes后redis-trib就会根据你想要的集群布局来生效。
You can now interact with the cluster, the first node will start at port 30001 by default. When you are done, stop the cluster with:你现在可以和集群环境交互了,第一个生效的节点运行的端口是30001。完成后,停止这个集群环境:

1
1. create-cluster stop.

请阅读目录里面的README文件获取更多关于这样运行这个脚本。

Playing with the cluster

Redis 集群现阶段的一个问题是客户端实现的库很少。
我知道的有以下一些实现:

  • redis-rb-cluster is a Ruby implementation written by me (@antirez) as a reference for other languages. It is a simple wrapper around the original redis-rb, implementing the minimal semantics to talk with the cluster efficiently.
  • redis-py-cluster A port of redis-rb-cluster to Python. Supports majority of redis-py functionality. Is in active development.
  • The popular Predis has support for Redis Cluster, the support was recently updated and is in active development.
  • The most used Java client, Jedis recently added support for Redis Cluster, see the Jedis Cluster section in the project README.
  • StackExchange.Redis offers support for C# (and should work fine with most .NET languages; VB, F#, etc)
  • thunk-redis offers support for Node.js and io.js, it is a thunk/promise-based redis client with pipelining and cluster.
  • redis-go-cluster is an implementation of Redis Cluster for the Go language using the Redigo library client as the base client. Implements MGET/MSET via result aggregation.
  • The redis-cli utility in the unstable branch of the Redis repository at GitHub implements a very basic cluster support when started with the -c switch.

一个最简单的测试Redis集群环境的方法是尝试使用客户端或者redis-cli命令行工具。下面是命令行测试的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ 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 集群节点来将它转向(redirect)至正确的节点。一个真正的(serious)集群客户端应该做得比这更好: 它应该用缓存记录起哈希槽与节点地址之间的映射(map), 从而直接将命令发送到正确的节点上面。这种映射只会在集群的配置出现某些修改时变化, 比如说, 在一次故障转移(failover)之后, 或者系统管理员通过添加节点或移除节点来修改了集群的布局(layout)之后, 诸如此类。

Writing an example app with redis-rb-cluster

在展示如何使用集群进行故障转移、重新分片等操作之前, 我们需要创建一个示例应用, 了解一些与 Redis 集群客户端进行交互的基本方法。
在运行示例应用的过程中, 我们会尝试让节点进入失效状态, 又或者开始一次重新分片, 以此来观察 Redis 集群在真实世界运行时的表现, 并且为了让这个示例尽可能地有用, 我们会让这个应用向集群进行写操作。
本节将通过两个示例应用来展示 redis-rb-cluster 的基本用法, 以下是本节的第一个示例应用, 它是一个名为 example.rb 的文件, 包含在redis-rb-cluster 项目里面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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 为值, 使用 SET 命令向数据库设置键值对,所以程序运行的结果流程大致如下:

1
2
3
4
SET foo0 0
SET foo1 1
SET foo2 2
And so forth...

代码中的每个集群操作都使用一个 begin 和 rescue 代码块(block)包裹着, 因为我们希望在代码出错时, 将错误打印到终端上面, 而不希望应用因为异常(exception)而退出。
程序的第14行是代码中第一个有趣的地方, 它创建了一个 Redis 集群对象, 其中创建对象所使用的参数及其意义如下:第一个参数是记录了启动节点的 startup_nodes 列表, 列表中包含了两个集群节点的地址。第二个参数指定了对于集群中的各个不同的节点, Redis 集群对象可以获得的最大连接数 ,第三个参数 timeout 指定了一个命令在执行多久之后, 才会被看作是执行失败。
启动列表中并不需要包含所有集群节点的地址, 但这些地址中至少要有一个是有效的。 一旦 redis-rb-cluster 成功连接上集群中的某个节点时, 集群节点列表就会被自动更新, 任何真正的的集群客户端都应该这样做。
现在, 程序创建的 Redis 集群对象实例被保存到 rc 变量里面, 我们可以将这个对象当作普通 Redis 对象实例来使用。
在18至26行, 我们先尝试阅读计数器中的值, 如果计数器不存在的话, 我们才将计数器初始化为 0 : 通过将计数值保存到 Redis 的计数器里面, 我们可以在示例重启之后, 仍然继续之前的执行过程, 而不必每次重启之后都从 foo0 开始重新设置键值对。为了让程序在集群下线的情况下, 仍然不断地尝试读取计数器的值, 我们将读取操作包含在了一个 while 循环里面, 一般的应用程序并不需要如此小心。
28至37行是程序的主循环, 这个循环负责设置键值对, 并在设置出错时打印错误信息。程序在主循环的末尾添加了一个 sleep 调用, 让写操作的执行速度变慢, 帮助执行示例的人更容易看清程序的输出。执行 example.rb 程序将产生以下输出:

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

这个程序并不是十分有趣, 稍后我们就会看到一个更有趣的集群应用示例, 不过在此之前, 让我们先使用这个示例来演示集群的重新分片操作。

Resharding the cluster

现在, 让我们来试试对集群进行重新分片操作。在执行重新分片的过程中, 请让你的 example.rb 程序处于运行状态, 这样你就会看到, 重新分片并不会对正在运行的集群程序产生任何影响, 你也可以考虑将 example.rb 中的 sleep 调用删掉, 从而让重新分片操作在近乎真实的写负载下执行 重新分片操作基本上就是将某些节点上的哈希槽移动到另外一些节点上面, 和创建集群一样, 重新分片也可以使用 redis-trib 程序来执行 执行以下命令可以开始一次重新分片操作:

1
./redis-trib.rb reshard 127.0.0.1:7000

你只需要指定集群中其中一个节点的地址, redis-trib 就会自动找到集群中的其他节点。
目前 redis-trib 只能在管理员的协助下完成重新分片的工作, 你不能说从一个节点移动5%的哈希槽到另一个节点。所以从一开始,首先就要确定重新分片的数量是多少个:

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

我们尝试重新对1000个哈希槽分片,如果我们的例子程序(example.rb)还在运行的话,那么应该有一定数量的键了。 然后redis-trib需要知道重新分片的目标是什么,也就是将要接收这些哈希槽的节点。我将使用第一个主节点,127.0.0.1:7000,但是我需要制定这个实例的节点ID。我们可以通过以下的命令获取到节点id:

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

好啦,我的目标节点ID是 97a3a64667477371c4479320d683e4c8db5858b1。
现在你将会被问到从哪些节点去获取这些键。我只需要输入all就可以从所有的主节点各获取一部分哈希槽。
最后确认后你将会看到每个redis-trib移动的槽的信息,每个key的移动的信息也会打印出来。
在重新分片的过程中,你的例子程序是不会受到影响的,你可以停止或者重新启动多次。
在重新分片结束后,你可以通过如下命令检查集群状态:

1
./redis-trib.rb check 127.0.0.1:7000

所有的插槽都将会被覆盖,但是这个主节点127.0.0.1:7000将会拥有更多的哈希槽,大约6461个。

Scripting a resharding operation

重新分片过程还可以自动执行而不需要互动,手动指定一些参数即可,命令格式如下:

1
./redis-trib.rb reshard --from <node-id> --to <node-id> --slots <number of slots> --yes <host>:<port>

这样的方式就可以经常自动的进行重新分片,目前redis-trib还不能够很好的自动重新负载集群,此功能会在将来添加。

A more interesting example application

我们在前面使用的示例程序 example.rb 并不是十分有趣, 因为它只是不断地对集群进行写入, 但并不检查写入结果是否正确。

比如说, 集群可能会错误地将 example.rb 发送的所有 SET 命令都改成了 SET foo 42 , 但因为 example.rb 并不检查写入后的值,我们并不会注意到集群实际上写入的值是错误的。
redis-rb-cluster项目里面有一个更有趣的程序是consistency-test.rb。 它创建了多个计数器(默认为 1000 个), 并通过发送 INCR 命令来增加这些计数器的值。
程序不仅仅是写操作,它做了两件事:

  • 使用INCR更新计数器的值,程序进行写操作。
  • 它在写之前会随机的去读取一个计数器,并且检查这个值是否是预期的值。

换句话说, 这个程序是一个简单的一致性检查器(consistency checker),并且能够告诉你集群里面哪些写操作丢失,又或者多执行了某些客户端没有确认到的 INCR 命令。在前一种情况中, consistency-test.rb 记录的计数器值将比集群记录的计数器值要大; 而在后一种情况中, consistency-test.rb 记录的计数器值将比集群记录的计数器值要小。
运行 consistency-test 程序将产生类似以下的输出:

1
2
3
4
5
6
7
8
$ 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) |

结果展示了执行的读和 写,和错误(由于系统不可用而没有接受的查询发生的错误)的数量。
如果程序察觉了不一致的情况出现, 它将在输出行的末尾显式不一致的详细情况。比如说, 如果我们在 consistency-test.rb 运行的过程中, 手动修改某个计数器的值:

1
2
3
4
5
6
7
8
9
$ 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 |

在我们修改计数器值的时候, 计数器的正确值是 114 (执行了 114 次 INCR 命令), 因为我们将计数器的值设成了 0 , 所以 consistency-test.rb 会向我们报告说丢失了 114 个 INCR 命令。
这个程序作为测试程序很有意思,所以我们用这个程序来测试故障恢复.

Testing the failover

注意:在这个测试过程中,你要一直运行consistency test程序。
要触发一次故障转移, 最简单的办法就是令集群中的某个主节点进入下线状态。首先用以下命令列出集群中的所有主节点:

1
2
3
4
$ 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 命令, 让这个主节点崩溃:

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

现在我们查看consistency test输出的报表信息:

1
2
3
4
5
6
7
8
9
10
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) |

从 consistency-test 的这段输出可以看到, 集群在执行故障转移期间, 总共丢失了 578 个读命令和 577 个写命令, 但是并没有产生任何数据不一致。这听上去可能有点奇怪, 因为在教程的开头我们提到过, Redis 使用的是异步复制, 在执行故障转移期间, 集群可能会丢失写命令。但是在实际上, 丢失命令的情况并不常见, 因为 Redis 几乎是同时执行将命令回复发送给客户端, 以及将命令复制给从节点这两个操作, 所以实际上造成命令丢失的时间窗口是非常小的。不过, 尽管出现的几率不高, 但丢失命令的情况还是有可能会出现的, 所以我们对 Redis 集群不能提供强一致性的这一描述仍然是正确的。现在, 让我们使用 cluster nodes 命令,查看集群在执行故障转移操作之后, 主从节点的布局情况:

1
2
3
4
5
6
7
$ 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

现在masters运行在 7000, 7001 和 7005端口上. 原来的master 7002现在变成了一个7005的一个从节点.
CLUSTER NODES 命令的输出看起来有点复杂,其实他非常的简单,含义如下:

  • 节点ID
  • IP:端口
  • 标志: master, slave, myself, fail, …
  • 如果是个从节点, 这里是它的主节点的NODE ID
  • 集群最近一次向节点发送 PING 命令之后, 过去了多长时间还没接到回复。.
  • 节点最近一次返回 PONG 回复的时间。
  • 节点的配置纪元(configuration epoch):详细信息请参考 Redis 集群规范 。
  • 节点连接状态。
  • 哈希槽的服务。

Manual failover

有的时候在主节点没有任何问题的情况下强制手动故障转移也是很有必要的,比如想要升级主节点的Redis进程,我们可以通过故障转移将其转为slave再进行升级操作来避免对集群的可用性造成很大的影响。
Redis集群使用 CLUSTER FAILOVER命令来进行故障转移,不过要在被转移的主节点的从节点上执行该命令。
手动故障转移比主节点失败自动故障转移更加安全,因为手动故障转移时客户端的切换是在确保新的主节点完全复制了失败的旧的主节点数据的前提下下发生的,所以避免了数据的丢失。

执行手动故障转移时查看从节点日志记录如下:

1
2
3
4
5
6
# 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.

基本过程如下:客户端不再链接我们淘汰的主节点,同时主节点向从节点发送复制偏移量,从节点得到复制偏移量后故障转移开始,接着通知主节点进行配置切换,当客户端在旧的master上解锁后重新连接到新的主节点上。

Adding a new node

添加新的节点的基本过程就是添加一个空的节点然后移动一些数据给它,一种情况是添加一个新的主节点,或者告诉这个节点设置为一个已知节点的副本,这种情况下它是一个从节点。
针对这两种情况,本节都会介绍,先从添加主节点开始。
两种情况执行的第一步都是添加一个空节点。
启动一个端口为7006的新节点(在我们已存在的6个节点里已经使用了7000到7005的端口),使用和其他节点相同的配置,除了端口不一样,按照以下流程执行:

  • 在终端打开一个新的标签页。
  • 进入cluster-test 目录。
  • 创建一个名为7006的目录。
  • 和其他节点一样,创建redis.conf文件,需要将端口号改成7006.
  • 最后启动节点 ../redis-server ./redis.conf

此时服务应该能够正常运行。
现在我们使用redis-trib手动将这个节点添加到已存在的集群环境。

1
./redis-trib.rb add-node 127.0.0.1:7006 127.0.0.1:7000

可以看到.使用add-node命令来添加节点,第一个参数是新节点的地址,第二个参数是任意一个已经存在的节点的IP和端口。
在操作过程中redis-trib其实帮助我们做了很少的事情,它只是给这个节点发送了一个CLUSTER MEET消息,这件事情你也可以手动执行。
redis-trib在操作之前会检查集群的状态,所以用redis-trib来执行操作是比较好的范式。
我们可以看到新的节点已经添加到集群中:

1
2
3
4
5
6
7
8
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-trib 程序, 将集群中的某些哈希桶移动到新节点里面, 新节点就会成为真正的主节点了。

Adding a new node as a replica

我们有两种方式添加新的副本。可以像添加主节点一样再次使用redis-trib 命令,但是要加上–slave选项,像这样:

1
./redis-trib.rb add-node --slave 127.0.0.1:7006 127.0.0.1:7000

此处的命令和添加一个主节点命令类似,此处并没有指定添加的这个从节点的主节点,这种情况下系统会在其他的复制集中的主节点中随机选取一个作为这个从节点的主节点。
当然你也可以使用以下命令来指定这个新副本的主节点是哪个:

1
./redis-trib.rb add-node --slave --master-id 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 127.0.0.1:7006 127.0.0.1:7000

这样我们就为新副本指定了一个主节点。
还有另一种方式就是通过使用CLUSTER REPLICATE命令转换一个空的主节点为另一个主节点的从节点。
举个例子:在我们的节点里面有一个节点127.0.0.1:7005 目前是节点IDNode ID为 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e的副本,只需要连接到新的节点(空主节点)发送以下命令:

1
redis 127.0.0.1:7006> cluster replicate 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e

就可以了。 我们新的从节点有了一些哈希槽,其他的节点也知道(过几秒后会更新他们自己的配置),可以使用如下命令确认::

1
2
3
$ 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 (新添加的)。

Removing a node

只需要通过redis-trib的del-node命令就可以移除一个从节点:

1
./redis-trib del-node 127.0.0.1:7000 `<node-id>`

第一个参数是集群中随机的一个节点,第二个参数是你想删除的节点ID。
使用同样的方法移除主节点,不过在移除主节点前,需要确保这个主节点是空的。如果不是空的,需要将这个节点的数据重新分片到其他主节点上。
移除的另一种方法是手动执行故障转移,等从节点作为新的主节点后在将它删除,不过这种情况下不会减少集群节点的数量,这种情况下,需要重新分片。

Replicas migration

在集群环境里可以在任何时间执行以下命令来重新配置从节点对应的主节点:

1
CLUSTER REPLICATE <master-node-id>

在特定的场景下,不需要系统管理员的协助下,自动将一个从节点从当前的主节点切换到另一个主节 的自动重新配置的过程叫做复制迁移(从节点迁移),从节点的迁移能够提高整个Redis集群的可用性。
注意:你可以阅读(Redis集群规范)了解细节。在这里我们只提供一部分信息告诉你怎么做。
The reason why you may want to let your cluster replicas to move from one master to another under certain condition, is that usually the Redis Cluster is as resistant to failures as the number of replicas attached to a given master.
For example a cluster where every master has a single replica can’t continue operations if the master and its replica fail at the same time, simply because there is no other instance to have a copy of the hash slots the master was serving. However while netsplits are likely to isolate a number of nodes at the same time, many other kind of failures, like hardware or software failures local to a single node, are a very notable class of failures that are unlikely to happen at the same time, so it is possible that in your cluster where every master has a slave, the slave is killed at 4am, and the master is killed at 6am. This still will result in a cluster that can no longer operate.
To improve reliability of the system we have the option to add additional replicas to every master, but this is expensive. Replica migration allows to add more slaves to just a few masters. So you have 10 masters with 1 slave each, for a total of 20 instances. However you add, for example, 3 instances more as slaves of some of your masters, so certain masters will have more than a single slave.
With replicas migration what happens is that if a master is left without slaves, a replica from a master that has multiple slaves will migrate to the orphaned master. So after your slave goes down at 4am as in the example we made above, another slave will take its place, and when the master will fail as well at 5am, there is still a slave that can be elected so that the cluster can continue to operate.
So what you should know about replicas migration in short?

  • The cluster will try to migrate a replica from the master that has the greatest number of replicas in a given moment.
  • To benefit from replica migration you have just to add a few more replicas to a single master in your cluster, it does not matter what master.
  • There is a configuration parameter that controls the replica migration feature that is called cluster-migration-barrier: you can read more about it in the example redis.conf file provided with Redis Cluster.

Upgrading nodes in a Redis Cluster

Upgrading slave nodes is easy since you just need to stop the node and restart it with an updated version of Redis. If there are clients scaling reads using slave nodes, they should be able to reconnect to a different slave if a given one is not available.
Upgrading masters is a bit more complex, and the suggested procedure is:

  1. Use CLUSTER FAILOVER to trigger a manual failover of the master to one of its slaves (see the “Manual failover” section of this documentation).
  2. Wait for the master to turn into a slave.
  3. Finally upgrade the node as you do for slaves.
  4. If you want the master to be the node you just upgraded, trigger a new manual failover in order to turn back the upgraded node into a master.

Following this procedure you should upgrade one node after the other until all the nodes are upgraded.

Migrating to Redis Cluster

Users willing to migrate to Redis Cluster may have just a single master, or may already using a preexisting sharding setup, where keys are split among N nodes, using some in-house algorithm or a sharding algorithm implemented by their client library or Redis proxy.
In both cases it is possible to migrate to Redis Cluster easily, however what is the most important detail is if multiple-keys operations are used by the application, and how. There are three different cases:

  1. Multiple keys operations, or transactions, or Lua scripts involving multiple keys, are not used. Keys are accessed independently (even if accessed via transactions or Lua scripts grouping multiple commands, about the same key, together).
  2. Multiple keys operations, transactions, or Lua scripts involving multiple keys are used but only with keys having the same hash tag, which means that the keys used together all have a {…} sub-string that happens to be identical. For example the following multiple keys operation is defined in the context of the same hash tag: SUNION {user:1000}.foo {user:1000}.bar.
  3. Multiple keys operations, transactions, or Lua scripts involving multiple keys are used with key names not having an explicit, or the same, hash tag.

The third case is not handled by Redis Cluster: the application requires to be modified in order to don’t use multi keys operations or only use them in the context of the same hash tag.
Case 1 and 2 are covered, so we’ll focus on those two cases, that are handled in the same way, so no distinction will be made in the documentation.
Assuming you have your preexisting data set split into N masters, where N=1 if you have no preexisting sharding, the following steps are needed in order to migrate your data set to Redis Cluster:

  1. Stop your clients. No automatic live-migration to Redis Cluster is currently possible. You may be able to do it orchestrating a live migration in the context of your application / environment.
  2. Generate an append only file for all of your N masters using the BGREWRITEAOF command, and waiting for the AOF file to be completely generated.
  3. Save your AOF files from aof-1 to aof-N somewhere. At this point you can stop your old instances if you wish (this is useful since in non-virtualized deployments you often need to reuse the same computers).
  4. Create a Redis Cluster composed of N masters and zero slaves. You’ll add slaves later. Make sure all your nodes are using the append only file for persistence.
  5. Stop all the cluster nodes, substitute their append only file with your pre-existing append only files, aof-1 for the first node, aof-2 for the second node, up to aof-N.
  6. Restart your Redis Cluster nodes with the new AOF files. They’ll complain that there are keys that should not be there according to their configuration.
  7. Use redis-trib fix command in order to fix the cluster so that keys will be migrated according to the hash slots each node is authoritative or not.
  8. Use redis-trib check at the end to make sure your cluster is ok.
  9. Restart your clients modified to use a Redis Cluster aware client library.

There is an alternative way to import data from external instances to a Redis Cluster, which is to use the redis-trib import command.
The command moves all the keys of a running instance (deleting the keys from the source instance) to the specified pre-existing Redis Cluster. However note that if you use a Redis 2.8 instance as source instance the operation may be slow since 2.8 does not implement migrate connection caching, so you may want to restart your source instance with a Redis 3.x version before to perform such operation.