使用 redis-rb-cluster 写一个示例应用
在后面介绍如何操作 Redis 集群之前,像故障转移或者重新分片这样的事情,我们需要创建一个示例应用,或者至少要了解简单的 Redis 集群客户端的交互语义。
我们采用运行一个示例,同时尝试使节点失效,或者开始重新分片这样的方式,来看看在真实世界条件下 Redis 集群如何表现。如果没有人往集群写的话,观察集群发生了什么也没有什么实际用处。
这一小节通过两个例子来解释 redis-rb-cluster 的基本用法。第一个例子在 redis-rb-cluster 发行版本的 exemple.rb 文件中,如下:
require './cluster'
startup_nodes = [
{:host => "127.0.0.1", :port => 7000},
{:host => "127.0.0.1", :port => 7001}
]
rc = RedisCluster.new(startup_nodes,32,:timeout => 0.1)
last = false
while not last
begin
last = rc.get("__last__")
last = 0 if !last
rescue => e
puts "error #{e.to_s}"
sleep 1
end
end
((last.to_i+1)..1000000000).each{|x|
begin
rc.set("foo#{x}",x)
puts rc.get("foo#{x}")
rc.set("__last__",x)
rescue => e
puts "error #{e.to_s}"
end
sleep 0.1
}
这个程序做了一件很简单的事情,一个一个地设置形式为 foo<number>
的键的值为一个数字。所以如果你运行这个程序,结果就是下面的命令流:
SET foo0 0
SET foo1 1
SET foo2 2
And so forth...
这个程序看起来要比通常看起来更复杂,因为这个是设计用来在屏幕上展示错误,而不是由于异常退出,所以每一个对集群执行的操作都被 begin rescue 代码块包围起来。
第 7 行是程序中第一个有意思的地方。创建了 Redis 集群对象,使用启动节点(startup nodes)的列表,对象允许的最大连接数,以及指定操作被认为失效的超时时间作为参数。 启动节点不需要是全部的集群节点。重要的是至少有一个节点可达。也要注意,redis-rb-cluster 一旦连接上了第一个节点就会更新启动节点的列表。你可以从任何真实的客户端中看到这样的行为。
现在,我们将 Redis 集群对象实例保存在 rc 变量中,我们准备像一个正常的 Redis 对象实例一样来使用这个对象。
第 11 至 19 行说的是:当我们重启示例的时候,我们不想又从 foo0 开始,所以我们保存计数到 Redis 里面。上面的代码被设计为读取这个计数值,或者,如果这个计数器不存在,就赋值为 0。
但是,注意这里为什么是个 while 循环,因为我们想即使集群下线并返回错误也要不断地重试。一般的程序不必这么小心谨慎。
第 21 到 30 行开始了主循环,键被设置赋值或者展示错误。
注意循环最后 sleep 调用。在你的测试中,如果你想尽可能快地往集群写入,你可以移除这个 sleep(相对来说,这是一个繁忙的循环而不是真实的并发,所以在最好的条件下通常可以得到每秒 10k 次操作)。
正常情况下,写被放慢了速度,让人可以更容易地跟踪程序的输出。
运行程序产生了如下输出:
ruby ./example.rb
1
2
3
4
5
6
7
8
9
^C (I stopped the program here)
这不是一个很有趣的程序,稍后我们会使用一个更有意思的例子,看看在程序运行时进行重新分片会发生什么事情。
重新分片集群(Resharding the cluster)
现在,我们准备尝试集群重分片。要做这个请保持 example.rb 程序在运行中,这样你可以看到是否对运行中的程序有一些影响。你也可能想注释掉 sleep 调用,这样在重分片期间就有一些真实的写负载。
重分片基本上就是从部分节点移动哈希槽到另外一部分节点上去,像创建集群一样也是通过使用 redis-trib 工具来完成。
开启重分片只需要输入:
./redis-trib.rb reshard 127.0.0.1:7000
你只需要指定单个节点,redis-trib 会自动找到其它节点。
当前 redis-trib 只能在管理员的支持下进行重分片,你不能只是说从这个节点移动 5%的哈希槽到另一个节点(但是这也很容易实现)。那么问题就随之而来了。第一个问题就是你想要重分片多少:
你想移动多少哈希槽(从 1 到 16384)?
我们尝试重新分片 1000 个哈希槽,如果没有 sleep 调用的那个例子程序还在运行的话,这些槽里面应该已经包含了不少的键了。
然后,redis-trib 需要知道重分片的目标了,也就是将接收这些哈希槽的节点。我将使用第一个主服务器节点,也就是 127.0.0.1:7000
,但是我得指定这个实例的节点 ID。这已经被 redis-trib 打印在一个列表中了,但是我总是可以在需要时使用下面的命令找到节点的 ID:
$ redis-cli -p 7000 cluster nodes | grep myself
97a3a64667477371c4479320d683e4c8db5858b1 :0 myself,master - 0 0 0 connected 0-5460
好了,我的目标节点是 97a3a64667477371c4479320d683e4c8db5858b1。
现在,你会被询问想从哪些节点获取这些键。我会输入 all,这样就会从所有其它的主服务器节点获取一些哈希槽。
在最后的确认后,你会看到每一个被 redis-trib 准备从一个节点移动到另一个节点的槽的消息,并且会为每一个被从一侧移动到另一侧的真实的键打印一个圆点。
在重分片进行的过程中,你应该能够看到你的示例程序运行没有受到影响。如果你愿意的话,你可以在重分片期间多次停止和重启它。
在重分片的最后,你可以使用下面的命令来测试一下集群的健康情况:
./redis-trib.rb check 127.0.0.1:7000
像平时一样,所有的槽都会被覆盖到,但是这次在 127.0.0.1:7000
的主服务器会拥有更多的哈希槽,大约 6461 个左右。
一个更有意思的示例程序
到目前为止一切挺好,但是我们使用的示例程序却不够好。不顾后果地(acritically)往集群里面写,而不检查写入的东西是否是正确的。
从我们的观点看,接收写请求的集群可能一直将每个操作都作为设置键 foo 值为 42,我们却根本没有察觉到。
所以在 redis-rb-cluster 仓库中,有一个叫做 consistency-test.rb 的更有趣的程序。这个程序有意思得多,因为它使用一组计数器,默认 1000 个,发送 INCR 命令来增加这些计数器。
但是,除了写入,程序还做另外两件事情:
- 当计数器使用 INCR 被更新后,程序记住了写操作。
- 在每次写之前读取一个随机计数器,检查这个值是否是期待的值,与其在内存中的值比较。
这个的意思就是,这个程序就是一个一致性检查器,可以告诉你集群是否丢失了一些写操作,或者是否接受了一个我们没有收到确认(acknowledgement)的写操作。在第一种情况下,我们会看到计数器的值小于我们记录的值,而在第二种情况下,这个值会大于。
运行 consistency-test 程序每秒钟产生一行输出:
$ ruby consistency-test.rb
925 R (0 err) | 925 W (0 err) |
5030 R (0 err) | 5030 W (0 err) |
9261 R (0 err) | 9261 W (0 err) |
13517 R (0 err) | 13517 W (0 err) |
17780 R (0 err) | 17780 W (0 err) |
22025 R (0 err) | 22025 W (0 err) |
25818 R (0 err) | 25818 W (0 err) |
每一行展示了执行的读操作和写操作的次数,以及错误数(错误导致的未被接受的查询是因为系统不可用)。
如果发现了不一致性,输出将增加一些新行。例如,当我在程序运行期间手工重置计数器,就会发生:
$ redis 127.0.0.1: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 时,真实值是 144,所以程序报告了 144 个写操作丢失(集群没有记住的 INCR 命令执行的次数)。
这个程序作为测试用例很有意思,所以我们会使用它来测试 Redis 集群的故障转移。
测试故障转移(Testing the failover)
注意:在测试期间,你应该打开一个标签窗口,一致性检查的程序在其中运行。
为了触发故障转移,我们可以做的最简单的事情(这也是能发生在分布式系统中语义上最简单的失败)就是让一个进程崩溃,在我们的例子中就是一个主服务器。
我们可以使用下面的命令来识别一个集群并让其崩溃:
$ 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 集群提供的一致性保证(即非强一致性,译者注)。