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

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
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;
}
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
1月前
|
消息中间件 算法 网络协议
选举机制理解描述
选举机制理解描述
29 1
选举机制理解描述
|
5月前
|
索引
Etcd/Raft 原理问题之follower会进入StateReplicate状态时的问题如何解决
Etcd/Raft 原理问题之follower会进入StateReplicate状态时的问题如何解决
Etcd/Raft 原理问题之follower会进入StateReplicate状态时的问题如何解决
|
7月前
Zookeeper的选举机制原理(图文深度讲解)——过半选举
Zookeeper的选举机制原理(图文深度讲解)——过半选举
530 0
|
NoSQL Redis
Redis集群选举原理分析
Redis集群选举原理分析
131 0
|
Sentinel
从哨兵Leader选举学习Raft协议实现(下)(一)
从哨兵Leader选举学习Raft协议实现(下)
59 0
|
监控 NoSQL 算法
从哨兵Leader选举学习Raft协议实现(上)
从哨兵Leader选举学习Raft协议实现(上)
102 0
Zookeeper Leader选举机制
Zookeeper Leader选举机制
86 0
|
存储 算法 安全
Raft 共识算法4-选举限制
本节通过添加对哪些服务器可以被选为领导者的限制来完成 Raft 算法。 该限制可确保任何给定任期的领导者都包含之前任期已提交的所有条目(@fig3 中的领导者完整性(Leader Completeness)属性)。 考虑到选举限制,然后我们使提交规则更加精确。 最后,我们展示了领导者完整性的证明草图,并展示了它如何保证复制状态机的正确行为。
141 0
|
算法
实现分布式 kv—2 raft leader 选举
raft 是一个分布式一致性算法,主要保证的是在分布式系统中,各个节点的数据一致性。raft 算法比较复杂,因为它所解决的分布式一致性问题本来就是一个比较棘手的问题,raft 算法的实现主要可以拆解为三个部分: • 领导选举 • 日志复制 • 安全性
135 2
|
Java 开发者 Spring
Raft 协议故障重新选举 | 学习笔记
快速学习 Raft 协议故障重新选举
139 0