Redis 3.0中文官方文档翻译计划(20)
——集群(中)
——集群(中)
使用redis-rb-cluster写一个示例应用
在后面介绍如何操作Redis集群之前,像故障转移或者重新分片这样的事情,我们需要创建一个示例应用,或者至少要了解简单的Redis集群客户端的交互语义。
我们采用运行一个示例,同时尝试使节点失效,或者开始重新分片这样的方式,来看看在真实世界条件下Redis集群如何表现。如果没有人往集群写的话,观察集群发生了什么也没有什么实际用处。
这一小节通过两个例子来解释redis-rb-cluster的基本用法。第一个例子在redis-rb-cluster发行版本的exemple.rb文件中,如下:
1 require './cluster' 2 3 startup_nodes = [ 4 {:host => "127.0.0.1", :port => 7000}, 5 {:host => "127.0.0.1", :port => 7001} 6 ] 7 rc = RedisCluster.new(startup_nodes,32,:timeout => 0.1) 8 9 last = false 10 11 while not last 12 begin 13 last = rc.get("__last__") 14 last = 0 if !last 15 rescue => e 16 puts "error #{e.to_s}" 17 sleep 1 18 end 19 end 20 21 ((last.to_i+1)..1000000000).each{|x| 22 begin 23 rc.set("foo#{x}",x) 24 puts rc.get("foo#{x}") 25 rc.set("__last__",x) 26 rescue => e 27 puts "error #{e.to_s}" 28 end 29 sleep 0.1 30 }
这个程序做了一件很简单的事情,一个一个地设置形式为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集群提供的一致性保证(即非强一致性,译者注)。
我们现在可以看看故障转移后的集群布局(注意,与此同时,我重启了崩溃的实例,所以它以从服务器的身份重新加入了集群):
$ 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 connect
现在,主服务器运行在7000,7001和7005端口。之前运行在7002端口的主服务器现在是7005的从服务器了。
CLUSTER NODES命令的输出看起来挺可怕的,但是实际上相当的简单,由以下部分组成:
- 节点ID
- ip:port
- flags: master, slave, myself, fail, ...
- 如果是从服务器的话,就是其主服务器的节点ID
- 最近一次发送PING后等待回复的时间
- 最近一次发送PONG的时间
- 节点的配置纪元(请看集群规范).
- 节点的连接状态
- 服务的哈希槽
===============================================================================
大家好,我是阮威。华中科技大学,计算机软件专业硕士。毕业后加入腾讯,先后在腾讯电子商务部和无线游戏产品部工作,现供职于欢聚时代基础产品部。IT男,至今。欢迎大家收听我的公众账号。