Redis集群介绍
Redis集群是Redis提供的分布式数据库方案,集群通过分片(sharding)来进行数据共享,并提供复制和故障转移功能
节点
一个节点就是一个 运行在集群模式下的Redis 服务器,Redis 服务器在启动时会根据cluster-enabled配置选项是否为yes来决定是否开启服务器的集群模式,如下图所示。
节点数据结构
clusterNode结构保存了一个节点的当前状态,比如节点的创建时间、节点的名字、节点的当前纪元、节点的IP地址和端口号等。
每个节点都会使用clusterNode结构来记录自己的状态,以及为集群的其他节点也创建clusterNode结构记录其他节点的状态
下面是clusterNode
struct clusterNode {
//创建节点的时间
mstime_t ctime;
//节点的名字,由40 个十六进制字符组成
//例如68eef66df23420a5862208ef5b1a7005b806f2ff
char name[REDIS_CLUSTER_NAMELEN];
//节点标识
//使用各种不同的标识值记录节点的角色(比如主节点或者从节点),
//以及节点目前所处的状态(比如在线或者下线)。
int flags;
//节点当前的配置纪元,用于实现故障转移
uint64_t configEpoch;
//节点的IP 地址
char ip[REDIS_IP_STR_LEN];
//节点的端口号
int port;
//保存连接节点所需的有关信息
clusterLink *link;
// ...
};
clusterNode结构的link属性是一个clusterLink结构,该结构保存了连接节点所需的有关信息,比如套接字描述符,输入缓冲区和输出缓冲区
typedef struct clusterLink {
//连接的创建时间
mstime_t ctime;
// TCP 套接字描述符
int fd;
//输出缓冲区,保存着等待发送给其他节点的消息(message)。
sds sndbuf;
//输入缓冲区,保存着从其他节点接收到的消息。
sds rcvbuf;
//与这个连接相关联的节点,如果没有的话就为NULL
struct clusterNode *node;
} clusterLink;
最后,每个节点都保存着一个clusterState结构,这个结构记录了在当前节点的视角下,集群目前所处的状态,例如集群是在线还是下线,集群包含多少个节点,集群当前的配置纪元
typedef struct clusterState {
//指向当前节点的指针
clusterNode *myself;
//集群当前的配置纪元,用于实现故障转移
uint64_t currentEpoch;
//集群当前的状态:是在线还是下线
int state;
//集群中至少处理着一个槽的节点的数量
int size;
//集群节点名单(包括myself节点)
//字典的键为节点的名字,字典的值为节点对应的clusterNode结构
dict *nodes;
// ...
} clusterState;
总结一下,节点内保存一个clusterState结构,而clusterState里面通过clusterNode结构维护本节点和其他节点的状态
节点构成集群
一个Redis集群通常由多个节点(node)组成,在刚开始的时候,每个节点都是相互独立的,它们都处于一个只包含自己的集群当中,要组建一个真正可工作的集群,我们必须将各个独立的节点连接起来,构成一个包含多个节点的集群
连接各个节点的工作可以CLUSTER MEET <ip> <port>命令完成,例如在127.0.0.1:7000节点输入
CLUSTER MEET 127.0.0.1 7001,就可以将节点7001添加到7000所在的集群中
令7000为A,7001为B,以下是CLUSTER MEET命令的具体的流程:
1、节点A会为节点B创建clusterNode结构,并且添加到clusterState.nodes中
2、节点A会根据ip和port向B发送一条MEET消息
3、节点B接收到A的MEET消息,节点B也会为A创建clusterNode结构,并且添加到clusterState.nodes中
4、节点B向节点A返回一条PONG消息
5、节点A收到节点B返回的PONG消息。通过这条消息,节点A知道节点B已经成功接收到了MEET消息。
6、之后,节点A再向B发送一条PING消息
7、节点B将收到A的PING消息,通过这条消息节点B可以知道节点A已经成功地接收到了自己返回的PONG消息,握手完成
下图展示了握手过程
之后,节点A会将节点B的信息通过Gossip协议传播给集群中的其他节点,让其他节点也与节点B进行握手,终,经过一段时间之后,节点B会被集群中的所有节点认识
槽指派
Redis集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为16384个槽(slot),数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点可以处理0个或最多16384个槽
当数据库中的16384个槽都有节点在处理时,集群处于上线状态(ok);相反地,如果数据库中有任何一个槽没有得到处理,那么集群处于下线状态(fail)
通过CLUSTER ADDSLOTS <slot> [slot ...]命令,我们可以将一个或多个槽指派给节点负责
举个例子:集群中有7000,7001,7002三个节点已经完成了握手,但是还没有指派槽,所以这个集群仍处于下线状态
通过给节点发送CLUSTER INFO命令,我们能够看到cluster_state:fail
为了让7000、7001、7002所在的集群进入上线状态,我们需要将16384个槽位都指派出去
将0-5000指派给7000
将5001-10000指派给7001
将10001-16383指派给7002
当所有槽位指派之后,集群进入上线状态
记录槽指派信息
指派的槽信息我们存在哪里?不难想出是存在clusterNode结构中,可以看到clusterNode确实有个slots数组
一个char占8个bit,数组的长度为16384/8,所以slots共有16384个bit,每一位分别对应了一个槽位,通过判断二进制位的1和0,判断该节点是否处理该槽位,使用二进制位有个好处,检查节点是否处理某个槽,或者将某个槽指派给节点,只需要O(1)的时间复杂度,而节点处理的总槽数就是二进制位1的总数量,这里单独用numslots记录
一个节点除了维护自己处理的槽信息之外,还会将自己负责的槽位信息传播给集群内的其他节点,而其他节点接收到信息之后,会在clusterState.nodes中查找发送消息的节点,并对该节点的clusterNode.slots和numslots进行更新
此时,集群中的每个节点都会知道其他节点处理哪些槽位,但是对于查询某个槽位被谁负责,查询起来还是比较复杂的,我们需要遍历clusterState.nodes,判断clusterNode.slots对应的槽位是否为1,假设集群中节点的数量为n,那么时间复杂度为O(n)。
为了能够在某个节点快速知道槽位是指派了哪个节点,clusterState中也维护了一个slots数组,记录了所有槽的指派节点
CLUSTER ADDSLOTS命令的实现
CLUSTER ADDSLOTS 命令接受-一个或多个槽作为参数,并将所有输人的槽指派给接收
该命令的节点负责,伪代码如下:
维护完
clusterState.slots和本节点(clusterState.myself)的的二进制数组slots之后,即CLUSTER ADDSLOTS命令执行完毕之后,节点会通过发送消息告知集群中的其他节点,自己目前正在负责处理哪些槽
在集群中执行命令
在对数据库中的16384个槽都进行了指派之后,集群就会进入上线状态,这时客户端
可以向集群中的节点发送数据命令了
执行命令的流程如下:
计算键属于哪个槽
节点使用以下算法来计算给定键key属于哪个槽:
其中CRC16(key)语句用于计算键key的CRC-16校验和,而&16383语句则用于计算出一个介于0至16383之间的整数作为键key的槽号
使用CLUSTER KEYSLOT <key>命令可以查看一个给定键属于哪个槽:
判断槽是否由当前节点负责处理
当节点计算出键所属的槽i之后,节点就会检查自己在clusterState.slots数组中的索引i,判断键所在的槽是否由自己负责:
1、如果clusterState.slots[i]等于clusterState.myself,说明槽i由自己负责,可以执行命令
2、如果clusterState.slots[i]不等于clusterState.myself,说明槽i不由自己负责,节点会根据clusterState.slots[i]指向的clusterNode结构所记录的IP和端口号,向客户端返回MOVED错误,指引客户端转向正在处理槽i的节点
MOVED错误
当节点发现键所在的槽并非由自己负责处理的时候,节点就会向客户端返回一个MOVED错误,指引客户端转向至正在负责槽的节点
MOVED错误的格式为:MOVED <slot> <ip>:<port>
其中slot为键所在的槽,ip和port则是负责处理槽slot的节点的ip地址和端口号
槽和键的关系维护
在4.0以前,节点将用clusterState.slots_to_keys跳跃表来保存槽和键的关系,每个节点的分值都是一个槽号,而每一个成员都是一个数据库键
而4.0以及4.0以后,将跳跃表更新成了,RAX(一种字典树),可以看一下插入key会发生什么
比如set name zhangsan,首先会计算出name的slot,假设是10086,将高八位和低八位都用char表示,后面再拼上name,即char((hashslot >> 8) & 11111111)拼接char(hashslot & 11111111)拼接name,将这个字符数组插入rax
重新分片
当集群中上线或下线节点的时候,会触发集群的重新分片操作,Redis集群的重新分片操作可以将任意数量已经指派给某个节点(源节点)的槽改为指派给另一个节点(目标节点),并且相关槽所属的键值对也会从源节点被移动到目标节点
重新分片操作可以在线(online)进行,在重新分片的过程中,集群不需要下线,并且源节点和目标节点都可以继续处理命令请求
重新分片的实现原理
Redis集群的重新分片操作是由Redis的集群管理软件redis-trib负责执行的,Redis提供了进行重新分片所需的所有命令,而redis-trib则通过向源节点和目标节点发送命令来进行重新分片操作
redis-trib对单个slot进行重新分片的步骤如下:
1、redis-trib对目标节点发送CLUSTER SETSLOT <slot> IMPORTING <source_id>命令,让目标节点准备好从源节点导入属于槽slot的键值对
2、redis-trib对源节点发送CLUSTER SETSLOT <slot> MIGRATING <target_id>命令,让源节点准备好将属于槽slot的键值对迁移至目标节点
3、redis-trib向源节点发送CLUSTER GETKEYSINSLOT <slot> <count>命令,获得最多count个属于槽slot的键值对的键名(key name)
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已经指派给了目标节点。
如果重新分片涉及多个槽,那么redis-trib将对每个给定的槽分别执行上面给出的步骤
下图展示了对槽slot进行重新分片的整个过程:
ASK错误
在进行重新分片期间,源节点向目标节点迁移一个槽的过程中,可能会出现这样一种情况:当客户端向源节点发送一个与数据库键有关的命令,并且命令要处理的数据库键正在迁移到目标节点的时候,会返回ASK错误
详细流程如下图:
ASK错误和MOVED错误的区别
ASK错误和MOVED错误都会导致客户端转向,它们的区别在于:
- MOVED错误代表槽的负责权已经从一个节点转移到了另一个节点:在客户端收到关于槽i的MOVED错误之后,客户端每次遇到关于槽i的命令请求时,都可以直接将命令请求发送至MOVED错误所指向的节点,因为该节点就是目前负责槽i的节点
- 与此相反,ASK错误只是两个节点在迁移槽的过程中使用的一种临时措施:在客户端收到关于槽i的ASK错误之后,客户端只会在接下来的一次命令请求中将关于槽i的命令请求发送至ASK错误所指示的节点,但这种转向不会对客户端今后发送关于槽i的命令请求产生任何影响,客户端仍然会将关于槽i的命令请求发送至目前负责处理槽i的节点,除非ASK错误再次出现
故障转移
Redis集群中的节点分为主节点(master)和从节点(slave),其中主节点用于处理槽,而从节点则用于复制某个主节点,并在被复制的主节点下线时,代替下线主节点继续处理命令请求
故障检测
集群中的每个节点都会定期地向集群中的其他节点发送PING消息,以此来检测对方是否在线,如果接收PING消息的节点没有在规定的时间内,向发送PING消息的节点返回PONG消息,那么发送PING消息的节点就会将接收PING消息的节点标记为疑似下线(probable fail, PFAIL)
举个例子,如果点7001向节点7000发送了一条PING消息,但是节点7000没有在规定的时间内,向节点7001返回一条PONG消息,那么节点7001就会在自己的clusterState. nodes字典中找到节点7000所对应的clusterNode结构,并在结构的flags属性中打开REDIS NODE PFAIL标识,以此表示节点7000进入了疑似下线状态
如图所示:
集群中的各个节点会通过互相发送消息的方式来交换集群中各个节点的状态信息,例如某个节点是处于在线状态、疑似下线状态(PFAIL),还是已下线状态(FAIL)
当一个主节点A通过消息得知主节点B认为主节点C进入了疑似下线状态时,主节点
A会在自己的clusterState. nodes字典中找到主节点C所对应的clusterNode结构,并将主节点B的下线报告(failure report)添加到clusterNode结构的fail_reports
链表里面:
举个例子,如果主节点7001在收到主节点7002、主节点7003发送的消息后得知,主节点7002和主节点7003都为主节点7000进入了疑似下线状态,那么主节点7001将为主节点7000创建下图所示的下线报告
如果在一个集群里面,半数以上负责处理槽的主节点都将某个主节点x报告为疑似下线,那么这个主节点x将被标记为已下线(FAIL),将主节点x标记为已下线的节点会向集群广播一条关于主节点x的FAIL消息,所有收到这条FAIL消息的节点都会立即将主节点x标记为已下线
故障转移步骤
当从节点发现自己正在复制的主节点进入了已下线的状态时,从节点开始对主节点进行故障转移,以下是故障转移的执行步骤:
当一个从节点发现自已正在复制的主节点进入了已下线状态时,从节点将开始对下线主,节点进行故障转移,以下是故障转移的执行步骤:
1、下线主节点的所有从节点中,会有一个从节点被选中:如果集群里有N个具有投票权的主节点,那么当一个从节点收集到大于等于N/2+1张支持票时,这个从节点就会当选为新的主节点
2、被选中的从节点会执行SLAVEOF no one命令,成为新的主节点
3、新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己
4、新的主节点向集群广播一条PONG消息,这条PONG消息可以让集群中的其他节点立即知道这个节点已经由从节点变成了主节点,并且这个主节点已经接管了原本由已下线节点负责处理的槽
5、新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成