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

41 阅读7分钟

主节点客观下线判断

sentinelCheckObjectivelyDown 函数通过遍历主节点记录的 sentinels 哈希表,就可以获取其他哨兵实例对同一主节点主观下线的判断结果。

sentinelCheckObjectivelyDown 函数会使用 quorum 变量,来记录判断主节点为主观下线的哨兵数量。如果当前哨兵已经判断主节点为主观下线,那么它会先把 quorum 值置为 1。然后,它会依次判断其他哨兵的 flags 变量,检查是否设置了 SRI_MASTER_DOWN 的标记。如果设置了,它就会把 quorum 值加 1。

当遍历完 sentinels 哈希表后,sentinelCheckObjectivelyDown 函数会判断 quorum 值是否大于等于预设定的 quorum 阈值,这个阈值保存在了主节点的数据结构中,也就是 master->quorum,而这个阈值是在 sentinel.conf 配置文件中设置的。

如果实际的 quorum 值大于等于预设的 quorum 阈值,sentinelCheckObjectivelyDown 函数就判断主节点为客观下线,并设置变量 odown 为 1,而这个变量就是用来表示当前哨兵对主节点客观下线的判断结果的。

void sentinelCheckObjectivelyDown(sentinelRedisInstance *master) {
…
//当前主节点已经被当前哨兵判断为主观下线
if (master->flags & SRI_S_DOWN) {
   quorum = 1; //当前哨兵将quorum值置为1
   
   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;
}

一旦 sentinelCheckObjectivelyDown 函数判断主节点客观下线了,它就会调用 sentinelEvent 函数发送 +odown 事件消息,然后在主节点的 flags 变量中设置 SRI_O_DOWN 标记,如下所示:

//判断主节点为客观下线
if (odown) {
   //如果没有设置SRI_O_DOWN标记
   if ((master->flags & SRI_O_DOWN) == 0) {
    sentinelEvent(LL_WARNING,"+odown",master,"%@ #quorum %d/%d",
                quorum, master->quorum); //发送+odown事件消息
    master->flags |= SRI_O_DOWN;  //在主节点的flags中记录SRI_O_DOWN标记
    master->o_down_since_time = mstime(); //记录判断客观下线的时间
   }
}

其他哨兵的 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 文件中),如下所示:

void sentinelAskMasterStateToOtherSentinels(sentinelRedisInstance *master, int flags) {
…
di = dictGetIterator(master->sentinels);
//遍历监听同一主节点的其他哨兵
while((de = dictNext(di)) != NULL) {
   sentinelRedisInstance *ri = dictGetVal(de);
   …
   //发送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"),
             master->addr->ip, port,
             sentinel.current_epoch,
             (master->failover_state > SENTINEL_FAILOVER_STATE_NONE) ?
                 sentinel.myid : "*");
}
…
}

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 所属的纪元。

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的纪元
…}
…}

其他哨兵的 SRI_MASTER_DOWN 标记是如何设置的呢?这实际上是和哨兵在 sentinelAskMasterStateToOtherSentinels 函数中,向其他哨兵发送 sentinel is-master-down-by-addr 命令时,设置的命令结果处理函数 sentinelReceiveIsMasterDownReply 有关。

sentinelReceiveIsMasterDownReply 函数

这个函数会进一步检查,“当前哨兵对主节点主观下线的判断结果”是否为 1。如果是的话,这就表明对应的哨兵已经判断主节点为主观下线了,那么当前哨兵就会把自己记录的对应哨兵的 flags,设置为 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;
        }

一个哨兵调用 sentinelCheckObjectivelyDown 函数,是直接检查其他哨兵的 flags 是否有 SRI_MASTER_DOWN 标记,而哨兵又是通过 sentinelAskMasterStateToOtherSentinels 函数,向其他哨兵发送 sentinel is-master-down-by-addr 命令,从而询问其他哨兵对主节点主观下线的判断结果的,并且会根据命令回复结果,在结果处理函数 sentinelReceiveIsMasterDownReply 中,设置其他哨兵的 flags 为 SRI_MASTER_DOWN。

哨兵选举

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 标记,表示已经开始故障切换,如下所示:

void sentinelStartFailover(sentinelRedisInstance *master) {
…
master->failover_state = SENTINEL_FAILOVER_STATE_WAIT_START;
master->flags |= SRI_FAILOVER_IN_PROGRESS;
…
}

在实际切换前,sentinelAskMasterStateToOtherSentinels 函数会被调用。这个函数除了会用来向其他哨兵询问对主节点状态的判断,它还可以用来向其他哨兵发起 Leader 选举。

在 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 函数让哨兵 B 投票的条件是:master 记录的 Leader 的纪元小于哨兵 A 的纪元,同时,哨兵 A 的纪元要大于或等于哨兵 B 的纪元。这两个条件保证了哨兵 B 还没有投过票,否则的话,sentinelVoteLeader 函数就直接返回当前 master 中记录的 Leader ID 了,这也是哨兵 B 之前投过票后记录下来的。

if (req_epoch > sentinel.current_epoch) {
   sentinel.current_epoch = req_epoch;
   …
   sentinelEvent(LL_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;
        …
}
return master->leader ? sdsnew(master->leader) : NULL;

此文章为10月Day24学习笔记,内容来源于极客时间《Redis 源码剖析与实战》