Redis 集群教程
Redis 集群101
- 自动切分数据集到多个节点上的能力
- 当部分节点宕机或无法通讯的情况下仍可继续处理命令
Redis 集群 TCP 端口
- 把客户端用来连接redis的普通客户端通讯端口(一般是6379)对所有客户端和其他节点开放(别的节点也会用这个端口来迁移数据)
- 所有节点之间的集群总线端口(客户端口加上10000所得)必须互相开通
Redis 集群数据分片
- 节点A 保存了从 0 到 5500 的哈希槽
- 节点B 保存了从 5501 到 11000的哈希槽
- 节点C 保存了从 11001 到 16384 的哈希槽
Redis 集群主从模型
Redis 集群一致性保证
- 你的客户端发送了一个写请求给master B 节点
- master B 节点回复了一个OK给你的客户端。
- master B 节点把这个写请求传播到它的 slave B1, B2, B3 节点上去。
注意, 在网络分裂出现期间, 客户端 Z1 可以向主节点 B 发送写命令的最大时间是有限制的, 这一时间限制称为节点超时时间(node timeout), 是 Redis 集群的一个重要的配置选项:
- 对于大多数一方来说, 如果一个主节点未能在节点超时时间所设定的时限内重新联系上集群, 那么集群会将这个主节点视为下线, 并使用从节点来代替这个主节点继续工作。
- 对于少数一方, 如果一个主节点未能在节点超时时间所设定的时限内重新联系上集群, 那么它将停止处理写命令, 并向客户端报告错误。
Redis 集群配置参数
- cluster-enabled <yes/no>: 该项如果设置成yes,该实例支持redis集群。否则该实例会像往常一样以独立模式启动。
- cluster-config-file <filename>: 必须注意到尽管该项是可选的,这并不是一个用户可以编辑的配置文件,这是redis集群节点自动生成的配置文件,每次一旦配置有修改它都通过该配置文件来持久化配置(基本上都是状态),这样在下次启动的时候可以重新读取这些配置。该文件中列出了该集群中的其他节点的状态,持久化变量等信息。 当节点收到一些信息的时候该文件就会被冲重写。
- cluster-node-timeout <milliseconds>: redis集群节点的最大超时时间。响应超过这个时间的话该节点会被认为是挂掉了。如果一个master节点超过一定的时候无法访问,它会被它的slave取代。 该参数在redis集群配置中很重要。很明显,当节点无法访问大部分master节点超过一定时间后,它会停止接受查询请求。
- cluster-slave-validity-factor <factor>:如果将该项设置为0,不管slave节点和master节点间失联多久都会一直尝试failover(设为正数,失联大于一定时间(factor*节点TimeOut),不再进行FailOver)。比如,如果节点的timeout设置为5秒,该项设置为10,如果master跟slave之间失联超过50秒,slave不会去failover它的master(意思是不会去把master设置为挂起状态,并取代它)。注意:任意非0数值都有可能导致当master挂掉又没有slave去failover它,这样redis集群不可用。在这种情况下只有原来那个master重新回到集群中才能让集群恢复工作。
- cluster-migration-barrier <count>: 一个master可以拥有的最小slave数量。该项的作用是,当一个master没有任何slave的时候,某些有富余slave的master节点,可以自动的分一个slave给它。具体参见手册中的replica migration章节
- cluster-require-full-coverage <yes/no>: 如果该项设置为yes(默认就是yes) 当一定比例的键空间没有被覆盖到(就是某一部分的哈希槽没了,有可能是暂时挂了)集群就停止处理任何查询炒作。如果该项设置为no,那么就算请求中只有一部分的键可以被查到,一样可以查询(但是有可能会查不全)
创建并使用 Redis 集群
1 2 3 4 5 |
port 7000 cluster-enabled yes cluster-config-file nodes.conf cluster-node-timeout 5000 appendonly yes |
文件中的 cluster-enabled 选项用于开实例的集群模式, 而 cluster-conf-file 选项则设定了保存节点配置文件的路径, 默认值为 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 , 从而在集群中保持一个唯一(unique)的名字。每个节点通过这个名字来记忆其他节点,我们把这个字符串称之为Node ID
创建一个集群
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个master3个slaveredis-trib 会打印出一份预想中的配置给你看, 如果你觉得没问题的话, 就可以输入 yes , redis-trib 就会将这份配置应用到集群当中,让各个节点开始互相通讯,最后可以得到如下信息:
这表示集群中的 16384 个槽都有至少一个主节点在处理, 集群运作正常。
1 |
[OK] All 16384 slots covered |
用create-cluster脚本搭建redis集群
1 2 |
create-cluster start create-cluster create |
在第2步,redis-trib utility 需要你接受集群方案的时候记得回答yes。你现在可以跟集群交互了。第一个节点会默认监听300001。如果你想停集群用以下命令:
1 |
create-cluster stop |
让我们开始玩集群吧
以下是我知道的客户端实现:
- redis-rb-cluster 是我写的一个ruby客户端实现(这边指的是作者@antirez) 。这个库对原生的 redis-rb 进行了一个简单的封装,用最小代码量实现了高效的对集群的操作。
- redis-py-cluster:这个客户端用python对redis-rb-cluster进行了转接。支持大部分 redis-py 的功能。
- Predis : 这个库最近很活跃,更新很快。基于PHP
- Jedis:最流行的java客户端,最近也支持redis集群了。你可以在项目的README里面的集群段落看到相关介绍。
- StackExchange.Redis : C# 的客户端
- thunk-redis:nodejs和io.js 的客户端
- Redis库的不稳定分支里面的 redis-cli 工具也提供了非常基本的集群支持。具体使用的方式是用 -c 来启动该工具可以切换到集群模式。
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" |
redis-cli 的集群功能只提供了非常基本的功能,所以他总是假定:客户端知道数据在哪个节点之上,并准确的连接数据所在的节点。但是一个实际使用的客户端应该应该要缓存哈希槽和节点之间的映射关系,通过这个映射关系来引导客户单连接指定的节点。该映射关系只有集群配置改变的时候才刷新。比如在一次failover之后或者系统管理员通过增加或者删除节点来改变集群的分布之后,该映射关系才刷新。
用 redis-rb-cluster 来写一个例子app
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 |
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 } |
- SET foo0 0
- SET foo1 1
- SET foo2 2
- 以此类推
1 2 3 4 5 6 7 8 9 10 11 |
ruby ./example.rb 1 2 3 4 5 6 7 8 9 ^C (我把程序给停了) |
这个程序并不是十分有趣, 稍后我们就会看到一个更有趣的集群应用示例, 不过在此之前, 让我们先使用这个示例来演示集群的重新分片操作。
集群重新分片
1 |
./redis-trib.rb reshard 127.0.0.1:7000 |
1 2 |
$redis-cli -p 7000 cluster nodes | grep myself 97a3a64667477371c4479320d683e4c8db5858b1 :0 myself,master - 0 0 0 connected 0-5460 |
1 |
./redis-trib.rb check 127.0.0.1:7000 |
所有的节点都会被该操作覆盖到。不过此时127.0.0.1:7000这个节点会拥有更多的哈希槽,大概会有6461个。
将重新分片操作做成一个脚本
1 |
./redis-trib.rb reshard <host>:<port> --from <node-id> --to <node-id> --slots --yes |
一个更有趣的示例应用
因为这个原因, redis-rb-cluster 项目包含了一个名为 consistency-test.rb 的示例应用, 这个应用比起 example.rb 有趣得多: 它创建了多个计数器(默认为 1000 个), 并通过发送 INCR 命令来增加这些计数器的值。
在增加计数器值的同时, consistency-test.rb 还执行以下操作:
- 每次使用 INCR 命令更新一个计数器时, 应用会记录下计数器执行 INCR 命令之后应该有的值。 举个例子, 如果计数器的起始值为 0 , 而这次是程序第 50 次向它发送 INCR 命令, 那么计数器的值应该是 50 。
- 在每次发送 INCR 命令之前, 程序会随机从集群中读取一个计数器的值, 并将它与自己记录的值进行对比, 看两个值是否相同。
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 运行的过程中, 手动修改某个计数器的值,那么 consistency-test.rb 将向我们报告不一致情况:
1 2 3 4 5 6 7 8 9 |
$ 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 | |
在我们修改计数器值的时候, 计数器的正确值是 114 (执行了 114 次 INCR 命令), 因为我们将计数器的值设成了 0 , 所以 consistency-test.rb 会向我们报告说丢失了 114 个 INCR 命令。因为这个示例程序具有一致性检查功能, 所以我们用它来测试 Redis 集群的故障转移操作。
失效备援(failover)测试
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 |
ok,现在7000,7001和7002 是master节点了。让我们用 DEBUG SEGFAULT 命令搞挂7002 节点:
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) | |
你可以看到在失效备援的时候系统拒绝了578个读请求和577个写请求,但是数据库中没有引发任何一个的不一致问题。这可能跟教程刚开始部分所说的不同。在教程刚开始的时候我们说到redis 集群之所以在失效备援的时候会丢失写请求是因为它使用的是异步复制机制。我在教程开始的时候没有提到这点:其实丢失写请求的情况是很少发生的。因为把请求的响应返回给客户端和发送复制命令给slave这两件事情几乎是同时发生的。所以丢失数据的时间窗口非常小。然而非常难发生并不意味这不可能发生。所以这并没有改变redis集群无法实现强一致性的事实。
我们现在可以查看失效备援之后集群的布局(注意我同时重启了崩溃的实例,这样可以把这个节点作为slave重新加入到系统中):
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 |
现在 7000, 7001 和 7005 是master节点。7002之前是master节点,现在是7005的一个slave节点。CLUSTER NODES 命令的结果可能看起来很复杂,但是它实际上是很简单的,并且是一下token的组合:
- 节点 ID
- ip:port
- flags :例如 master 、 slave 、 myself 、fail
- 如果节点是一个从节点的话, 那么跟在 flags 之后的将是主节点的节点 ID
- 集群最近一次向节点发送 PING 命令之后, 过去了多长时间还没接到回复。
- 节点最近一次返回 PONG 回复的时间。
- 节点的配置纪元(configuration epoch):详细信息请参考 Redis 集群规范 。
- 本节点的网络连接情况:例如 connected 。
- 节点目前包含的槽:例如 127.0.0.1:7001 目前包含号码为 5960 至 10921 的哈希槽。
手动失效备援
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已经被我们用失效备援停止了。与此同时,master节点发送跟slave之间的复制位移量(就是现在还差多少没有复制)。slave会停止下来等待复制位移量被消除。当达到复制位移量的时候才开始失效备援,然后旧master被告知要进行配置切换。当客户端从旧master解锁的时候他们已经重定向到新master节点了。
添加新节点
本节将对以上两种情况进行介绍,首先介绍主节点的添加方法:
- 在终端里创建一个新的标签页。
- 进入 cluster-test 文件夹。
- 创建并进入 7006 文件夹。
- 将 redis.conf 文件复制到 7006 文件夹里面,然后将配置中的端口号选项改为 7006 。
- 使用命令 ../../redis-server redis.conf 启动节点。
1 |
./redis-trib.rb add-node 127.0.0.1:7006 127.0.0.1:7000 |
正如你所见我使用 add-node 命令的时候第1个参数用来指定新节点的地址,第2个参数可以随便使用集群中的任何一个节点。
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 |
新节点现在已经连接上了集群, 成为集群的一份子, 并且可以对客户端的命令请求进行转向了, 但是和其他主节点相比, 新节点还有两点区别
- 新节点没有包含任何数据, 因为它没有包含任何哈希桶。
- 尽管新节点没有包含任何哈希桶, 但它仍然是一个主节点, 所以在集群需要将某个从节点升级为新的主节点时, 这个新节点不会被选中。
添加一个slave节点
1 |
./redis-trib.rb add-node --slave 127.0.0.1:7006 127.0.0.1:7000 |
注意到这条命令跟我们之前添加master的命令非常像。我们并不用具体指定要添加slave到哪个master。这样redis-trib 会在有比较少slave的master节点中随机的找一个master来挂载slave节点。
不过你也可以精确的指定你要挂载这个slave及诶单到哪个master上:
1 |
./redis-trib.rb add-node --slave --master-id 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 127.0.0.1:7006 127.0.0.1:7000 |
这就是我们添加一个slave到指定master的方法。一个更手动添加slave到指定master的方式是:添加一个空节点然后通过 CLUSTER REPLICATE 命令来将其转化为一个slave节点。这个方法也同样适用于当一个节点已经是slave节点的时候你想将它转换为另一个master的slave。
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… 现在拥有了两个slave节点,分别是 7002 (之前就有的) 和 7006 (我们现在加上去的)。
移除一个节点
1 |
./redis-trib del-node 127.0.0.1:7000 `<node-id>` |
可以用集群中随便一个节点作为第1个参数。第2个参数是你要移除的节点ID.你也可以用这条命令来移除master节点,但是在移除master节点之前必须确保它是空的。如果你要移除的master节点不是空的,你需要先用重新分片命令来把数据移到其他的节点。另外一个移除master节点的方法是先进行一次手动的失效备援,等它的slave被选举为新的master,并且它被作为一个新的slave被重新加到集群中来之后再移除它。很明显,如果你是想要减少集群中的master数量,这种做法没什么用。在这种情况下你还是需要用重新分片来移除数据后再移除它。
复制迁移
虽然在redis集群中通过以下命令是可以将一个slave节点重新配置为另外一个master的slave:
1 |
CLUSTER REPLICATE <master-node-id> |
然而有时候你不想找系统管理员来帮忙,又想自动的将一个复制节点从一个master下移动到另外一个master下。 这种情况下的复制节点的自动重配置被称为复制迁移。复制迁移可以提升系统的可靠性。
- 集群在迁移的时候会尝试去迁移拥有最多slave数量的master旗下的slave。
- 想利用复制迁移特性来增加系统的可用性,你只需要增加一些slave节点给单个master(哪个master节点并不重要)。
- 复制迁移是由配置项cluster-migration-barrier控制的: 你可以从Redis集群提供的默认配置文件 redis.conf 样例中了解到更多关于复制迁移的知识。
在Redis集群中升级节点
- 使用 CLUSTER FAILOVER 命令来使用手动失效备援,这样来把master切换为slave
- 等待master切换为slave完成
- 就像你升级普通slave一样升级它
- 如果你希望刚刚升级好的节点再次作为master在集群中运行,那就再触发一次手动失效备援让这个及节点重新成为master
迁移到redis集群
- 不使用多key操作或者事务操作或者Lua脚本(涉及到多key)。对key的访问都是独立的。
- 使用多key操作,事务或者lua脚本(涉及到多key),但是只作用于相同的哈希槽,即这些key都有一个{…}包裹起来的部分相同。比如以下的多key操作都是在同一个哈希标签下的:SUNION {user:1000}.foo {user:1000}.bar.
- 使用了多key操作,事务或者lua脚本(涉及到多key),操作的key并没有相同的哈希标签。
- 停止你的客户端。目前redis集群还没有动态迁移功能。
- 通过BGREWRITEAOF 命令生成一个AOF(append only file)文件。并等待该AOF文件生成完毕
- 把AOF文件命名为 aof-1 到 aof-N 。此时你可以停止你的旧实例 (在实际情况下一般会用相同的机器来跑新集群)
- 建一个有N个master节点但没有slave节点的集群。你可以吃些添加slave。确保你所有的节点都使用AOF。
- 停止所有节点把它们的aof文件替换成之前保存的aof文件,aof-1对应第1节点,aof-n对应第n个节点。
- 重启你的redis集群。
- 用 redis-trib 命令修复集群,让key可以被迁移过来
- 最后,用 redis-trib 来检查你的集群是否迁移成功。
- 重启客户端。
发表评论