Redis集群是Redis提供的分布式数据库方案,集群通过分片(sharding)来进行数据共享,并提供复制和故障转移功能。本文将对集群的节点、槽指派、命令执行、重新分片、转向、故障转移、消息等各个方面进行介绍。
1. 节点
一个Redis集群通常由多个节点(node)组成,在刚开始的时候,每个节点都是相互独立的,它们都处于一个只包含自己的集群当中,要组建一个真正可工作的集群,我们必须将各个独立的节点连接起来,构成一个包含多个节点的集群。连接各个节点的工作可以使用CLUSTER MEET 命令来完成,该命令的格式如下:
1 | CLUSTER MEET<tp><port> |
向一个节点node发送CLUSTER MEET命令,可以让node节点与ip和port所指定的节点进行握手(handshake),当握手成功时,node节点就会将ip和port所指定的节点添加到node节点当前所在的集群中。
如下示例便是端口为7000、7001和7002三个独立节点形成集群的过程,图17-1至17-5展示了这三个节点进行握手的整个过程。
1.1 启动节点
一个节点就是一个运行在集群模式下的Redis服务器,Redis服务器在启动时会根据cluster-enabled配置选项是否为yes来决定是否开启服务器的集群模式,如图17-6所示。
1.2 集群数据结构
clusterNode结构保存了一个节点的当前状态,比如节点的创建时间、节点的名字、节点当前的配置纪元、节点的IP地址和端口号等等。每个节点都会使用一个clusterNode结构来记录自已的状态,并为集群中的所有其他节点(包括主节点和伊节点)都创建一个相应的clusterNode结构,以此来记录其他节点的状态:
1 | struct clusterNode { |
clusterNode结构的link属性是一个clusterLink结构,该结构保存了连接节点所需的有关信息,比如套接字描述符,输入缓冲区和输出缓冲区:
1 | typedef struct clusterLink { |
最后,每个节点都保存着一个clusterState结构,这个结构记录了在当前节点的视角下,集群目前所处的状态,例如集群是在线还是下线,集群包含多少个节点,集群当前的配置纪元,诸如此类:
1 | typedef struct clusterLink { |
以前面介绍的7000、7001、7002三个节点为例,图17-7展示了节点7000创建的clusterState结构:
1.3 CLUSTER MEET 命令的实现
图17-8展示了命令实现的握于过程:
2. 槽指派
Redis集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为16384个槽(slot),数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点可以处理0个或最多16384个槽。当数据库中的16384个槽都有节点在处理时,集群处于上线状态(ok);相反地,如果数据库中有任何一个槽没有得到处理,那么集群处于下线状态(fail)。
在上文,我们使用CLUSTER MEET命令将7000、7001、7002三个节点连接到了同一个集群里面,不过这个集群目前仍然处于下线状态,因为集群中的三个节点都没有在处理任何槽:
通过向节点发送CLUSTER ADDSLOTS命令,我们可以将一个或多个槽指派(assign)给节点负责:
1 | CLUSTER ADDSLOTS <slot> [slot…] |
举个例子,执行以下命令可以将槽0至槽5000指派给节点7000负责:
1 | 127.0.0.1:7000> CLUSTER ADDSLOTS 0 1 2 3 4 ...5000 |
7001 和 7002 同理,当 16384 个槽全部指派给集群节点之后,集群才进入上线状态。
2.1 记录节点的槽指派信息
clusterNode 结构的 slots 属性和numslot属性记录了节点负责处理哪些槽:
1 | struct clusterNode { |
slots属性是一个二进制位数组(bitarray),这个数组的长度为16384/8=2048个字节,共包含16384个二进制位。Redis以0为起始索引,16383为终止索引,对slots数组中的16384个二进制位进行编号,并根据索引i上的二进制位的值来判断节点是否负责处理槽i。如果二进制值为1则表示节点负责处理槽i,为0则未处理。
2.2 传播节点的槽指派信息
一个节点除了会将自己负责处理的槽记录在clusterNode结构的slots属性和 numslots 属性之外.它还会将自己的 slots 数组通过消息发送给集群中的其他节点,以此来告知其他节点自己目前负责处理哪些槽。当接收节点从发送方接收到槽信息时,接收节点会在自己的clusterState.nodes字典中查找发送方对应的clusterNode结构,并对结构中的slots数组进行保存或者更新。
2.3 记录集群所有槽的指派信息
clusterState结构中的slots数组记录了集群中所有16384个槽的指派信息:
1 | typedef struct clusterState { |
slots数组包含16384个项,每个数组项都是一个指向clusterNode结构的指针:
- 如果slots[i]指针指向NULL,那么表示槽i尚未指派给任何节点。
- 如果slots[i]指针指向一个clusterNode结构,那么表示槽i已经指派给了clusterNode结构所代表的节点。
3. 在集群中执行命令
在对数据库中的16384个槽都进行了指派之后,集群就会进入上线状态,这时客户端就可以向集群中的节点发送数据命令了。当客户端向节点发送与数据库键有关的命令时,接收命令的节点会计算出命令要处理的数据库键属于哪个槽,些检查这个槽是否指派给了自己:
- 如果键所在的槽正好就指派给了当前节点,那么节点直接执行这个命令。
- 如果键所在的槽并没有指派给当前节点,那么节点会向客户端返回一个MOVED错误,指引客户端转向(redirect)至正确的节点,并再次发送之前想要执行的命令。
图17-18展示了这两种情况的判断流程。
3.1 判断槽是否由当前节点负责处理
当节点计算出键所属的槽i之后,节点就会检查自已在clusterState.slots数组中的项 i,判断键所在的槽是否由自己负责:
- 如果clusterState.slots[i]等于clusterState.myself,那么说明槽i由当前节点负责,节点可以执行客户端发送的命令。
- 如果clusterState.slots[i]不等于cLlusterSstate.myself,那么说明槽i并非由当前节点负责,节点会根据clusterState.slots[i]指向的clusterNode结构所记录的节点IP和端口号,向客户端返回MOVED错误,指引客户端转向至正在处理槽i的节点。
3.2 节点数据库的实现
节点和单机服务器在数据库方面的一个区别是,节点只能使用0号数据库,而单机Redis服务器则没有这一限制。另外,除了将键值对保存在数据库里面之外,节点还会用clusterState结构中的slotstokeys跳跃表来保存槽和键之间的关系。slots_to_keys跳跃表每个节点的分值(score)都是一个槽号,而每个节点的成员(member)都是一个数据库键。
4. 重新分片
Redis集群的重新分片操作可以将任意数量已经指派给某个节点(源节点)的槽改为指派给另一个节点(目标节点),并且相关槽所属的键值对也会从源节点被移动到目标节点。重新分片操作可以在线(online)进行,在重新分片的过程中,集群不需要下线,并且源节点和目标节点都可以继续处理命令请求。
图17-25展示了对果s1ot进行重新分片的整个过程。
5. ASK 错误
在进行重新分片期间,源节点向目标节点迁移一个槽的过程中,可能会出现这样一种情况:属于被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对则保存在目标节点里面。当客户端向源节点发送一个与数据库键有关的命令,并且命令要处理的数据库键恰好就属于正在被迁移的槽时:
源节点会先在自己的数据库里面查找指定的键,如果找到的话,就直接执行客户端发送的命令。
相反地,如果源节点没能在自己的数据库里面找到指定的键,那么这个键有可能已经被迁移到了目标节点,源节点将向客户端返回一个ASK错误,指引客户端转向正在导入槽的目标节点,并再次发送之前想要执行的命令。
图17-26展示了源节点判断是否需要向客户端发送ASK错误的整个过程。
6. 复制与故障转移
Redis集群中的节点分为主节点(master)和从节点(slave),其中主节点用于处理果,而从节点则用于复制某个主节点,并在被复制的主节点下线时,代替下线主节点继续处理命令请求。集群中的每个节点都会定期地向集群中的其他节点发送PING消息,以此来检测对方是否在线,如果接收PING消息的节点没有在规定的时间内,向发送PING消息的节点返回PONG消息,那么发送PING消息的节点就会将接收PING消息的节点标记为疑似下线(probable fail,PFAIL)。
如果在一个集群里面,半数以上负责处理槽的主节点都将某个主节点x报告为疑似下线,那么这个主节点x将被标记为已下线(FAIL),将主节点x标记为已下线的节点会向集群广播一条关于主节点x的FAIL消息,所有收到这条FAIL消息的节点都会立即将主节点x标记为已下线。
7. 消息
集群中的各个节点通过发送和接收消息(message)来进行通信,我们称发送消息的节点为发送者(sender),接收消息的节点为接收者(receiver),如图17-40所示。
集群中的节点通过发送和接收消息来进行通信,常见的消息包括MEET、PING、PONG、PUBLISH、FAIL五种。