这篇教程是Redis集群的简要介绍,而非讲解分布式系统的复杂概念。它主要从一个使用者的角度介绍如何搭建、测试和使用Redis集群,至于Redis集群的详细设计将在“Redis集群规范”中进行描述。
本教程以redis使用者的角度,用简单易懂的方式介绍Redis集群的可用性和一致性。
注意: 本教程要求redis3.0或以上的版本。
如果你打算部署redis集群,你可以读一些关于集群的详细设计,当然,这不是必须的。由这篇教程入门,先大概使用一下Redis的集群,然后再读Redis集群的详细设计,也是不错的选择。
Redis集群 101
redis集群在启动的时候就自动在多个节点间分好片。同时提供了分片之间的可用性:当一部分redis节点故障或网络中断,集群也能继续工作。但是,当大面积的节点故障或网络中断(比如大部分的主节点都不可用了),集群就不能使用。
所以,从实用性的角度,Redis集群提供以下功能:
- 自动把数据切分到多个redis节点中
- 当一部分节点挂了或不可达,集群依然能继续工作
Redis集群的TCP端口
redis集群中的每个节点都需要建立2个tcp连接,监听这2个端口:一个端口称之为“客户端端口”,用于接受客户端指令,与客户端交互,比如6379;另一个端口称之为“集群总线端口”,是在客户端端口号上加10000,比如16379,用于节点之间通过二进制协议通讯。各节点通过集群总线检测宕机节点、更新配置、故障转移验证等。客户端只能使用客户端端口,不能使用集群总线端口。请确保你的防火墙允许打开这两个端口,否则redis集群没法工作。客户端端口和集群总线端口之间的差值是固定的,集群总线端口比客户端端口高10000。
注意,关于集群的2个端口:
- 客户端端口(一般是6379)需要对所有客户端和集群节点开放,因为集群节点需要通过该端口转移数据。
- 集群总线端口(一般是16379)只需对集群中的所有节点开放
这2个端口必须打开,否则集群没法正常工作。
集群节点之间通过集群总线端口交互数据,使用的协议不同于客户端的协议,是二进制协议,这可以减少带宽和处理时间。
Redis集群数据的分片
Redis集群不是使用一致性哈希,而是使用哈希槽。整个redis集群有16384个哈希槽,决定一个key应该分配到那个槽的算法是:计算该key的CRC16结果再模16834。
集群中的每个节点负责一部分哈希槽,比如集群中有3个节点,则:
- 节点A存储的哈希槽范围是:0 – 5500
- 节点B存储的哈希槽范围是:5501 – 11000
- 节点C存储的哈希槽范围是:11001 – 16384
这样的分布方式方便节点的添加和删除。比如,需要新增一个节点D,只需要把A、B、C中的部分哈希槽数据移到D节点。同样,如果希望在集群中删除A节点,只需要把A节点的哈希槽的数据移到B和C节点,当A节点的数据全部被移走后,A节点就可以完全从集群中删除。
因为把哈希槽从一个节点移到另一个节点是不需要停机的,所以,增加或删除节点,或更改节点上的哈希槽,也是不需要停机的。
如果多个key都属于一个哈希槽,集群支持通过一个命令(或事务, 或lua脚本)同时操作这些key。通过“哈希标签”的概念,用户可以让多个key分配到同一个哈希槽。哈希标签在集群详细文档中有描述,这里做个简单介绍:如果key含有大括号”{}”,则只有大括号中的字符串会参与哈希,比如”this{foo}”和”another{foo}”这2个key会分配到同一个哈希槽,所以可以在一个命令中同时操作他们。
Redis集群的主从模式
为了保证在部分节点故障或网络不通时集群依然能正常工作,集群使用了主从模型,每个哈希槽有一(主节点)到N个副本(N-1个从节点)。在我们刚才的集群例子中,有A,B,C三个节点,如果B节点故障集群就不能正常工作了,因为B节点中的哈希槽数据没法操作。但是,如果我们给每一个节点都增加一个从节点,就变成了:A,B,C三个节点是主节点,A1, B1, C1 分别是他们的从节点,当B节点宕机时,我们的集群也能正常运作。B1节点是B节点的副本,如果B节点故障,集群会提升B1为主节点,从而让集群继续正常工作。但是,如果B和B1同时故障,集群就不能继续工作了。
Redis集群的一致性保证
Redis集群不能保证强一致性。一些已经向客户端确认写成功的操作,会在某些不确定的情况下丢失。
产生写操作丢失的第一个原因,是因为主从节点之间使用了异步的方式来同步数据。
一个写操作是这样一个流程:
从上面的流程可以看出来,主节点B并没有等从节点B1,B2,B3写完之后再回复客户端这次操作的结果。所以,如果主节点B在通知客户端写操作成功之后,但同步给从节点之前,主节点B故障了,其中一个没有收到该写操作的从节点会晋升成主节点,该写操作就这样永远丢失了。
就像传统的数据库,在不涉及到分布式的情况下,它每秒写回磁盘。为了提高一致性,可以在写盘完成之后再回复客户端,但这样就要损失性能。这种方式就等于Redis集群使用同步复制的方式。
基本上,在性能和一致性之间,需要一个权衡。
如果真的需要,Redis集群支持同步复制的方式,通过WAIT指令来实现,这可以让丢失写操作的可能性降到很低。但就算使用了同步复制的方式,Redis集群依然不是强一致性的,在某些复杂的情况下,比如从节点在与主节点失去连接之后被选为主节点,不一致性还是会发生。
这种不一致性发生的情况是这样的,当客户端与少数的节点(至少含有一个主节点)网络联通,但他们与其他大多数节点网络不通。比如6个节点,A,B,C是主节点,A1,B1,C1分别是他们的从节点,一个客户端称之为Z1。
当网络出问题时,他们被分成2组网络,组内网络联通,但2组之间的网络不通,假设A,C,A1,B1,C1彼此之间是联通的,另一边,B和Z1的网络是联通的。Z1可以继续往B发起写操作,B也接受Z1的写操作。当网络恢复时,如果这个时间间隔足够短,集群仍然能继续正常工作。如果时间比较长,以致B1在大多数的这边被选为主节点,那刚才Z1发给B的写操作都将丢失。
注意,Z1给B发送写操作是有一个限制的,如果时间长度达到了大多数节点那边可以选出一个新的主节点时,少数这边的所有主节点都不接受写操作。
这个时间的配置,称之为节点超时(node timeout),对集群来说非常重要,当达到了这个节点超时的时间之后,主节点被认为已经宕机,可以用它的一个从节点来代替。同样,在节点超时时,如果主节点依然不能联系到其他主节点,它将进入错误状态,不再接受写操作。
Redis集群参数配置
我们后面会部署一个Redis集群作为例子,在那之前,先介绍一下集群在redis.conf中的参数。
- cluster-enabled
<yes/no>
: 如果配置”yes”则开启集群功能,此redis实例作为集群的一个节点,否则,它是一个普通的单一的redis实例。 - cluster-config-file
<filename>
: 注意:虽然此配置的名字叫“集群配置文件”,但是此配置文件不能人工编辑,它是集群节点自动维护的文件,主要用于记录集群中有哪些节点、他们的状态以及一些持久化参数等,方便在重启时恢复这些状态。通常是在收到请求之后这个文件就会被更新。 - cluster-node-timeout
<milliseconds>
: 这是集群中的节点能够失联的最大时间,超过这个时间,该节点就会被认为故障。如果主节点超过这个时间还是不可达,则用它的从节点将启动故障迁移,升级成主节点。注意,任何一个节点在这个时间之内如果还是没有连上大部分的主节点,则此节点将停止接收任何请求。 - cluster-slave-validity-factor
<factor>
: 如果设置成0,则无论从节点与主节点失联多久,从节点都会尝试升级成主节点。如果设置成正数,则cluster-node-timeout乘以cluster-slave-validity-factor得到的时间,是从节点与主节点失联后,此从节点数据有效的最长时间,超过这个时间,从节点不会启动故障迁移。假设cluster-node-timeout=5,cluster-slave-validity-factor=10,则如果从节点跟主节点失联超过50秒,此从节点不能成为主节点。注意,如果此参数配置为非0,将可能出现由于某主节点失联却没有从节点能顶上的情况,从而导致集群不能正常工作,在这种情况下,只有等到原来的主节点重新回归到集群,集群才恢复运作。 - cluster-migration-barrier
<count>
:主节点需要的最小从节点数,只有达到这个数,主节点失败时,它从节点才会进行迁移。更详细介绍可以看本教程后面关于副本迁移到部分。 - cluster-require-full-coverage <yes/no>:在部分key所在的节点不可用时,如果此参数设置为”yes”(默认值), 则整个集群停止接受操作;如果此参数设置为”no”,则集群依然为可达节点上的key提供读操作。
创建和使用Redis集群
注意:手动部署Redis集群能够很好的了解它是如何运作的,但如果你希望尽快的让集群运行起来,可以跳过本节和下一节,直接到”使用create-cluster脚本创建Redis集群”章节。
要创建集群,首先需要以集群模式运行的空redis实例。也就说,以普通模式启动的redis是不能作为集群的节点的,需要以集群模式启动的redis实例才能有集群节点的特性、支持集群的指令,成为集群的节点。
下面是最小的redis集群的配置文件:
port 7000
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
开启集群模式只需打开cluster-enabled配置项即可。每一个redis实例都包含一个配置文件,默认是nodes.conf,用于存储此节点的一些配置信息。这个配置文件由redis集群的节点自行创建和更新,不能由人手动地去修改。
一个最小的集群需要最少3个主节点。第一次测试,强烈建议你配置6个节点:3个主节点和3个从节点。
开始测试,步骤如下:先进入新的目录,以redis实例的端口为目录名,创建目录,我们将在这些目录里运行我们的实例。
类似这样:
mkdir cluster-test
cd cluster-test
mkdir 7000 7001 7002 7003 7004 7005
在7000-7005的每个目录中创建配置文件redis.conf,内容就用上面的最简配置做模板,注意修改端口号,改为跟目录一致的端口。
把你的redis服务器(用GitHub中的不稳定分支的最新的代码编译来)拷贝到cluster-test目录,然后打开6个终端页准备测试。
在每个终端启动一个redis实例,指令类似这样:
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称之为节点ID(Node ID)。
创建集群
现在6个实例已经运行起来了,我们需要给节点写一些有意义的配置来创建集群。redis集群的命令工具redis-trib可以让我们创建集群变得非常简单。redis-trib是一个用ruby写的脚本,用于给各节点发指令创建集群、检查集群状态或给集群重新分片等。redis-trib在Redis源码的src目录下,需要gem redis来运行redis-trib。
gem install redis
创建集群只需输入指令:
./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
这里用的命令是create,因为我们需要创建一个新的集群。选项”–replicas 1”表示每个主节点需要一个从节点。其他参数就是需要加入这个集群的redis实例的地址。
我们创建的集群有3个主节点和3个从节点。
redis-trib会给你一些配置建议,输入yes表示接受。集群会被配置并彼此连接好,意思是各节点实例被引导彼此通话并最终形成集群。最后,如果一切顺利,会看到类似下面的信息:
[OK] All 16384 slots covered
这表示,16384个哈希槽都被主节点正常服务着。
使用create-cluster脚本创建redis集群
如果你不想像上面那样,单独的手工配置各节点的方式来创建集群,还有一个更简单的系统(当然也没法了解到集群运作的一些细节)。
在utils/create-cluster目录下,有一个名为create-cluster的bash脚本。如果需要启动一个有3个主节点和3个从节点的集群,只需要输入以下指令
1. create-cluster start
2. create-cluster create
在步骤2,当redis-trib要你接受集群的布局时,输入”yes”。
现在你可以跟集群交互,第一个节点的起始端口默认是30001。当你完成后,停止集群用如下指令:
1. create-cluster stop.
请查看目录下的README,它有详细的介绍如何使用此脚本。
试用一下集群
在这个阶段集群的其中一个问题就是客户端库比较少。
我知道的一些客户端库有:
- redis-rb-cluster 我用ruby写的,作为其他语言实现的一个参考。它是原来的redis-rb的简单封装,实现了集群交互的最基础功能。
- redis-py-cluster redis-rb-cluster导出的python接口。支持大部分redis-py的功能,它处于活跃开发状态。
- Predis 在最近的更新中已经支持Redis集群,并且在活跃开发状态。
- java最常用的客户端 Jedis 最近加入对集群的支持,具体请查看Jedis README中关于集群章节。
- StackExchange.Redis 支持C#,对于大部分.NET语言,VB,F#等应该都支持。
- thunk-redid 支持Node.js和io.js。它是支持pipelining和集群的 thunk/promise-based redis 客户端
- redis-cli 在不稳定版分支中,对集群提供了最基础的支持,使用-c指令开启。
可以用上面提供的客户端或redis-cli命令来测试集群。
下面用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"
注意:如果你用脚本来创建的集群,你的redis可能监听在不同的端口,默认是从30001开始。
redis-cli利用集群的任意节点会告知客户端正确节点的特性,实现了集群客户端的最基础功能。实现得比较严谨的客户端可以缓存哈希槽到节点的映射关系,让客户端直接连接到正确的节点,只有集群的节点配置有更新时才刷新缓存,比如发生了故障迁移,或者管理员增加或减少了节点等。
用redis-rb-cluster写一个应用例子
在进一步学习如何操作Redis集群之前,比如了解故障迁移、重新分片等,我们先理解客户端是如何与集群交互的。
我们通过一个例子,让部分节点故障或重新分片等,来了解这实际运作中,redis集群是如何处理的。如果这期间没有客户端对集群发起写操作,将不益于我们了解情况。
这节通过2个例子来演示redis-rb-cluster的基础用法,下面是第一个例子,源码在redis-rb-cluster目录下的example.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>”的数值为number,等效于于运行下面的指令:
这脚本看起来比平普通的脚本复杂,因为在遇到错误的时候,它需要把错误显示出来,而不是因为一个异常就停止运行,所以每个操作都用”begin” “rescure”包裹起来。
第7行,创建了一个Redis Cluster对象,使用的3个参数分别是:第一个参数startup_nodes,客户端需要连接的集群的部分或全部节点列表;第二个参数是此对象连接集群内的各节点时允许创建的最大连接数;第三个参数是一个操作多久得不到响应则被判为失败的超时设定。
第一个参数startup_nodes不要求包含集群的所有节点,但要求至少有一个节点是正常运作的。注意,redis-rb-cluster在它连上第一个节点后,会自动更新startup_nodes,实现得比较严谨的其他客户端也应该是这样的。
现在,我们可以像使用普通的Redis对象实例一样,来使用变量名为rc的Redis集群对象实例了。
第11到19行:每次我们重新运行此脚本的时候,我们不希望每次都从”foo0″开始执行,所以我们在redis中存储了一个计数器,记录我们执行到哪了。这几行代码就是读这个计数器,如果这个计数器不存在,就给他个初始值:0。
注意一下while循环,我们希望这个脚本可以一直运行,哪怕在集群宕机的时候,打印一个error提示然后要继续运行。普通的应用程序可以不用这样。
第21到30行:写key的主循环。
注意最后一行的sleep调用,如果希望往集群中写的速度快点,可以把这行sleep调用删除(在最好的情况下,每秒可以执行1万个请求)。
为了方便我们跟踪脚本的情况,一般情况下我们会让它执行得慢点。
下面是我运行脚本的输出
ruby ./example.rb
1
2
3
4
5
6
7
8
9
^C (我停止了此脚本)
这个脚本很无趣(后面我们再写个有趣点的),但它已经足够帮助我们了解重新分片的情况(需要让它一直运行着)。
重新分片
现在,我们开始重新分片。在重新分片时,请让刚才的example.rb脚本继续运行,这样可以看到重新分片对此脚本的影响,同时,你可以把sleep调用注释掉,以便在重新分片过程中的增加写入的压力。
重新分片简单的说就是把哈希槽从一些节点移动到另外一些节点。重新分片可以像创建集群一样,使用redis-trib来完成。
开始重新分片,输入以下指令:
./redis-trib.rb reshard 127.0.0.1:7000
你只需要指定集群中的一个节点,redis-trib会自动找到集群中的其他节点。
目前,redis-trib只支持管理员操作,不能够说:移动50%的哈希槽从这个节点到那个节点。它以问问题的方式开始。第一个问题是,你需要重新分片多少个哈希槽:
How many slots do you want to move (from 1 to 16384)?
由于我们之前的脚本一直在运行,而且没有用sleep调用,这时候应该已经插入了比较多的key了。我们可以尝试给1000个哈希槽重新分片。
然后,redis-trib需要知道我们要把这1000个哈希槽移动到哪个节点去,也就是接受这1000个哈希槽的节点。我想用127.0.0.1:7000这个节点。需要用节点ID来告知redis-trib是哪个节点。redis-trib已经在屏幕上列出了所有的节点和他们的ID。也可以通过以下命令找到指定节点的ID:
$ redis-cli -p 7000 cluster nodes | grep myself
97a3a64667477371c4479320d683e4c8db5858b1 :0 myself,master - 0 0 0 connected 0-5460
好了,我的目标节点是97a3a64667477371c4479320d683e4c8db5858b1。
现在redis-trib会问:你想从哪些节点中挪走这些哈希槽呢?我输入all,会从其他的主节点中挪走哈希槽。
在输入最后确认之后,redis-trib在屏幕上会输出每一个哈希槽将从哪个节点转移到哪个节点。每实际移动一个key屏幕就会打印一个点。
在重新分片的过程中,你可以看到,你刚才运行的脚本不受影响,你甚至可以在重新分片的过程中,反复的重新运行该例子脚本。
在重新分片结束后,你可以检查集群的当前状态是否正常,运行下面的命令:
./redis-trib.rb check 127.0.0.1:7000
所有的哈希槽都存在,这时候127.0.0.1:7000的主节点有多一点的哈希槽,有大概6461个。
脚本化重新分片
重新分片可以不用以交互的方式进行,使用下面的指令可以自动执行:
./redis-trib.rb reshard --from <node-id> --to <node-id> --slots <number of slots> --yes <host>:<port>
如果你想要经常的重新分片,可以使用上面的指令自动分片,但是目前redis-trib脚本不会根据节点上的key的分布来做负载均衡、智能地迁移哈希槽。这个特性在将来我们会添加的。
一个更有趣的例子
我们之前写的例子不太好,因为它只是简单的向集群写数据,却不检查写的数据是不是正确的。假设它一直往集群中写的都是把”set foo 42”, 我们也不会发现。
所以在redis-rb-cluster中有一个更有趣的例子,叫consistency-test.rb,它使用一组计数器,通过INCR指令来增加这些计数器。
除了只是INCR的写之外,它还做了2件其他事情:
*当一个计数器使用INCR指令的时候,该应用程序记录着它的返回值
*每次写之前,先随机地读一个计数器,比较一下它的结果是否跟缓存的结果一致
这个程序是一个简单的一致性检查器(consistency checker),如果计数器在redis中的数值小于缓存中的数值,则认为丢失了部分写;如果是大于,则认为多了一些不属于此应用程序加进去的数据。
运行此测试脚本,每秒会在屏幕显示类似以下的数据:
$ 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 |
当我把一个本来是114的计数器设置成0的时候,此程序就报告有114个写丢失了。
这个例子作为一个测试用例非常有趣,下面我们用它来测试集群的故障迁移。
测试故障迁移
注意:测试过程中,请让上面的一致性测试的应用程序一直运行中。
为了触发故障迁移,最简单的办法是让一个进程宕机,在我们的用例中,就是让其中一个主节点进程宕机。
我们可以用下面的指令区分集群节点:
$ 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是主节点,我们要使7002当机,使用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-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了)。
“cluster nodes”指令的输出看起来蛮吓人的,其实很简单,它的每列意义如下:
*节点ID
*IP:端口
*标记位:主节点,从节点,自己,失败。。。
*如果是从节点,则接下来是它的主节点的ID
*最后一次发送ping依然等待响应的时间
*最后一次收到pong的时间
*上次更新此配置的时间
*节点的连接状态
*存储的哈希槽
手动故障转移
有时候,手动故障转移是非常有用的,它不会给主节点带来任何问题,比如,需要升级某个主节点的redis进程,可以先通过手动故障转移使之成为从节点,让升级对集群可用性的影响达到最低。
redis集群支持通过指令”CLUSTER FAILOVER”产生故障转移,但需要在被失效的主节点的一个从节点上执行该命令。
相对于真的主节点宕机,手动故障转移是比较安全的,它可以避免数据丢失,当新的主节点复制完所有数据之后,会让客户端从原来的主节点重定向到新的主节点。
下面是在其中一个从节点上执行了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.
简单的说:客户端停止连接被故障转移的原主节点;同时该原主节点把还没同步的复制集同步给从节点;当从节点收到所有复制集之后,故障转移开始,原来主节点被通知配置更新,主节点更换了;客户端被重定向到新的主节点。
新增新的节点
新增一个节点,就增加一个空的节点到集群。有两种情况:如果新增的是主节点,则是从集群的其他节点中转移部分数据给它;如果新增的是从节点,则告诉它从一个已知的节点中同步复制集。
我们2种情况都试试。首先是新增一个新的主节点到集群中。
两种情况,都是需要先加入一个空的节点到集群中
鉴于我们前面已经启动了6个节点,端口号7000-7005已经用了,新增节点的端口号就用7006吧。新增一个新的空节点,就跟上面启动前面6个节点的步骤一样(记得改配置文件的端口号):
现在该节点应该运行起来了。
现在,我们使用redis-trib来增加一个新节点到集群中:
./redis-trib.rb add-node 127.0.0.1:7006 127.0.0.1:7000
使用add-node指令来新增节点,第一个地址是需要新增的节点地址,第二个地址是集群中任意一个节点地址。
redis-trib脚本只是给发送CLUSTER MEET消息给节点,这也可以手动地通过客户端发送,但redis-trib在发送之前会检查集群的状态,所以,还是用redis-trib脚本来操作集群会比较好。
现在我们可以连上新的节点,看看它是不是已经加入集群了:
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
虽然现在此新节点已经连到集群,并且可以重定向客户端到正确的集群节点了,但是它跟集群的其他主节点有个不同的地方:
*它没有数据,因为没有分配哈希槽给它
*因为它是一个没有哈希槽的主节点,当一个从节点需要被选举成新主节点时,它没有参与权
可以通过redis-trib的重新分片指令来给新节点增加哈希槽。由于前面已经介绍过如何重新分片了,这里就不做详细介绍。
添加一个从节点
新增从节点有两种方法,第一个是使用上面的redis-trib脚本,增–slave选项,类似这样:
./redis-trib.rb add-node --slave 127.0.0.1:7006 127.0.0.1:7000
注意到上面的命令行跟我们加主节点的命令行类似,所以没有没有指定新增的从节点的主节点是哪个,这时候redis-trib会在拥有最少从节点的主节点中随机选一个作为新增节点的主节点。
当然也可以通过如下的命令指定新增从节点的主节点:
./redis-trib.rb add-node --slave --master-id 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 127.0.0.1:7006 127.0.0.1:7000
用上面的指令,我们可以指定新的从节点是那个主节点的副本集。
另一个方法,先把新节点以主节点的形式加入到集群,然后再用“CLUSTER REPLICATE”指令把它变为从节点。这个方式也适用于给从节点更换主节点。
比如,已有主节点127.0.0.1:7005,它存储的哈希槽范围是11423-16383,节点ID为3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e,我们希望给它新增从节点。首先用之前的方法新增一个空的主节点,然后连上该新节点,发送如下指令:
redis 127.0.0.1:7006> cluster replicate 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e
这样,新的从节点添加成功,而且集群中其他所有节点都已经知道新节点了(可能需要一些时间来更新配置)。我们可以通过以下指令来验证:
$ 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…有2个从节点,分别是原来的运行在7002端口的节点和刚刚新增的7006端口的节点。
删除节点
使用redis-trib的指令”del-node”可以删除节点:
./redis-trib del-node 127.0.0.1:7000 `node-id`
第一个参数是集群的任意一个节点,第二个参数是需要删除的节点的ID。
同样的方法可以删除主节点,但是在删除之前,需要通过重新分片把数据都移走。
另一个删除主节点的方式是通过手动故障转移,让它的其中一个从节点升级成主节点后再把此节点删除。但这样并不会减少集群的主节点数,如果需要减少主节点数,重新分片在所难免。
复制集迁移
在redis集群中,可以通过如下指令,在任意时间给从节点更换主节点:
CLUSTER REPLICATE <master-node-id>
有一种特殊的场景:系统自动地更改复制集的主节点,而不是要管理员手动处理。这种自动重新配置从节点的情景叫副本集迁移(replicas migration),它可以增加redis集群的健壮性。
注意:你可以通过《Redis Cluster Specification》了解更多详细的内容,这里只是对它的简单介绍以及它的用处。
假如一个每个主节点只有一个从节点的集群,在一个主节点和从节点同时故障的情况下,集群将不能继续工作,因为已经故障的节点中存储的哈希槽数据已经没法读写。虽然网络断开很可能会让一大批节点同时被隔离,但是还有很多其他情况会导致节点故障,比如硬件或者软件的故障导致一个节点宕机,也是非常重要的导致节点故障的原因,这种情况一般不会所有节点同时故障。比如,集群中每个主节点都有一个从节点,在4点的时候一个从节点被kill,该主节点在6点被kill。这样依然会导致集群不能工作。
为了增强系统的可用性,可以给每个主节点再增加一个从节点,但这样做是比较昂贵的。复制集迁移可以让我们只给部分主节点增加多些从节点。比如有10个主节点,每个主节点有1个从节点,总共20个节点,然后可以再增加一些从节点(比如3个从节点)到一些主节点,这样就有部分主节点的从节点数超过1。
当一个主节点没有从节点时,如果集群中存在一个主节点有多个从节点时,复制集迁移机制在这些多个从节点中找一个节点,给没有从节点的主节点做复制集。所以当4点钟一个从节点宕机,另一个从节点将会代替它成为该主节点的从节点;然后在5点钟主节点宕机时还有一个从节点可以升级成主节点,这样集群可以继续运行。
所以,简单的说副本集迁移就是:
*集群会找到拥有最多从节点的主节点,在它的从节点中挑选一个,进行复制集迁移
*为了让复制集迁移生效,只需要在集群中多加几个从节点,随便加到哪个主节点都可以
*关于复制集迁移,有一个配置参数叫“cluster-migration-barrier”,在集群的样板配置文件中有详细说明,需要了解清楚
升级集群中的节点
升级从节点非常简单,只要停止它再重启更新过的版本即可。如果客户端连到了从节点,在该节点不可用时,客户端需要重连到另外可用的从节点上。
升级主点则相对复杂,下面是推荐的流程:
1. 使用CLUSTER FAILOVER指令触发手动故障转移,让主节点变成从节点
2. 等到主节点成为从节点
3.升级该从节点
4. 如果你想让升级过的节点重新变成主节点,则再次触发手动故障转移,让它变成新的主节点。
用这样的步骤,一个个的升级所有节点。
迁移到redis集群
用户需要把redis的数据迁移到redis集群,原来的数据可能是只有一个主节点,也可能是用已有的方式分片过,key被存储在N个几节点中。
上面2中情况都很容易迁移,特别重要的细节是是否使用了多个key以及是如何使用多个key的。下面是3种不同的情况:
1. 没有操作多个key(包括操作多个key的指令、事务、lua脚本)。所有key都是独立操作的.
2. 操作了多个key(包括操作多个key的指令、事务、lua脚本),但这些key都有相同的哈希标签,比如这些被同时操作的key:SUNION{user:1000}.foo {user:1000}.bar
3. 操作了多个key(包括操作多个key的指令、事务、lua脚本),这些key没有特别处理,也没有相同标签。
第三种情况redis集群没法处理,需要修改应用程序,不要使用多个key,或者给这些key加上相同的哈希标签。
第一和第二种情况可以处理,而且他们的处理方式一样。
假设你已有的数据被分成N个主节点存储(当N=1时,就是没有分片的情况),要把数据迁移到redis集群,需要执行下面几个步骤:
1. 停止你的客户端。目前没有自动在线迁移到redis集群的方法。你可以自己策划如何让你的应用程序支持在线迁移。
2. 使用BGREWRITEAOF指令让所有主节点产生AOF文件,并且等待这些文件创建完成。
3. 把这些AOF文件保存下来,分别命名为aof-1, aof-2, ..aof-N,如果需要,可以停止原来的redis实例(对于非虚拟化部署,需要重用这台电脑来说,把旧进程停掉很有帮助)。
4. 创建N个主节点+0个从节点的redis集群。晚些时候再添加从节点。请确认所有节点都开启了appendonly的配置。
5. 停止集群的所有节点,然后用刚才保存的AOF文件,代替每个节点的AOF文件,aof-1给第一个节点,aof-2给第二个节点,以此类推。
6. 重启所有节点,这些节点可能会提示说根据配置有些key不应该存储在这个节点。
7. 使用redis-trib fix指令,让集群自动根据哈希槽迁移数据
8. 使用redis-trib check指令确保你的集群是正常的
9. 让你的客户端使用redis集群客户端库,并重启它。
还有一个方法可以从已有的redis实例中导入数据到redis集群,使用redis-trib import指令。该指令会把源实例中的数据都删除,并把数据写入事先部署好的集群中。需要注意的是,如果你的源实例使用的是redis2.8版本,这个导入过程可能会比较长,因为2.8版本没有实现数据迁移的连接缓存,所以最好把源实例的redis版本先升级到3.x的版本。
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/117745.html