redis设计与实现读书笔记——集群
节点
- 启动节点
一个节点就是运行在集群模式下的redis服务器,redis服务器在启动时会根据cluster-enabled配置选项是否位yes来决定是否开启服务器的集群模式。
- 集群数据结构
//一个节点的当前状态
struct clusterNode{
//创建节点的时间
mstime_t ctime;
//节点的名字,由40个16进制字符组成
char name[REDIS_CLUSTER_NAMELEN];
//节点标识
int flags;
//节点当前的配置纪元,用于实现故障转移
uint64_t configEpoch;
//节点的ip地址
char ip[REDIS_IP_STR_LEN];
//节点的端口号
int port;
//保存连接节点所需的有关信息
clusterLink *link;
//……
};
//连接节点有关信息
typedef struct clusterLink{
//创建的时间
mstime_t ctime;
//TCP套接字描述符
int fd;
//输出缓冲区,保存着等待发送给其他节点的消息
sds sndbuf;
//输入缓冲区,保存着从其他节点收到的消息
sds rcvbuf;
//与这个节点相关联的节点,如果没有的话就为NULL
struct clusterNode *node;
}clusterLink;
//当前节点视角下集群目前所处的状态
typedef struct clusterState{
//指向当前节点的指针
clusterNode *myself;
//集群当前的配置纪元,用于实现故障转移
uint64_t currentEpoch;
//集群当前的状态:上线还是下线
int state;
//集群中至少处理着一个槽的节点的数量
int size;
//集群节点名单:键为节点的名字,值为节点对应的clusterNode指针
dict *nodes;
//……
};
- CLUSTER MEET命令的实现
连接工作节点:CLUSTER MEET <ip> <port>
收到命令的节点A和节点B进行握手,以此来确认彼此的存在,并为将来的进一步通信打好基础:
- 节点A为节点B创建一个clusterNode结构,并将该结构添加到自己的clusterState.nodes字典中。
- 节点A根据ip和port发送meet消息给节点B。
- 如果一切顺利,节点B收到meet消息,为节点A创建一个clusterNode结构,并将该结构添加到自己的clusterState.nodes字典中。
- 如果一切顺利,节点B向节点A发送PONG消息
- 如果一切顺利,节点A向节点B返回PING消息
- 如果一切顺利,至此,握手完成
指派槽
当数据库中的16384个槽都有节点再处理时,集群处于上线状态;否则,集群处于下线状态。
- 记录节点的指派槽信息
//一个节点的当前状态
struct clusterNode{
//……
//记录处理那些槽
//如果slots数组在索引i上的二进制位的值为1,那么表示节点负责处理槽i;否则表示节点不负责处理槽i
unsigned char slots[16384/8];
//记录自己负责处理的槽的数量
int numslots;
//……
};
- 传播节点的指派槽信息
一个节点除了会将自己负责处理的槽记录在clusterNode结构的slots属性和numslots属性之外,它还会将自己的slots数组通过消息发送给集群中其他的节点,以此来告知其他节点自己目前负责处理哪些槽。当节点A通过消息从节点B那里接收到节点B的slots数组时,节点A会在自己的clusterState.nodes字典中查找接电脑B对应的clusterNode结构,并对结构中的slots数组进行保存或者更新。
- 记录集群所有槽的指派信息
//当前节点视角下集群目前所处的状态
typedef struct clusterState{
//……
//slots[i]指针如果指向NULL,说明槽i尚未被指派给任何节点;slots[i]指针如果指向一个clusterNode
//结构,说明槽i已经被指派给了这个clusterNode结构所戴白哦的机构;
clusterNode *slot[16384];
//……
};
- CLUSTER ADDSLOTS命令的实现
指派槽命令:CLUSTER ADDSLOTS <slot> [slot ...]
#遍历所有输出槽
for i in all_input_slots;
#如果有任意一个槽已经被指派给了某个节点,那么向客户端返回错误,并终止命令执行
if clusterState.slots[i] != NULL
reply_error()
return
#如果通过检查,再一次遍历所有槽,将这些槽指派给当前节点
for i in all_input_slots;
#设置clusterState.slots数组,将slots[i]的指针指向代表当前节点的clusterNode结构
clusterState.slots[i] = clusterState.myself
#访问当前节点的clusterNode结构的slots数组,将数组在索引i上的二进制位设置位1
setSlotBit(clusterState.myself.slots,i)
#发送消息告知集群中的其他节点,自己目前正在负责处理那些槽
在集群中执行命令
- 计算键属于哪个槽
- 判断槽是否由当前节点负责处理
当节点计算出键所属的槽i之后,节点就会检查自己在clusterState.slots数组中的项i,判断键所在的槽是否由自己负责:
- 如果clusterState.slots[i]等于clusterState.myself,那么说明槽i由当前节点负责,节点可以执行客户端发送的命令;
- 否则,节点会根据clusterState.slots[i]所指向的clusterNode结构所记录的节点IP和端口号,向客户端返回MOVED错误并指引客户端转向正在处理槽i的节点。
- MOVED错误
格式:MOVED <slot> <ip>:<port>
重新分片
Redis集群的重新分片操作可以将任意数量已经指派给某个节点(源节点)的槽改为指派给另一个节点(目标节点),并且相关槽所属的键值对也会从源节点移动到目标节点。
redis-trib对集群的单个槽slot进行重新分片的步骤如下:
- redis-trib对目标节点发送CLUSTER SETSLOT <slot> IMPORTING <source_id> 命令,让目标节点准备好从源节点导入槽slot的键值对
- redis-trib对源节点发送CLUSTER SETSLOT <slot> MIGRATING <source_id> 命令,让源节点准备好将属于槽slot的键值对迁移至目标节点
- redis-trib对源节点发送CLUSTER GETKEYSINSLOT<slot> <count> 命令,获得最多count个属于槽slot的键值对的键名。
- 对于步骤三获得的每个键名,redis-trib都向源节点发送一个MIGRATE <target_ip> <target_port> <key_name> 0 <timeout>命令,将被选中的键原子的从源节点迁移至目标节点
- 重复步骤3和4,直到源节点保存的所有属于槽slot的键值对都被迁移到目标节点为止。
- redis-trib向集群中的任意一个节点发送CLUSTER SETSLOT <slot> NODE <target_id>命令,将槽slot指派给目标节点的信息发送给整个集群。
ASK错误
在进行重新分片期间,源节点向目标节点迁移一个槽的过程中,可能会出现这样一种情况:属于被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对保存在目标节点里面。
- CLUSTER SETSLOT IMPORTING命令的实现
格式:CLUSTER SETSLOT <slot> IMPORTING <source_id>
clusterState结构的importing_slots_from数组记录了当前节点正在从其他节点导入的槽。
typedef struct clusterState{
//……
//如果importing_slots_from[i]的值不为NULL,而是指向一个clusterNode结构,表示当前节点正在从
//clusterNode所代表的节点导入槽i
clusterNode *importing_slots_from[16384];
//……
}clusterState;
- CLUSTER SETSLOT MIGRATING命令的实现
clusterState结构的migrating_slots_to数组记录了当前节点正在迁移至其他节点的槽。
typedef struct clusterState{
//……
//如果migrating_slots_to[i]的值不为NULL,而是指向一个clusterNode结构,表示当前节点正在将
//槽i迁移至clusterNode所代表的节点
clusterNode *migrating_slots_to[16384];
//……
}clusterState;
- ASK错误
返回该错误后,客户端根据ASK错误提供的IP地址和端口号,转向正在导入槽的目标节点,然后首先向目标节点发送一个ASKING命令,之后再重新发送原本想要执行的命令。
- ASKING命令
ASKING命令要做的就是开打发送该命令的客户端的REDIS_ASKING标识。如果该客户端的REDIS_ASKING标识未打开,直接发送请求,由于槽的迁移过程还未完成,请求的键还属于源节点,此时直接请求目标节点,目标节点会返回一个MOVED错误。
- ASK错误和MOVED错误的区别
- MOVED错误代表槽的负责全已经从一个结点转移到了另一个节点
- ASK错误只是两个节点在迁移槽的过程中使用的一种临时措施
复制与故障转移
- 设置从节点
向一个节点发送命令:CLUSTER REPLICATE <node_id>
- 接收到这个命令的节点首先会再自己的clusterState.node字典中找到node_id所对应的clusterNode,并将自己的clusterState.myself.slaveof指针指向这个结构,以此来记录这个节点正在复制的主节点。
- 然后节点会修改自己在clusterState.myself.flags中的属性,关闭原本的REDIS_NODE_MASTER标识,打开REDIS_NODE_SLAVE标识,表示这个节点已经由原来的主节点变为从节点。
- 最后,节点会调用复制代码,并根据clusterState.myself.slaveof指向的clusterNode结构所保存的IP地址和端口号,对主节点进行复制。
- 故障检测
集群中的每个节点都会定期地向集群中的其他节点发送PING消息,以此来检测对方是否在线,如果接收PING消息地节点没有在规定的时间内向发送PING消息的节点返回PONG消息,那么发送PING消息的节点就会将接收PING消息的节点标记位疑似下线。
当一个主节点A通过消息得知B认为主节点C进入了疑似下线状态时,主节点A会在自己的clusterState.nodes字典中找到主节点C所对应的clusterNode结构,并将主节点B的下线报告添加到clusterNode结构的fail_reports链表里面。
//一个节点的当前状态
struct clusterNode{
//……
//一个链表,记录了所有其他节点对该节点的下线报告
list *fail_reports;
//……
};
如果在一个集群里面,半数以上负责处理槽的主节点都将某个主节点 x报告未疑似下线,那么这个主节点x将被标记未已下线,将主节点x标记未已下线的节点会向集群广播一条关于主节点x的FAIL消息,所有收到这条FAIL消息的节点都会立即将主节点x标记为已下线。
- 故障转移
- 复制下线主节点的所有从节点中,会有一个被选中。
- 被选中的从节点将执行slaveof no one命令,成为新的主节点
- 新的主节点撤销下线主节点对指派槽的管理,并将这些槽全部指派给自己
- 新的主节点向集群广播一条PONG消息,告诉集群中的其他节点自己成为了新的主节点。
- 新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成。
- 选举新的主节点
- 当从节点发现自己复制的主节点进入已下线状态,就会向集群广播一条消息,要求所有收到这条消息并且具有投票权的主节点向这个从节点投票
- 集群中的主节点接收到这条广播消息,如果该主节点没有把票投给过其他从节点,则将票投给当前这个从节点
- 每个从节点统计收到的票数,如果票数大于等于 n/2+1,则当前节点成为新的主节点
消息
集群中节点发送的消息主要由5种:
- MEET消息:当发送者接到客户端发送的CLUSTER MEET命令时,发送者会向接收者发送MEET消息,请求接收者加入发送者当前所处的集群中。
- PING消息:集群里的每个节点默认每隔一秒钟就会从已知节点列表中随机选出五个节点,然后对这五个节点中最长时间没有发送过PING消息的节点发送PING消息,以此来检测选中的节点是否在线
- PONG消息:当接收者收到发送者发来的MEET消息或者PING消息时,为了向发送者确认这条MEET消息或者PING消息已到达,接收者会向发送者返回一条PONG消息
- FAIL消息:当一个主节点A判断另一个主节点B已经进入FAIL状态时,节点A会向集群广播一条关于B的FAIL消息,所有收到这条消息的节点都会立即将节点B标记为已下线。
- PUBLISH消息:当节点接收到一个PUBLISH命令时,节点会执行这个命令,并向集群广播一条PUBLISH消息,所有接收到这条PUBLISH消息的节点都会执行相同的PUBLISH消息。
- 消息头
typedef struct{
//消息的长度(包括这个消息头的长度和消息正文的长度)
unit32_t totlen;
//消息的类型
uint16_t type;
//消息正文包含的节点信息数量,只在发送MEET、PING、PONG这小中Gossip协议消息时使用
uint16_t count;
//发送者所处的配置纪元
uint64_t currentEpoch;
//如果发送者是一个主节点,那么这里记录的是发送者的配置纪元
//如果发送者是一个从节点,那么这里记录的是发送者正在复制的主节点的配置纪元
uint64_t configEpoch;
//发送者的名字(ID)
char sender[REDIS_CLUSTER_NAMELEN];
//发送者目前的指派槽信息
unsigned char myslots[REDIS_CLUSTER_SLOTS/8];
//如果发送者是一个从节点,那么这里记录的是发送者正在复制的主节点的名字
//如果发送者是一个主节点,那么这里记录的是REDIS_NONE_NULL_NAME
char slaveof[REDIS_CLUSTER_NAMELEN];
//发送者的端口号
uint16_t port;
//发送者的标识值
uint16_t flags;
//发送者所处集群的状态
unsigned char state;
//消息的正文
union clusterMsgData data;
}clusterMsg;
union clusterMsgData{
//MEET、PING、PONG消息的正文
struct{
//每条MEET、PING、PONG消息都包括两个clusterMsgDataGossip结构
clusterMsgDataGossip gossip[1];
}ping;
//FAIL消息的正文
struct{
clusterMsgDataFail about;
}fail;
//PUBLISH消息的正文
struct{
clusterMsgDataPublish msg;
}publish;
//其他消息的正文……
};
- MEET、PING、PONG消息的实现
通过消息头的type判断是三种消息中的哪一种。每次发送MEET、PING、PONG消息时,发送者都会从自己的已知节点中随机选出两个节点(可以是主节点或者从节点),并将这两个被选中节点的信息分别保存到两个clusterMsgDataGossip中。接收者收到消息,会取出这两个clusterMsgDataGossip,并根据其中的信息对自己的clusterState.nodes进行更新。
总结:两个数据结构clusterState和clusterNode
//一个节点的当前状态
struct clusterNode{
//创建节点的时间
mstime_t ctime;
//节点的名字,由40个16进制字符组成
char name[REDIS_CLUSTER_NAMELEN];
//节点标识
int flags;
//节点当前的配置纪元,用于实现故障转移
uint64_t configEpoch;
//节点的ip地址
char ip[REDIS_IP_STR_LEN];
//节点的端口号
int port;
//保存连接节点所需的有关信息
clusterLink *link;
//记录处理哪些槽
//如果slots数组在索引i上的二进制位的值为1,那么表示节点负责处理槽i;否则表示节点不负责处理i
unsigned char slots[16384/8];
//记录自己负责处理的槽的数量
int numslots;
//一个链表,记录了所有其他节点对该节点的下线报告
list *fail_reports;
//……
};
//当前节点视角下集群目前所处的状态
typedef struct clusterState{
//指向当前节点的指针
clusterNode *myself;
//集群当前的配置纪元,用于实现故障转移
uint64_t currentEpoch;
//集群当前的状态:上线还是下线
int state;
//集群中至少处理着一个槽的节点的数量
int size;
//集群节点名单:键为节点的名字,值为节点对应的clusterNode指针
dict *nodes;
//slots[i]指针如果指向NULL,说明槽i尚未被指派给任何节点;slots[i]指针如果指向clusterNode
//结构,说明槽i已经被指派给了这个clusterNode结构所指的机构;
clusterNode *slot[16384];
//如果importing_slots_from[i]的值不为NULL,而是指向一个clusterNode结构,表示当前节点正在从
//clusterNode所代表的节点导入槽i
clusterNode *importing_slots_from[16384];
//如果migrating_slots_to[i]的值不为NULL,而是指向一个clusterNode结构,表示当前节点正在将
//槽i迁移至clusterNode所代表的节点
clusterNode *migrating_slots_to[16384];
//……
};
上一篇: JavaScript学习笔记整理之引用类型_javascript技巧
下一篇: Redis集群