八:判断实例是否客观下线
当前哨兵一旦监测到某个主节点实例主观下线之后,就会向其他哨兵发送”is-master-down-by-addr”命令,询问其他哨兵是否也认为该主节点主观下线了。如果有超过quorum个哨兵(包括当前哨兵)反馈,都认为该主节点主观下线了,则当前哨兵就将该主节点实例标记为客观下线。
注意,客观下线的概念只针对主节点实例,而与从节点和哨兵实例无关。
1:发送”is-master-down-by-addr”命令
”is-master-down-by-addr”命令有两个作用:一是询问其他哨兵是否认为某个主节点已经主观下线;二是开始故障迁移时,当前哨兵向其他哨兵实例进行"拉票",让其选自己为领导节点。
本节只关注该命令的第一个作用,此时,该命令的格式是:
"SENTINEL is-master-down-by-addr <masterip> <masterport> <sentinel.current_epoch> *";
在哨兵的“主函数”sentinelHandleRedisInstance中,通过调用函数sentinelAskMasterStateToOtherSentinels来向其他哨兵发送”is-master-down-by-addr”命令。该函数的代码如下:
void sentinelAskMasterStateToOtherSentinels(sentinelRedisInstance *master, int flags) { dictIterator *di; dictEntry *de; di = dictGetIterator(master->sentinels); while((de = dictNext(di)) != NULL) { sentinelRedisInstance *ri = dictGetVal(de); mstime_t elapsed = mstime() - ri->last_master_down_reply_time; char port[32]; int retval; /* If the master state from other sentinel is too old, we clear it. */ if (elapsed > SENTINEL_ASK_PERIOD*5) { ri->flags &= ~SRI_MASTER_DOWN; sdsfree(ri->leader); ri->leader = NULL; } /* Only ask if master is down to other sentinels if: * * 1) We believe it is down, or there is a failover in progress. * 2) Sentinel is connected. * 3) We did not received the info within SENTINEL_ASK_PERIOD ms. */ if ((master->flags & SRI_S_DOWN) == 0) continue; if (ri->flags & SRI_DISCONNECTED) continue; if (!(flags & SENTINEL_ASK_FORCED) && mstime() - ri->last_master_down_reply_time < SENTINEL_ASK_PERIOD) continue; /* Ask */ ll2string(port,sizeof(port),master->addr->port); retval = redisAsyncCommand(ri->cc, sentinelReceiveIsMasterDownReply, NULL, "SENTINEL is-master-down-by-addr %s %s %llu %s", master->addr->ip, port, sentinel.current_epoch, (master->failover_state > SENTINEL_FAILOVER_STATE_NONE) ? server.runid : "*"); if (retval == REDIS_OK) ri->pending_commands++; } dictReleaseIterator(di); }
在函数中,轮训字典master->sentinels,针对其中的每一个哨兵实例ri:
属性ri->last_master_down_reply_time表示上次收到该哨兵实例ri对于"SENTINEL IS-MASTER-DOWN-BY-ADDR"命令回复的时间,如果该时间距离当前时间已经超过了5倍的SENTINEL_ASK_PERIOD,则清除其对于master的过时的状态记录:将SRI_MASTER_DOWN标记从实例标志位中清除;释放实例中的leader属性并置为NULL;
接下来开始向哨兵实例ri发送命令,但是在发送命令之前需要满足一定的条件,这些条件分别是:主节点master已经被标记为主观下线了;该哨兵实例处于连接状态;参数flags中设置了SENTINEL_ASK_FORCED标记,或者距离上次收到该哨兵实例的命令回复已超过SENTINEL_ASK_PERIOD;
满足以上所有条件之后,调用redisAsyncCommand向ri异步发送命令,命令的回调函数是sentinelReceiveIsMasterDownReply。
2:其他哨兵收到”is-master-down-by-addr”命令后的处理
当哨兵收到其他哨兵发来的”SENTINEL is-master-down-by-addr”命令后,调用函数sentinelCommand进行处理。该函数中处理”is-master-down-by-addr”的部分代码是:
void sentinelCommand(redisClient *c) { ... else if (!strcasecmp(c->argv[1]->ptr,"is-master-down-by-addr")) { /* SENTINEL IS-MASTER-DOWN-BY-ADDR <ip> <port> <current-epoch> <runid>*/ sentinelRedisInstance *ri; long long req_epoch; uint64_t leader_epoch = 0; char *leader = NULL; long port; int isdown = 0; if (c->argc != 6) goto numargserr; if (getLongFromObjectOrReply(c,c->argv[3],&port,NULL) != REDIS_OK || getLongLongFromObjectOrReply(c,c->argv[4],&req_epoch,NULL) != REDIS_OK) return; ri = getSentinelRedisInstanceByAddrAndRunID(sentinel.masters, c->argv[2]->ptr,port,NULL); /* It exists? Is actually a master? Is subjectively down? It's down. * Note: if we are in tilt mode we always reply with "0". */ if (!sentinel.tilt && ri && (ri->flags & SRI_S_DOWN) && (ri->flags & SRI_MASTER)) isdown = 1; /* Vote for the master (or fetch the previous vote) if the request * includes a runid, otherwise the sender is not seeking for a vote. */ if (ri && ri->flags & SRI_MASTER && strcasecmp(c->argv[5]->ptr,"*")) { leader = sentinelVoteLeader(ri,(uint64_t)req_epoch, c->argv[5]->ptr, &leader_epoch); } /* Reply with a three-elements multi-bulk reply: * down state, leader, vote epoch. */ addReplyMultiBulkLen(c,3); addReply(c, isdown ? shared.cone : shared.czero); addReplyBulkCString(c, leader ? leader : "*"); addReplyLongLong(c, (long long)leader_epoch); if (leader) sdsfree(leader); } ... }首先从命令参数中取出master的port,以及req_epoch。然后根据参数中的master的ip和port信息,调用函数getSentinelRedisInstanceByAddrAndRunID得到主节点实例ri;
如果当前哨兵没有处于TILT模式,并且找到的主节点实例ri确实是主节点,并且该主节点实例已经被标记为主观下线了,则设置isdown为1,否则isdown为0;
如果命令参数中的第5个参数不是"*",说明该命令是用于"拉票"的,因此调用函数sentinelVoteLeader进行投票,该函数返回本哨兵所选择的领导节点的运行ID,以及该领导的epoch,也就是leader和leader_epoch;
最后,回复给哨兵消息,回复消息中包含:isdown,leader和leader_epoch(如果该命令不是用来"拉票",则leader字段为"*",leader_epoch为0);
3:哨兵收到其他哨兵的”is-master-down-by-addr”命令回复信息后的处理
之前在sentinelAskMasterStateToOtherSentinels函数中,发送”is-master-down-by-addr”命令时,设置的回调函数是sentinelReceiveIsMasterDownReply。当收到其他哨兵对于”is-master-down-by-addr”命令的回复信息时,就调用该函数进行处理。该函数的代码如下:
void sentinelReceiveIsMasterDownReply(redisAsyncContext *c, void *reply, void *privdata) { sentinelRedisInstance *ri = c->data; redisReply *r; REDIS_NOTUSED(privdata); if (ri) ri->pending_commands--; if (!reply || !ri) return; r = reply; /* Ignore every error or unexpected reply. * Note that if the command returns an error for any reason we'll * end clearing the SRI_MASTER_DOWN flag for timeout anyway. */ if (r->type == REDIS_REPLY_ARRAY && r->elements == 3 && r->element[0]->type == REDIS_REPLY_INTEGER && r->element[1]->type == REDIS_REPLY_STRING && r->element[2]->type == REDIS_REPLY_INTEGER) { ri->last_master_down_reply_time = mstime(); if (r->element[0]->integer == 1) { ri->flags |= SRI_MASTER_DOWN; } else { ri->flags &= ~SRI_MASTER_DOWN; } if (strcmp(r->element[1]->str,"*")) { /* If the runid in the reply is not "*" the Sentinel actually * replied with a vote. */ sdsfree(ri->leader); if ((long long)ri->leader_epoch != r->element[2]->integer) redisLog(REDIS_WARNING, "%s voted for %s %llu", ri->name, r->element[1]->str, (unsigned long long) r->element[2]->integer); ri->leader = sdsnew(r->element[1]->str); ri->leader_epoch = r->element[2]->integer; } } }
首先,如果回复中的第一个参数值为1,说明发送回复的哨兵也认为主节点实例主观下线了,因此增加SRI_MASTER_DOWN标记到该哨兵实例的标志位中;否则,将哨兵实例标志位中的SRI_MASTER_DOWN标记清除;
如果回复中的第二个参数不是"*",说明发送回复的哨兵返回了其选择的领导节点及其epoch,分别将其选择的领导节点的运行ID和epoch记录到ri->leader和ri->leader_epoch中;
4:判断实例是否客观下线
在哨兵的“主函数”sentinelHandleRedisInstance中,调用sentinelCheckObjectivelyDown函数检测实例是否客观下线。该函数的代码如下:
void sentinelCheckObjectivelyDown(sentinelRedisInstance *master) { dictIterator *di; dictEntry *de; unsigned int quorum = 0, odown = 0; if (master->flags & SRI_S_DOWN) { /* Is down for enough sentinels? */ quorum = 1; /* the current sentinel. */ /* Count all the other sentinels. */ di = dictGetIterator(master->sentinels); while((de = dictNext(di)) != NULL) { sentinelRedisInstance *ri = dictGetVal(de); if (ri->flags & SRI_MASTER_DOWN) quorum++; } dictReleaseIterator(di); if (quorum >= master->quorum) odown = 1; } /* Set the flag accordingly to the outcome. */ if (odown) { if ((master->flags & SRI_O_DOWN) == 0) { sentinelEvent(REDIS_WARNING,"+odown",master,"%@ #quorum %d/%d", quorum, master->quorum); master->flags |= SRI_O_DOWN; master->o_down_since_time = mstime(); } } else { if (master->flags & SRI_O_DOWN) { sentinelEvent(REDIS_WARNING,"-odown",master,"%@"); master->flags &= ~SRI_O_DOWN; } } }
变量quorum表示认为主节点主观下线的哨兵实例的个数。如果master的标志位中设置了SRI_S_DOWN,则将其置为1,表明本哨兵实例认为其主观下线了;然后轮训字典master->sentinels,针对其中的每一个哨兵实例,只要其标志位中设置了SRI_MASTER_DOWN标记,说明已经收到过该哨兵对于"IS-MASTER-DOWN-BY-ADDR"命令的回复,并且它也认为该master主观下线了,因此将quorum加1;
轮训完所有哨兵实例之后,如果quorum的值大于等于master->quorum,则认为该主节点客观下线了,置变量odown为1;
如果odown为1,并且主节点之前没有被置为客观下线过,则将SRI_O_DOWN标记增加到主节点实例的标志位中,表示该主节点客观下线了;
如果odown为0,并且主节点之前已经被置为客观下线了,则将SRI_O_DOWN标记从主节点实例的标志位中清除;
九:故障转移流程之选举领导节点
1:故障转移流程
当哨兵监测到某个主节点客观下线之后,就会开始故障转移流程。具体步骤就是:
a:在所有哨兵中发起一次“选举”,让其他哨兵选择“我”(当前哨兵)为领导节点;
b:如果“我”能赢得大部分的选票,也就是在共有n个哨兵节点的情况下,如果有超过n/2个哨兵都将选票投给了“我”,则“我”就赢得了本界选举,成为领导节点,从而可以继续下面的流程。如果我没有赢得本界选举,则不能进行下面的流程了,而是随机等待一段时间后,开始下一轮选举;
c:“我”赢得选举后,就会从客观下线主节点的所有下属从节点中,按照一定规则选择一个从节点,使其升级为新的主节点;
d:当选中的从节点升级为主节点之后,“我”就会向剩下的从节点发送”SLAVEOF”命令,使它们与新的主节点进行同步;
e:最后,更新新主节点的信息,并通过”PUBLISH”命令,将新主节点的信息传播给其他哨兵。
2:选举领导节点原理
故障转移流程中,最难理解的部分就是选举领导节点的过程。因为多个哨兵实际上是组成了一个分布式系统,它们之间需要相互协作,通过交换信息,最终选出一个领导节点。
sentinel选举的过程,借鉴了分布式系统中的Raft协议。Raft协议是用来解决分布式系统一致性问题的协议,在很长一段时间,Paxos被认为是解决分布式系统一致性的代名词。但是Paxos难于理解,更难以实现。而Raft协议设计的初衷就是容易实现,保证对于普遍的人群都可以十分舒适容易的去理解。
有关Raft算法,可以参考官网https://raft.github.io/中的介绍。如果想要以最快的速度了解Raft算法的基本原理,可以参考这个PPT,非常形象且容易理解:http://thesecretlivesofdata.com/raft/
要理解哨兵的选举过程,关键就在于理解选举纪元(epoch)的概念。所谓的选举纪元,直白的解释就是“第几届选举”。
选举纪元实际上就是一个计数器。当哨兵进程启动时,其选举纪元就被初始化,默认的初始化值为0,不过该值也可以在配置文件中进行配置。
哨兵运行起来之后,哨兵之间通过HELLO消息来交换信息。HELLO消息中,除了有主节点信息之外,还包含哨兵本地的选举纪元值(sentinel.current_epoch)。当哨兵收到其他哨兵发布的HELLO消息后,解析其中的选举纪元值,如果该值大于“我”本地的选举纪元值,则会用它的选举纪元更新“我”的选举纪元。
因此,同一个监控单位内的所有哨兵,他们的选举纪元最终就会达成一个统一的值,这也就是Raft中,最终一致性的意思。
当哨兵A发现某个主节点客观下线后,它就会发起新一届的选举。第一件事就是将本地的选举纪元加1,这个加1的意思,实际上就是表示“发起新一届选举”。之后,哨兵A就会向其他哨兵发送”is-master-down-by-addr”命令,用于拉票,其中就包含了A的选举纪元。
投票采用先到先得的策略,因此当哨兵B收到A发来的”is-master-down-by-addr”命令之后,得到A的选举纪元,如果其值大于本地的选举纪元,说明本界选举中还没有投过票,则会更新本地的选举纪元,同时把票投给A。
现实当然不会这么简单,分布式系统因为涉及多个机器,就会有各种可能的情况发生。比如哨兵C几乎同时也发起了新一届的选举,它也会把本地的选举纪元加1,并发送”is-master-down-by-addr”命令。当B收到C发来的命令之后,得到C的选举纪元,发现其值并不大于本地的选举纪元(因为刚才已经根据A的选举纪元更新了),因此就不会再次投票了,而是将之前投票给A的结果反馈给C。
通过上面的介绍可知,在同一届选举(同一个选举纪元的值)中,每个哨兵只会投一次票。因此,在一界选举中,只可能有一个哨兵能获得超过半数的投票,从而赢得选举。
当然,也有可能产生选举失败的情况。也就是没有一个哨兵能获得超过半数的投票。比如有4个哨兵节点A、B、C、D。哨兵A和C几乎同时发起了新的选举,最终B和C将选票投给了A,而A和D将选票投给了C。因此,A和C都只得到了2票,没有超过半数,因此都不能成为新的领导节点。这种情况下,A和C都会随机等待一段时间之后,重新发起新的选举。这种随机性能减少下一轮选举的冲突,从而降低选举失败的可能。
3:判断是否开始故障转移
在哨兵的“主函数”sentinelHandleRedisInstance中,调用sentinelStartFailoverIfNeeded函数,判断是否开始一次新的故障转移流程。该函数的代码如下:
int sentinelStartFailoverIfNeeded(sentinelRedisInstance *master) { /* We can't failover if the master is not in O_DOWN state. */ if (!(master->flags & SRI_O_DOWN)) return 0; /* Failover already in progress? */ if (master->flags & SRI_FAILOVER_IN_PROGRESS) return 0; /* Last failover attempt started too little time ago? */ if (mstime() - master->failover_start_time < master->failover_timeout*2) { if (master->failover_delay_logged != master->failover_start_time) { time_t clock = (master->failover_start_time + master->failover_timeout*2) / 1000; char ctimebuf[26]; ctime_r(&clock,ctimebuf); ctimebuf[24] = ' '; /* Remove newline. */ master->failover_delay_logged = master->failover_start_time; redisLog(REDIS_WARNING, "Next failover delay: I will not start a failover before %s", ctimebuf); } return 0; } sentinelStartFailover(master); return 1; }
是否能开始一次新的故障转移流程,需要满足下面三个条件:
a:主节点master被标记为客观下线了;
b:当前没有针对该master进行故障转移流程;
c:最重要的条件是,针对该master,当前时间与master->failover_start_time之间的时间差,已经超过了master->failover_timeout*2。也就是说,当前距离上次进行故障转移流程的开始时间,或者是距离上次投票给其他哨兵的时间,已经等待了足够长的时间;
当创建实例时,master->failover_start_time属性值为0,这样第一次进行故障转移时就可以立即开始。
该属性会在两个地方更新,一个是开始一次新的故障转移流程时;一个是当前哨兵收到其他哨兵发来的用于拉票的”is-master-down-by-addr”命令,并且当前哨兵把票投给了其他哨兵,而不是自己时。
更新该属性的方法是master->failover_start_time=mstime()+rand()%1000,因此该属性中具有随机性,这就相当于将下次故障转移开始的时间随机化,从而可以减少冲突的发生(比如两个哨兵针对同一个主节点,同时开始进行故障转移,但是因为都没有获得足够的选票。因此这两个哨兵会等待一段时间后再次进行故障转移流程,因此master->failover_start_time属性的随机化,实际上就是等待时间的随机化);
而且,该属性还能防止当哨兵A已经开始故障转移时,另一个哨兵B开始针对同一个主节点进行故障转移(因为哨兵B收到了A的"拉票"命令,并且B把票投给了A,因此,B中会更新master->failover_start_time的值,因此B在开始故障转移时,会等待足够长的时间);
如果不满足以上任何一个条件,则返回0。如果满足以上条件的情况下,则调用sentinelStartFailover函数,开始故障转移流程,然后返回1。
4:开始新一轮的故障转移流程
在sentinelStartFailoverIfNeeded函数中,一旦满足条件后,就会调用函数sentinelStartFailover,开始新一轮的故障转移流程。sentinelStartFailover函数的代码如下:
void sentinelStartFailover(sentinelRedisInstance *master) { redisAssert(master->flags & SRI_MASTER); master->failover_state = SENTINEL_FAILOVER_STATE_WAIT_START; master->flags |= SRI_FAILOVER_IN_PROGRESS; master->failover_epoch = ++sentinel.current_epoch; sentinelEvent(REDIS_WARNING,"+new-epoch",master,"%llu", (unsigned long long) sentinel.current_epoch); sentinelEvent(REDIS_WARNING,"+try-failover",master,"%@"); master->failover_start_time = mstime()+rand()%SENTINEL_MAX_DESYNC; master->failover_state_change_time = mstime(); }
该函数实际上就是修改主节点实例的一些状态:
将主节点的master->failover_state属性置为SENTINEL_FAILOVER_STATE_WAIT_START,这是故障转移流程的第一个状态;
将SRI_FAILOVER_IN_PROGRESS标记增加到主节点标志位中,表示该主节点进入故障转移流程;
将选举纪元sentinel.current_epoch加1,并赋值给master->failover_epoch,表示马上开始新一轮的选举;
将master->failover_start_time属性设置为当前时间加上一个1000(1s)内的随机数;将master->failover_state_change_time置为当前时间戳;
5:发送”is-master-down-by-addr”命令进行拉票
在哨兵的“主函数”sentinelHandleRedisInstance中,sentinelStartFailoverIfNeeded函数返回1,表示开始了一次新的故障转移流程。接下来就会调用函数sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED),向所有哨兵发送”is-master-down-by-addr”命令进行拉票,请求其他哨兵投票给自己。
sentinelAskMasterStateToOtherSentinels函数的代码,之前已经讲过,不再赘述。这里只需要知道,用于拉票的”is-master-down-by-addr”命令格式是:
"SENTINEL is-master-down-by-addr <masterip> <masterport> <sentinel.current_epoch> <server.runid>";
其中的sentinel.current_epoch,就是当前哨兵的选举纪元。
6:其他哨兵收到”is-master-down-by-addr”命令后进行投票
当哨兵收到其他哨兵发来的”SENTINEL is-master-down-by-addr”命令后,调用函数sentinelCommand进行处理。该函数中处理”is-master-down-by-addr”的部分代码之前已经讲过,不再赘述,这里需要注意的是,在这部分代码中,调用sentinelVoteLeader函数进行投票。
sentinelVoteLeader函数的代码如下:
char *sentinelVoteLeader(sentinelRedisInstance *master, uint64_t req_epoch, char *req_runid, uint64_t *leader_epoch) { if (req_epoch > sentinel.current_epoch) { sentinel.current_epoch = req_epoch; sentinelFlushConfig(); sentinelEvent(REDIS_WARNING,"+new-epoch",master,"%llu", (unsigned long long) sentinel.current_epoch); } if (master->leader_epoch < req_epoch && sentinel.current_epoch <= req_epoch) { sdsfree(master->leader); master->leader = sdsnew(req_runid); master->leader_epoch = sentinel.current_epoch; sentinelFlushConfig(); sentinelEvent(REDIS_WARNING,"+vote-for-leader",master,"%s %llu", master->leader, (unsigned long long) master->leader_epoch); /* If we did not voted for ourselves, set the master failover start * time to now, in order to force a delay before we can start a * failover for the same master. */ if (strcasecmp(master->leader,server.runid)) master->failover_start_time = mstime()+rand()%SENTINEL_MAX_DESYNC; } *leader_epoch = master->leader_epoch; return master->leader ? sdsnew(master->leader) : NULL; }
哨兵调用本函数进行投票选举领导节点。参数master表示要进行故障转移的主节点;req_epoch表示选举纪元,也就是"第几届选举";req_runid表示进行拉票的哨兵实例的运行ID;leader_epoch是输出参数,返回当前哨兵最新投票的选举纪元。该函数返回当前哨兵最新一次投票选择的领导节点的运行ID;
首先如果req_epoch大于当前哨兵的当前选举纪元,则将当前哨兵的sentinel.current_epoch属性更新为req_epoch;
然后,如果master->leader_epoch小于req_epoch,并且sentinel.current_epoch小于等于req_epoch的话,说明当前哨兵实例,针对第req_epoch界选举,尚未投票。因此可以将选票投给req_runid所表示的哨兵。因此,这种情况下,将master->leader更新为req_runid,并且将master->leader_epoch赋值为sentinel.current_epoch,表示对于主节点master,当前哨兵最新的一次投票投给了master->leader,并且将本次投票的选举纪元记录到master->leader_epoch中;
这里,如果”我"选择的领导节点不是我自己,则更新master->failover_start_time属性为当前时间加1s内的随机时间,这样,针对同一个主节点,可以推迟"我"进行故障转移的时间;
最后,将leader_epoch赋值为master->leader_epoch,并且返回master->leader的值。
7:哨兵收到其他哨兵的”is-master-down-by-addr”命令回复信息后的处理
当收到其他哨兵对于”is-master-down-by-addr”命令的回复信息时,哨兵调用函数sentinelReceiveIsMasterDownReply进行处理。该函数之前已经介绍过了,不再赘述。只需要知道,当收到回复后,会把其他哨兵的投票结果记录到哨兵实例的leader和leader_epoch属性中。
8:统计投票
当故障转移流程处于SENTINEL_FAILOVER_STATE_WAIT_START状态时,会调用sentinelFailoverWaitStart函数进行处理,而在该函数中,第一件事就是调用sentinelGetLeader函数,统计本界选举的投票结果。
sentinelGetLeader函数的代码如下:
char *sentinelGetLeader(sentinelRedisInstance *master, uint64_t epoch) { dict *counters; dictIterator *di; dictEntry *de; unsigned int voters = 0, voters_quorum; char *myvote; char *winner = NULL; uint64_t leader_epoch; uint64_t max_votes = 0; redisAssert(master->flags & (SRI_O_DOWN|SRI_FAILOVER_IN_PROGRESS)); counters = dictCreate(&leaderVotesDictType,NULL); voters = dictSize(master->sentinels)+1; /* All the other sentinels and me. */ /* Count other sentinels votes */ di = dictGetIterator(master->sentinels); while((de = dictNext(di)) != NULL) { sentinelRedisInstance *ri = dictGetVal(de); if (ri->leader != NULL && ri->leader_epoch == sentinel.current_epoch) sentinelLeaderIncr(counters,ri->leader); } dictReleaseIterator(di); /* Check what's the winner. For the winner to win, it needs two conditions: * 1) Absolute majority between voters (50% + 1). * 2) And anyway at least master->quorum votes. */ di = dictGetIterator(counters); while((de = dictNext(di)) != NULL) { uint64_t votes = dictGetUnsignedIntegerVal(de); if (votes > max_votes) { max_votes = votes; winner = dictGetKey(de); } } dictReleaseIterator(di); /* Count this Sentinel vote: * if this Sentinel did not voted yet, either vote for the most * common voted sentinel, or for itself if no vote exists at all. */ if (winner) myvote = sentinelVoteLeader(master,epoch,winner,&leader_epoch); else myvote = sentinelVoteLeader(master,epoch,server.runid,&leader_epoch); if (myvote && leader_epoch == epoch) { uint64_t votes = sentinelLeaderIncr(counters,myvote); if (votes > max_votes) { max_votes = votes; winner = myvote; } } voters_quorum = voters/2+1; if (winner && (max_votes < voters_quorum || max_votes < master->quorum)) winner = NULL; winner = winner ? sdsnew(winner) : NULL; sdsfree(myvote); dictRelease(counters); return winner; }本函数用于得到:针对master主节点,选举纪元为epoch的选举结果。如果已经有某个哨兵实例赢得了超过半数的选票,则返回该实例的运行ID,否则,返回NULL;
首先创建字典counters,它用于统计每个哨兵实例的选票。它以哨兵的运行ID为key,以得到的选票数为value;然后取值voters为监控master主节点的所有哨兵个数,包括"我"自己;
接下来轮训字典master->sentinels,针对其中的每一个哨兵实例,如果其leader属性不为空,并且其leader_epoch属性等于当前选举纪元的话,说明该哨兵实例在本界选举中将选票投给了ri->leader。因此,在字典counters中增加ri->leader的选票数;
轮训完所有哨兵实例后,开始轮训字典counters进行"唱票",最终得到获得票数最多的哨兵实例winner,以及其获得的票数max_votes;
接下来是统计"我"的选票。如果得到winner的话,则调用sentinelVoteLeader:如果在选举纪元epoch中,"我"之前还没有投过票,则"我"也投给winner;如果"我"之前已经投过票了,则返回"我"选择的领导节点。
类似的,如果winner为NULL,说明其他哨兵没有投过选票,则调用函数sentinelVoteLeader:如果在选举纪元epoch中,"我"之前还没有投过票,则"我"将票投给我自己;如果"我"之前已经投过票了,则返回"我"选择的领导节点。
不管"我"之前有没有投过票,函数sentinelVoteLeader的返回值myvote,都是"我"所选择的领导节点,leader_epoch都是"我"投票时的选举纪元;如果sentinelVoteLeader返回的选举纪元leader_epoch就是当前纪元的话,则增加myvote的选票,并且更新winner及其票数max_votes;
要想真正赢得选举,winner必须得到超过半数的哨兵的支持,也就是其票数必须大于等于voters/2+1;而且其票数还必须大于等于master->quorum;
满足以上条件的话,winner就是选举纪元为epoch时,最终选出的领导节点,因此返回winner;不满足以上条件,说明选举纪元为epoch时,还没有人赢得选举,因此返回NULL。
参考:
https://github.com/maemual/raft-zh_cn/blob/master/raft-zh_cn.md
http://weizijun.cn/2015/04/30/Raft%E5%8D%8F%E8%AE%AE%E5%AE%9E%E6%88%98%E4%B9%8BRedis%20Sentinel%E7%9A%84%E9%80%89%E4%B8%BELeader%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/