16.集群

Redis集群是Redis提供的分布式数据库方案,集群通过分片来进行数据共享,并提供复制和故障转移功能。

节点

一个Redis集群通常由多个节点组成,连接各个节点node发送CLUSTER MEET命令,可以让node节点与ip和port所指定的节点进行握手,当握手成功时,node节点就会将ip和port所指定的节点添加到node节点当前所在的集群中。

启动节点

一个节点就是一个运行在集群模式下的Redis服务器,Redis服务器在启动时会根据cluster-enable配置选项是否为yes来决定是否开启服务器的集群模式。
这里写图片描述
节点会继续使用所有在单机模式中使用的服务器组件,比如说:
1.节点会继续使用文件事件处理器来处理命令请求和返回命令回复。
2.节点会继续使用时间事件处理器来执行serverCron函数,而serverCron函数又会调用集群模式特有的clusterCron函数。clusterCron函数负责执行在集群模式下需要执行的常规操作,例如像集群中的其他节点发送Gossip消息,检查节点是否断线,或者检查是否需要对下线节点进行自动故障转移。
3.节点会继续使用数据库来保存键值对数据,键值对依然会是各种不同类型的对象。
4.节点会继续使用RDB持久化模块和AOF持久化模块来执行持久化工作。
5.节点会继续使用发布与订阅模块来执行PUBLISH、SUBSCRIBE等命令
6.节点会继续使用复制模块来进行节点的复制工作。

集群数据结构

clusterNode结构保存了一个节点的当前状态,比如节点的创建时间,节点的名字,节点当前的配置纪元,节点的IP地址和端口号等等。
每个节点都会使用一个clusterNode结构来记录自己的状态,并为集群中的所有其他节点都创建一个向右的clusterNode结构, 以此来记录其他的节点状态
这里写图片描述
clusterNode结构的link属性是一个clusterLink结构,该结构保存了连接节点所需的有关心,比如套接字描述符,输入缓冲区和输出缓冲区:
这里写图片描述
redisClient结构和clusterLink结构的结构和不同之处
redisClient结构和clusterLink结构都有自己的套接字描述符和输入、输出缓冲区,这两个结构的区别在于,redisClient结构中的套接字和缓冲区适用于连接客户端的,而clusterLink结构中的套接字和缓冲区则是用于连接节点的。
每个节点都保存一个clusterstate结构,这个结构记录了在当前节点的视角下,集群目前所处的状态 。
这里写图片描述

CLUSTER MEET命令的实现

通过向节点A发送CLUSTER MEET命令,客户端可以让接受命令的节点A将另一个节点B添加到节点A当前所在的集群里面:
CLUSTER MEET < ip >< port>
收到命令的节点A将于节点B握手,一次来确认彼此的存在,并为将来的进一步通信打好基础:
1)节点A会为节点B创建一个clusterNode结构,并将该结构添加到自己的clusterState.nodes字典里面。
2)之后,节点A将根据CLUSTER MEET命令给定的IP地址和端口号,向节点B发送一条MEET消息
3)如果一切顺利,节点B将接受到节点A发送的MEET消息,节点B会为节点A创建一个clusterNode结构,并将该结构添加到自己的clusterstate.nodes字典里面。
4)之后,节点B将向节点A返回一条PONG消息
5)如果一切顺利,节点A将接受到节点B返回的PONG消息,通过这条PONG消息节点A可以知道节点B已经成功第接收到了自己发送的MEET消息。
6)之后,节点A将向节点B返回一条PING消息
7)如果一切顺利,节点B将接收到节点A返回的PING消息,通过这条PING消息节点B可以知道节点A已经成功第接收到了自己返回的PONG消息,握手完成。
这里写图片描述

槽指派

Redis集群通过分片的方式来保存数据库的键值对:集群的整个数据库被分为16384个槽(slot),数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点可以处理0个或者最多16384个槽。
当数据库中的16384个槽都有节点在处理时,集群处于上线状态(OK);相反地,如果数据库中任何一个槽没有得到处理,那么集群处于下线状态。
通过向节点发送CLUSTER ADDSLOTS命令,可以将一个或多个槽指派给节点负责:
CLUSTER ADDSLOTS < slot > [slot …]

记录节点的槽指派信息

clusterNode结构的slots属性和numslot属性记录了节点负责处理哪些槽:
struct clusterNode{
//…
unsigned char slots[16384/8];
int numslots;
};
slots属性是一个二进制位数组,这个数组的长度为16384/8 = 2048个字节,共包含16384个二进制位。
Redis以0位其实索引,16383位终止索引,对slots数组中的16384个二进制位进行编号,并根据索引i上的二进制位的值来判断节点是否负责处理槽i:
如果slots数组在索引i上的二进制位的值为1,那么表示节点负责处理槽i。
如果slots数组在索引i上的二进制位的值为0,那么表示节点不负责处理槽i。
numslots属性记录节点负责处理的槽的数量

传播节点的槽指派信息

一个节点除了将自己负责处理的槽记录在clusterNode结构的slots和numslots属性之外,它还会降自己的slots数组通过消息发送给集群中的其他节点,以此来告知其他节点自己目前负责哪些槽。
当节点A通过消息从节点B那里接收到节点B的slots数组时,节点A会在自己 的clusterState.nodes字典中查找节点B对应的clusterNode结构,并对结构中的slots数组进行保存或者更新。

记录集群所有槽的指派信息

clusterState结构中的slots数组记录了集群中所有16384个槽的指派信息:

typedef struct clusterState{
//...
clusterNode* slots[16384];
//...
}clusterState;

slots数组包含16384个项,每个数组项都是一个指向clusterNode结构的指针:
如果slots[i]指针指向NULL,那么表示槽i尚未指派给任何阶段。
如果slots[i]指针指向一个clusterNode结构,那么表示槽i已经指派给了clusterNode结构所代表的节点。
如果节点只使用clusterNode.slots数组来记录槽的指派信息,会出现一些无法高效地解决的问题,而clusterState.slots数组的存在解决了这些问题:
1)为了知道i是否已经被指派,或者槽i被指派给了哪个节点,程序需要遍历clusterState.nodes字典中的所有clusterNode结构,检查这些结构的slots数组,知道找到负责处理槽i的节点为止,这个过程的复杂度为O(N),其中N为clusterState.nodes字典保存的clusterNode结构的数量。
2)通过将所有槽的指派信息保存在clusterstate.slots数组里面,程序要检查槽i是否已经被指派,又或者取得负责处理槽i的节点,只需要访问clusterState.slots[i]的值即可,这个操作的复杂度为O(1).
虽然clusterstate.slots数组记录了集群中所有槽的指派信息,但使用clusterNode结构的slots数组来记录单个节点的槽指派信息仍然有必要:
1)当程序需要将某个节点的槽指派信息通过消息发送给其他节点时,程序只需要将相应节点的clusterNode.slots数组整个发送出去就可以了。
2)如果Redis不使用clusterNode.slots数组,而单独使用clusterState.slots数组的话,那么每次要将节点A的槽指派信息传播给其他节点时,程序必须先遍历整个clusterState.slots数组,记录节点A负责处理哪些槽,然后才能发送节点A的槽指派信息,这比直接发送clusterNode.slots数组要麻烦和低效得多。
clusterState.slots数组记录了集群中所有槽的指派信息,而clusterNode.slots数组值记录了clusterNode结构所代表的节点的槽指派信息,这时两个slots数组的关键区别所在。

在集群中执行命令

在对数据库中的16384个槽都进行了指派之后,集群就会进入上线状态,这时客户端就可以向集群中的节点发送数据命令了。
当客户端向节点发送与数据库键有关的命令时,接受命令的节点会计算出命令要处理的数据库键属于哪个槽,并检查这个槽是否指派给了自己:
1.如果键所在的槽正好就指派给了当前节点,那么节点之间执行这个命令
2.如果键所在的槽并没有指派给当前节点,那么节点会向客户端返回一个MOVED错误,指引客户端转向至正确的节点,并在此发送之前想要执行的命令。

MOVED错误

当节点发现键所在的槽并非由自己负责处理的时候,节点就会向客户端返回一个MOVED错误,指引客户端转向至正在负责槽的节点。
MOVED错误的格式为:
MOVED < slot > < ip>:< port>
其中,slot为键所在的槽,而ip和port为处理槽slot的节点的IP地址和端口号。
一个集群客户端通常会与集群中的多个节点创建套接字连接,而所谓的节点转向实际上就是换一个套接字来发送命令。如果客户端尚未与想要转向的节点创建套接字连接,那么客户端会先根据MOVED错误提供的IP地址和端口号来连接节点,然后再进行转向。

节点数据库的实现

节点和单机服务器在数据库方面的一个区别是,节点只能使用0好数据库,而单机Redis服务器则没有这一限制。
除了将键值对保存在数据库里面之外,节点还会用clusterState结构中的slots_to_keys跳跃表来保存槽和键之间的关系:

typedef struct clusterState{
    //...
    zskiplist *slots_to_keys;
    //...
}clusterState;

slots_to_keys跳跃表每个节点的分值都是一个槽号,而每个节点的成员都是一个数据库键:
每当节点网数据库中添加一个新的键值对时,节点就会将这个件以及件的槽号关联到slot_to_keys跳跃表
当节点删除数据库中的某个键值对时,节点就会在slots_to_keys跳跃表接触被删除键与槽号的关联。

重新分片

Redis集群的重新分片操作可以将任意数量已经指派给某个节点(源节点)的槽改为指派给另一个节点(目标节点),并去相关槽所属的键值对也会从源节点被移动到目标节点。
从新分片操作可以在线进行,在重新分片的过程中,集群不需要下线,并且源节点和目标节点都可以记下处理命令请求。
重新分片的实现原理
Redis集群的重新分片操作是由Redis的集群管理软件redis-trib负责执行的,Redis提供了进行重新分配所需的所有命令,而redis-trib则通过向源节点和目标节点发送命令来进行重新分片操作。
redis-trib对集群的单个槽slot进行重新分片的步骤如下:
1)redis-trib对目标节点发送CLUSTER SETSLOT< slot> IMPORTING < source_id>命令,让目标节点准备好从源节点导入(import)属于槽slot的键值对。
2)redis-trib对源节点发送CLUSTER SETSLOT < slot> MIGRATING < target_id>命令,让源节点准备好将属于槽slot的键值对迁移(migrate)至目标节点
3)redis-trib向源节点发送CLUSTER GETKEYSINSLOT< slot> < count>命令,获得最多count个属于槽slot键值对的键名。
4)对于步骤3)所得的每个键名,redis-trib都向源节点发送一个MIGRATE< target_ip> < target_port> < key_name> 0 < timeout>命令,将被选中的键原子地从源节点迁移至目标节点。
5)重复执行步骤3和步骤4,知道源节点保存的所有属于槽slot的键值对都被迁移至目标节点为止。每次迁移键的过程如下图所示:
这里写图片描述
6)redis-trib向集群中的任何一个节点发送CLUSTER SETSLOT< slot> NODE < target_id>命令,将槽slot指派给目标节点,这一指派信息会通过消息发送至整个集群,最终集群中所有节点都会知道槽slot已经指派给了目标节点。

ASK错误

当客户端向源节点发送一个与数据库键有关的命令,并且命令要处理的数据库键恰好就属于正在被迁移的槽时:
源节点会现在自己的数据库里面查找指定的键,如果找到的话 ,就直接指向客户端发送的命令。
如果源节点没有能在自己的数据库里面找到知道的键,那么这个键有可能已经被迁移到了目标节点,源节点将向客户端返回一个ASK错误,指引客户端转向正在导入槽的目标节点,并再次发送之前想要执行的命令。

CLUSTER SETSLOT IMPORTING命令的实现

clusterState结构的importing_slots_from数组记录了当前节点正在从其他节点导入的槽:

typedef struct clusterState{
    //...
    clusterNode *importing_slots_from[16384];
    //...
}clusterState;

如果importing_slots_from[i]的值不为NULL,而是指向一个clusterNode结构,那么表示当前节点正在从clusterNode所代表的节点导入槽i。
对集群进行重新分片的时候,向目标节点发送命令:
CLUSTER SETSLOT < slot > IMPORTING < source_id>
可以将目标节点clusterState.importing_slots_from[i]的值设置为source_id所代表的节点的clusterNode结构。

CLUSTER SETSLOT MIGRATING命令的实现

clusterState结构的migrating_slots_to数组记录了当前节点正在迁移至其他节点的槽:

typedef struct clusterState{
    //...
    clusterNode* migrating_slots_to[16384];
    //...
}clusterState;

如果migrating_slots_from[i]的值不为NULL,而是指向一个clusterNode结构,那么表示当前节点正在将槽i迁移至clusterNode所代表的节点。
对集群进行重新分片的时候,向源节点发送命令:
CLUSTER SETSLOT < slot > MAGRATING < target_id>
可以将源节点clusterState.magrating_slots_from[i]的值设置为target_id所代表的节点的clusterNode结构。

ASK错误

如果节点收到一个关于键key的命令请求,并且键key所属的槽i正好就指派给了这个节点,那么节点会尝试在自己的数据库里查找键key,如果找到了的话,节点就直接执行客户端发送的命令。
与此相反,如果节点没有在自己的数据库里找到键key,那么节点会检查自己的clusterState.migrating_slots_to[i],看键key所属的槽i是否正在进行迁移,如果槽i的确在进行迁移的话,那么节点会向客户端发送一个ASK错误,引导客户端到正在导入槽i的节点去查找键key。
接到ASK错误的客户端会根据错误提供的IP地址和端口号,转向至正在导入槽的目标节点,然后首先向目标节点发送一个ASKING命令,之后再重新发送原本想要执行的命令。

ASKING命令

ASKING命令唯一要做的就是打开发送该命令的客户端的REDIS_ASKING标识。
在一般情况下,客户端向节点发送一个关于槽i的命令,而槽i又没有指派给这个节点的话,那么节点将向客户端返回一个MOVED错误;但是,如果节点的clusterState.importing_slot_from[i]显示节点正在导入槽,并且发送命令的客户端带有REDIS_ASKING标识,那么节点将破例执行这个关于槽i的命令一次。
当客户端接收到ASK错误并转向至正在导入槽的节点时,客户端会先向节点发送一个ASKING命令,然后才重新发送想要执行的命令,这时因为如果客户端不发送ASKING命令,而直接发送想要执行的命令的话,那么客户端发送的命令将被节点拒绝执行,并返回MOVED错误。

ASK错误和MOVED错误的区别

ASK错误和MOVED错误都会导致客户端转向,它们的区别在于:
1)MOVED错误代表槽的负责权已经从一个节点转移到了另一个节点:在客户端收到关于槽i的MOVED错误之后,客户端每次遇到关于槽i的请求命令时,都可以直接将命令请求发送至MOVED错误所指向的节点,因为该节点就是目前负责槽i的节点。
2)ASK错误知识两个节点在迁移槽的过程中使用的一种临时措施:在客户端收到关于槽i的ASK错误之后,客户端只会在接下来的一次命令请求中将关于槽i的命令请求发送至ASK错误所指示的节点,但这种转向不会对客户端今后发送关于槽i的命令请求产生任何影响,客户端仍然会将关于槽i的命令请求发送至目前负责处理槽i的节点,除非ASK错误再次出现。

复制与故障转移

Redis集群中的节点分为主节点和从节点,其中主节点用于处理槽,而从节点则用于复制某个主节点,并在被复制的主节点下线时,代替下线主节点继续处理命令请求。

设置从节点

向一个节点发送命令:
CLUSTER REPLICATE < node_id>
可以让接受命令的节点成为node_id所指定节点的从节点,并开始对主节点进行复制:
接收到该命令的节点首先会在自己的clusterState.nodes字典中找到node_id所对应节点的clusterNode结构,并将自己的clusterState.myself.slaveof指针指向这个结构,以此来记录这个节点正在复制的主节点:

struct clusterNode{
    //...
    //如果这个一个从节点,那么指向主节点
    struct clusterNode *slaveof;
};

然后节点会修改自己在clusterState.myself.flags中的属性,关闭原来的REDIS_NODE_MASTER标识,打开REDIS_NODE_SLAVE标识,表示这个节点已经由原来的主节点变成了从节点。
最后,节点会调用复制代码,并根据clusterState.myself.slaveof指向的clusterNode结构所保存的IP地址和端口号,对主节点进行复制。因为节点的复制功能和单机Redis服务器的复制功能使用了相同的代码,所有让从节点相当于向从节点发送命令SLOAVEOF < master_ip> < master_port>
一个节点成为从节点,并开始复制某个主节点这一信息会通过消息发送给集群中的其他节点,最终洁群中的所有节点都会知道某个从节点正在复制某个主节点。
集群中的所有节点都会在代表主节点的clusterNode结构的slaves属性和numslaves属性中记录正在复制这个主节点的从节点名单:

struct clusterNode{
//正在复制这个主节点的从节点数量
    int numslaves;
    //一个数组
    //每个数组指向一个正在复制这个主节点的从节点的clusterNode结构
    struct clusterNode **slaves;
};

故障检测

集群中的每个节点都会定期地向集群中的其他节点发送PING消息,一次来检测对方是否在线,如果接收PING消息的节点没有在规定的时间内,向发送PING消息的节点返回PONG消息,那么发送PING消息的节点就会将接收PING消息的节点标记为疑似下线。
集群中的各个节点会通过互相发送消息的方式来交换集群中各个节点的状态信息。
当一个主节点A通过消息得知主节点B认为主节点C进入了疑似下线状态时,主节点A会在自己的clusterState.nodes字典中找到主节点C所对应的clusterNode结构,并将主节点B的下线节点添加到clusterNode结构的fail_reports链表里面:

struct clusterNode{
//...
//一个链表,记录了所有其他节点对该节点的下线报告
    list *fail_reports;
//...
};

每个下线报告由一个clusterNodeFailReport结构表示:

struct clusterNodeFailReport{
//报告目标节点已经下线的节点
    struct clusterNode *node;
//最后一次从node节点收到下线报告的时间
//程序使用这个时间戳来检查下线报告是否过期(与当前时间相差太久的下线报告会被删除)
    mstime_t time;
}typedef clusterNodeFailReport;

如果在一个集群里面,板书以上负责处理槽的主节点豆浆某个主节点x报告为疑似下线,那么这个主节点x将被标记为已下线(FAIL),将主节点x标记为已下线的节点会向集群广播一条关于主节点x的FAIL消息,所有收到这条FAIL消息的节点都会立即将主节点x标记为已下线。

故障转移

当一个从节点发现自己正在复制的主节点进入了已下线状态时,从节点将开始对下线主节点进行故障转移,故障转移的执行步骤:
1)复制下线主节点的所有从节点里面,会有一个从节点被选中。
2)被选中的从节点会执行SLAVEOF no one命令,称为新的主节点。
3)新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己。
4)新的主节点向集群广播一条PONG消息,这条PONG消息可以让集群中的其他节点立即知道这个节点已经由从节点变成了主节点,并且这个主节点已经接管了原本由已下线节点负责处理的槽。
5)新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成。

选举新的主节点

新的主节点是通过选举产生的。
事件选举新的主节点的方法:
1)集群里的配置纪元是一个自增计数器,它的初始值是0
2)当集群里的某个节点开始一次故障转移操作时,集群配置纪元的值会被增一。
3)对于每个配置纪元,集群里每个负责处理槽的主节点都有一次投票的机会,而第一个向主节点要求投票的从节点将获得主节点的投票。
4)当从节点发现自己正在复制的主节点进入已下线状态时,从节点会向集群广播一条CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求所有收到这条消息、并且具有投票权的主节点向这个节点投票。
5)如果一个主节点具有投票权(它正在复制处理槽),并且这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示这个主节点支持从节点成为新的主节点。
6)每个参与选举的从节点都会接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,并根据自己收到了多少条这种消息来统计自己获得了多少主节点的支持。
7)如果集群里面有N个具有投票权的主节点,那么当一个从节点收集到大于等于N/2+1张支持票时,这个从节点就会当选为新的主节点。
8)因为在每一个配置纪元里面,每个具有投票权的主节点只能投一次票,所以如果有N个主节点进行投票,那么具有大于等于N/2+1张支持票的从节点只会有一个,这确保了新的主节点只有一个。
9)如果在一个配置纪元里面没有从节点能收集到足够多的支持票,那么集群进入一个新的配置纪元,并再次进行选举,直到选出新的主节点为止。

猜你喜欢

转载自blog.csdn.net/xiaomimi1993/article/details/81637691