从哨兵Leader选举学习Raft协议实现(下)
上篇文章,我给你介绍了 Raft 协议的基本流程,以及哨兵实例工作的基本过程。哨兵是通过 serverCron 函数的周期性执行,进而在 serverCron 中调用 sentinelTimer 函数,实现周期性处理哨兵相关的时间事件。而 sentinelTimer 函数处理的时间事件,就包括了对哨兵监听的每个主节点,它会通过调用 sentinelHandleRedisInstance 函数,来检查主节点的在线状态,并在主节点客观下线时进行故障切换。
另外,我还带你了解了 sentinelHandleRedisInstance 函数执行过程的前三步操作,分别是重连断连的实例、周期性给实例发送检测命令,检测实例是否主观下线,这也分别对应了 sentinelReconnectInstance、sentinelSendPeriodicCommands 和 sentinelCheckSubjectivelyDown 这三个函数,你可以再回顾下。
那么,今天这篇文章,我接着来给你介绍 sentinelHandleRedisInstance 函数执行过程中的剩余操作,分别是检测主节点是否客观下线、判断是否需要执行故障切换,以及需要故障切换时的哨兵 Leader 选举的具体过程。
学完这节课的内容,你就可以对哨兵工作的过程有个全面了解了。并且,你可以掌握如何在代码层面实现 Raft 协议来完成 Leader 选举。这样,当你日后在分布式系统中实现分布式共识时,这部分内容就能帮助指导你的代码设计与实现了。
接下来,我们先来看下主节点的客观下线判断。
主节点客观下线判断
现在我们知道,哨兵在 sentinelHandleRedisInstance 函数中会调用 sentinelCheckObjectivelyDown 函数(在 sentinel.c 文件中),来检测主节点是否客观下线。
而 sentinelCheckObjectivelyDown 函数在执行时,除了会检查当前哨兵对主节点主观下线的判断结果,还需要结合监听相同主节点的其他哨兵,对主节点主观下线的判断结果。它把这些判断结果综合起来,才能做出主节点客观下线的最终判断。
从代码实现层面来看,在哨兵用来记录主节点信息的 sentinelRedisInstance 结构体中,本身已经用哈希表保存了监听同一主节点的其他哨兵实例,如下所示:
typedef struct sentinelRedisInstance {
…
dict *sentinels;
…
}
这样一来,sentinelCheckObjectivelyDown 函数通过遍历主节点记录的 sentinels 哈希表,就可以获取其他哨兵实例对同一主节点主观下线的判断结果。这也是因为,sentinels 哈希表中保存的哨兵实例,它们同样使用了 sentinelRedisInstance 这个结构体,而这个结构体的成员变量 flags,会记录哨兵对主节点主观下线的判断结果。
具体来说,sentinelCheckObjectivelyDown 函数会使用 quorum 变量,来记录判断主节点为主观下线的哨兵数量。如果当前哨兵已经判断主节点为主观下线,那么它会先把 quorum 值置为 1。然后,它会依次判断其他哨兵的 flags 变量,检查是否设置了 SRI_MASTER_DOWN 的标记。如果设置了,它就会把 quorum 值加 1。
当遍历完 sentinels 哈希表后,sentinelCheckObjectivelyDown 函数会判断 quorum 值是否大于等于预设定的 quorum 阈值,这个阈值保存在了主节点的数据结构中,也就是 master->quorum,而这个阈值是在 sentinel.conf 配置文件中设置的。
如果实际的 quorum 值大于等于预设的 quorum 阈值,sentinelCheckObjectivelyDown 函数就判断主节点为客观下线,并设置变量 odown 为 1,而这个变量就是用来表示当前哨兵对主节点客观下线的判断结果的。
这部分的判断逻辑如下代码所示,你可以看下:
/* Is this instance down according to the configured quorum?
*
* Note that ODOWN is a weak quorum, it only means that enough Sentinels
* reported in a given time range that the instance was not reachable.
* However messages can be delayed so there are no strong guarantees about
* N instances agreeing at the same time about the down state. */
// 此实例是否根据配置的quorum而关闭?
// 请注意,ODOWN 是一个弱quorum,它仅表示在给定时间范围内报告了足够多的 Sentinel 报告该实例无法访问。
// 但是消息可能会延迟,因此无法保证 N 个实例同时同意关闭状态
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
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);
// 如果quorum值大于预设的quorum阈值,那么设置odown为1。
if (quorum >= master->quorum) odown = 1;
}
/* Set the flag accordingly to the outcome. */
// 根据结果设置flag
if (odown) {
// 如果没有设置SRI_O_DOWN标记
if ((master->flags & SRI_O_DOWN) == 0) {
// 发送+odown事件消息
sentinelEvent(LL_WARNING,"+odown",master,"%@ #quorum %d/%d",
quorum, master->quorum);
// 在主节点的flags中记录SRI_O_DOWN标记
master->flags |= SRI_O_DOWN;
// 记录判断客观下线的时间
master->o_down_since_time = mstime();
}
} else {
if (master->flags & SRI_O_DOWN) {
sentinelEvent(LL_WARNING,"-odown",master,"%@");
master->flags &= ~SRI_O_DOWN;
}
}
}
另外,这里我也画了一张图,展示了该判断逻辑,你可以再来回顾下。
也就是说,sentinelCheckObjectivelyDown 函数是通过遍历监听同一主节点的其他哨兵的 flags 变量,来判断主节点是否客观下线的。
不过,你看完刚才的代码可能会有一个疑问,在上节课学习的 sentinelCheckSubjectivelyDown 函数中,如果哨兵判断主节点为主观下线,是会在主节点的 flags 变量中设置 SRI_S_DOWN 标记,如下所示:
//哨兵已判断主节点为主观下线
…
//对应主节点的sentinelRedisInstance结构中flags没有记录主观下线
if ((ri->flags & SRI_S_DOWN) == 0) {
…
ri->flags |= SRI_S_DOWN; //在主节点的flags中记录主观下线的标记,
}
但是,sentinelCheckObjectivelyDown 函数,是检查监听同一主节点的其他哨兵 flags 变量中的 SRI_MASTER_DOWN 标记,那么其他哨兵的 SRI_MASTER_DOWN 标记是如何设置的呢?
这就和 sentinelAskMasterStateToOtherSentinels 函数(在 sentinel.c 文件中)有关系了,下面,我们来具体了解下这个函数。
sentinelAskMasterStateToOtherSentinels 函数
sentinelAskMasterStateToOtherSentinels 函数的主要目的,是向监听同一主节点的其他哨兵发送 is-master-down-by-addr 命令,进而询问其他哨兵对主节点的状态判断。
它会调用 redisAsyncCommand 函数(在async.c文件中),依次向其他哨兵发送 sentinel is-master-down-by-addr 命令,同时,它设置了收到该命令返回结果的处理函数为 sentinelReceiveIsMasterDownReply(在 sentinel.c 文件中),如下所示:
/* If we think the master is down, we start sending
* SENTINEL IS-MASTER-DOWN-BY-ADDR requests to other sentinels
* in order to get the replies that allow to reach the quorum
* needed to mark the master in ODOWN state and trigger a failover. */
// 如果我们认为 master 宕机了,我们开始向其他 sentinel 发送 SENTINEL IS-MASTER-DOWN-BY-ADDR 请求
// 以获取允许达到将 master 标记为 ODOWN 状态并触发故障转移所需的约定人数的回复
#define SENTINEL_ASK_FORCED (1<<0)
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 receive the info within SENTINEL_ASK_PERIOD ms. */
// 仅在以下情况下询问 master 是否已关闭到其他哨兵:
// 1)我们认为它已关闭,或者正在进行故障转移。
// 2) Sentinel 已连接。
// 3) 我们没有在 SENTINEL_ASK_PERIOD (1000) 毫秒内收到信息。
if ((master->flags & SRI_S_DOWN) == 0) continue;
if (ri->link->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);
// 发送sentinel is-master-down-by-addr命令
retval = redisAsyncCommand(ri->link->cc,
sentinelReceiveIsMasterDownReply, ri,
"%s is-master-down-by-addr %s %s %llu %s",
sentinelInstanceMapCommand(ri,"SENTINEL"),
announceSentinelAddr(master->addr), port,
sentinel.current_epoch,
(master->failover_state > SENTINEL_FAILOVER_STATE_NONE) ?
sentinel.myid : "*");
if (retval == C_OK) ri->link->pending_commands++;
}
dictReleaseIterator(di);
}
另外从代码中,我们可以看到,sentinel is-master-down-by-addr 命令中还包括主节点 IP、主节点端口号、当前纪元(sentinel.current_epoch)和实例 ID。下面展示的就是这个命令的格式:
sentinel is-master-down-by-addr 主节点IP 主节点端口 当前epoch 实例ID
这其中,哨兵会根据当前主节点所处的状态来设置实例 ID。如果主节点已经要开始进行故障切换了,那么,实例 ID 会被设置为当前哨兵自身的 ID,否则就会被设置为 * 号
这里你需要注意的是,主节点的数据结构是使用了 master->failover_state 来记录故障切换的状态,其初始值为 SENTINEL_FAILOVER_STATE_NONE(对应的数值为 0),当主节点开始故障切换时,这个状态值就会大于 SENTINEL_FAILOVER_STATE_NONE 了。
好了,在了解了 sentinelAskMasterStateToOtherSentinels 函数的基本执行过程之后,我们还需要知道:sentinelAskMasterStateToOtherSentinels 函数向其他哨兵发出了 sentinel is-master-down-by-addr 命令后,其他哨兵是如何处理的呢?
sentinel is-master-down-by-addr 命令的处理
其实,哨兵对于 sentinel 开头的命令,都是在 sentinelCommand 函数(在 sentinel.c 文件)中进行处理的。sentinelCommand 函数会根据 sentinel 命令后面跟的不同子命令,来执行不同的分支,而 is-master-down-by-addr 就是一条子命令。
在 is-master-down-by-addr 子命令对应的代码分支中,sentinelCommand 函数会根据命令中的主节点 IP 和端口号,来获取主节点对应的 sentinelRedisInstance 结构体。
紧接着,它会判断主节点的 flags 变量中是否有 SRI_S_DOWN 和 SRI_MASTER 标记,也就是说,sentinelCommand 函数会检查当前节点是否的确是主节点,以及哨兵是否已经将该节点标记为主观下线了。如果条件符合,那么它会设置 isdown 变量为 1,而这个变量表示的就是哨兵对主节点主观下线的判断结果。
然后,sentinelCommand 函数会把当前哨兵对主节点主观下线的判断结果,返回给发送 sentinel 命令的哨兵。它返回的结果主要包含三部分内容,分别是当前哨兵对主节点主观下线的判断结果、哨兵 Leader 的 ID,以及哨兵 Leader 所属的纪元。
sentinelCommand 函数,对 sentinel 命令处理的基本过程如下所示:
void sentinelCommand(client *c) {
…
// is-master-down-by-addr子命令对应的分支
else if (!strcasecmp(c->argv[1]->ptr,"is-master-down-by-addr")) {
…
//当前哨兵判断主节点为主观下线
if (!sentinel.tilt && ri && (ri->flags & SRI_S_DOWN) && (ri->flags & SRI_MASTER))
isdown = 1;
…
addReplyMultiBulkLen(c,3); //哨兵返回的sentinel命令处理结果中包含三部分内容
addReply(c, isdown ? shared.cone : shared.czero); //如果哨兵判断主节点为主观下线,第一部分为1,否则为0
addReplyBulkCString(c, leader ? leader : "*"); //第二部分是Leader ID或者是*
addReplyLongLong(c, (long long)leader_epoch); //第三部分是Leader的纪元
…}
你也可以参考下图:
好了,到这里你就已经知道,哨兵会通过 sentinelAskMasterStateToOtherSentinels 函数,向监听同一节点的其他哨兵发送 sentinel is-master-down-by-addr 命令,来获取其他哨兵对主节点主观下线的判断结果。而其他哨兵是使用 sentinelCommand 函数,来处理 sentinel is-master-down-by-addr 命令,并在命令处理的返回结果中,包含自己对主节点主观下线的判断结果。
不过从刚才的代码中,你也可以看到,在其他哨兵返回的 sentinel 命令处理结果中,会包含哨兵 Leader 的信息。其实,这是因为 sentinelAskMasterStateToOtherSentinels 函数发送的 sentinel is-master-down-by-addr 命令本身,也可以用来触发哨兵 Leader 选举。这个我稍后会给你介绍。
那么,我们再回到前面讲主节点客观下线判断时提出的问题,sentinelCheckObjectivelyDown 函数要检查监听同一主节点的其他哨兵 flags 变量中的 SRI_MASTER_DOWN 标记,但是,其他哨兵的 SRI_MASTER_DOWN 标记是如何设置的呢?
这实际上是和哨兵在 sentinelAskMasterStateToOtherSentinels 函数中,向其他哨兵发送 sentinel is-master-down-by-addr 命令时,设置的命令结果处理函数 sentinelReceiveIsMasterDownReply 有关。
sentinelReceiveIsMasterDownReply 函数
在 sentinelReceiveIsMasterDownReply 函数中,它会判断其他哨兵返回的回复结果。回复结果会包含我刚才介绍的三部分内容,分别是当前哨兵对主节点主观下线的判断结果、哨兵 Leader 的 ID,以及哨兵 Leader 所属的纪元。这个函数会进一步检查,其中第一部分内容“当前哨兵对主节点主观下线的判断结果”是否为 1。
如果是的话,这就表明对应的哨兵已经判断主节点为主观下线了,那么当前哨兵就会把自己记录的对应哨兵的 flags,设置为 SRI_MASTER_DOWN。
下面的代码就展示了 sentinelReceiveIsMasterDownReply 函数判断其他哨兵回复结果的执行逻辑,你可以看下。
/* Receive the SENTINEL is-master-down-by-addr reply, see the
* sentinelAskMasterStateToOtherSentinels() function for more information. */
// 接收 SENTINEL is-master-down-by-addr 回复,更多信息请参见 sentinelAskMasterStateToOtherSentinels() 函数
void sentinelReceiveIsMasterDownReply(redisAsyncContext *c, void *reply, void *privdata) {
sentinelRedisInstance *ri = privdata;
instanceLink *link = c->data;
redisReply *r;
if (!reply || !link) return;
link->pending_commands--;
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. */
// 忽略每个错误或意外回复。请注意,如果命令由于任何原因返回错误,我们将结束清除 SRI_MASTER_DOWN 标志以表示超时。
// r是当前哨兵收到的其他哨兵的命令处理结果
// 如果返回结果包含三部分内容,并且第一,二,三部分内容的类型分别是整数、字符串和整数
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();
// 如果返回结果第一部分的值为1,则在对应哨兵的flags中设置SRI_MASTER_DOWN标记
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. */
// 如果回复中的 runid 不是“*”,则 Sentinel 实际上回复了投票。
sdsfree(ri->leader);
if ((long long)ri->leader_epoch != r->element[2]->integer)
serverLog(LL_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;
}
}
}
所以到这里,你就可以知道,一个哨兵调用 sentinelCheckObjectivelyDown 函数,是直接检查其他哨兵的 flags 是否有 SRI_MASTER_DOWN 标记,而哨兵又是通过 sentinelAskMasterStateToOtherSentinels 函数,向其他哨兵发送 sentinel is-master-down-by-addr 命令,从而询问其他哨兵对主节点主观下线的判断结果的,并且会根据命令回复结果,在结果处理函数 sentinelReceiveIsMasterDownReply 中,设置其他哨兵的 flags 为 SRI_MASTER_DOWN。下图也展示了这个执行逻辑,你可以再来整体回顾下。
那么,掌握了这个执行逻辑后,我们再来看下,哨兵选举是什么时候开始执行的。
哨兵选举
这里,为了了解哨兵选举的触发,我们先来复习下在上节课,我讲过的 sentinelHandleRedisInstance 函数中针对主节点调用关系,如下图所示:
从图中可以看到,sentinelHandleRedisInstance 会先调用 sentinelCheckObjectivelyDown 函数,再调用 sentinelStartFailoverIfNeeded 函数,判断是否要开始故障切换,如果 sentinelStartFailoverIfNeeded 函数的返回值为非 0 值,那么 sentinelAskMasterStateToOtherSentinels 函数会被调用。否则的话,sentinelHandleRedisInstance 就直接调用 sentinelFailoverStateMachine 函数,并再次调用 sentinelAskMasterStateToOtherSentinels 函数。
那么,在这个调用关系中,sentinelStartFailoverIfNeeded 会判断是否要进行故障切换,它的判断条件有三个,分别是:
- 主节点的 flags 已经标记了 SRI_O_DOWN;
- 当前没有在执行故障切换;
- 如果已经开始故障切换,那么开始时间距离当前时间,需要超过 sentinel.conf 文件中的 sentinel failover-timeout 配置项的 2 倍。
这三个条件都满足后,sentinelStartFailoverIfNeeded 就会调用 sentinelStartFailover 函数,开始启动故障切换,而 sentinelStartFailover 会将主节点的 failover_state 设置为 SENTINEL_FAILOVER_STATE_WAIT_START,同时在主节点的 flags 设置 SRI_FAILOVER_IN_PROGRESS 标记,表示已经开始故障切换,如下所示:
// 设置 Master 状态以启动故障转移。
void sentinelStartFailover(sentinelRedisInstance *master) {
serverAssert(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(LL_WARNING,"+new-epoch",master,"%llu",
(unsigned long long) sentinel.current_epoch);
sentinelEvent(LL_WARNING,"+try-failover",master,"%@");
master->failover_start_time = mstime()+rand()%SENTINEL_MAX_DESYNC;
master->failover_state_change_time = mstime();
}
而一旦 sentinelStartFailover 函数将主节点的 failover_state 设置为 SENTINEL_FAILOVER_STATE_WAIT_START 后,接下来,sentinelFailoverStateMachine 函数就会执行状态机来完成实际的切换。不过,在实际切换前,sentinelAskMasterStateToOtherSentinels 函数会被调用。
看到这个调用关系,你可能会有个疑问:sentinelAskMasterStateToOtherSentinels 函数是用来向其他哨兵询问对主节点主观下线的判断结果的,如果 sentinelStartFailoverIfNeeded 判断要开始执行故障切换,那么为什么还要调用 sentinelAskMasterStateToOtherSentinels 函数呢?
其实,这就和 sentinelAskMasterStateToOtherSentinels 函数的另一个作用有关了,这个函数除了会用来向其他哨兵询问对主节点状态的判断,它还可以用来向其他哨兵发起 Leader 选举。
在刚才给你介绍这个函数时,我提到它会给其他哨兵发送 sentinel is-master-down-by-addr 命令,这个命令包括主节点 IP、主节点端口号、当前纪元(sentinel.current_epoch)和实例 ID。其中,如果主节点的 failover_state 已经不再是 SENTINEL_FAILOVER_STATE_NONE,那么实例 ID 会被设置为当前哨兵的 ID。
而在 sentinel 命令处理函数中,如果检测到 sentinel 命令中的实例 ID 不为 * 号,那么就会调用 sentinelVoteLeader 函数来进行 Leader 选举。
//当前实例为主节点,并且sentinel命令的实例ID不等于*号
if (ri && ri->flags & SRI_MASTER && strcasecmp(c->argv[5]->ptr,"*")) {
//调用sentinelVoteLeader进行哨兵Leader选举
leader = sentinelVoteLeader(ri,(uint64_t)req_epoch, c->argv[5]->ptr,
&leader_epoch);
}
下面,我们来具体了解下这个 sentinelVoteLeader 函数。
sentinelVoteLeader 函数
sentinelVoteLeader 函数会实际执行投票逻辑,这里我通过一个例子来给你说明。
假设哨兵 A 判断主节点 master 客观下线了,它现在向哨兵 B 发起投票请求,哨兵 A 的 ID 是 req_runid。那么哨兵 B 在执行 sentinelVoteLeader 函数时,这个函数会判断哨兵 A 的纪元(req_epoch)、哨兵 B 的纪元(sentinel.current_epoch),以及 master 记录的 Leader 的纪元(master->leader_epoch)。按照 Raft 协议的定义,哨兵 A 就是 Candidate 节点,而哨兵 B 就是 Follower 节点。
我在上节课给你介绍 Raft 协议时有提到过,Candidate 发起投票都是有轮次记录的,Follower 在一轮投票中只能投一票。这里的纪元正是起到了轮次记录的作用。而 sentinelVoteLeader 函数判断纪元也是按照 Raft 协议的要求,让 Follower 在一轮中只能投一票。
那么,sentinelVoteLeader 函数让哨兵 B 投票的条件是:master 记录的 Leader 的纪元小于哨兵 A 的纪元,同时,哨兵 A 的纪元要大于或等于哨兵 B 的纪元。这两个条件保证了哨兵 B 还没有投过票,否则的话,sentinelVoteLeader 函数就直接返回当前 master 中记录的 Leader ID 了,这也是哨兵 B 之前投过票后记录下来的。
下面的代码展示了刚才介绍的这部分逻辑,你可以看下。
/* Vote for the sentinel with 'req_runid' or return the old vote if already
* voted for the specified 'req_epoch' or one greater.
*
* If a vote is not available returns NULL, otherwise return the Sentinel
* runid and populate the leader_epoch with the epoch of the vote. */
// 用“req_runid”投票给哨兵,如果已经投票给指定的“req_epoch”或更大,则返回之前的投票
// 如果投票不可用,则返回 NULL,否则返回 Sentinel runid 并使用投票的 epoch 填充 leader_epoch
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(LL_WARNING,"+new-epoch",master,"%llu",
(unsigned long long) sentinel.current_epoch);
}
// master 记录的 Leader 的纪元小于哨兵 A 的纪元,同时,哨兵 A 的纪元要大于或等于哨兵 B 的纪元
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(LL_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. */
// 如果我们没有为自己投票,请将 master 故障转移开始时间设置为现在,
// 以便在我们可以为同一个 master 启动故障转移之前强制延迟。
if (strcasecmp(master->leader,sentinel.myid))
master->failover_start_time = mstime()+rand()%SENTINEL_MAX_DESYNC;
}
*leader_epoch = master->leader_epoch;
// 直接返回当前 master 中记录的 Leader ID
return master->leader ? sdsnew(master->leader) : NULL;
}
那么现在,你就了解了 sentinelVoteLeader 函数是如何使用纪元判断来按照 Raft 协议完成哨兵 Leader 选举的了。
接下来,发起投票的哨兵仍然是通过 sentinelReceiveIsMasterDownReply 函数来处理其他哨兵对 Leader 投票的返回结果。这个返回结果,就像刚才给你介绍的,它的第二、三部分内容是哨兵 Leader 的 ID,和哨兵 Leader 所属的纪元。发起投票的哨兵就可以从这个结果中获得其他哨兵对 Leader 的投票结果了。
最后,发起投票的哨兵在调用了 sentinelAskMasterStateToOtherSentinels 函数让其他哨兵投票后,会执行 sentinelFailoverStateMachine 函数。
如果主节点开始执行故障切换了,那么,主节点的 failover_state,会被设置成 SENTINEL_FAILOVER_STATE_WAIT_START。在这种状态下,sentinelFailoverStateMachine 函数会调用 sentinelFailoverWaitStart 函数。而 sentinelFailoverWaitStart 函数,又会调用 sentinelGetLeader 函数,来判断发起投票的哨兵是否为哨兵 Leader。发起投票的哨兵要想成为 Leader,必须满足两个条件:
- 一是,获得超过半数的其他哨兵的赞成票
- 二是,获得超过预设的 quorum 阈值的赞成票数。
这两个条件,也可以从 sentinelGetLeader 函数中的代码片段看到,如下所示。
/* Scan all the Sentinels attached to this master to check if there
* is a leader for the specified epoch.
*
* To be a leader for a given epoch, we should have the majority of
* the Sentinels we know (ever seen since the last SENTINEL RESET) that
* reported the same instance as leader for the same epoch. */
// 扫描所有连接到这个master的sentinel,检查是否有指定epoch的leader。
// 要成为给定 epoch 的leader,我们应该让我们知道的大多数哨兵(自上次 SENTINEL RESET 以来见过)报告相同实例作为同一时期的leader
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;
serverAssert(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. */
// 检查获胜者是什么。获胜者获胜,需要两个条件:
// 1)哨兵数量的绝对多数(50%+ 1)。
// 2)无论如何至少有master->quorum多的投票。
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,sentinel.myid,&leader_epoch);
if (myvote && leader_epoch == epoch) {
uint64_t votes = sentinelLeaderIncr(counters,myvote);
if (votes > max_votes) {
max_votes = votes;
winner = myvote;
}
}
// voters是所有哨兵的个数,max_votes是获得的票数
// 赞成票的数量必须是超过半数以上的哨兵个数
voters_quorum = voters/2+1;
// 如果赞成票数不到半数的哨兵个数或者少于quorum阈值,那么Leader就为NULL
if (winner && (max_votes < voters_quorum || max_votes < master->quorum))
winner = NULL;
// 确定最终的Leader
winner = winner ? sdsnew(winner) : NULL;
sdsfree(myvote);
dictRelease(counters);
return winner;
}
下图就展示了刚才介绍的确认哨兵 Leader 时的调用关系,你可以看下。
好了,到这里,最终的哨兵 Leader 就能被确定了。
小结
好了,今天这节课的内容就到这里,我们来小结下。今天这篇文章,我在上篇文章的基础上,重点给你介绍了哨兵工作过程中的客观下线判断,以及 Leader 选举。因为这个过程涉及哨兵之间的交互询问,所以并不容易掌握,你需要好好关注以下我提到的重点内容。首先,客观下线的判断涉及三个标记的判断,分别是主节点 flags 中的 SRI_S_DOWN 和 SRI_O_DOWN,以及哨兵实例 flags 中的 SRI_MASTER_DOWN,我画了下面这张表,展示了这三个标记的设置函数和条件,你可以再整体回顾下。
而一旦哨兵判断主节点客观下线了,那么哨兵就会调用 sentinelAskMasterStateToOtherSentinels 函数进行哨兵 Leader 选举。这里,你需要注意的是,向其他哨兵询问主节点主观下线状态,以及向其他哨兵发起 Leader 投票,都是通过 sentinel is-master-down-by-addr 命令实现的,而 Redis 源码是用了同一个函数 sentinelAskMasterStateToOtherSentinels 来发送该命令,所以你在阅读源码时,要注意区分 sentinelAskMasterStateToOtherSentinels 发送的命令是查询主节点主观下线状态还是进行投票。
最后,哨兵 Leader 选举的投票是在 sentinelVoteLeader 函数中完成的,为了符合 Raft 协议的规定,sentinelVoteLeader 函数在执行时主要是要比较哨兵的纪元,以及 master 记录的 Leader 纪元,这样才能满足 Raft 协议对 Follower 在一轮投票中只能投一票的要求。
好了,到今天这篇文章,我们就了解了哨兵 Leader 选举的过程,你可以看到,虽然哨兵选举的最后执行逻辑就是在一个函数中,但是哨兵选举的触发逻辑是包含在了哨兵的整个工作过程中的,所以我们也需要掌握这个过程中的其他操作,比如主观下线判断、客观下线判断等。
每课一问:哨兵在 sentinelTimer 函数中调用 sentinelHandleDictOfRedisInstances 函数,对每个主节点都执行 sentinelHandleRedisInstance 函数,并且还会对主节点的所有从节点也执行 sentinelHandleRedisInstance 函数,那么,哨兵会判断从节点的主观下线和客观下线吗?
首先,在 sentinelHandleDictOfRedisInstances 函数中,它会执行一个循环流程,针对当前哨兵实例监听的每个主节点,都执行 sentinelHandleRedisInstance 函数。
在这个处理过程中,存在一个递归调用,也就是说,如果当前处理的节点就是主节点,那么 sentinelHandleDictOfRedisInstances 函数,会进一步针对这个主节点的从节点,再次调用 sentinelHandleDictOfRedisInstances 函数,从而对每个从节点执行 sentinelHandleRedisInstance 函数。
这部分的代码逻辑如下所示:
/* Perform scheduled operations for all the instances in the dictionary.
* Recursively call the function against dictionaries of slaves. */
// 对哈希表中的所有实例执行预定操作。针对从节点的哈希表递归调用该函数
void sentinelHandleDictOfRedisInstances(dict *instances) {
dictIterator *di;
dictEntry *de;
sentinelRedisInstance *switch_to_promoted = NULL;
/* There are a number of things we need to perform against every master. */
// 我们需要针对每个 master 节点执行许多事情
// 获取哈希表的迭代器
di = dictGetIterator(instances);
while((de = dictNext(di)) != NULL) {
// 获取哨兵实例监听的每个主节点
sentinelRedisInstance *ri = dictGetVal(de);
// 调用sentinelHandleRedisInstance处理实例
// 无论是主节点还是从节点,都会检查是否主观下线
sentinelHandleRedisInstance(ri);
if (ri->flags & SRI_MASTER) {
//如果当前节点是主节点,那么调用sentinelHandleDictOfRedisInstances对它的所有从节点进行处理。
sentinelHandleDictOfRedisInstances(ri->slaves);
sentinelHandleDictOfRedisInstances(ri->sentinels);
if (ri->failover_state == SENTINEL_FAILOVER_STATE_UPDATE_CONFIG) {
switch_to_promoted = ri;
}
}
}
if (switch_to_promoted)
sentinelFailoverSwitchToPromotedSlave(switch_to_promoted);
dictReleaseIterator(di);
}
然后,在 sentinelHandleRedisInstance 函数执行时,它会调用 sentinelCheckSubjectivelyDown 函数,来判断当前处理的实例是否主观下线。这步操作没有任何额外的条件约束,也就是说,无论当前是主节点还是从节点,都会被判断是否主观下线的。这部分代码如下所示:
void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {
...
sentinelCheckSubjectivelyDown(ri); //无论是主节点还是从节点,都会检查是否主观下线
...
}
但是要注意,sentinelHandleRedisInstance 函数在调用 sentinelCheckObjectivelyDown 函数,判断实例客观下线状态时,它会检查当前实例是否有主节点标记,如下所示:
void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {
…
if (ri->flags & SRI_MASTER) { //只有当前是主节点,才检查是否客观下线
sentinelCheckObjectivelyDown(ri);
…}
}
那么总结来说,对于主节点和从节点,它们的 sentinelHandleRedisInstance 函数调用路径就如下所示:
主节点:sentinelHandleRedisInstance -> sentinelCheckSubjectivelyDown -> sentinelCheckObjectivelyDown从节点:sentinelHandleRedisInstance -> sentinelCheckSubjectivelyDown
所以,回到这道题目的答案上来说,哨兵会判断从节点的主观下线,但不会判断其是否客观下线。
此外,还通过分析代码,看到了从节点被判断为主观下线后,是不能被选举为新主节点的。这个过程是在 sentinelSelectSlave 函数中执行的,这个函数会遍历当前的从节点,依次检查它们的标记,如果一个从节点有主观下线标记,那么这个从节点就会被直接跳过,不会被选为新主节点。
下面的代码展示了 sentinelSelectSlave 函数这部分的逻辑,你可以看下。
sentinelRedisInstance *sentinelSelectSlave(sentinelRedisInstance *master) {
sentinelRedisInstance **instance =
zmalloc(sizeof(instance[0])*dictSize(master->slaves));
sentinelRedisInstance *selected = NULL;
int instances = 0;
dictIterator *di;
dictEntry *de;
mstime_t max_master_down_time = 0;
if (master->flags & SRI_S_DOWN)
max_master_down_time += mstime() - master->s_down_since_time;
max_master_down_time += master->down_after_period * 10;
di = dictGetIterator(master->slaves);
// 遍历主节点的每一个从节点
while((de = dictNext(di)) != NULL) {
// 得到主节点的一个从结点实例
sentinelRedisInstance *slave = dictGetVal(de);
mstime_t info_validity_time;
// 如果一个从节点有主观下线标记,那么这个从节点就会被直接跳过,不会被选为新主节点。
if (slave->flags & (SRI_S_DOWN|SRI_O_DOWN)) continue;
// 断开链接
if (slave->link->disconnected) continue;
// 超时
if (mstime() - slave->link->last_avail_time > SENTINEL_PING_PERIOD*5) continue;
if (slave->slave_priority == 0) continue;
/* If the master is in SDOWN state we get INFO for slaves every second.
* Otherwise we get it with the usual period so we need to account for
* a larger delay. */
if (master->flags & SRI_S_DOWN)
info_validity_time = SENTINEL_PING_PERIOD*5;
else
info_validity_time = SENTINEL_INFO_PERIOD*3;
if (mstime() - slave->info_refresh > info_validity_time) continue;
if (slave->master_link_down_time > max_master_down_time) continue;
instance[instances++] = slave;
}
dictReleaseIterator(di);
if (instances) {
qsort(instance,instances,sizeof(sentinelRedisInstance*),
compareSlavesForPromotion);
selected = instance[0];
}
zfree(instance);
return selected;
}