si
什么是Redis的集群?
我们先了解下什么是集群?
当单个服务器处理到达瓶颈的时候,将其复制几份,这样就构成了一个“集群”。集群中每台服务器就叫做这个集群的一个“节点”,所有节点构成了一个集群。每个节点都提供相同的服务,那么这样系统的处理能力就提升了好几倍(有瓶颈)。
Redis的集群是Redis提供的分布式数据库方案,集群通过分片来进行数据共享,并提供复制和故障转移功能,Redis集群至少需要6台Redis服务器。
节点与集群
一个节点就是一个普通的Redis服务器,一个Redis集群通常由多个节点组成,开始每个节点都是相互独立的,它们都处于一个只包含自己的集群当中。要组建一个真正可以工作的集群,必须将各个独立的节点连接起来,构成一个包含多个节点的集群。
节点的信息都保存在自身的一个数据结构clusterNode
当中,clusterNode
结构保存了一个节点的当前状态,比如节点的创建时间、节点名字、节点当前的配置纪元、节点的IP地址和端口号、节点处理的槽等等信息。
启动节点
普通的Redis服务器配置没有开启集群功能,自然也就不能说它是一个节点。既然这样,那我们就去配置文件开启集群开关,配置文件加入内容:
cluster-enabled yes //打开集群模式
cluster-config-file nodes.conf //设定节点配置文件名
cluster-node-timeout 5000 //设定节点超时时间,超过该时间集群自动进行主从切换
下面为例子redis6380.conf
文件的配置内容:
include /usr/local/etc/redis.conf
port 6380
pidfile "/var/run/redis_6380.pid"
dbfilename "dump6380.rdb"
cluster-enabled yes
cluster-config-file nodes6380.conf
cluster-node-timeout 5000
配置完成后指定这个配置文件启动即可:
$ redis-server /usr/local/etc/redis6380.conf
34342:C 13 Apr 2019 10:11:29.949 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
34342:C 13 Apr 2019 10:11:29.949 # Redis version=5.0.4, bits=64, commit=00000000, modified=0, pid=34342, just started
34342:C 13 Apr 2019 10:11:29.949 # Configuration loaded
查看一下该进程,发现后面多了个[cluster]
的标识,即说明该Redis服务器开启了集群模式(如果要连接该节点需要使用redis-cli -c - p <port>
命令,表示该节点开启了集群)。
$ ps -ef|grep redis
501 34343 1 0 10:11上午 ?? 0:00.09 redis-server *:6380 [cluster]
节点组成集群
集群由多个节点组成,所以需要先将各个节点连接起来,连接各个节点的工作可以使用cluster meet
命令来完成:
127.0.0.1:6379>cluster meet <IP> <Port>
客户端连接节点后向另一个节点发送该命令可以让这两个节点进行握手,握手成功后,另一个节点就会被添加到发起命令节点所在的集群中,过程如下图:
举个例子,启动3台Redis服务器(当前电脑6380、6381、6382、)后(注意
:正常集群至少需要6台Redis服务器,3主3从,此处只是实验),6380节点将6381和6382节点加入进自身集群中:
//启动完的6个节点
501 32901 1 0 10:04下午 ?? 0:00.09 redis-server *:6380 [cluster]
501 32910 1 0 10:04下午 ?? 0:00.07 redis-server *:6381 [cluster]
501 32918 1 0 10:04下午 ?? 0:00.07 redis-server *:6382 [cluster]
//将6381和6382节点加入自身集群
$ redis-cli -c -p 6380
127.0.0.1:6380> cluster meet 127.0.0.1 6381
OK
127.0.0.1:6380> cluster meet 127.0.0.1 6382
OK
具体过程如下图(名字不同,意思了解即可):
节点通信消息
集群中的各个节点通过发送和接收消息来进行通信,发送消息的节点为发送者,接收消息的节点为接收者。
节点发送的消息主要为以下五种:
-
MEET消息:当发送者接到客户端发送的
cluster meet
命令时,发送者会向接收者发送MEET
消息,请求接收者加入到发送者当前所处集群里; -
PING消息:集群的每个节点默认每隔1秒钟就会从已知节点列表随机挑选5个节点,然后对这5个节点中最长时间没有发送过
PING
消息的节点发送PING
消息,以此来检测被选中节点是否在线,其他特殊情况也会发送该消息; -
PONG消息:接收者确认
MEET
或PING
消息已到达时,会向发送者发送该消息告知。特殊情况如故障转移成功后会向集群广播一条该消息; -
FALL消息:当一个主节点A判断另一个主节点B已经进入
FALL
状态时,节点A会向集群广播B的FALL
消息,所有收到这条消息的节点都会立即将节点B标记为已下线; -
PUBLISH消息:当节点接收到一个
publish
命令时,节点会执行这个命令,并向集群广播一条PUBLISH
消息,所有接收到这条消息的节点都会执行相同的publish
命令。
槽(slot
)
Redis集群通过分片的方式来保存数据库中的键值对,集群的整个数据库被分为16384个可用插槽(slot
),数据库中的每个键都属于这16834个插槽中的一个,集群中的每个节点可以处理0个或最多16834个插槽。
当数据库中的16834个槽都有节点在处理时,集群处于上线状态(ok
),相反地,如果数据库中有任何一个槽没有得到处理,集群处于下线状态(fail
).
前面我们搭建的集群极为下线状态,因为集群中的3个节点都没有处理任何槽,我们可以通过cluster info
命令查看其信息:
127.0.0.1:6380> cluster info
cluster_state:fail //集群未上线
cluster_slots_assigned:0
cluster_slots_ok:0
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:3
cluster_size:0
cluster_current_epoch:2
cluster_my_epoch:0
cluster_stats_messages_ping_sent:24
cluster_stats_messages_pong_sent:25
cluster_stats_messages_meet_sent:2
cluster_stats_messages_sent:51
cluster_stats_messages_ping_received:25
cluster_stats_messages_pong_received:26
cluster_stats_messages_received:51
指派槽给不同节点
既然集群未上线,需要指派所有槽给不同节点,那么我们指派就好了,通过redis-cli -h <IP> -p <Port> cluster addslots {0..5460}
命令可以指派相应槽给指定节点:
$ redis-cli -h 127.0.0.1 -p 6380 cluster addslots {0..5460}
OK
# wksky @ localhost in ~ [10:47:57]
$ redis-cli -h 127.0.0.1 -p 6381 cluster addslots {5461..11111}
OK
# wksky @ localhost in ~ [10:48:29]
$ redis-cli -h 127.0.0.1 -p 6382 cluster addslots {11112..16383}
OK
因为16384个槽都分配完毕,所以现在集群处于上线状态:
$ redis-cli -c -p 6380
127.0.0.1:6380> cluster info
cluster_state:ok //集群上线
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:3
cluster_size:3
cluster_current_epoch:2
cluster_my_epoch:1
cluster_stats_messages_ping_sent:106
cluster_stats_messages_pong_sent:108
cluster_stats_messages_meet_sent:2
cluster_stats_messages_sent:216
cluster_stats_messages_ping_received:108
cluster_stats_messages_pong_received:108
cluster_stats_messages_received:216
127.0.0.1:6380> cluster nodes
f661beaff3b734caa56c8e92a62c0e4e7f661c2d 127.0.0.1:6381@16381 master - 0 1555130707063 0 connected 5461-11111
d033f0bde8d29441ea01ba275ad93714a80219ba 127.0.0.1:6382@16382 master - 0 1555130708072 2 connected 11112-16383
ab8516a5f78a700fb982b03db954fb0946ee7489 127.0.0.1:6380@16380 myself,master - 0 1555130705000 1 connected 0-5460
节点中的槽信息(可跳过)
前面说过,不同的节点都保存了一个clusterNode
结构,该结构中的slots
和numslot
属性记录了节点复制处理哪些槽。slots
属性是一个二进制数组,长度2048字节,包含16384个二进制位,将二进制位编号为索引,根据索引上的二进制位的值判断节点是否负责处理槽i,numslot
即位处理槽的数量(所有值相加,下图数量为7),如下图:
一个节点除了会将自己负责除了的槽信息记录在2个属性当中,还会将自己的
slots
数组通过消息发送给集群中的其他节点,以此来告知其他节点自己目前负责处理哪些槽,如下图:在集群中执行命令
在对数据库中的16384个槽都进行了指派之后,集群就会进入上线状态,这时客户端就可以向集群中的节点发送数据命令了。
当客户端向节点发送与数据库键有关的命令时(如set username lucy
),接受命令的节点会计算出命令要处理的数据库键属于哪个槽,并检查这个槽是否指派给了自己,如下图:
比如我们在之前6380、6381、6382三个节点组成的集群中,用客户端连接节点6380,执行下面的命令:
127.0.0.1:6380> set date 2019-4-13
OK
因为键date
所在的槽正是节点6380负责处理的,所以执行命令成功,如果不是呢?比如键username
不是当前节点能处理的,它会说该键由14315槽处理,而该槽需要在节点6382才能处理,所以重定向到6382节点进行处理,代码如下:
127.0.0.1:6380> set username lucy
-> Redirected to slot [14315] located at 127.0.0.1:6382
OK
127.0.0.1:6382>
键所在槽位置计算
不同键所属槽可以通过它给定的算法(CRC)计算,此处不再介绍。
节点数据库
集群节点保存键值对以及键值对过期时间的方式和单机Redis服务器保存键值对以及键值对过期时间的方式完全相同,此处不再介绍。
节点和单机数据库在数据库方面的一个区别是:节点只能使用0号数据库,而单机Redis服务器则没有这一限制。
重新分片
Redis集群的重新分片操作可以将任意数量已经指派给某个节点(源节点)的槽指派给另一个节点(目标)节点,并且相关槽所属的键值对也会从源节点被移动到目标节点。
重新分片操作可以在线进行,该操作集群不需要下线,并且源节点和目标节点都可以继续处理命令请求。
举个例子,对之前包含6380、6381、6382三个节点的集群来说,我们可以向这个集群添加一个IP为127.0.0.1,端口号为6390的节点(请先启动该节点):
127.0.0.1:6380> cluster meet 127.0.0.1 6390
OK
127.0.0.1:6380> cluster nodes
f661beaff3b734caa56c8e92a62c0e4e7f661c2d 127.0.0.1:6381@16381 master - 0 1555130814019 0 connected 5461-11111
a6e9d00dc85f274259ed9e21417885d1085f4f33 127.0.0.1:6390@16390 master - 0 1555130813009 3 connected
d033f0bde8d29441ea01ba275ad93714a80219ba 127.0.0.1:6382@16382 master - 0 1555130812000 2 connected 11112-16383
ab8516a5f78a700fb982b03db954fb0946ee7489 127.0.0.1:6380@16380 myself,master - 0 1555130812000 1 connected 0-5460
从节点信息可以很明显的看到6390节点没有指派任何一个槽,现在我们将6382指派过的槽中的11112槽指派给6390节点:
127.0.0.1:6390> cluster delslots 11112
OK
127.0.0.1:6390> cluster addslots 11112
OK
a6e9d00dc85f274259ed9e21417885d1085f4f33 127.0.0.1:6390@16390 myself,master - 0 1555133779000 3 connected 11112
d033f0bde8d29441ea01ba275ad93714a80219ba 127.0.0.1:6382@16382 master - 0 1555133780877 2 connected 11113-16383
f661beaff3b734caa56c8e92a62c0e4e7f661c2d 127.0.0.1:6381@16381 master - 0 1555133780574 0 connected 5461-11111
ab8516a5f78a700fb982b03db954fb0946ee7489 127.0.0.1:6380@16380 master - 0 1555133780000 1 connected 0-5460
这样分片十分麻烦,所以一般使用Redis的集群管理软件redis-trib
来进行分片,介绍见一篇文章
注意:
重新分片过程中可能出现ASK错误,即客户端发送获取键的命令的时候,该键所处的槽正在从一个节点迁移到另一个节点,就会报ASK错误,不过节点会根据这个错误提供的IP和端口号转向目标节点发送ASKING
命令,之后在重新发送原本想要执行的命令,所以没啥事。。。
节点复制与故障转移
Redis集群中的节点分为主节点(master)和从节点(slave),其中主节点用于处理槽,从节点用于复制某个主节点(从节点不含槽),并在被复制的主节点下线时,代替下线主节点继续处理命令请求.
举个例子,对于包含7000、7001、7002、7003、7004、7005的六个主节点集群来说,我们可以将7004和7005设定为节点7000的从节点,如下图:
设定各个节点状态即它们工作如下图:
当节点7000进入下线状态,集群经过一段时间后将进行故障转移工作,仍在运行的几个主节点就会将节点7000的两个从节点之一挑选出一个新的作为新的主节点,其接管原来节点7000负责处理的槽,并继续处理客户端发送的命令请求:
故障转移之后,下线的7000节点重新上线成为节点7004的从节点,如下图:
当然这些工作并没有这么简单就完成,要经过下面3个步骤:
①设置从节点;
②故障检测;
③故障转移。
设置从节点
通过向一个节点发送命令cluster replicate <node_id>
即可让当前节点成为node_id
节点的从节点,并开始对node_id
节点进行复制:
127.0.0.1:7004> cluster replicate f661beaff3b734caa56c8e92a62c0e4e7f661c2d
OK
127.0.0.1:7005> cluster replicate f661beaff3b734caa56c8e92a62c0e4e7f661c2d
OK
故障检测
集群中的每个节点都会定期地向集群中的其他节点发送PING
消息,以此来检测对方是否在线,如果接收PING
消息的节点没有在规定时间内向发送PING
的节点返回PONG
消息,接收节点就会被发送节点标记为疑似下线。
如果在一个集群里面,半数以上的负责处理槽的主节点都将某个主节点标记为疑似下线,那么这个主节点将被标记为已下线,并向集群广播一条关于该节点的FALL
消息。
故障转移
当一个从节点发现自己正在复制的主节点进入了已下线状态时,从节点将开始对下线主节点进行故障转移,执行下面步骤:
1)从下线主节点的所有从节点基于一定规则选中一个执行slaveof no one
命令,成为新的主节点;
2)新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己;
3)新的主节点会向集群广播一条PONG
消息,让其他节点立即知道当前节点由从节点变为了主节点,并且新主已接管所有旧主负责处理的槽;
4)新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成。
快速搭建集群
如果节点多的话手动组建集群很麻烦,所以我们可以一次性指定多个节点为一个集群(前提是所有节点之间未曾握手构成小集群):
redis-cli --cluster create 127.0.0.1:6380 127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6390 127.0.0.1:6391 127.0.0.1:6392 --cluster-replicas 1
//--cluster-replicas 1意味着我们希望每个创建的主服务器都有一个从服务器
>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 127.0.0.1:6391 to 127.0.0.1:6380
Adding replica 127.0.0.1:6392 to 127.0.0.1:6381
Adding replica 127.0.0.1:6390 to 127.0.0.1:6382
>>> Trying to optimize slaves allocation for anti-affinity
[WARNING] Some slaves are in the same host as their master
M: b6477a2b03d54a3e5fda45f4939c971724a80346 127.0.0.1:6380
slots:[0-5460] (5461 slots) master
M: e355801e186e02f9a0b7cbb989914a564c104721 127.0.0.1:6381
slots:[5461-10922] (5462 slots) master
M: 886791bf33ad1dc49463a992e5ff28305f1b02e5 127.0.0.1:6382
slots:[10923-16383] (5461 slots) master
S: 129888f320539c541599359765536e85280b96a0 127.0.0.1:6390
replicates 886791bf33ad1dc49463a992e5ff28305f1b02e5
S: 04b64979a1ae62144bb6f0474a181ec4832e517d 127.0.0.1:6391
replicates b6477a2b03d54a3e5fda45f4939c971724a80346
S: 6761ca00082ebdf75cfd1101189e2cfcde064755 127.0.0.1:6392
replicates e355801e186e02f9a0b7cbb989914a564c104721
Can I set the above configuration? (type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join
...
>>> Performing Cluster Check (using node 127.0.0.1:6380)
M: b6477a2b03d54a3e5fda45f4939c971724a80346 127.0.0.1:6380
slots:[0-5460] (5461 slots) master
1 additional replica(s)
S: 129888f320539c541599359765536e85280b96a0 127.0.0.1:6390
slots: (0 slots) slave
replicates 886791bf33ad1dc49463a992e5ff28305f1b02e5
M: e355801e186e02f9a0b7cbb989914a564c104721 127.0.0.1:6381
slots:[5461-10922] (5462 slots) master
1 additional replica(s)
S: 04b64979a1ae62144bb6f0474a181ec4832e517d 127.0.0.1:6391
slots: (0 slots) slave
replicates b6477a2b03d54a3e5fda45f4939c971724a80346
M: 886791bf33ad1dc49463a992e5ff28305f1b02e5 127.0.0.1:6382
slots:[10923-16383] (5461 slots) master
1 additional replica(s)
S: 6761ca00082ebdf75cfd1101189e2cfcde064755 127.0.0.1:6392
slots: (0 slots) slave
replicates e355801e186e02f9a0b7cbb989914a564c104721
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
显然,我们要求的唯一设置是创建一个包含3个主服务器和3个从服务器的集群。
当你输入完命令后,Redis会给你一份配置清单(将决定哪个服务器是主服务器,哪个服务器是从服务器),之后会提示你是否希望这样配置,输入yes
接受建议的配置,执行配置将给定所有节点加入集群,这意味着节点将被引导为彼此通信。最后,如果一切顺利,你会看到这样的消息:
[OK] All 16384 slots covered
这意味着至少有一个主实例为16384个可用插槽提供服务。
现在我们来看一下集群的信息:
$ redis-cli -p 6380
127.0.0.1:6380> cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:6
cluster_my_epoch:1
cluster_stats_messages_ping_sent:1826
cluster_stats_messages_pong_sent:1872
cluster_stats_messages_sent:3698
cluster_stats_messages_ping_received:1867
cluster_stats_messages_pong_received:1826
cluster_stats_messages_meet_received:5
cluster_stats_messages_received:3698
参考资料
《redis设计与实现》(第二版)