集群
节点
启动集群
一个集群由多个节点组成,在刚开始的时候各个节点都是相互独立的,我们先以集群模式开启客户端:
redis-cli -c -h <host> -p <port>
然后执行下列命令就可以查看集群中的所有节点:
cluster nodes
可以看到目前集群只包含自己一个节点,在连接构建之前,独立节点的集群只有该节点一个。
我们要把各个独立的节点连接起来构成集群。连接各个节点的工作可以用下列命令来完成:
cluster meet <ip> <port>
假设现在有三个节点A(127.0.0.1:7000)、B(127.0.0.1:7001)、C(127.0.0.1:7002),我们用客户端连接A,然后向B发送:
cluster meet 127.0.0.1 7001
此时7000会给7001发送握手消息,然后7001响应握手,这样7001节点就会被添加到节点7000所在的集群,向7002发送类似的命令就能形成一个三节点集群:
![](https://img2020.cnblogs.com/blog/1742600/202006/1742600-20200627235702225-2061097938.png)
redis服务器在启动时会根据配置文件中的cluster-enabled选项来决定是否开启服务器的集群模式,如果该值是yes就会开启集群模式。集群模式下的服务器被称为节点,节点和普通服务器很像,但是会执行集群模式下的各类方法,基本的数据结构如redisServer和redisClient还会使用,集群模式下会有一些特殊的数据结构,接下来就是这些特殊数据结构的介绍。
集群数据结构
每个节点都会使用clusterNode结构来记录自己的状态,并为集群中的所有其他节点都创建一个相应的clusterNode:
struct clusterNode{
//创建节点的时间
mstime_t ctime;
//节点的名字,由40个十六进制字符组成
char name[REDIS_CLUSTER_NAMELEN];
//节点标识
int flags;
//节点当前配置纪元
uint64_t configEpoch;
//节点的IP地址
char ip[REDIS_IP_STR_LEN];
//节点的端口号
int port;
//保存连接节点所需的有关信息
clusterLink *link;
...
};
clusterLink结构保存了连接节点所需的有关信息:
typedef struct clusterLink{
//连接的创建时间
mstime_t ctime;
//TCP套接字描述符
int fd;
//输出缓冲区,保存着等待发送给其他节点的消息
sds sndbuf;
//输入缓冲区,保存着从其他节点接受到的消息
sds rcvbuf;
//与这个连接相关联的节点,如果没有的话就为NULL
struct clusterNode *node;
} clusterLink;
此外,每个节点都保存着一个clusterState结构,这个结构记录了当前节点的视角下集群的状态:
typedef struct clusterState{
//指向当前节点的指针
clusterNode *myself;
//集群当前的配置纪元,用于实现故障转移
uint64_t currentEpoch;
//集群当前的状态,是在线还是下线
int state;
//集群中至少处理着一个槽的节点的数量,当集群处于下线状态时为0
int size;
//集群节点名单,一个字典,键为节点名,值为节点对应的clusterNode
dict *nodes;
} clusterState;
以前面的7000、7001、7002三个节点为例,下图展示了7000对应的clusterState结构,myself指针指向自己的clusterNode,同时nodes指向集群中所有node组成的字典,size为0代表当前集群还处于下线状态:
![](https://img2020.cnblogs.com/blog/1742600/202006/1742600-20200627235722807-64708132.png)
cluster meet命令的实现
通过向A发送cluster meet命令可以将另一个节点B添加到A所在的集群,这个过程是通过握手来完成的。具体过程如下:
1、节点A为节点B创建一个clusterNode结构,并将该结构添加到自己的clusterState.nodes字典中,之后根据命令提供的IP和端口向节点B发送一条meet消息。
2、节点B收到meet消息,然后节点B为节点A创建一个clusterNode结构,然后将该结构添加到自己的clusterState.nodes字典中,然后节点B向A返回一条pong消息。
3、节点A收到pong消息,节点A会向B发送一条ping消息。握手完成,具体过程可以由下图表示:
![](https://img2020.cnblogs.com/blog/1742600/202006/1742600-20200627235740575-403995271.png)
之后节点A会将节点B的信息通过Gossip协议传播给集群中的其他节点,让其他节点和B握手,最终节点B就和集群中所有节点都建立联系。
槽指派
集群的整个数据库被分为16384个槽,数据库的每个键都属于某个槽,集群中的每个节点都可以处理0-16384个槽。当数据库中的16384个槽都有节点在处理时,集群处于上线状态,否则处于下线状态。
通过下列命令我们可以查看集群的基本信息:
cluster info
可以发现即使节点之间建立了联系,集群状态还是下线,需要执行槽指派命令才能让集群变为上线。
比如我们可以把槽0至5000指派给节点7000:
cluster addslots 0 1 2 3 4 ... 5000
当所有槽都指派完毕后集群就会变为上线状态。
记录槽的指派信息
槽的指派信息存储在两种数据结构中:
1、clusterNode中的slots数组,它是一个大小为16384的二进制数组,对应位置i如果为1代表该节点负责处理槽i,例如下图代表该节点处理槽0到槽7:
![](https://img2020.cnblogs.com/blog/1742600/202006/1742600-20200627235757957-1269285010.png)
clusterNode中的numslots是一个int数,代表了该节点负责处理的槽数量。
节点除了会维护自己clusterNode中的slots数组,还会把这个数组通过消息发送给集群中的其他节点,其他节点收到消息后会更新对应节点的clusterNode,这样集群中的每个节点都知道整个集群槽的指派信息。
2、clusterState中的slots数组记录了所有槽的指派信息,它的每个数组项都指向一个clusterNode,代表i位置的槽由对应的clusterNode处理,如果指针指向null代表该槽没有被分配。
集群通过这两种方式保存槽指派信息,这样可以快速解决两类问题:
1、要知道槽i被指派给谁,只需要查看clusterState中的slots数组对应位置。
2、要知道对应节点负责管理哪些槽的时候,只需要查看clusterNode中的slots数组,clusterNode中的slots数组的存在解决了一个重要问题,那就是槽指派信息传播问题,如果要传播节点A的指派信息,只需要发送对应clusterNode中的slots数组即可。
cluster addslots命令的实现
这个命令在指派槽信息时,必须保证槽没有被指派过,否则会报错,在执行命令的时候,需要将槽指派信息更新到之前提到的两种数据结构。
在集群中执行命令
集群进入上线状态后就可以执行命令了,当客户端向节点发送与数据库键相关的命令时,执行如下:
1、节点计算数据库键属于哪个槽,计算键名的CRC-16校验和,然后和16383相与,得到一个0-16383的槽号,使用下列命令可以查看对应的key在哪个槽:
cluster keyslot <key>
2、查看本节点的clusterState.slots,确定槽i是否属于当前节点负责,如果是就可以执行客户端的命令,如果不是节点会找到处理该槽的节点,然后给客户端返回一个moved错误:
moved <slot> <ip>:<port>
其中slot是键所在的槽。客户端收到moved错误后,会转向负责处理槽i的节点,并向目标节点重新发送命令。如果客户端和要转向的节点之间没有建立套接字连接,那么客户端会先创建连接再转向。
在集群模式下moved错误会被自动处理,但是在单机模式的客户端,客户端收到moved错误之后就会报错,不会自动转向。
节点数据库的特点
节点和单机服务器在数据库方面有一个区别是节点只能使用0号数据库。
节点还有一个特殊的数据结构是slots_to_keys,它是属于clusterState的一个跳表,用来保存槽和键之间的关系。该跳跃表的每一个分值都是槽号,成员是一个数据库键,每次更改数据库时都会维护这个跳表,有了这个数据结构就可以对某个或某些槽对应的数据库键进行批量操作,如下列命令:
cluster getkeysinslot <slot> <count>
会返回最多count个属于槽slot的数据库键,这个功能就是通过遍历这个跳表来完成的。
重新分片
redis集群的重新分片操作可以将已经指派给某个节点的槽改为指派给另一个节点,而且所有相关的键值对也会进行迁移,在重新分片操作进行的过程中,集群不需要下线,依然可以继续处理命令请求。
重新分片操作是由redis的集群管理软件redis-trib负责执行的,重新分片的步骤如下:
1、redis-trib对目标节点发送以下命令:
cluster setslot <slot> importing <source_id>
让目标节点准备好从源节点导入属于槽solt的键值对。
2、redis-trib对源节点发送以下命令:
cluster setslot <slot> migrating <target_id>
让源节点准备好将槽slot的键值对迁移到目标节点。
3、redis-trib对源节点发送以下命令:
cluster getkeysinslot <slot> <count>
获得最多count个属于槽slot的键名。
4、对上一步获得的键名,redis-trib都会向源节点发送一条命令:
migrate <target_ip> <target_port> <key_name> 0 <timeout>
来将键值对迁移到对应的节点。然后重复执行直到源节点槽对应的所有键值对都迁移完毕。
5、redis-trib会向集群中的任意一个节点发送以下命令:
cluster setslot <slot> node <target_id>
这一指派信息会通过消息发送至整个集群,最终集群中的所有节点都会知道槽slot已经指派给了目标节点。
ASK错误
在重新分片期间会出现一种特殊情况:属于被迁移槽的一部分键保存在源节点,另一部分保存在目标节点,此时如果向源节点发送与键有关的命令,而命令要处理的键正好已经被迁移,源节点就会向客户端返回一个ask错误,指引客户端转向正在导入槽的目标节点,并再次发送命令。
和moved错误类似,在集群模式的客户端会自动处理这种错误,而在单机模式的客户端会报错且不能自动转向。(ask自动转向的功能可能不能实现)
转向发送命令之前,会向目标节点发送一个asking命令,之后再发送原本要执行的命令,这个asking命令的作用是打开发送该命令的客户端的REDIS_ASKING标识。常规情况下,如果一个命令和槽i相关,而槽i又不属于目标节点,目标节点会直接拒绝请求并返回一个moved错误,但是如果发送的客户端带有REDIS_ASKING标识,且查询数据结构发现正在迁移时,目标节点就会破例执行这个命令,执行一次命令后REDIS_ASKING标识就会被移除。
复制与故障转移
redis集群中的节点分为主节点和从节点,主节点负责处理槽,从节点是复制某个主节点的,用来进行故障转移,如果某个主节点进入下线状态,那么其下的某个从节点就会成为新的主节点。
设置从节点
给一个节点A发送命令:
cluster replicate <node_id>
就可以将节点A设置为目标节点的从节点,并开始对目标主节点进行复制。
节点A会在自己的clusterState.nodes字典中找到node_id对应的clusterNode结构,然后设置为自己的clusterState.myself.slaveof,记录下主节点的信息,并修改clusterState.myself.flags,关闭REDIS_NODE_MASTER,打开REDIS_NODE_SLAVE,设置自己从节点的身份。
建立完联系后,从节点就会向主节点发送slaveof命令进行复制。
一个节点称为从节点并开始复制某个主节点这一信息会通过消息发送给集群中其他的节点,最终集群都会知道某个从节点正在复制某个主节点,集群中的所有节点都会在代表主节点的clusterNode结构的slaves属性和numslaves属性中记录正在复制这个主节点的从节点名单。
故障检测
集群中的每个节点都会定期向集群中的其他节点发送PING消息,如果接受消息的节点没有在规定的时间内返回PONG,那么该节点就会被标记位疑似下线,也就是找到对应节点的clusterNode的flags属性,打开REDIS_NODE_PFAIL。
(集群中的每个节点默认每隔一秒从已知节点列表中随机选5个,然后对这5个中最长时间没有发送过PING消息的节点发送PING消息,以此来检测节点是否在线。当节点A最后一次收到B发送的PONG消息的时间,距离当前时间已经超过了节点A的cluster_node_timeout的一半,那么节点A会向B发送PING,以此来避免太久没有通信)
当节点A通过消息得知B认为C进入了疑似下线状态时,A会找到C对应的clusterNode,并将B提供的下线报告添加到该clusterNode的fail_reports链表中,这个链表记录所有其他节点对该节点的下线报告,每个下线报告如下:
struct clusterNodeFailReport{
//下线报告来自于哪个节点
struct clusterNode *node;
//最后一次从node收到下线报告的时间,系统用这个属性检查报告是否过期,如果和现在差太远报告会被删除
mstime_t time;
}
如果一个集群中半数以上主节点都将某个主节点x标记位疑似下线,那么这个x会被标记位已下线,将x标记为已下线的节点会向集群广播一条关于主节点x的FAIL消息,收到消息的节点都会将x标记为已下线。
故障转移
当一个从节点发现自己正在复制的主节点进入了已下线状态,从节点就会开始进行故障转移,步骤如下:
1、原主节点下的所有从节点中会有一个从节点被选中,每个处理槽的主节点都有一次投票的机会,从节点会向集群广播一条cluster_type_failover_auth_request消息,要求主节点向它投票,第一个向主节点要求投自己的从节点会获得主节点的投票,此时主节点会向从节点返回一条cluster_type_failover_auth_ack消息,每个从节点都会统计自己的信息回复来确定有多少主节点支持自己,如果集群中有N个节点,那么投票数大于等于N/2+1的从节点会当选为新的主节点,如果在一个配置纪元中没有从节点能收集到足够的票数,那么集群就会进入新的配置纪元,然后再次进行选举直到选举完成。
2、被选中的从节点会执行下列命令,成为新的主节点:
slaveof no one
3、新的主节点会将下线主节点的槽指派全部转换为自己。
4、新主节点向集群广播一条PONG消息,让其他节点知道替换工作已完成。
消息
集群中的各个节点通过发送和接受消息message来进行通信,消息总共分为5种:
1、MEET消息:是cluster meet命令的回复消息,加入集群时响应使用。
2、PING消息:故障检测时使用。
3、PONG消息:作为MEET消息和PING消息的响应,而且一个节点可以通过广播PONG消息来刷新其他节点对本节点的认知,如故障转移后。
4、FAIL消息:标记已下线时广播使用。
5、PUBLISH消息:当节点收到一个publish命令时,节点会执行该命令,然后向集群广播PUBLISH消息,让其他节点都执行此publish命令。
每种消息都由消息头和消息正文组成,消息头记录了发送者的一些信息以及消息的关键信息。