从哨兵Leader选举学习Raft协议实现(下)(二)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 从哨兵Leader选举学习Raft协议实现(下)

哨兵选举

这里,为了了解哨兵选举的触发,我们先来复习下在上节课,我讲过的 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;
}
相关文章
|
Arthas 测试技术 网络安全
The telnet port 3658 is used by process
是否在本地使用Arthas的时候,遇到The telnet port 3658 is used by process 34725 instead of target process 44848, you will connect to an unexpected process的异常,其实解决方法很简单。
2471 0
The telnet port 3658 is used by process
|
6月前
|
数据采集 搜索推荐 项目管理
通用型埋点系统完整开源方案-ClkLog新升级更强大、更易用
我们希望ClkLog开源社区版,不是“精简试用版”,而是一个真正能被部署和使用的完整方案。 过去这一年,我们一直在倾听大家的反馈,并不断思考:一款开源行为分析系统,真正顺利地被用起来,需要具备哪些要素和功能? 为了让大家在使用过程中更流畅更便捷,ClkLog开源社区版迎来了一次新升级! 现在上Gitee、Github、GitCode 即可获取最新的更新代码
|
人工智能 安全 IDE
【AI帮我写代码,上班摸鱼不是梦】手摸手图解CodeWhisperer的安装使用
除了借助ChatGPT通过问答的方式生成代码,也可以通过IDEA插件在写代码是直接帮助我们生成代码。 目前,IDEA插件有CodeGeeX、CodeWhisperer、Copilot。其中,CodeGeeX和CodeWhisperer是完全免费的,Copilot是收费的,每月10美元。 下面我们来了解CodeWhisperer的安装和使用,如果你还想了解其他的可以在评论告诉我。
535 4
|
Kubernetes 安全 Linux
使用kubeadm快速部署一个k8s集群
使用kubeadm快速部署一个k8s集群
|
算法 安全 搜索推荐
TLS 协议-对称加密原理
TLS 协议-对称加密原理
673 0
|
6月前
|
搜索推荐 小程序 开发工具
Gitee推荐项目!埋点+用户分析系统,适合中小团队的开源方案
一款好用的用户行为分析工具,对产品经理、运营人员和开发者来说,都越来越重要。 目前市面上主流的工具,不是价格高昂、数据不透明,就是部署复杂,很难维护。 ClkLog,适合中小团队的开源方案,已经在Gitee上开源,社区也在持续更新中。
|
IDE 编译器 开发工具
C/C++开发环境
C/C++开发环境
306 4
|
机器学习/深度学习 传感器 边缘计算
深度学习之边缘计算与云计算结合
边缘计算与云计算结合是现代人工智能和物联网领域的重要技术方向。通过将边缘计算的实时处理能力和云计算的强大计算资源结合起来,可以实现高效、低延迟的智能应用。
321 1
|
存储 人工智能 自然语言处理
论文介绍:Mamba:线性时间序列建模与选择性状态空间
【5月更文挑战第11天】Mamba是新提出的线性时间序列建模方法,针对长序列处理的效率和内存问题,采用选择性状态空间模型,只保留重要信息,减少计算负担。结合硬件感知的并行算法,优化GPU内存使用,提高计算效率。Mamba在多种任务中展现出与Transformer相当甚至超越的性能,但可能不适用于所有类型数据,且硬件适应性需进一步优化。该模型为长序列处理提供新思路,具有广阔应用前景。[论文链接](https://arxiv.org/abs/2312.00752)
508 3
|
存储 固态存储 测试技术
LabVIEW RT在非NI硬件上的应用与分析
LabVIEW RT在非NI硬件上的应用与分析
170 0