Redis源码阅读(六)集群-故障迁移(下)

时间:2023-03-08 15:53:33
Redis源码阅读(六)集群-故障迁移(下)

Redis源码阅读(六)集群-故障迁移(下)

  最近私人的事情比较多,没有抽出时间来整理博客。书接上文,上一篇里总结了Redis故障迁移的几个关键点,以及Redis中故障检测的实现。本篇主要介绍集群检测到某主节点下线后,是如何选举新的主节点的。注意到Redis集群是无中心的,那么使用分布式一致性的算法来使集群中各节点能对在新主节点的选举上达成共识就是一个比较可行的方案。

  在工程上,Raft一致性算法是比较易于实现和理解的分布式一致性算法;Redis也是使用了Raft来做主节点选举的。所以这里先简单介绍下Raft的原理:

一、Raft算法

  Raft为解决一致性问题, 引入了Leader中心。简单来说就是通过选举出一个单Leader,让Leader来接收来自客户端的所有数据,Leader会将数据分发给所有节点,当Leader判断出数据已经分发到系统中大半节点以后,即认为该数据已经可以在系统中持久化存储,并通知客户端该数据提交成功,后续系统中数据的一致性可以让分布式系统内部通过数据同步的方式实现。(注意到这里和Redis去中心化的思想不符,所以Redis只是利用了其中选举的部分)

算法主要部分如下:

  1. Leader选举

  2. 客户数据一致化处理(即论文中的log repliecation部分)

Leader选举

  Raft给系统中的节点分配了三种状态,分别是Follower, Candidates, Leader;选举过程即是在分布式式系统中选出Leader状态节点的过程。三种状态的转换关系如下所示:
 Redis源码阅读(六)集群-故障迁移(下)

  当系统中没有Leader时,所有节点初始状态均为Follewer,每个节点在经历time_out时间后,会自动转变为Candidate,并尝试发起投票以便成为新的Leader;为了保证系统中只存在一个Leader,当选新Leader的条件是Candidate收到了超过半数节点以上的投票(每个节点在每轮投票中只能投给唯一的节点,通常是投个第一个发来邀票请求的节点),达到该条件后,Candidate即变为Leader。注意到投票是有轮次的,只有收到当前轮次的投票才是有效票。在状态机中,用term来表示投票的轮次。

根据上面介绍的流程,容易注意到为实现Leader的选举,有几个前提:1. 每个节点都需要知道系统中到底有哪些节点存在(为了能向每个节点邀票,同时要知道节点总数才能判断最终是否收到了多数的投票); 2. 所有节点应该有统一的初始化term,且后续需要不断的同步term

  第一点前提,只需要初始化阶段给所有节点置统一的term即可;而第二点前提则要求各节点主动拥抱新term,无情抛弃老term;具体来说就是每个节点收到旧term的消息,可以不处理消息请求,并把自身的较高的term返回;而每个节点收到新的term之后,则要积极响应,并在响应结束之后更新自己的term。

  有一种可能,就是在选举轮次中所有的节点都没有收到多数节点的投票认可,那么就需要发起新一轮的投票;为了减少新一轮投票依旧无法选出Leader的概率,每个Candidate节点的time_out时间是随机的,这样通常会有一个最先发出请求的节点占得先机,能收获大多数的选票。

客户数据一致化处理

  由于Redis中并没有用到Raft算法的一致化数据处理,这里不过多描述(该部分内容比较复杂,需要考虑的异常场景较多);详细的介绍可以看这篇博文,https://www.cnblogs.com/mindwind/p/5231986.html;个人觉得这篇博文很直观的介绍了分布式系统下在各种异常情况下,数据的一致性是如何保证的。

二、Redis选举新主节点的实现

  Redis选举新的主节点,有一个很重要的概念需要先理解下,就是epoch——纪元,类似于Raft中term的意义。Redis的数据结构中记录了两个epoch,分别是currentepoch和configepoch;这两个纪元的作用是不同的。

  currentepoch: 这个变量是记录整个集群投票轮次的,初始时,集群中所有节点的currentepoch都是0;后面虽然没个发起投票的节点都会自增该值,但也会同时将该值作为投票轮次信息发给其他节点,整个集群最终还是会具有相同的currentepoch值;

  configepoch:  这个变量是记录节点自身的特征值,主要用于判断槽的归属节点是否需要更新;考虑如下的场景,A节点负责槽1,2,3且configepoch=n,A节点掉线后从属节点B接替了A的工作成为新的主节点,那么B此时就负责1,2,3槽,B的configepoch=n1(n1一定大于n,因为每当有从节点通过故障迁移接替主节点工作时,该从节点的configepoch就会变更为整个集群中最大的configepoch);当A节点恢复工作后,还不知道自己已经被替代了,还向其他节点宣称自己是1,2,3槽的负责节点。其他节点已将1,2,3槽的负责节点改为B了,当其他节点收到A恢复之后的心跳包,会比较1,2,3槽所属节点B的configepoch(=n1)与A的configepoch(=n)哪个更大,发现n1更大就不会变更自己的记录,反过来还要通知A它所负责的槽已经被接管了。

介绍下Redis选举主节点的流程:

  1. 从节点检测到自己从属的主节点下线,开始发起一次选举;发起方式是向所有主节点广播发送一条投票请求,希望其他主节点同意自己为新的主节点;(带上自己记录的currentepoch,即Raft算法中的term),源码中如下:

 /*
从节点的故障转移,是在函数clusterHandleSlaveFailover中处理的,该函数在集群定时器函数clusterCron中调用。本函数
用于处理从节点进行故障转移的整个流程,包括:判断是否可以发起选举;判断选举是否超时;判断自己是否拉
到了足够的选票;使自己升级为新的主节点这些所有流程。
*/
//slave调用
void clusterHandleSlaveFailover(void) { //clusterBeforeSleep对CLUSTER_TODO_HANDLE_FAILOVER状态的处理,或者clusterCron中实时处理
//也就是当前从节点与主节点已经断链了多长时间,从通过ping pong超时,检测到本slave的master掉线了,从这时候开始算
mstime_t data_age;
//该变量表示距离发起故障转移流程,已经过去了多少时间;
mstime_t auth_age = mstime() - server.cluster->failover_auth_time;
//该变量表示当前从节点必须至少获得多少选票,才能成为新的主节点
int needed_quorum = (server.cluster->size / ) + ;
//表示是否是管理员手动触发的故障转移流程;
int manual_failover = server.cluster->mf_end != &&
server.cluster->mf_can_start; //说明向从发送了cluster failover force要求该从进行强制故障转移
int j;
//该变量表示故障转移流程(发起投票,等待回应)的超时时间,超过该时间后还没有获得足够的选票,则表示本次故障转移失败;
mstime_t auth_timeout,
//该变量表示判断是否可以开始下一次故障转移流程的时间,只有距离上一次发起故障转移时,已经超过auth_retry_time之后,
//才表示可以开始下一次故障转移了(auth_age > auth_retry_time);
auth_retry_time; server.cluster->todo_before_sleep &= ~CLUSTER_TODO_HANDLE_FAILOVER; /* Compute the failover timeout (the max time we have to send votes
* and wait for replies), and the failover retry time (the time to wait
* before waiting again.
*
* Timeout is MIN(NODE_TIMEOUT*2,2000) milliseconds.
* Retry is two times the Timeout.
*/
auth_timeout = server.cluster_node_timeout*;
if (auth_timeout < ) auth_timeout = ;
auth_retry_time = auth_timeout*; /* Pre conditions to run the function, that must be met both in case
* of an automatic or manual failover:
* 1) We are a slave.
* 2) Our master is flagged as FAIL, or this is a manual failover.
* 3) It is serving slots. */
/*
当前节点是主节点;当前节点是从节点但是没有主节点;当前节点的主节点不处于下线状态并且不是手动强制进行故障转移;
当前节点的主节点没有负责的槽位。满足以上任一条件,则不能进行故障转移,直接返回即可;
*/
if (nodeIsMaster(myself) ||
myself->slaveof == NULL ||
(!nodeFailed(myself->slaveof) && !manual_failover) ||
myself->slaveof->numslots == ) {
//真正把slaveof置为NULL在后面真正备选举为主的时候设置,见后面的replicationUnsetMaster
/* There are no reasons to failover, so we set the reason why we
* are returning without failing over to NONE. */
server.cluster->cant_failover_reason = REDIS_CLUSTER_CANT_FAILOVER_NONE;
return;
}; //slave从节点进行后续处理,并且和主服务器断开了连接 /* Set data_age to the number of seconds we are disconnected from
* the master. */
//将data_age设置为从节点与主节点的断开秒数
if (server.repl_state == REDIS_REPL_CONNECTED) { //如果主从之间是因为网络不通引起的,read判断不出epoll err事件,则状态为这个
data_age = (mstime_t)(server.unixtime - server.master->lastinteraction)
* ; //也就是当前从节点与主节点最后一次通信过了多久了
} else {
//这里一般都是直接kill主master进程,从epoll err感知到了,会在replicationHandleMasterDisconnection把状态置为REDIS_REPL_CONNECT
//本从节点和主节点断开了多久,
data_age = (mstime_t)(server.unixtime - server.repl_down_since) * ;
} /* Remove the node timeout from the data age as it is fine that we are
* disconnected from our master at least for the time it was down to be
* flagged as FAIL, that's the baseline. */
// node timeout 的时间不计入断线时间之内 如果data_age大于server.cluster_node_timeout,则从data_age中
//减去server.cluster_node_timeout,因为经过server.cluster_node_timeout时间没有收到主节点的PING回复,才会将其标记为PFAIL
if (data_age > server.cluster_node_timeout)
data_age -= server.cluster_node_timeout; //从通过ping pong超时,检测到本slave的master掉线了,从这时候开始算 /* Check if our data is recent enough. For now we just use a fixed
* constant of ten times the node timeout since the cluster should
* react much faster to a master down.
*
* Check bypassed for manual failovers. */
// 检查这个从节点的数据是否较新:
// 目前的检测办法是断线时间不能超过 node timeout 的十倍
/* data_age主要用于判断当前从节点的数据新鲜度;如果data_age超过了一定时间,表示当前从节点的数据已经太老了,
不能替换掉下线主节点,因此在不是手动强制故障转移的情况下,直接返回;*/
if (data_age >
((mstime_t)server.repl_ping_slave_period * ) +
(server.cluster_node_timeout * REDIS_CLUSTER_SLAVE_VALIDITY_MULT))
{
if (!manual_failover) {
clusterLogCantFailover(REDIS_CLUSTER_CANT_FAILOVER_DATA_AGE);
return;
}
} /* If the previous failover attempt timedout and the retry time has
* elapsed, we can setup a new one. */ /*
例如集群有7个master,其中redis1下面有2个slave,突然redis1掉了,则slave1和slave2竞争要求其他6个master进行投票,如果这6个
master投票给slave1和slave2的票数都是3,也就是3个master投给了slave1,另外3个master投给了slave2,那么两个slave都得不到超过一半
的票数,则只有靠这里的超时来进行重新投票了。不过一半这种情况很少发生,因为发起投票的时间是随机的,因此一半一个slave的投票报文auth req会比
另一个slave的投票报文先发出。越先发出越容易得到投票
*/
/*
如果auth_age大于auth_retry_time,表示可以开始进行下一次故障转移了。如果之前没有进行过故障转移,则auth_age等
于mstime,肯定大于auth_retry_time;如果之前进行过故障转移,则只有距离上一次发起故障转移时,已经超过
auth_retry_time之后,才表示可以开始下一次故障转移。
*/
if (auth_age > auth_retry_time) {
//每次超时从新发送auth req要求其他主master投票,都会先走这个if,然后下次调用该函数才会走if后面的流程
server.cluster->failover_auth_time = mstime() +
+ /* Fixed delay of 500 milliseconds, let FAIL msg propagate. */
random() % ; /* Random delay between 0 and 500 milliseconds. */ //等到这个时间到才进行故障转移
server.cluster->failover_auth_count = ;
server.cluster->failover_auth_sent = ;
server.cluster->failover_auth_rank = clusterGetSlaveRank();//本节点按照在master中的repl_offset来获取排名
/* We add another delay that is proportional to the slave rank.
* Specifically 1 second * rank. This way slaves that have a probably
* less updated replication offset, are penalized. */
server.cluster->failover_auth_time +=
server.cluster->failover_auth_rank * ; /* However if this is a manual failover, no delay is needed. */ /*
注意如果是管理员发起的手动强制执行故障转移,则设置server.cluster->failover_auth_time为当前时间,表示会
立即开始故障转移流程;最后,调用clusterBroadcastPong,向该下线主节点的所有从节点发送PONG包,包头部分带
有当前从节点的复制数据量,因此其他从节点收到之后,可以更新自己的排名;最后直接返回;
*/
if (server.cluster->mf_end) {
server.cluster->failover_auth_time = mstime();
server.cluster->failover_auth_rank = ;
}
redisLog(REDIS_WARNING,
"Start of election delayed for %lld milliseconds "
"(rank #%d, offset %lld).",
server.cluster->failover_auth_time - mstime(),
server.cluster->failover_auth_rank,
replicationGetSlaveOffset());
/* Now that we have a scheduled election, broadcast our offset
* to all the other slaves so that they'll updated their offsets
* if our offset is better. */
/*
调用clusterBroadcastPong,向该下线主节点的所有从节点发送PONG包,包头部分带
有当前从节点的复制数据量,因此其他从节点收到之后,可以更新自己的排名;最后直接返回;
*/
clusterBroadcastPong(CLUSTER_BROADCAST_LOCAL_SLAVES);
return;
} /* 进行故障转移 */ /* It is possible that we received more updated offsets from other
* slaves for the same master since we computed our election delay.
* Update the delay if our rank changed.
*
* Not performed if this is a manual failover. */
/*
如果还没有开始故障转移,则调用clusterGetSlaveRank,取得当前从节点的最新排名。因为在开始故障转移之前,
可能会收到其他从节点发来的心跳包,因而可以根据心跳包中的复制偏移量更新本节点的排名,获得新排名newrank,
如果newrank比之前的排名靠后,则需要增加故障转移开始时间的延迟,然后将newrank记录到server.cluster->failover_auth_rank中;
*/
if (server.cluster->failover_auth_sent == &&
server.cluster->mf_end == ) //还没有进行过故障庄毅
{
int newrank = clusterGetSlaveRank();
if (newrank > server.cluster->failover_auth_rank) {
long long added_delay =
(newrank - server.cluster->failover_auth_rank) * ;
server.cluster->failover_auth_time += added_delay;
server.cluster->failover_auth_rank = newrank;
redisLog(REDIS_WARNING,
"Slave rank updated to #%d, added %lld milliseconds of delay.",
newrank, added_delay);
}
} /* Return ASAP if we can't still start the election. */
// 如果执行故障转移的时间未到,先返回
if (mstime() < server.cluster->failover_auth_time) {
clusterLogCantFailover(REDIS_CLUSTER_CANT_FAILOVER_WAITING_DELAY);
return;
} /* Return ASAP if the election is too old to be valid. */
// 如果距离应该执行故障转移的时间已经过了很久
// 那么不应该再执行故障转移了(因为可能已经没有需要了)
// 直接返回
if (auth_age > auth_timeout) {// 如果auth_age大于auth_timeout,说明之前的故障转移超时了,因此直接返回;
clusterLogCantFailover(REDIS_CLUSTER_CANT_FAILOVER_EXPIRED);
return;
} /* Ask for votes if needed. */
// 向其他节点发送故障转移请求
if (server.cluster->failover_auth_sent == ) { // 增加配置纪元
server.cluster->currentEpoch++; // 记录发起故障转移的配置纪元
server.cluster->failover_auth_epoch = server.cluster->currentEpoch; redisLog(REDIS_WARNING,"Starting a failover election for epoch %llu.",
(unsigned long long) server.cluster->currentEpoch); //向其他所有节点发送信息,看它们是否支持由本节点来对下线主节点进行故障转移
clusterRequestFailoverAuth(); // 打开标识,表示已发送信息
server.cluster->failover_auth_sent = ; // TODO:
// 在进入下个事件循环之前,执行:
// 1)保存配置文件
// 2)更新节点状态
// 3)同步配置
clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG|
CLUSTER_TODO_UPDATE_STATE|
CLUSTER_TODO_FSYNC_CONFIG);
return; /* Wait for replies. */
} /* Check if we reached the quorum. */
// 如果当前节点获得了足够多的投票,那么对下线主节点进行故障转移
if (server.cluster->failover_auth_count >= needed_quorum) {
// 旧主节点
clusterNode *oldmaster = myself->slaveof; //在后面clusterSetNodeAsMaster中把slaveof置为NULL redisLog(REDIS_WARNING,
"Failover election won: I'm the new master.");
redisLog(REDIS_WARNING,
"configEpoch set to %llu after successful failover",
(unsigned long long) myself->configEpoch); /* We have the quorum, perform all the steps to correctly promote
* this slave to a master.
*
* 1) Turn this node into a master.
* 将当前节点的身份由从节点改为主节点
*/
clusterSetNodeAsMaster(myself);
// 让从节点取消复制,成为新的主节点
replicationUnsetMaster(); /* 2) Claim all the slots assigned to our master. */
// 接收所有主节点负责处理的槽 轮训16384个槽位,当前节点接手老的主节点负责的槽位;
for (j = ; j < REDIS_CLUSTER_SLOTS; j++) {
if (clusterNodeGetSlotBit(oldmaster,j)) {
// 将槽设置为未分配的
clusterDelSlot(j);
// 将槽的负责人设置为当前节点
clusterAddSlot(myself,j);
}
} /* 3) Update my configEpoch to the epoch of the election. */
// 更新集群配置纪元 本节点此时的配置epoch就是集群中最大的configEpoch
myself->configEpoch = server.cluster->failover_auth_epoch; /* 4) Update state and save config. */
// 更新节点状态
clusterUpdateState();
// 并保存配置文件
clusterSaveConfigOrDie(); //如果一个主master下面有2个savle,如果master挂了,通过选举slave1被选为新的主,则slave2通过这里来触发重新连接到新主,即slave1,见clusterUpdateSlotsConfigWith
/* 5) Pong all the other nodes so that they can update the state
* accordingly and detect that we switched to master role. */
// 向所有节点发送 PONG 信息
// 让它们可以知道当前节点已经升级为主节点了
clusterBroadcastPong(CLUSTER_BROADCAST_ALL); //真正触发其他本来属于同一个master的slave节点,连接到这个新选举出的master,是在clusterUpdateSlotsConfigWith
/* 6) If there was a manual failover in progress, clear the state. */ // 如果有手动故障转移正在执行,那么清理和它有关的状态
resetManualFailover();
} else {
//说明没有获得足够的票数,打印:Waiting for votes, but majority still not reached.
clusterLogCantFailover(REDIS_CLUSTER_CANT_FAILOVER_WAITING_VOTES); //例如6个主节点,现在只有1个主节点投票auth ack过来了,则会打印这个
}
}

  2. 主节点接收所有从节点发来的投票请求,但会比较该请求附带的配置纪元是否为新的(大于主节点自己记录的currentepoch),只有带着大于等于的消息才是有效请求;主节点只会投票给第一个发来有效投票请求的从节点。

 void clusterSendFailoverAuthIfNeeded(clusterNode *node, clusterMsg *request) {//node为sender
//主节点对slave发送过来的CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST进行投票
/*
集群中所有节点收到用于拉票的CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST包后,只有负责一定槽位的主节点能投票,其他没资格的节点直接忽略掉该包。
*/
// 请求节点的主节点
clusterNode *master = node->slaveof; // 请求节点的当前配置纪元
uint64_t requestCurrentEpoch = ntohu64(request->currentEpoch); // 请求节点想要获得投票的纪元
uint64_t requestConfigEpoch = ntohu64(request->configEpoch);
// 请求节点的槽布局 unsigned char *claimed_slots = request->myslots;
int force_ack = request->mflags[] & CLUSTERMSG_FLAG0_FORCEACK;
int j; /* IF we are not a master serving at least 1 slot, we don't have the
* right to vote, as the cluster size in Redis Cluster is the number
* of masters serving at least one slot, and quorum is the cluster
* size + 1 */ // 如果节点为从节点,或者是一个没有处理任何槽的主节点,
// 那么它没有投票权
if (nodeIsSlave(myself) || myself->numslots == ) return; //从节点和不负责槽位处理的直接返回,不参与投票 /* Request epoch must be >= our currentEpoch. */
// 请求的配置纪元必须大于等于当前节点的配置纪元
/*
如果发送者的currentEpoch小于当前节点的currentEpoch,则拒绝为其投票。因为发送者的状态与当前集群状态不一致,
可能是长时间下线的节点刚刚上线,这种情况下,直接返回即可;
*/
if (requestCurrentEpoch < server.cluster->currentEpoch) {
redisLog(REDIS_WARNING,
"Failover auth denied to %.40s: reqEpoch (%llu) < curEpoch(%llu)",
node->name,
(unsigned long long) requestCurrentEpoch,
(unsigned long long) server.cluster->currentEpoch);
return;
} /* I already voted for this epoch? Return ASAP. */
// 已经投过票了
/*
如果当前节点lastVoteEpoch,与当前节点的currentEpoch相等,说明本界选举中,当前节点已经投过票了,不
在重复投票,直接返回(因此,如果有两个从节点同时发起拉票,则当前节点先收到哪个节点的包,就只给那个
节点投票。注意,即使这两个从节点分属不同主节点,也只能有一个从节点获得选票);
*/
if (server.cluster->lastVoteEpoch == server.cluster->currentEpoch) {
redisLog(REDIS_WARNING,
"Failover auth denied to %.40s: already voted for epoch %llu",
node->name,
(unsigned long long) server.cluster->currentEpoch);
return;
} /* Node must be a slave and its master down.
* The master can be non failing if the request is flagged
* with CLUSTERMSG_FLAG0_FORCEACK (manual failover). */
/*
如果发送节点是主节点;或者发送节点虽然是从节点,但是找不到其主节点;或者发送节点的主节点并未下线
并且这不是手动强制开始的故障转移流程,则根据不同的条件,记录日志后直接返回;
*/
if (nodeIsMaster(node) || master == NULL ||
(!nodeFailed(master) && !force_ack)) { //如果从接收到cluster failover,然后发起auth req要求投票,则master受到后,就是该master在线也需要进行投票
if (nodeIsMaster(node)) { //auth request必须由slave发起
redisLog(REDIS_WARNING,
"Failover auth denied to %.40s: it is a master node",
node->name);
} else if (master == NULL) {
//slave认为自己的master下线了,但是本节点不知道他的master是那个,也就不知道是为那个master的slave投票,
//因为我们要记录是对那个master的从节点投票的,看if后面的流程
redisLog(REDIS_WARNING,
"Failover auth denied to %.40s: I don't know its master",
node->name);
} else if (!nodeFailed(master)) { //从这里也可以看出,必须集群中有一个主节点判断出某个节点fail了,才会处理slave发送过来的auth req
//slave认为自己的master下线了,于是发送过来auth request,本主节点收到该信息后,发现
//该slave对应的master是正常的,因此给出打印,不投票
redisLog(REDIS_WARNING,
"Failover auth denied to %.40s: its master is up",
node->name);
}
return;
}
/* We did not voted for a slave about this master for two
* times the node timeout. This is not strictly needed for correctness
* of the algorithm but makes the base case more linear. */
/*
针对同一个下线主节点,在2*server.cluster_node_timeout时间内,只会投一次票,这并非必须的限制条
件(因为之前的lastVoteEpoch判断,已经可以避免两个从节点同时赢得本界选举了),但是这可以使得获
胜从节点有时间将其成为新主节点的消息通知给其他从节点,从而避免另一个从节点发起新一轮选举又进
行一次没必要的故障转移;
*/
// 如果之前一段时间已经对请求节点进行过投票,那么不进行投票
if (mstime() - node->slaveof->voted_time < server.cluster_node_timeout * )
{
redisLog(REDIS_WARNING,
"Failover auth denied to %.40s: "
"can't vote about this master before %lld milliseconds",
node->name,
(long long) ((server.cluster_node_timeout*)-
(mstime() - node->slaveof->voted_time)));
return;
} /* The slave requesting the vote must have a configEpoch for the claimed
* slots that is >= the one of the masters currently serving the same
* slots in the current configuration. */
/*
判断发送节点,对其宣称要负责的槽位,是否比之前负责这些槽位的节点,具有相等或更新的配置纪元configEpoch:
该槽位当前的负责节点的configEpoch,是否比发送节点的configEpoch要大,若是,说明发送节点的配置信息不是最新的,
可能是一个长时间下线的节点又重新上线了,这种情况下,不能给他投票,因此直接返回;
*/
for (j = ; j < REDIS_CLUSTER_SLOTS; j++) { // 跳过未指派节点
if (bitmapTestBit(claimed_slots, j) == ) continue; // 查找是否有某个槽的配置纪元大于节点请求的纪元
if (server.cluster->slots[j] == NULL || server.cluster->slots[j]->configEpoch <= requestConfigEpoch)
//这里就是configEpoch真正发挥作用的地方
{
continue;
} // 如果有的话,说明节点请求的纪元已经过期,没有必要进行投票
/* If we reached this point we found a slot that in our current slots
* is served by a master with a greater configEpoch than the one claimed
* by the slave requesting our vote. Refuse to vote for this slave. */
redisLog(REDIS_WARNING,
"Failover auth denied to %.40s: "
"slot %d epoch (%llu) > reqEpoch (%llu)",
node->name, j,
(unsigned long long) server.cluster->slots[j]->configEpoch,
(unsigned long long) requestConfigEpoch);
return;
} /* We can vote for this slave. */
// 为节点投票
clusterSendFailoverAuth(node);
// 更新时间值
server.cluster->lastVoteEpoch = server.cluster->currentEpoch;
node->slaveof->voted_time = mstime(); redisLog(REDIS_WARNING, "Failover auth granted to %.40s for epoch %llu",
node->name, (unsigned long long) server.cluster->currentEpoch);
}

  3. 如果有从节点收到了系统中超过半数主节点的投票,则变为新的主节点,并向所有节点广播自己当选的信息;

  4,.如果从节点在超时时间内没有收到多数主节点的投票,也没有收到其他从节点升级为新主节点的广播,就会再次发起投票,重复第3步。

  新的主节点向所有其他节点广播之后,其他的节点会更新自己的配置,将新的主节点和其负责的槽对应起来。再有和相关槽对应的命令发到集群就会被转发给这个新的主节点;至此故障迁移结束。我们可以考虑下,如果同时有半数以上的服务器掉线了,是否会导致集群彻底失效呢?