集群(中)

黑派客     最近更新时间:2020-08-04 05:37:59

479

使用 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 集群提供的一致性保证(即非强一致性,译者注)。

展开阅读全文