17.5 ASK错误
在进行重新分片期间,源节点向目标节点迁移一个槽的过程中,可能会出现这样一种情况:属于被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对则保存在目标节点里面。当客户端向源节点发送一个与数据库键有关的命令,并且命令要处理的数据库键恰好就属于正在迁移的槽时:
源节点会先在自己的数据库里查找指定的键,如果找到的话,就直接执行客户端发送的命令
否则,这个键有可能已经被迁移到了目标节点,源节点向客户端返回一个ASK错误,指引客户端转向正在导入槽的目标节点,并再次发送之前想要执行的命令
和接到MOVED错误时的情况类似,集群模式的cli接到ASK错误时也不会打印错误,单机模式的才会。
17.5.1 CLUSTER SETCLOT IMPORTING命令的实现
clusterState结构的importing_slots_from数组记录了当前节点正在从其他节点导入的槽,如果importing_slots_from[i] 的值不为NULL,而是指向一个clusterNode结构,那么表示当前节点正在从clusterNode所代表的节点导入槽。
在对集群进行重新分片的时候,向目标节点发送命令:
CLUSTER SETSLOT IMPORTING <source_id> 可以将目标节点clusterState.importing_slots_from[i]的值设置为source_id所代表的节点的clusterNode结构。
17.5.2 CLUSTER SETSLOT MIGRATING 命令的实现
clusterState结构的migrating_slots_to数组记录了当前节点正在迁移至其他节点的槽,如果migrating_slots_to[i]的值不为NULL,而是指向一个clusterNode结构,那么表示当前节点正在将槽i迁移至clusterNode所代表的节点
在对集群重新分片的时候,向源节点发送命令,CLUSTER SETSLOT _ MIGRATING __ 可以将源节点 clusterState.migrating_slots_to[i]的值设置为target_id所代表节点的clusterNode结构 _
17.5.3 ASK错误
接到ASK错误的客户端会根据错误提供的IP地址和端口号,转向至正在导入槽的目标节点,首先向目标节点发送一个ASKING命令,之后再重新发送原本想要执行的命令
17.5.4 ASKING命令
唯一要做的就是打开发送该命令的客户端的REDIS_ASKING标识,即clients.flags |= REDIS_ASKING
一般情况下,如果客户端向节点发送一个关于槽i的命令,而槽i又没有指派给这个节点的话,那么节点将向客户端返回一个MOVED错误;但是如果节点的clusterState.importing_slots_from[i]显示节点正在导入槽,并且发送命令的客户端带有REDIS_ASKING标识,节点将破例执行这个关于槽i的命令一次
当客户端接收到ASK错误并转向至正在导人槽的节点时,客户端会先向节点发送一个ASKING命令,然后才重新发送想要执行的命令,这是因为如果客户端不发送ASKING命令,而直接发送想要执行的命令的话,那么客户端发送的命令将被节点拒绝执行,并返回MOVED 错误。
另外要注意的是,客户端的REDIS_ASKING标识是一个一次性标识,当节点执行了一个带有REDIS_ASKING标识的客户端发送的命令之后,客户端的REDIS_ASKING标识就会被移除。
17.5.5 ASK错误和MOVED错误的区别
MOVED错误代表槽的负责权已经从一个节点转移到了另一个节点,以后都会直接将命令请求发送至MOVED错误所指向的节点
ASK错误只是两个节点在迁移槽的过程中使用的一种临时措施:在客户端收到关于槽i的ASK错误之后,客户端只会在接下来的一次命令请求中将关于槽i的命令请求发送到ASK错误所指示的节点,但是今后仍旧发送到目前负责处理槽i的节点
17.6 复制与故障转移
Redis集群中的节点分为主节点master和从节点slave,其中主节点用于处理槽,从节点用于复制某个主节点,并在被复制的主节点下线时,代替下线主节点继续处理命令请求。
17.6.1 设置从节点
向一个节点发送命令 CLUSTER REPLICATE <node_id> 可以让接收命令的节点成为node_id所指定的从节点,并开始对主节点进行复制。
- 接收到命令的节点首先会在自己的clusterState.nodes字典中找到node_id所对应节点的clusterNode结构,并将自己的clusterState.myself.slaveof指针指向这个结构,以此来记录这个节点正在复制的主节点。
- 节点会修改自己clusterState.myself.flags中的属性,关闭原本的REDIS_NODE_MASTER标识,打开REDIS_NODE_SLAVE标识,表示这个节点已经由原来的主节点变成了从节点
- 节点调用复制代码,并根据clusterState.myself.slaveof指向的clusterNode结构所保存的IP地址和端口号,对主节点进行复制,相当于向从节点发送
SLAVEOF <master_ip> <master_port>
一个节点成为从节点,并开始复制某个主节点这一信息并通过信息发送给集群中的其他节点,最终集群中的所有节点都会知道某个从节点正在复制某个主节点。
集群中的所有节点都会在代表主节点的clusterNode结构的slaves属性和numslaves属性中记录正在复制这个主节点的从节点名单。
17.6.2 故障检测
集群中的每个节点都会定期地向集群中其他节点发送PING消息,以此来检测对方是否在线。如果接收PING消息的节点没有在规定的时间内,向发送PING消息的节点返回PONG消息,那么发送PING消息的节点就会将接收PING消息的节点标记为疑似下线PFAIL
集群中的各个节点会通过相互发消息的方式来交换集群中各个节点的状态信息,当一个主节点A通过消息得知主节点B认为主节点C进入了疑似下线状态时,主节点A会在自己的clusterState.nodes字典中找到主节点C对应的clusterNode结构,并将主节点B的下线报告添加到clsuterNode结构的fail_reports链表里。
每个下线报告由一个clusterNodeFailReport结构表示,包含一个clusterNode类型,表示报告目标节点已经下线的节点,还有一个mstime_t类型,表示最后一次从node节点收到下线报告的时间。
如果在一个集群里,半数以上负责处理槽的主节点都将某个主节点x报告未疑似下线,那么这个主节点x将被标记为已下线FAIL,将主节点x标记为已下线的节点会向集群广播一条关于主节点x的FAIL消息,所有收到这条FAIL消息的节点都会立即将主节点x标记为已下线。
17.6.3 故障转移
当一个从节点发现自己正在复制的主节点进入了已下线状态时,从节点将开始对下线主节点进行故障转移,以下是故障转移的执行步骤:
复制下线主节点的所有从节点里,会有一个从节点被选中
被选中的从节点会执行SLAVEOF no one命令,成为新的主节点
新主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己
新的主节点向集群广播一条PONG消息,这条PONG消息可以让集群中的其他节点立即知道这个节点已经由从节点变成了主节点,并且主节点已经接管了原本由已下线节点负责处理的槽
新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成
17.6.4 选举新的主节点
和选举领头Sentinel的方法很相似,都是基于Raft算法的领头选举
17.7 消息
集群中的各个节点通过发送和接收消息来进行通信,发送消息的节点叫发送者,接收消息的节点为接收者。
发送消息的类型:
- MEET消息:发送者收到客户端发送的CLUSTER MEET命令时,发送者会向接收者发送METT消息,请求接收者加入发送者当前所处的集群里
- PING消息:集群里的每个节点默认每隔一秒钟都会从已知节点列表中随机选出5个节点,对这5个节点中最长时间没有发送过PING消息的节点发送PING消息,以此来检测被选中的节点是否在线。除此之外,如果节点A最后一次收到节点B发送的PONG消息的时间距离当前时间已经超过了节点A的cluster-node-timeout选项设置时长的一半,那么节点A也会向节点B发送PING消息。
- PONG消息:接收者收到发送者发来的MEET消息或PING消息时,为了向发送者确认这条MEET消息或是PING消息已到达,接收者会向发送者返回一条PONG消息。另外,一个节点也可以通过集群广播自己的PONG消息来让集群的其他节点立即刷新关于这个节点的认知。
- FAIL消息:当主节点A判断主节点B已经进入FAIL状态时,节点A会向集群广播一条关于节点B的FAIL消息,所有收到这个消息的节点都会立刻把节点B标记为已下线
- PUBLISH消息:节点接收到一个PUBLISH命令时,节点会执行命令并向集群广播一条PUBLISH消息。所有收到这条PUBLISH消息的节点都会执行相同的PUBLISH命令
一条消息由消息头header和消息正文data组成
17.1.1 消息头
节点发送的所有消息都由一个消息头包裹,除了包含消息正文外,还包含消息发送者自身的一些信息。
clusterMsg结构的currentEpoch、sender、myslots等属性记录了发送者自身的节点信息,接收者收到这些信息后,在自己的clusterState.nodes字典里找到发送者对应的clusterNode结构,并对结构更新。
17.7.2 MEET PING PONG消息的实现
Redis的各个节点通过 Gossip协议来交换各自关于不同节点的状态信息,Gossip协议由这三种消息实现,正文都是由2个cluster.h/clusterMsgDataGossip结构组成
节点通过消息头的type属性 来判断一条消息是MEET、PING还是PONG消息。
每次发送MEET\PING\PONG消息的时候,发送者都从自己的已知节点列表中随机选取两个节点,并将这两个被选中节点的信息分别保存到两个clusterMsgDataGossip结构里面。clusterMsgDataGossip记录了被选中节点的名字、发送者与被选中节点的最后一次发送和接收PING/PONG消息的时间戳、被选中节点的IP地址和端口号,以及被选中节点的标识值。
当接收者收到MEET/PING/PONG消息的时候,接收者会访问消息正文中的两个clusterMsgDataGossip结构,并根据自己是否认识clusterMsgDataGossip结构中记录的被选中节点来选择进行哪种操作:
- 被选中节点不存在于接收者的已知节点列表,和被选中节点进行握手
- 被选中节点已经存在接收者的已知节点列表,更新信息
17.7.3 FAIL消息的实现
集群的主节点A将主节点B标记为已下线FAIL时,主节点A向集群广播一条关于主节点B的FAIL消息,所有收到这条FAIL消息的节点都会把主节点B标记为已下线。
在集群的节点数量比较大的情况下,单纯使用Gossip协议来传播节点的已下线信息会给节点的信息更新带来一定延迟。
FAIL消息的正文由clusterMsgDataFail结构表示,只包含一个nodename属性,该属性记录了已下线节点的名字(集群的每个节点都是独一无二的名字)
17.7.4 PUBLISH消息的实现
当客户端向集群的某个节点发送命令PUBLISH <channel> <message>
,会导致集群的所有节点都向channel频道发送message消息。
消息正文是clusterMsgDataPublish结构
cpp
type struct {
uint32_t channel_len;
uint32_t message_len;
char bulk_data[8]
}clusterMsgDataPublish
数据存在字节数组里,bulk_data的0~channel_len-1字节保存的是channel参数,后面保存的是message参数