Redis设计与实现-13.集群
Redis集群是Redis提供的分布式数据库方案,集群通过分片来进行数据共享,并提供复制和故障转移。
节点
一个Redis集群通常由多个节点组成,每个节点互相关联,构成一个多个节点组成的集群。可以通过CLUSTER MEET命令完成连接各个节点的工作。
CLUSTER MEET <ip> <port>
假设有三个独立的节点7000,7001,7002,首先连接7000客户端,并且通过命令查看集群节点信息,发现只有一个节点7000
$redis-cli -c -p 7000
127.0.0.1:7000> CLUSTER NODES
asdxcv231fga182esadnash123asds989csa :0 myself,master - 0 0 0 connected
连接其他两个节点
127.0.0.1:7000> CLUSTER MEET 127.0.0.1 7001 127.0.0.1:7000> CLUSTER MEET 127.0.0.1 7002
查看集群信息
127.0.0.1:7000> CLUSTER NODES asdxcv231fga182esadnash123asds989csa 127.0.0.1:7001, master - 0 1388204848376 0 connected asd18783cv231fga182hgdnash3asd9csaq 127.0.0.1:7002, master - 0 1388204848621 0 connected 71vbnik8iopj9azxcj4234iyhmqd552uoipol :0 myself,master - 0 0 0 connected
启动节点
集群下的每个redis节点服务器启动,首先会根据cluster-enabled配置选项是否为yes来决定是否开启服务器的集群模式。如果yes,表示开启集群模式,如果no表示开启单机Redis服务器。
即使是身为redis集群的节点,也会按照单机模式中使用的相关组件功能,比如定时定期函数以及任务,以及RDB和AOF持久化,发布订阅等。
槽
Redis通过分片的方式保存数据库中的键值对:集群的整个数据库被分为16384个槽(slot),数据库中的每个键都属于其中一个槽,集群中的每个节点可以处理0~16384个槽。当数据库中的16384个槽都有节点在处理时,集群处于上线状态,相反的,如果数据库中有任何一个槽没有得到处理,集群就处于下线状态。
指派
可以通过CLUSTER ADDSLOTS命令进行槽指派。
127.0.0.1:7000>CLUSTER ADDSLOTS 0 1 2 3 4 ... 5000
ok
127.0.0.1:7000> CLUSTER NODES
asdxcv231fga182esadnash123asds989csa 127.0.0.1:7001, master - 0 1388204848376 0 connected
asd18783cv231fga182hgdnash3asd9csaq 127.0.0.1:7002, master - 0 1388204848621 0 connected
71vbnik8iopj9azxcj4234iyhmqd552uoipol :0 myself,master - 0 0 0 connected 0-5000
以此类推,将16384个槽分别指派给三个服务器,查看集群状态
127.0.0.1:7000> CLUSTER INFO cluster_state:OK cluster_slots_assigned:16384 cluster_slots_ok:16384 cluster_slots_pfail:0 cluster_slots_fail:0 cluster_konwn_nodes:3 cluster_size:3 cluster_current_epoch:0 cluster_stats_message_sent:3699 cluster_stats_message_received:2617
槽指派信息记录
clusterNode结构的slots属性和numslot属性记录了几点负责处理哪些槽。
其中slots属性是二进制数组,数组长度为16384/8=2048个字节,共包含16384个二进制位。每个二进制位都代表一个槽位置,根据对应索引上的二进制为的值来判断节点是否处理该槽(0表示不处理,1表示处理)。
因为取出以及设置slots数组中的任意一个二进制的值时间复杂度为O(1),所以对于一个给定节点的slots数组来说,检查处理某个槽和指派某个槽的操作时间复杂度都为O(1)。
至于numslot属性则记录负责处理的槽的数量
传播节点的槽信息
一个节点除了会将自己负责处理的槽记录在clusterNode结构的slots属性和numslots属性外,还会将自己的slots数组通过消息发送给集群中其他节点,来告诉其他节点目前自己处理那些槽。
记录集群中所有槽信息
Redis集群通过clusterState结构中的slots属性记录槽指派信息。如图所示
clusterState.slots数组记录了集群中所有槽的信息。同样使用clusterNode结构中的slots数组来记录单个的也是有必要的:
- 程序需要将某个节点的槽指派信息通过消息发送给其他节点时候,只需要将对应节点的slots数组发出去就可以了
- 如果redis不使用clusterNode.slots数组,而单独使用clusterState.slots数组的话,那么每次都要将节点A的槽指派信息传给其他节点的话,程序必须遍历整个clusterState.slots数组。
集群中的命令执行
槽指派之后集群就进入上线状态,客户端就可以向集群发送数据命令了。具体流程如下图:
计算键属于哪个槽
def slot_number(key): return CRC16(key)&16383
节点使用上面的算法计算键属于哪个槽
CRC16(key)语句用于计算key的CRC-16校验和,而&16383则是用于计算出一个位于0和16383之间的整数作为键key的槽号。
判断槽是否由当前节点处理
当节点计算出键的所属槽之后,节点会检查自己的clusterState.slots数组中的项i,判断键所在的槽是否由自己负责:
- 如果clusterState.slots[i]等于clusterState.myself,那么说明槽i由当前节点负责,节点可以执行客户端发送的命令。
- 反之,如果不相等,那么即诶单会根据clusterState.slots[i]指向的clusterNode结构锁记录的节点IP和端口号,向客户端返回MOVED错误,指引客户端转向至正在处理槽i的节点。
MOVED错误信息
如果请求的Redis节点不处理当前槽,那么会返回moved错误信息,但是集群模式下redis客户端在接受到MOVED错误的时候,并不会打印出MOVED错误,而是自行进行转点,打印出转向信息。所以我们是看不到MOVED错误信息的。
但是当我们是单机模式下的redis客户端,就会打印出MOVED错误,原因是单机模式下的redis客户端不清楚MOVED错误的作用,所以就会打印出MOVED错误信息。
节点数据库
我们都直到,redis服务器默认会有16个数据库,单机模式下我们可以使用任意一个数据库,但是集群节点中,我们只可以使用0号数据库。
节点存储数据的方式以及键过期的方式和单机数据库是一样的,只是我们的键值对除了保存在数据库中之外,节点还会用clusterState.slot_to_key属性来保存槽与键之间的关系,而slot_to_key使用跳跃表来实现。
重新分片
redis集群的重新分片操作可以将任意数量已经指派给某个节点的槽改为指派给另外一个新的节点,并且相关槽所属的键值对也会从源节点被移动到目标节点。
重新分片可以在线操作,不需要下线节点,任何节点都可以正常工作。具体流程如下:
ASK错误
在进行重新分片期间,源节点和目标节点迁移的过程中,会出现请求槽中一部分键已经迁移成功,一部分还未迁移的情况。处理这种特殊命令的流程如下:
ASK错误和MOVED错误一样,在集群模式下,客户端不会打印ASK错误,而是直接进行转向动作,但是单机模式下会打印出ASK错误。
ASK错误和MOVED错误
ASK错误和MOVED错误都会使客户端转向,但是他们是有区别的:
- MOVED错误代表槽的负责权已经从一个节点转移到另一个节点:客户端收到关于槽i的MOVED错误之后,客户端每次遇到关于槽i的命令请求时,都可以直接将命令请求发送至MOVED错误指向的节点,因为该节点是目前负责槽i的节点。
- ASK错误只是出现在两个节点在迁移槽的过程中使用的一种临时措施:在客户端接收到槽i的ASK错误之后,客户端只会在接下来一次命令请求中将关于槽i的命令请求转向至ASK错误指向的节点,但是对于之后的客户端请求槽i的命令不会产生任何影响。
复制和故障转移
Redis集群中的节点分为主节点和从节点,主节点们主要用来处理槽,从节点用于复制某一个主节点,并在被复制的主节点下线的时候,代替下线的主节点继续处理命令请求。
设置从节点
向一个节点发送命令,可以让接受命令的节点成为node_id所指定的节点的从节点,并开始对主节点进行复制。
CULSTER REPLICATE <node_id>
- 接收到命令的节点首先会在自己的clusterState.nodes字典里面找到node_id所对应的节点的clusterNode结构,并将自己的clusterState.myself.slaveof指针指向这个结构,以此来记录这个节点正在复制的节点
- 然后修改自己clusterState.myself.flags中的属性,关闭原本的REDIS_NODE_MASTER标识,打开REDIS_NODE_SLAVE标识,表示这个节点已经由主节点变为从节点
- 最后调用复制代码,并根据clusterState.myself.slaveof指向的clusterNode结构所保存的ip以及端口号,对主节点进行复制。(和单机Redis的复制是一样的功能,命令为slaveof <master_ip> <master_port>)
故障检测
集群中每个节点会定期的向集群中的其他节点发送PING消息,以此来检测对方是否在线,如果接收到PING消息的节点在规定时间内,向发送方返回PONG消息,那么发送PING消息的节点会将被发送方标记为疑似下线。
如果在集群中,半数以上分负责处理槽的主节点都将某个X节点标记为疑似下线,那么这个主节点将会被标记为已下线,并且会通过集群广播的方式发送主节点X被FAIL的消息,所有收到消息的节点都会将主节点X标记为已下线。
故障转移
- 复制下线主节点的所有从节点中会有一个节点被选中。
- 被选中的从节点执行SLAVEOF no one命令,成为新的主节点。
- 新的主节点会撤销所有对已下线的主节点的槽指派,并将这些槽全部指派给自己。
- 新的主节点向集群广播发布PONG消息,这条PONG消息告诉集群中其他节点,自己变成了主节点,并且将负责之前主节点负责的槽
- 新的主节点负责处理槽的命令,故障转移完成。
消息
集群中各个节点通过发送和接收消息进行通信,消息分为五种,分别是:MEET、PING、PONG、FAIL、PUBLISH。
- MEET:处理节点加入集群的消息。
- PING:节点与节点之前检测是否在线的消息,每一秒随机找到集群列表中5个最长时间没有发送过PING消息的节点进行PING消息发送。
- PONG:当接收者收到MEET或者PING消息的时候,进行回复的消息类型,表示已经收到消息。
- FAIL:集群中节点下线的消息,当A判断出B已经下线的时候,会发送关于B的FAIL消息,让集群中其他节点将B标记为FAIL状态。
- PUBLISH:当节点接收到一个PUBLISH命令时,节点会执行这个命令,并向集群广播一条PUBLISH消息,所有接收到这个消息的节点都会执行相同的PUBLISH命令。