集群故障转移
节点下线
在集群定时任务clusterCron
中,会遍历集群中的节点,对每个节点进行检查,判断节点是否下线。与节点下线相关的状态有两个,分别为CLUSTER_NODE_PFAIL
和CLUSTER_NODE_FAIL
。
CLUSTER_NODE_PFAIL
:当前节点认为某个节点下线时,会将节点状态改为CLUSTER_NODE_PFAIL
,由于可能存在误判,所以需要根据集群中的其他节点共同决定是否真的将节点标记为下线状态,CLUSTER_NODE_PFAIL
可以理解为疑似下线,类似哨兵集群中的主观下线。
CLUSTER_NODE_FAIL
:集群中有过半的节点标认为节点已下线,此时将节点置为CLUSTER_NODE_FAIL
标记节点下线,CLUSTER_NODE_FAIL
表示节点真正处于下线状态,类似哨兵集群的客观下线。
1 |
疑似下线(PFAIL)
在集群定时任务遍历集群中的节点进行检查时,遍历到的每个节点记为node
,当前节点记为myself
,检查的内容主要有以下几个方面:
一、判断孤立主节点的个数
如果当前节点myself
是从节点,正在遍历的节点node
是主节点,并且node
节点不处于下线状态,会判断孤立节点的个数,满足以下三个条件时,认定node
是孤立节点,孤立节点个数增1:
node
的从节点中处于非下线状态的节点个数为0node
负责的slot数量大于0,node
节点处于CLUSTER_NODE_MIGRATE_TO状态
二、检查连接
这一步主要检查和节点间的连接是否正常,有可能节点处于正常状态,但是连接有问题,此时需要释放连接,在下次执行定时任务时会进行重连,释放连接需要同时满足以下几个条件:
- 与节点
node
之间的连接不为空,说明之前进行过连接 - 当前时间距离连接创建的时间超过了超时时间
- 距离向
node
发送PING消息的时间已经超过了超时时间的一半 - 距离收到
node
节点发送消息的时间超过了超时时间的一半
三、疑似下线判断
ping_delay
记录了当前时间距离向node
节点发送PING消息的时间,data_delayd
记录了node
节点向当前节点最近一次发送消息的时间,从ping_delay和data_delay中取较大的那个作为延迟时间。
如果延迟时间大于超时时间,判断node
是否已经处于CLUSTER_NODE_PFAIL
或者CLUSTER_NODE_FAIL
状态,如果都不处于,将节点状态置为CLUSTER_NODE_PFAIL
,认为节点疑似下线。
也就是说如果在规定的超时时间内,当前节点长时间未向node
节点发送PING消息,或者长时间未收到node
节点向当前节点发送的消息,当前节点就认为node
疑似下线状态。
上述检查完成之后,会判断当前节点是否是从节点,如果不处于CLUSTER_MODULE_FLAG_NO_FAILOVER
状态,调用clusterHandleSlaveFailover
处理故障转移,不过需要注意此时只是将节点置为疑似下线,并不满足故障转移条件,需要等待节点被置为FAIL下线状态之后,再次执行集群定时任务进入到clusterHandleSlaveFailover
函数中才可以开始处理故障转移。
1 | void clusterCron(void) { |
下线(FAIL)
当前节点认为某个node下线时,会将node状态置为CLUSTER_NODE_PFAIL
疑似下线状态,在定时向集群中的节点交换信息也就是发送PING消息时,消息体中记录了node的下线状态,其他节点在处理收到的PING消息时,会将认为node节点下线的那个节点加入到node的下线链表fail_reports中,并调用markNodeAsFailingIfNeeded
函数判断是否有必要将节点置为下线FAIL状态:
1 | void clusterProcessGossipSection(clusterMsg *hdr, clusterLink *link) { |
markNodeAsFailingIfNeeded
markNodeAsFailingIfNeeded用于判断是否有必要将某个节点标记为FAIL状态:
- 计算quorum,为集群节点个数一半 + 1,记为
needed_quorum
- 如果节点已经被置为FAIL状态,直接返回即可
- 调用
clusterNodeFailureReportsCount
函数,获取节点下线链表node->fail_reports
中元素的个数,node->fail_reports
链表中记录了认为node
下线的节点个数,节点个数记为failures
- 如果当前节点是主节点,
failures
增1,表示当前节点也认为node
需要置为下线状态 - 判断是否有过半的节点认同节点下线,也就是
failures
大于等于needed_quorum
,如果没有过半的节点认同node
需要下线,直接返回即可 - 如果有过半的节点认同
node
需要下线,此时取消节点的疑似下线标记PFAIL状态,将节点置为FAIL状态 - 在集群中广播节点的下线消息,以便让其他节点知道该节点已经下线
1 | void markNodeAsFailingIfNeeded(clusterNode *node) { |
故障转移处理
clusterHandleSlaveFailover
由上面的内容可知,节点客观下线时会被置为CLUSTER_NODE_FAIL
状态,下次执行集群定时任务时,在故障转移处理函数clusterHandleSlaveFailover
中,就可以根据状态来检查是否需要执行故障转移。
不过在看clusterHandleSlaveFailover
函数之前,先看一下clusterState
中和选举以及故障切换相关的变量定义:
1 | typedef struct clusterState { |
clusterHandleSlaveFailover函数中的一些变量
data_age
:记录从节点最近一次与主节点进行数据同步的时间。如果与主节点处于连接状态,用当前时间减去最近一次与master节点交互的时间,否则使用当前时间减去与master主从复制中断的时间。
auth_age
:当前时间减去发起选举的时间,也就是距离发起选举过去了多久,用于判断选举超时、是否重新发起选举使用。
needed_quorum
:quorum的数量,为集群中节点的数量的一半再加1。
auth_timeout
:等待投票超时时间。
auth_retry_time
:等待重新发起选举进行投票的时间,也就是重试时间。
发起选举
一、故障转移条件检查
首先进行了一些条件检查,用于判断是否有必要执行故障转移,如果处于以下几个条件之一,将会跳出函数,结束故障转移处理:
当前节点
myself
是master节点,因为如果需要进行故障转移一般是master节点被标记为下线,需要从它所属的从节点中选举节点作为新的master节点,这个需要从节点发起选举,所以如果当前节点是主节点,不满足进行故障转移的条件。当前节点
myself
所属的主节点为空当前节点
myself
所属主节点不处于客观下线状态并且不是手动进行故障转移,可以看到这里使用的是CLUSTER_NODE_FAIL
状态来判断的1
如果开启了不允许从节点执行故障切换并且当前不是手动进行故障转移
当前节点
myself
所属主节点负责的slot数量为0
二、主从复制进度校验
cluster_slave_validity_factor
设置了故障切换最大主从复制延迟时间因子,如果不为0需要校验主从复制延迟时间是否符合要求。
如果主从复制延迟时间data_age
大于 mater向从节点发送PING消息的周期 + 超时时间 * 故障切换主从复制延迟时间因子
并且不是手动执行故障切换,表示主从复制延迟过大,不能进行故障切换终止执行。
三、是否需要重新发起选举
如果距离上次发起选举的时间大于超时重试时间,表示可以重新发起投票。
设置本轮选举发起时间,并没有直接使用当前时间,而是使用了当前时间 + 500毫秒 + 随机值(0到500毫秒之间)进行了一个延迟,以便让上一次失败的消息尽快传播。
重置获取的投票数量
failover_auth_count
和是否已经发起选举failover_auth_sent
为0,等待下一次执行clusterHandleSlaveFailover
函数时重新发起投票。获取当前节点在所属主节点的所有从节点中的等级排名,再次更新发起选举时间,加上当前节点的rank * 1000,以便让等级越低(rank值越高)的节点,越晚发起选举,降低选举的优先级。
注意这里并没有恢复
CLUSTER_TODO_HANDLE_FAILOVER
状态,因为发起投票的入口是在集群定时任务clusterCron
函数中,所以不需要恢复。如果是手动进行故障转移,不需要设置延迟时间,直接使用当前时间,rank设置为0,然后将状态置为
CLUSTER_TODO_HANDLE_FAILOVER
,在下一次执行beforeSleep
函数时,重新进行故障转移。向集群中广播消息并终止执行本次故障切换。
四、延迟发起选举
- 如果还未发起选举投票,节点等级有可能在变化,所以此时需要更新等级以及发起投票的延迟时间。
- 如果当前时间小于设置的选举发起时间,需要延迟发起选举,直接返回,等待下一次执行。
- 如果距离发起选举的时间大于超时时间,表示本次选举已超时,直接返回。
五、发起投票
如果满足执行故障的条件,接下来需从节点想集群中的其他节点广播消息,发起投票,不过只有主节点才有投票权。failover_auth_sent
为0表示还未发起投票,此时开始发起投票:
- 更新节点当前的投票纪元(轮次)
currentEpoch
,对其进行增1操作 - 设置本次选举的投票纪元(轮次)
failover_auth_epoch
,与currentEpoch
一致 - 向集群广播,发送CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息到其他节点进行投票
failover_auth_sent
置为1 ,表示已经发起了投票- 发起投票后,直接返回,等待其他节点的投票。
六、执行故障切换
当某个节点获取到了集群中大多数节点的投票,即可进行故障切换,这里先不关注,在后面的章节会讲。
1 | void clusterHandleSlaveFailover(void) { |
获取节点等级
clusterGetSlaveRank用于计算当前节点的等级,遍历所属主节点的所有从节点,根据主从复制进度repl_offset
计算,repl_offset
值越大表示复制主节点的数据越多,所以等级越高,对应的rank
值就越低。
从节点在发起选举使用了rank
的值作为延迟时间,值越低延迟时间越小,意味着选举优先级也就越高。
1 | int clusterGetSlaveRank(void) { |
主节点进行投票
当从节点认为主节点故障需要发起投票,重新选举主节点时,在集群中广播了CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST
消息,对应的处理在clusterProcessPacket
函数中,里面会调用clusterSendFailoverAuthIfNeeded
函数进行投票:
1 | int clusterProcessPacket(clusterLink *link) { |
clusterSendFailoverAuthIfNeeded
clusterSendFailoverAuthIfNeeded函数用于进行投票,处理逻辑如下:
- 由于只有主节点才可以投票,如果当前节点不是主节点或者当前节点中负责slot的个数为0,当前节点没有权限投票,直接返回。
- 需要保证发起请求的投票轮次要等于或者大于当前节点中记录的轮次,所以如果请求的纪元(轮次)小于当前节点中记录的纪元(轮次) ,直接返回。
- 如果当前节点中记录的上次投票的纪元(轮次)等于当前投票纪元(轮次),表示当前节点已经投过票,直接返回。
- 如果发起请求的节点是主节点或者发起请求的节点所属的主节点为空,或者主节点不处于下线状态并且不是手动执行故障转移,直接返回。
- 如果当前时间减去节点投票时间
node->slaveof->voted_time
小于超时时间的2倍,直接返回。node->slaveof->voted_time
记录了当前节点的投票时间,在未超过2倍超时时间之前不进行投票。 - 处理slot,需要保证当前节点中记录的slot的纪元小于等于请求纪元,如果不满足此条件,终止投票,直接返回。
以上条件校验通过,表示当前节点可以投票给发送请求的节点,此时更新lastVoteEpoch
,记录最近一次投票的纪元(轮次),更新投票时间node->slaveof->voted_time
,然后向发起请求的节点回复CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK
消息。
1 | void clusterSendFailoverAuthIfNeeded(clusterNode *node, clusterMsg *request) { |
投票回复消息处理
主节点对发起投票请求节点的回复消息CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK
同样在消息处理函数clusterProcessPacket
中,会对发送回复消息的节点进行验证:
- 发送者是主节点
- 发送者负责的slot数量大于0
- 发送者记录的投票纪元(轮次)大于或等于当前节点发起故障转移投票的轮次
同时满足以上三个条件时,表示发送者对当前节点进行了投票,更新当前节点记录的收到投票的个数,failover_auth_count
加1,此时有可能获取了大多数节点的投票,先调用clusterDoBeforeSleep
设置一个CLUSTER_TODO_HANDLE_FAILOVER
标记,在周期执行的时间事件中会调用对状态进行判断决定是否执行故障转移。
1 | int clusterProcessPacket(clusterLink *link) { |
等待处理故障转移
从节点收到投票后,会添加CLUSTER_TODO_HANDLE_FAILOVER
标记,接下来看下对CLUSTER_TODO_HANDLE_FAILOVER
状态的处理。
在beforeSleep
函数(server.c文件中),如果开启了集群,会调用clusterBeforeSleep
函数,里面就包含了对CLUSTER_TODO_HANDLE_FAILOVER
状态的处理:
1 | void beforeSleep(struct aeEventLoop *eventLoop) { |
beforeSleep
函数是在Redis事件循环aeMain
方法中被调用的,详细内容可参考事件驱动框架源码分析 文章。
1 | void aeMain(aeEventLoop *eventLoop) { |
clusterBeforeSleep
在clusterBeforeSleep函数中,如果节点带有CLUSTER_TODO_HANDLE_FAILOVER
标记,会调用clusterHandleSlaveFailover
函数进行处理:
1 | void clusterBeforeSleep(void) { |
故障转移处理
clusterHandleSlaveFailover
函数在上面我们已经见到过,这次我们来关注集群的故障转移处理。
如果当前节点获取了大多数的投票,也就是failover_auth_count
(得到的投票数量)大于等于needed_quorum
,needed_quorum
数量为集群中节点个数的一半+1,即可执行故障转移,接下来会调用clusterFailoverReplaceYourMaster
函数完成故障转移。
1 | void clusterHandleSlaveFailover(void) { |
执行故障转移
clusterFailoverReplaceYourMaster
如果从节点收到了集群中过半的投票,就可以成为新的master节点,并接手下线的master节点的slot,具体的处理在clusterFailoverReplaceYourMaster函数中,主要处理逻辑如下:
- 将当前节点设为主节点
- 将下线的master节点负责的所有slots设置到新的主节点中
- 更新相关状态并保存设置
- 广播PONG消息到其他节点,通知其他节点当前节点成为了主节点
- 如果是手动进行故障转移,清除手动执行故障状态
1 | void clusterFailoverReplaceYourMaster(void) { |
Redis版本:redis-6.2.5