背景

原文链接

同事在工作中,Redis集群环境下使用lua脚本,提示”commond keys must be in same slot”,故而译文。

—–分割线—–

本文是Redis集群的简要说明,不涉及难以理解的分布式系统的概念。本文提供了怎样搭建/测试/操作Redis集群,不会深入细节,仅仅从使用者视角去描述系统的行为。系统内部算法和设计理念等会在Redis Cluster specification中覆盖。

然而本教程尝试以最终用户的视角,用简单易懂的方式阐释Redis集群高可用性和一致性特点。

注意本教程要求Redisb版本3.0或者更高。

如果你打算认真的部署Redis集群,建议阅读更多的正式的规范,即便这不是严格的要求。但是从本教程开始是个好主意,先尝试使用Redis集群一段时间,然后再读那些规范。

Redis集群101

Redis集群提供了一种运行Redis安装的方式,数据自动跨多个Redis节点分享

Redis集群同样提供了分区之间某种程度的高可用,实际上是在某些节点失败或者不能通信的情况下仍然能够继续操作的能力。然而集群在大面积失败(例如当大部分的主节点不可用)的情况下会停止操作。

因此,实际上Redis集群能带来什么?

  • 节点间自动分割数据集的能力
  • 当一部分节点失败或者没法与集群外部通信的时集群仍然可用的能力

Redis集群TCP端口

每个Redis集群的节点需要打开两个TCP链接。平常的TCP端口去服务客户端链接。例如6379,加上10000就是数据端口,例子中使用在6379上加了10000所以就是16379端口。

第二个端口被集群总线使用,那是一个使用二进制协议的节点到节点的通信通道。集群总线被节点使用来探测失败,配置更新,故障转移等。客户端绝不应该试着同集群总线端口通信,应该仅和平常使用的Redis的命令端口通信。

命令端口和集群总线端口的偏移是固定的,通常是10000。

所以如果你想要一个Redis集群正常工作,针对每个节点就需要:

1.平常客户端沟通时使用的端口(通常是6379)需要保持开放,这样所有的客户端可以链接集群,同时集群中的其他节点也可以通过该接口链接(其他节点使用客户端端口进行键值的迁移)。 2.集群总线端口(客户端端口+10000)必须保证集群中其他的节点可以到达。

如果你不打开两个TCP端口,集群不会按照预期执行。

集群总线使用不同的,二进制协议来进行节点到节点的数据交换,二进制协议降低了节点交换的带宽和处理时间。

Redis集群和Docker

当前Redis集群不支持被NAT的网络环境,以及在IP地址和TCP端口被重新映射的一般网络环境。

Docker使用了一个叫做_端口映射_ 的技术:Docker可以将Docker容器中运行的程序使用的端口暴露成另外一个端口。这个在同一个服务器,同一时间运行多个Docker容器时很有用。

为了让Docker兼容Redis集群,需要使用Docker的host networking mode。可以参考Docker文档–net=host选项,获取更多信息。

Redis集群数据分片

Redis集群不使用一致性哈希,但使用了另外一种不同形式的分片方式,将键值放到了我们称作的哈希槽中。

Redis集群中有16384(16k)个哈希槽,计算指定key的哈希槽的方法是计算key的CRC16值余16384。

Redis集群中的每个节点都是哈希槽的子集,例如你可能有3个节点的集群:

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

这将允许集群很轻松的添加和删除节点。比如我想加一个新的节点D,我需要从A,B和C节点移动一些哈希槽移动到D。相似的如果我想从集群中删除节点A我可以仅仅移动被A节点/B节点/C节点服务的哈希槽。当节点A空了,我们可以从集群中完整的移除节点A。

由于从一个节点移动哈希槽到另一个节点不需要停止操作,添加/删除节点,或者更改节点上哈希槽的百分比不需要任何停机。

Redis集群支持多key操作,但是要求所有在一个命令(或事务/Lua脚本)key都在一个哈希槽中执行。用户可以使用_hash tags_ 这种方式强制多个key到一个哈希槽中。

Hash tag在Redis Cluster specification中详细介绍,但是Hash tag的要旨就是使用key中使用大括号{}包裹的子key,仅仅是大括号中的内容会被哈希计算,例如this{foo}key和another{foo}key会被保证在一个哈希槽中。可以在带有多个键作为参数的命令中使用。

Redis集群主从(master-slave)模式

为了保证当一部分主节点失败或者不能与大部分节点通信时集群仍然可用,Redis集群使用了master-slave模式,每个哈希槽都有一个主(主节点本身)和N个副本(N-1个附加从节点)。

在我们有A/B/C三个节点的集群中,如果节点B失败集群会停止,因为我们不能服务5501-11000的哈希槽了。

然而当集群创建(或集群启动后)我们给每个主节点添加一个从节点,这样集群最终由A/B/C三个主节点和A1/B1/C1三个从节点,当主节点B失败后,集群仍能正常运行。

节点B1备份节点B,当B失败的时候,集群将把B1提升为新的主节点,并继续正常操作。

然而,需要注意,如果B和B1同时失败了,集群将不会继续执行。

Redis集群的一致性保证

Redis集群不保证强一致性。实际上这意味着在特殊情况下,Redis集群会丢失写,即便集群已经反馈给客户端操作成功。

Redis集群会丢失写的第一个原因是由于集群使用异步复制。这意味着在写期间发生了如下过程

  • 客户端写到主节点B
  • 主节点B反馈客户端写入成功
  • 主节点B传播写到从节点B1/B2/B3

可以看到,B并不等待B1/B2/B3写入结果的反馈,就返回给客户端写入成功了,之所以不等待B1/B2/B3的写入反馈,是由于这样会给Redis带来较高的时间延迟,所以如果客户端写入,B反馈了写入,当时在将写入传播给他的从节点前崩溃了,一个没有收到这个写操作的从节点将会提升为主节点,将永远丢失这个写操作。

这与大部分数据库配置每秒刷新磁盘这种情况非常类似,这种情况在不涉及分布式系统的传统数据库中就存在。同样的你可以在反馈给客户端前强制刷新到磁盘,但这将导致极低的性能。对于Redis集群这相当于同步复制。

基本上需要在性能和一致性上进行权衡。

Redis集群在需要的情况下支持同步写,通过WAIT命令实现,这使得丢失写入的可能性大大降低,但是需要明确Redis集群即便使用了同步复制仍不能保持强一致性:被选举为主节点的从节点,在更加复杂的情况下仍有可能写入失败。

另外一种场景Redis集群也可能丢失写入,客户端的网络分区包括至少一个主节点的少数实例隔离。

以我们A/B/C三个主节点A1/B1/C1三个从节点共6个节点组成的Redis集群为例,客户端我们称之为Z1。

网络分区发生后,一边的分区可能是A/C/A1/B1/C1,另一边的分区是B/Z1。

Z1仍然可以写入B,B也会正常服务于Z1。如果网络分区在很短的时间内恢复,那么集群仍然正常工作。然而当网络隔离时间足够长是,在主要的分区那边,B1会被提升为主节点,这样客户端Z1对B的写入会被丢失。

请注意,在Z1发送到B有一个最大的窗口,如果已经有足够的时间让分区的多数方选举出从节点为主节点,则少数方的所有主节点都将停止接受写入。

这个时间量是Redis集群非常重要的配置项,被叫做node timeout。当节点超时时间已过,主节点就被考虑为失败,将会被他的一个从节点替换。相似的,当节点超时,主节点感知不到其他的主节点,他将进入错误状态并停止接受写入。

Redis集群配置参数

我们计划创建一个Redis集群例子。在我们深入之前,先介绍一下Redis集群在redis.conf文件中的配置参数。以便您阅读过程中更加清晰。

  • cluster-enabled <yes/no>:yes,指定的Redis实例支持Redis集群。否则Redis实例像一个普通的实例一样。

  • **cluster-config-file **:注意到,尽管有这个选项的名字,这不是用户可以编辑的配置文件,这个文件是Redis集群发生改变时,节点自动保留的集群配置(基本上状态这些)的文件,以便能够在启动时重新读取他。这个文件列出了集群中其他节点的状态/常量等。通常,由于收到某些消息,此文件将被重写并刷新到磁盘。

  • **cluster-node-timeout **:Redis集群节点不可用的最长时间,超过这个时间节点将被认为是失败的。如果一个主节点超过指定时间不可达,他将会被他的从节点转移。这个参数在集群中有着其他重要的作用。值得注意的是,每个超过这个时间不能到达主节点的节点将会停止查询。

  • **cluster-slave-validity-factor **:如果设置为0,无论主从节点断开多长时间,从节点会一直尝试针对主节点进行故障转移。如果这个值设置为正值,最大的的失联时间会被计算为_node timeout_ 乘以该因子的值,如果节点是从节点,当主节点失联超过这个最大失联时间后,从节点就不会再尝试进行故障转移。例如,如果_node timeout_ 设置为5秒,_valid factor_ 设置为10,从节点在与主节点失联50秒后就不会再进行故障转移。注意,当任何非0值被设置时且主节点失败且没有从节点可以进行故障转移时将导致Redis集群不可用。那种情况下只有当最初的主节点重新加入集群中,才能使集群可用。

  • **cluster-migration-barrier **:主节点持续连接的最少从节点的数量,为了让另外一个从节点迁移到没有从节点的主节点。可以参阅本教程[副本迁移](https://redis.io/topics/cluster-tutorial#replicas-migration)的相关章节获得更多信息。

  • cluster-require-full-coverage <yes/no>:如果设置成yes(默认值),如果集群中没有任何节点可以覆盖一定比例的哈希槽,集群将拒绝写入。如果设置为no,即使仅可以提供部分子集键值的请求,集群将继续可以查询。

  • cluster-allow-reads-when-down <yes/no>:如果设置为no(默认值),当集群被标记为失败,或者当一个节点不能链接到一定数量的主节点或没有被全覆盖,该节点将停止所有流量。由于该节点不能感知集群中的写,这样防止了潜在的不一致的读。选项可以被设置为yes,在错误状态下允许该节点进行读,这对于优先保证读高可用但仍然希望防止写入不一致的程序有用。当使用1或2个分片的Redis集群同样有用,当主失败且不能自动故障转移时,允许节点持续写入。

创建和使用Redis集群

提示:手动搭建一个Redis集群是非常重要的方式去学习影响集群运行的方方面面。然而,如果你打算尽快搭建一个集群同时让其运行起来跳过本章到下一节使用创建集群脚本创建一个Redis集群

为了创建一个集群,第一件事我们需要一些运行在cluster mode模式下的Redis实例。基本上这意味着不使用常规Redis实例创建集群,因为需要配置特殊模式,以便Redis实例启动集群特定的功能和命令。

下面是最小化Redis集群配置文件:

port 7000                                                     
cluster-enabled yes 
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes

可以看到,集群模式仅仅是启用cluster-enabled指示。每个实例同样包含这个节点存储配置文件的文件路径,默认是nodes.conf。这个配置文件不应该被人为修改,他仅仅是保证Redis集群实例启动时和需要更新时被节点使用。

请注意,按预期工作的最小集群要求至少包含3个主节点。对于第一个测试,强烈建议启动6个节点,3个主节点3个从节点。

为了部署6个节点,进入一个新的文件夹,并创建以下目录,这些目录以我们要运行的实例的端口号命名。 像下面那样:

mkdir cluster-test
cd cluster-test
mkdir 7000 7001 7002 7003 7004 7005

在从7000到7005的文件夹中创建redis.conf文件。就用上面例子中的最小配置,需要注意端口要改成和文件夹名一致。

现在拷贝你的redis-server可运行程序,从Github上编译的最新的不稳定版本到cluster-test文件夹,使用你喜欢的终端工具打开6个终端页。

像下面一样启动每个实例:

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

你可以从每个实例的日志中看到,由于nodes.conf不存在,每个节点赋予自己一个新ID。

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

这个ID将会这个实例一直使用,目的是在集群上下文中有一个独一无二的名称。每个节点使用这个ID记录其他节点,而不通过IP或者端口,IP地址和端口有可能改变,但是这个唯一的节点标识在节点的整个生命周期中都不会改变。我们简单的称这个ID为节点ID

创建集群

我们已经有一定数量的运行中的Redis实例,我们需要通过给节点写一些有意义的配置来创建集群。

如果你正在用Redis 5,这将很容易通过嵌入在redis-cli中的Redis集群命令行工具来完成创建集群,检查或重新分片一个已经存在的集群等。

对于Redis 3和4版本,有一个简单的老的工具叫做redis-trib。你可以在Redis源码文件夹的src目录找到他,你需要安装redis gem以运行redis-trib。

gem install redis

第一个例子,集群创建,将会使用Redis 5的redis-cli和Redis 3和4的redis-trib演示,但是所有接下来的例子都只会使用Redis 5的redis-cli演示。因为你可以看到这很简单,而且你可以使用redis-trib的help得到老的语法的使用。注意:如果你希望,可以无差别使用Redis 5的redis-cli配合Redis 4的集群使用。

为Redis 5使用redis-cli创建集群仅需:

redis-cli --cluster create 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\
--cluster-relicas 1

Redis 3和4 使用redis-trib.rb需要:

./redis-trib create 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

这里使用的命令是创建,由于我们想要创建一个新的集群。–cluster-replicas 1我们想要每个逐渐点有一个从节点。其他参数是我像创建集群的实例的地址。

很明显我们想要按照我们的要求创建有3个主节点和3个从节点的集群。 redis-cli将要提示你配置过程。输入yes接受这个提议的配置。集群将被配置和_加入_ ,也就意味着实例将启动并相互通信。最后如果一切正常,你会看到如下信息:

[OK] All 16384 slots covered

这意味着至少有一个主节点实例服务每个16384个哈希槽。

使用create-cluster脚本创建Redis集群

如果你不想按照上面的步骤手动配置和执行单个Redis实例,有一个相对简单的系统(但是您就不能学会相等数量的操作细节)。

在Redis发布版本中检查utils/create-cluster目录。在这个文件夹中有一个叫做create-cluster(和文件夹的名字一样)的脚本,这是一个简单的脚本。为了启动3个主节点3个从节点仅仅输入如下命令:

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

第二步中redis-cli工具希望你接受集群布局时,选择是。

现在你可以和集群进行交互了,第一个节点在默认30001端口启动。当你需要停止集群时,输入如下命令:

1.create-cluster stop

获取运行该脚本的更多信息,请阅读文件夹下的README文档。

初试集群

这阶段Redis集群的一个问题是缺少客户端库的实现。

我知道的有如下实现:

  • redis-rb-cluster是作者(@antirez)写的一个ruby实现,作为其他语言的参考。是初始redis-rb的简单封装,实现了和集群语义上交互的最小集。

  • redis-py-clusterredis-rb-cluster针对Python的实现。

  • 流行的Predis已经支持Redis集群,支持是最近更新,并在活跃开发。

  • 用的最多的Java客户端Jedis最近添加入了对集群的支持,查看工程中README关于_Jedis Cluster_ 的章节。

  • StackExchange.Redis提供了对C#的支持(对大部分.NET语言都支持;VB,F#等)。

  • thunk-redis为Node.js和io.js提供支持,是基于pipeline和cluster的精打细算和基于承诺的redis客户端。

  • redis-go-cluster是基于redigo library client的go语言集群实现,通过结果汇总实现了MGET/MSET。

  • ioredis是一个流行的Redis集群客户端,对Redis集群提供了健壮的支持。

  • redis-cli命令行工具当时用-c开关时实现了基本的集群支持

测试Redis群集的一种简单方法是尝试上述任何客户端,或者仅尝试redis-cli命令行实用程序。

以下是使用后者进行交互的示例:

$ 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群集节点能够将客户端重定向到正确的节点。一个严谨的客户端能够做的更好,缓存哈希槽和节点地址之间的映射,直接使用正确的链接到正确的节点。仅当集群配置变更时刷新缓存,例如故障转移之后或在系统管理员通过添加或删除节点来更改集群布局之后。

使用redis-rb-cluster写一个实例程序

在直接展示怎样操作Redis集群之前,做故障转移这类的事情,或者重新分片数据,我们需要创建一些示例程序,或至少能够理解简单的Redis集群客户端的交互语义。

使用这种方式我们可以运行一个例子同时试着让节点失败,或启动一个从新分片,去观察在真实世界Redis集群是怎样运行的。没有集群写入的情况下,观看Redis集群的行为并不是很有帮助。

这个章节解释了一些redis-rb-cluster的基本用法,展示了两个例子。第一个如下,是redis-rb-cluster分发包中的example.rb

 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这样类型的key的值为number。所以,如果运行这个程序,程序的结果就是下面的命令流:

  • SET foo0 0
  • SET foo1 1
  • SET foo2 2
  • 等等

这个程序看起来比他实现的功能更复杂,因为被设计为当出现错误时在屏幕打印错误,而不是抛出异常,所以每个操作都包裹了begin rescue块。

这个程序中第一个值得注意的是第14行。使用一系列_启动节点_ /集群中不同节点间允许的最大链接数/操作被认定为失败的超时时间作为参数,创建集群。

启动节点不必是集群中的所有节点。重要的是,至少一个节点是可到达的。同时注意,redis-rb-cluster连接上第一个节点后会更新启动节点。任何其他系列的终端也是这样的行为。

现在我们将Redis集群对象实例存储在了rc变量中,我们能够像使用普通的Redis实例一样使用集群了。

我们来看下18到26行发生了什么:当我们重启例子的时候,我们不打算从foo0开始,所以我们在Redis内部存储了计数器。上面的代码就是读取计数器的过程,如果计数器不存在设置他的值为0。

但是请注意,这是一个循环,因为我们想要不断的尝试,即便集群停止或返回错误。平常的程序不需要这样小心。

28到37行启动设置键值或打印错误的主循环。

注意在循环最后的sleep。在你的测试程序中如果你想尽可能快的写入集群,你可以移除这个sleep。(相对于真实情况而言,这是一个没有并行性的繁忙循环,在最好的情况下,你能达到10k ops/second)。

通常写入会被减慢,目的是方便人们进行跟踪,启动程序,产生如下输出:

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

这并不是一个非常有趣的程序,一会我们会使用一个更好的程序,我们可以看到在程序运行过程中,重新分片会发生什么。

现在我们已经准备好尝试集群重新分片。分片时请保持example.rb运行,这样可以看到重新分片会不会对正在运行的程序产生影响。同时你可能想要注释掉sleep函数,这样在重新分片时会有更加严重的写负载。

分片意味着从一个节点移动哈希槽到另一个节点,就像创建集群一样,我们使用redis-cli工具来完成这个工作。

启动一个分片仅仅输入:

redis-cli --cluster reshard 127.0.0.1:7000

你只需要指定一个节点,集群会自动找到其他节点。

当前redis-cli仅在管理员支持下才能进行重新分片,你不能仅仅说把5%的哈希槽从一个节点移动到另外一个节点(但这实现起来很简单)。所以从问题开始,第一个是你想做多少分片:

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

我们可以尝试重新分片1000个哈希槽,如果example.rb的程序不调用sleep运行。那么这1000个哈希槽应该包含了少量的键值。

redis-cli需要知道重新分片的目标节点,也就是接受哈希槽的节点。我将是用第一个节点127.0.0.1:7000,但是我需要指定这个实例的ID。实例ID已经被redis-cli打印在列表中,但是我需要的时候可以使用如下命令,找到节点ID:

[root@izbp13bgrt56ko6xg1vgy8z tech]# redis-cli -p 7000 cluster nodes | grep myself
d1a7dc981d64b750ce7ad8d44e658844ea5afd27 127.0.0.1:7000@17000 myself,slave 46d39c4c9567371d549ef7589027ac7b03b868d8 0 1593263296000 1 connected

好的,那我的目标ID就i是d1a7dc981d64b750ce7ad8d44e658844ea5afd27。

现在你会问,你将从哪个节点拿这些键值呢?我会输入all目的是都从其他主节点拿一些哈希槽。

最终确认后,您会看到一个消息,表明redis-cli将要从一个节点移动到另外一个节点,并且键值从一边移动到另外一边时,会打印一个点。

当分片正在进行的时候,你会看到你的实例程序将不受影响。如果你愿意,在分片期间,你可以停止,启动example程序多次。

分片的最后你可以使用如下命令来检测节点的健康状态:

redis-cli --cluster check 127.0.0.1:7000

所有的哈希槽都像平常一样被覆盖,但这次在127.0.0.1:7000的主节点将会有更多的哈希槽,6461附近。

脚本化分片操作

数据分片可以无需互动模式下手动输入参数,可以自动执行。可以使用如下的的命令:

redis-cli reshard <host>:<port> --cluster-from <node-id> --cluster-to <node-id> --cluster-slots <number of slots> --cluster-yes

如果您需要经常分片,上述指令允许你使用自动化操作,但是现阶段redis-cli还没有能力智能的自动化重新对不同集群节点上的键值进行重新负载,不能按需移动哈希槽。这个特性之后会被添加。

一个更加有趣的例子

我们之前写的示例程序并不是特别好。仅仅是直接写入到集群,甚至没有检查写入的值是否正确。

在我们看来集群接收的每个写的操作都会把键值foo写到42,而且我们根本不会注意到。

所以在redis-rb-cluster仓库中,有一个叫consistency-test.rb。他用了一系列计数器,默认1000,为了增加这个计数器,发送INCR命令。

然而不只是写入,这个程序做了两个额外的事情:

  • 当计数器使用INCR,程序记录这个写入。
  • 它还在每次写入之前读取一个随机计数器,并检查该值是否符合我们的预期,并将其与内存中的值进行比较。

这意味值这个程序是一个简单的一致性检查器,能够告诉你集群是否丢失了一些写,或者接受了我们没有收到确认通知的写。在第一种情况下,我们将看到计数器的值小于我们记录的值,然而在第二种情况下,他的值将大于我们记录的值。

启动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) |

这些行展示了执行的读R和写W的数量,以及错误(由于系统不可用的错误,查询没有被接受)的数量。

当一些不一致性出现时,新行会被加入到输出。例如我在程序运行过程中手动的复位了一个计数器。

$ 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 |

当我复位一个计数器为0,但是真实值其实是114时,程序报告有114个写丢失(INCR命令没被集群记录)。

这个程序作为一个测试程序更为有趣,因此我们使用他来测试集群故障转移。

### 测试故障转移

注意:测试期间,需要打开一个tab页保持一致性测试程序运行。

为了触发故障转移,我们能做的最简单的事情(同样是分布式系统中语义上最简单的失败)是崩溃一个单一进程,对我们来说就是一个master进程。

我们可以标识一个master进程,使用如下命令使他崩溃

 $ 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是master节点。我们使用DEBUG SEGFAULT命令让其崩溃。

 $ 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示例,他作为从服务器重新加入的集群)。

$ 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端口。之前运行在7002的主服务器现在是7005服务实例的从服务器。

集群节点的输出有点吓人,但是他其实相当简单,由一下标记组成:

  • Node ID
  • ip:port
  • flags:master,slave,myself,fail,…
  • 如果是从节点,显示其主节点的Node ID
  • 最后一个仍在等待反馈的未决的PING的时间
  • 最后一个收到的PONG的时间
  • 此节点的配置时期(查看Cluster specification)
  • 到该链接的链接状态
  • 服务的槽…

手动故障转移

有时在一个主节点上强制进行故障转移时很有用的。比如为了升级一个Redis主节点的进程,通过故障转移将这个主节点转化为从节点在可用性上带来的影响最小。

Redis集群可以使用CLUSTER FAILOVER命令支持,必须在主服务的一个服务器执行

手动故障转移比较特殊,而且相比真实的主服务故障转移更加安全,因为他的方式方式避免了过程中的数据丢失,确保新的主节点从老的主节点处理了所有的复制流,再切换所有的客户端到新的主节点。

下面是当你执行手动故障转移时将在从节点中看到的内容

# 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.

基本上,客户端链接上我们要进行故障转移的主节点。同时主节点发送给从节点复制偏移,等待从节点到达该复制偏移。当复制偏移到达时,故障转移开始,老的主节点被通知配置转换。当客户端在老的主节点上不被阻塞时,他们会被重定向到新的主节点。

添加新节点

添加一个新节点基本上是添加一个空节点,然后移动一些数据给他,这种情况下他是一个新主节点,或者告诉作为一个已知节点的备份,这种情况下他是一个从节点。

我们都会展示,先展示添加一个主节点。

所有情况下都需要添加一个空节点

这就像在7006端口启动一个新的Redis实例一样简单(我们已经使用了7000到7005作为我们现存的6个节点),使用除了端口以外的相同配置,这样你可以使用符合我们先前节点使用的配置:

  • 终端中创建一个新的tab页。
  • 进入cluster-test目录。
  • 创建7006目录。
  • 在7006目录中创建redis.conf文件,除了端口和其他节点的配置相同。
  • 最后启动服务 ../redis-server ./redis.conf

这时服务应该运行。

现在我们使用redis-cli添加这个节点到现存的集群中。

redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000

你可以看到,第一个参数用add-node命令指定了新节点,第二个参数是现存集群中随机存在的节点地址。

实际上,redis-cli这里对我们并没有特别大的帮助,他仅仅给节点发送了CLUSTER MEET消息,同样也可以手动完成。然而,redis-cli同样在操作之前同样检查集群的状态,因此,即便你知道内部怎么工作的时候,仍然使用redis-cli操作集群是一个好主意。现在我们可以链接到这个新的节点去查看他是否真正的加入了集群:

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

注意到这个节点已经连接到集群,并且可以重定向请求,通常来讲他已经是集群的一部分了。但是他有两个地方和其他主节点不同:

  • 由于没有存储hash槽,他没有保存数据
  • 由于他是一个没有存储hash槽的主节点,当一个从节点打算成为主节点时,他不参与选举。

现在可以使用redis-cli的resharding特性给这个节点设置hash槽。像我们上一节所做的一样,基本没有必要显示这些信息,没有区别,只是resharding时目标节点是空节点。

添加一个新节点作为一个从节点

添加一个从节点可以有两种方式进行。很明显的是使用redis-cli,使用–cluster-slave选项,就像这样:

redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000 --cluster-slave

注意这个命令很像我们使用的添加新节点的命令,我们没有指定哪个主节点去添加这个从节点。在这种情况下,发生的事情是redis-cli将新节点添加为副本较少的主节点中的随机主节点的副本。

你可以使用如下命令指定打算把从节点加入到哪个主机点:

redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000 --cluster-slave --cluster-master-id 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e

我们使用这种方式将从节点加入到主节点。

一个更加手动的方式添加从节点为空节点然后指定主节点是使用CLUSTER REPLICATE命令。同样适用于节点被添加为从节点,但是把他移动到另外一个主节点的操作。

例如为了给127.0.0.1:7005节点添加从节点,该节点服务11423-16383哈希槽,Node ID是3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e。我需要做的就是链接上新的节点(已经被加入为空主节点)然后执行命令:

redis 127.0.0.1:7006> cluster replicate 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e

就是这样,现在我们针对这些hash槽有了一个新的副本,并且所有其他的节点都已经知道(几秒后需要刷新配置)。我们可以使用以下命令验证配置:

$ 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端口。

删除一个节点

删除一个节点仅需要执行redis-cli的del-node命令:

redis-cli --cluster del-node 127.0.0.1:7000 `<node-id>`

第一个参数是集群中的一个随机节点地址,第二个参数是你想删除的节点ID。

你可以按照这个方式同样的删除一个主节点,但是为了移除一个主节点他必须是空的。如果主节点不是空的,需要提前将该主节点中的数据resharding到其他节点。删除主节点的另一种方法是在它的一个从节点上对其执行手动故障转移,并在该节点成为新主节点的从节点之后删除该节点。显然这种方法在你想减少集群中主节点的数量无济于事,如果想减少主节点的数量,分片是必须的。

副本迁移

在Redis集群中,任何时间去重新配置从节点到不同的主节点是可能的。使用如下命令:

CLUSTER REPLICATE <master-node-id>

然而这有一个特殊的情形,在没有管理员帮助下打算把一个副本从一个主节点自动迁移到另外一个主节点。这种自动重新配置副本叫做 副本迁移,能够提高Redis集群的可用性。

注意:你可以通过Redis Cluster Specification详细阅读副本迁移的内容,这里我们仅仅提供一些副本迁移的主要思想,以及你怎么从中受益。

特定情况下,你希望你的Redis集群中的副本从一个主节点到另外一个主节点的原因是,通常Redis集群抵抗失败的能力是和主节点副本数量成正比的。

例如每个主节点有一个从节点的集群,在主节点和从节点同时失败的情况下,就不能继续提供服务了,仅仅是因为没有实例存在该主节点服务的哈希槽的数据。然而net-splits很可能在同一时间内让一部分节点同时不可用,还有其他一些针对单节点类型的失败,像节点的硬件软件错误,这些显著的失败一般不会同时发生,但是在每个主服务有一个从服务的集群中,有可能出现的情况是从节点在4am失败,主节点在6am失败。这同样会导致集群不可用。

为了提高可用性,我们可以提高每个主节点的从节点数量,但这很昂贵。副本迁移允许给一些主节点添加更多的从节点。所以你有10个主节点每个主节点存在一个从节点,一共20个实例。然而你多添加3个从节点,所以有些主节点将存在不止一个从节点。

使用副本迁移如果主节点失去从节点后,有多个从节点的主节点的从节点将会迁移到该 孤儿节点。所以当你的从节点4am停掉后,根据上面的例子,另外一个从节点将要取代他的位置。当主节点5am崩溃后,仍然有一个从节点可以选举为主节点,保证集群的可用性。

所以简短的说,副本迁移你需要了解?

  • 给定时间内集群将从最多从节点的主节点中迁移副本。
  • 为了从副本迁移中收益,仅需要在集群中多添加一些副本给单一的主节点,无论这个主节点是什么。
  • 有个配置参数控制副本迁移特性,cluster-migration-barrier:你可以阅读例子中Redis集群提供的redis.conf的更多例子。

Redis集群中升级节点

升级一个从节点比较简单,仅需要重新启动一个升级版本的Redis实例即可。如果有客户端使用从节点进行scaling reads,当当前从节点不可用时,他们仅需要重新链接一个不同的从节点就好。

升级一个主节点比较复杂,推荐的步骤是:

  1. 在他的一个从节点上手动触发CLUSTER FAILOVER命令(查看本文的”手动故障迁移”章节)。
  2. 等待主节点变为从节点。
  3. 最后像升级从节点一样进行升级即可。
  4. 如果你仍然想要主节点是你刚才升级的节点,触发一个手动的故障迁移将升级的节点转换为主节点。

迁移到集群

仅有一个Redis主节点的用户可能有意愿升级到Redis集群,或者已经使用了一个已经存在的分片步骤,键值分散在N个节点,根据客户端或者Redis代理使用内部算法或分片算法。所有的情况下都能容易的升级到Redis集群,但是一个重要的细节是应用程序怎样使用多Key,有不同三种情况:

  1. 涉及多个键的多个键操作/事务/Lua脚本等使用不涉及多Key操作。键值都是独立访问的(即使是使用事务或Lua脚本组多Key命令,使用同一个键值)。
  2. 涉及多个键的多个键操作/事务/Lua脚本等使用涉及多Key操作,但是使用了同样的hash tag,也就是说他们的Key中使用{…}子字符串作为标识。例如下面的多Key操作被定义为相同的hash tag:SUNION {user:1000}.foo {user:1000}.bar。
  3. 涉及多个键的多个键操作/事务/Lua脚本与不具有显式或相同哈希标签的键名称一起使用。

Redis集群不能处理第三种情况:应用程序需要修改,不能使用涉及多个键的操作,或者显式的使用hash tag来使用他们。

情况1和情况2可以被Redis集群处理,所以我们聚焦在这两个情况下,他们被同样的掌控,所以文档不做区分。

假设你有一个数据集分割在N个主节点中,这里N=1,如果你没有对数据进行分片,下面的步骤帮助你迁移你的数据集到Redis集群。

  1. 停掉你的客户端。当前无法自动实时迁移到Redis集群。您可能在您的应用程序/环境中编排实时迁移。
  2. 使用BGREWRITEAOF命令为你的N个主节点生成append only file,等待AOF文件完全生成。
  3. 从aof-1到aof-N存储你的AOF文件。如果你希望,你停掉你的老的实例(如果不是虚拟部署,这会很有用,因为你通常需要复用这个相同的电脑)。
  4. 创建一个由N个主节点和0个从节点组成的Redis集群。后面你再添加从节点。确保所有的节点都使用AOF文件做持久化。
  5. 停掉所有的集群节点,使用你之前存储的aof文件替换他们的aof文件,aof-1对第一个节点,aof-2对应第二个节点,直到aof-N。
  6. 使用新的AOF文件重启集群。他们将会抱怨根据配置有些键值不应该在该实例。
  7. 使用reids-cli –cluster fix命令修补这个集群,这样根据节点和哈希槽是否匹配集群将会迁移这些键值。
  8. 最后使用redis-cli –cluster check确认集群是ok的。
  9. 使用知名Redis集群客户端库重启客户端。

有一个可选的方式从外部实例中导入数据到Redis集群,使用redis-cli –cluster import命令。

这个命令移动正在运行的所有的键值(从源实例中删除)到一个已经存在的Redis集群。但是需要注意,如果你使用的是Redis 2.8的实例作为源,这个操作将会很慢,因为2.8版本的Redis没有实实现迁移链接缓存,所以你可能需要在执行命令前使用Redis 3.X版本重启你的实例。

END 2020-07-24 ~30735