八:判断实例是否客观下线

当前哨兵一旦监测到某个主节点实例主观下线之后,就会向其他哨兵发送”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] = '\0'; /* 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/

Redis源码解析:22sentinel(三)客观下线以及故障转移之选举领导节点的更多相关文章

  1. Redis源码阅读(三)集群-连接初始化

    Redis源码阅读(三)集群-连接建立 对于并发请求很高的生产环境,单个Redis满足不了性能要求,通常都会配置Redis集群来提高服务性能.3.0之后的Redis支持了集群模式. Redis官方提供 ...

  2. Cwinux源码解析(三)

    我在我的 薛途的博客 上发表了新的文章,欢迎各位批评指正. Cwinux源码解析(三)

  3. vueJs 源码解析 (三) 具体代码

    vueJs 源码解析 (三) 具体代码 在之前的文章中提到了 vuejs 源码中的 架构部分,以及 谈论到了 vue 源码三要素 vm.compiler.watcher 这三要素,那么今天我们就从这三 ...

  4. Volley源码解析(三) 有缓存机制的情况走缓存请求的源码分析

    Volley源码解析(三) 有缓存机制的情况走缓存请求的源码分析 Volley之所以高效好用,一个在于请求重试策略,一个就在于请求结果缓存. 通过上一篇文章http://www.cnblogs.com ...

  5. Netty 源码解析(三): Netty 的 Future 和 Promise

    今天是猿灯塔“365篇原创计划”第三篇. 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty 源码解析(一): 开始 Netty 源码解析(二): Netty 的 Channel 当前:Ne ...

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

    Redis源码阅读(六)集群-故障迁移(下) 最近私人的事情比较多,没有抽出时间来整理博客.书接上文,上一篇里总结了Redis故障迁移的几个关键点,以及Redis中故障检测的实现.本篇主要介绍集群检测 ...

  7. Redis源码阅读(五)集群-故障迁移(上)

    Redis源码阅读(五)集群-故障迁移(上) 故障迁移是集群非常重要的功能:直白的说就是在集群中部分节点失效时,能将失效节点负责的键值对迁移到其他节点上,从而保证整个集群系统在部分节点失效后没有丢失数 ...

  8. [源码解析] 深度学习分布式训练框架 horovod (14) --- 弹性训练发现节点 & State

    [源码解析] 深度学习分布式训练框架 horovod (14) --- 弹性训练发现节点 & State 目录 [源码解析] 深度学习分布式训练框架 horovod (14) --- 弹性训练 ...

  9. Redis源码解析之跳跃表(三)

    我们再来学习如何从跳跃表中查询数据,跳跃表本质上是一个链表,但它允许我们像数组一样定位某个索引区间内的节点,并且与数组不同的是,跳跃表允许我们将头节点L0层的前驱节点(即跳跃表分值最小的节点)zsl- ...

随机推荐

  1. 判断语句(if...else)if...else语句是在指定的条件成立时执行代码,在条件不成立时执行else后的代码

    判断语句(if...else) if...else语句是在指定的条件成立时执行代码,在条件不成立时执行else后的代码. 语法: if(条件) { 条件成立时执行的代码 } else { 条件不成立时 ...

  2. 【颓废篇】人生苦短,我用python(一)

    谁渴望来一场华(ang)丽(zang)的python交易! 最近突然产生了系统学习python的想法. 其实自从上次luogu冬日绘板dalao们都在写脚本就有这种想法了. 最近被计算几何势力干翻的我 ...

  3. html--伪等高布局

    <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title&g ...

  4. Rainbow的信号

    Rainbow的信号 有一串长度为n的数列,现在从中等概率选出l,r,如果l大于r,则交换,有三个询问 l~r间的数与和的数学期望 l~r间的数或和的数学期望 l~r间的数异或和的数学期望 对于100 ...

  5. UOJ450 复读机

    题意:n个位置,k种颜色.求有多少种方案使得每种颜色恰出现d的倍数次. 解:d=1就快速幂,n,k很小就DP,记得乘组合数来分配位置. d = 2 / 3的时候,考虑生成函数. f(x) = ∑[d ...

  6. 「题解」:[AHOI2013]作业

    问题: 作业 时间限制: 10 Sec  内存限制: 512 MB 题面 题目描述 此时己是凌晨两点,刚刚做了Codeforces的小A掏出了英语试卷.英语作业其实不算多,一个小时刚好可以做完.然后是 ...

  7. SQLite C++操作种

    SQLite C++操作类 为了方便SQLite的使用,封装了一个SQLite的C++类,同时支持ANSI 和UNICODE编码.代码如下:   头文件(SQLite.h) /************ ...

  8. 聊聊MVC和模块化以及MVVM和组件化

    原文链接 小寒的博客,带你理解更深的世界 面向对象,模块化和MVC 面向对象是指把写程序映射到现实生活,从而一来逻辑性更强,更容易写好代码,二来代码很贴切,通俗易懂,更被人理解,三来更加容易拓展和管理 ...

  9. [洛谷P1966] 火柴排队

    题目链接: 火柴排队 题目分析: 感觉比较顺理成章地就能推出来?似乎是个一眼题 交换的话多半会往逆序对上面想,然后题目给那个式子就是拿来吓人的根本没有卵用 唯一的用处大概是告诉你考虑贪心一波,很显然有 ...

  10. 7 Serialize and Deserialize Binary Tree 序列化及反序列化二叉树

    原题网址:http://www.lintcode.com/zh-cn/problem/serialize-and-deserialize-binary-tree/# 设计一个算法,并编写代码来序列化和 ...