聊聊 Redis 哨兵选举与故障转移的实现

数据库 Redis
这一篇我们将接着之前的思路,将哨兵获取客观下线结果并结合raft协议完成哨兵leader选举完成故障转移的流程分析完成,希望对你有帮助。

上一篇文章我们将哨兵主观下线的核心流程都分析完成,这一篇我们将接着之前的思路,将哨兵获取客观下线结果并结合raft协议完成哨兵leader选举完成故障转移的流程分析完成,希望对你有帮助。

详解哨兵选举与故障转移流程

1. 获取客观下线结果判断

当前哨兵主观认定master下线之后,为了明确知晓master节点是否真的下线,哨兵节点还会通过cc即异步命令指针所维护的socket连接发起is-master-down-by-addr的sentinel指令进行询问,其他哨兵所回复的结果都会通过回调函数sentinelReceiveIsMasterDownReply函数处理。

这段请求最终会被其他哨兵sentinel命令所对应的函数sentinelCommand执行,他们各自会在内部查看自己对于master判断是否是主观下线,如果是则返回1。

最后我们的哨兵收到这个结果1,则通过位运算加master节点状态flags类加上客观下线的判断标识64,这里redis为了提升运算效率,采用的二进制|=运算,这一点我们在阅读大量的redis中源码都会看到二进制运算这一点优化:

对此我们也给出哨兵处理每一个master实例的函数入口,可以看到在调用sentinelCheckSubjectivelyDown完成主观下线的检查之后,又会调用sentinelAskMasterStateToOtherSentinels并传入SENTINEL_NO_FLAGS即仅仅检查其他哨兵对于当前master的主观判断结果:

//这个入参包含恰哨兵实例和当前主节点的从节点信息
void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {
   //......
    //3. 主观判断是否下线
    sentinelCheckSubjectivelyDown(ri);

   //......

    /* Only masters */
    if (ri->flags & SRI_MASTER) {
      
       //......
        //传入master信息ri以及标识SENTINEL_NO_FLAGS意味仅了解其他哨兵对于master节点状态的判断
        sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
    }
}

步入sentinelAskMasterStateToOtherSentinels即可看到哨兵询问其他哨兵对于master判断的逻辑,可以看到它遍历出每一个哨兵实例,通过异步连接cc指针所指向的连接发起SENTINEL is-master-down-by-addr指令获取其他哨兵节点对于master下线的看法,并注册sentinelReceiveIsMasterDownReply函数处理返回结果:

#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);
        //......

        /* Ask */
        ll2string(port,sizeof(port),master->addr->port);
        //发送is-master-down-by-addr命令获取其他哨兵客观下线的结果,并通过sentinelReceiveIsMasterDownReply作为回调处理接收结果
        retval = redisAsyncCommand(ri->cc,
                    sentinelReceiveIsMasterDownReply, NULL,
                    "SENTINEL is-master-down-by-addr %s %s %llu %s",
                    master->addr->ip, port,
                    sentinel.current_epoch,
                    //若大于SENTINEL_FAILOVER_STATE_NONE则说明执行故障切换,传入server.runid  
                    (master->failover_state > SENTINEL_FAILOVER_STATE_NONE) ?
                    server.runid : "*");
        if (retval == REDIS_OK) ri->pending_commands++;
    }
    dictReleaseIterator(di);
}

其他哨兵收到sentinel指令后就会调用sentinelCommand处理这条指令,其内部会判断自己所维护的master的flags二进制位是否包含SRI_S_DOWN,如果是则说明被请求的哨兵节点同样认为master已下线,则直接回复master的leaderid以及shared.cone即1(代表确认当前master确实下线):

void sentinelCommand(redisClient *c) {
    //......
    else if (!strcasecmp(c->argv[1]->ptr,"is-master-down-by-addr")) {//处理客观下线请求
        //......
       

       
        //如果master主观判定下线即flags包含SRI_S_DOWN这个主观下线标识,则isdown设置为1
        if (!sentinel.tilt && ri && (ri->flags & SRI_S_DOWN) &&
                                    (ri->flags & SRI_MASTER))
            isdown = 1;

        //上文isdown 设置为1,返回 shared.cone告知对应leaderid的master被我方认定为下线
        //响应3部分内容,下线状态、leader id以及当前leader的纪元
        addReplyMultiBulkLen(c,3);
        addReply(c, isdown ? shared.cone : shared.czero);
        addReplyBulkCString(c, leader ? leader : "*");
        addReplyLongLong(c, (long long)leader_epoch);
        if (leader) sdsfree(leader);
    } //......
    return;
//......
}

最终我们的sentinel的回调函数sentinelReceiveIsMasterDownReply处理对端的结果,发现返回值为1,说明该节点对于我们的来说客观认为master下线了。

所以我们的哨兵就需要记录这个消息,因为我们维护master->sentinels的字典记录其他哨兵信息,所以定位到其他哨兵客观下线的回复后,我们就会从这个字典中找到这个哨兵的结构体将其flags累加一个SRI_MASTER_DOWN的常数值64,意味这个哨兵客观认定这个master下线了:

void sentinelReceiveIsMasterDownReply(redisAsyncContext *c, void *reply, void *privdata) {
  //......


    if ( //......)
    {
    //更新上次响应时间
        ri->last_master_down_reply_time = mstime();
        if (r->element[0]->integer == 1) {//如果返回(cone默认设置为1)1则说明其他哨兵认为master下线,累加将当前维护的哨兵字段的flags累加SRI_MASTER_DOWN
            ri->flags |= SRI_MASTER_DOWN;
        } else {
           //......
        }
        //......
    }
}

2. 启动故障转移

上一步收集其他哨兵的判断并更新到各自的flags位后,当前哨兵的定时任务再次遍历master调用sentinelHandleRedisInstance处理当前master,其内部会遍历当前哨兵维护的哨兵数组获取这些哨兵对于master下线的看法,如果累加到的哨兵对于下线的看法大于或者等于我们配置quorum之后,则会判定会客观下线:

我们还是从sentinelHandleRedisInstance方法查看方法入口,可以看到哨兵定时执行该方法时会调用sentinelCheckObjectivelyDown检查客观下线状态:

void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {
   //......
    if (ri->flags & SRI_MASTER) {
       //......
       //检查其当前是否客观下线
        sentinelCheckObjectivelyDown(ri);
        //......
    }
}

步入其内部即可看到笔者所说的,遍历哨兵查看下线结果并更新master下线状态的逻辑:

void sentinelCheckObjectivelyDown(sentinelRedisInstance *master) {
   //......
    //如果是主观下线,步入该逻辑
    if (master->flags & SRI_S_DOWN) {
        //自己的票数设置进去,quorum为1
        quorum = 1; /* the current sentinel. */
      
      
        //遍历其他哨兵,如果为客观下线则累加quorum
        di = dictGetIterator(master->sentinels);
        while((de = dictNext(di)) != NULL) {
            sentinelRedisInstance *ri = dictGetVal(de);

            if (ri->flags & SRI_MASTER_DOWN) quorum++;
        }
       //如果投票数大于配置的quorum,则odown 为1,即说明客观认定下线了
       
        if (quorum >= master->quorum) odown = 1;
    }

    //如果明确客观下线,则广播+odown事件
    if (odown) {
        if ((master->flags & SRI_O_DOWN) == 0) {
            sentinelEvent(REDIS_WARNING,"+odown",master,"%@ #quorum %d/%d",
                quorum, master->quorum);
         //累加标识,并更新master下线时间
            master->flags |= SRI_O_DOWN;
            master->o_down_since_time = mstime();
        }
    } else {
       //......
    }
}

3. 发起新纪元leader选举

基于上述结果redis会判断是否发起故障转移,若需要则通知其他哨兵进行leader选举,收到通知的哨兵会检查当前纪元是否小于发起选举的哨兵纪元,若符合要求且在此期间没有别的哨兵发起选举,则向其投票。

后续我们的哨兵收到并收集这些响应之后,更新自己所维护的哨兵数组中的leader_epoch,通过遍历这个哨兵数组中的leader_epoch是否和自己所生成的leader_epoch一致,如果统计结果超过半数,则说明自己当选leader,由此开始进行故障转移:

(1) 选举源码入口

我们还是以sentinelHandleRedisInstance作为程序入口,可以看到其内部调用sentinelStartFailoverIfNeeded判断是否需要进行故障转移,然后调用sentinelAskMasterStateToOtherSentinels并传入SENTINEL_ASK_FORCED发起leader选举请求:

//这个入参包含恰哨兵实例和当前主节点的从节点信息
void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {
    //......
    if (ri->flags & SRI_MASTER) {
          //......
        //  判断是否要进行故障切换,若需要则调用sentinelAskMasterStateToOtherSentinels传入SENTINEL_ASK_FORCED进行leader选举
        if (sentinelStartFailoverIfNeeded(ri))
            sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);
        // 执行故障切换
        sentinelFailoverStateMachine(ri);
       //......
    }
}

(2) 确认故障转移

我们步入sentinelStartFailoverIfNeeded即可看到其对于是否进行故障转移的判断,逻辑比较简单:

  • 明确是否客观认定下线。
  • 明确是否处于故障转移。
  • 近期是否有进行故障转移。

如果伤处条件都排除则:

  • failover_state 即故障转移状态设置为等待故障转移,后续的函数状态机会根据这个标识进行故障转移处理。
  • flags标识累加处于故障转移中。
  • 更新master纪元为哨兵纪元+1,用于后续哨兵leader选举后更新纪元使用。

对此我们给出sentinelStartFailoverIfNeeded的判断,可以看到它会按照上文所说的流程进行判断,明确排除三种情况后调用sentinelStartFailover设置故障转移状态:

int sentinelStartFailoverIfNeeded(sentinelRedisInstance *master) {
   //是否客观下线,若不是则返回0
    if (!(master->flags & SRI_O_DOWN)) return 0;

   //是否处于故障转移中,如果是则直接返回0
    if (master->flags & SRI_FAILOVER_IN_PROGRESS) return 0;

   
    //距离上次故障转移时间是否小于2倍的超时时间,如果是则返回0
    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;
    }
    //启动故障转移 并返回1
    sentinelStartFailover(master);
    return 1;
}

步入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;
   //......
}
(3) 发起投票

结果上述步骤明确知晓redis需要进行故障转移之后,哨兵会再次调用sentinelAskMasterStateToOtherSentinels方法传入当前哨兵的server.runid向其他哨兵发起投票请求,并通过sentinelReceiveIsMasterDownReply处理响应结果:

void sentinelAskMasterStateToOtherSentinels(sentinelRedisInstance *master, int flags) {
    //遍历其他哨兵
    di = dictGetIterator(master->sentinels);
    while((de = dictNext(di)) != NULL) {
     

       //......
       //因为failover_state 在上一步已经改为传入自己的SENTINEL_FAILOVER_STATE_WAIT_START即等待故障转移,故大于SENTINEL_FAILOVER_STATE_NONE,于是传入哨兵的server.runid发起投票选举
        retval = redisAsyncCommand(ri->cc,
                    sentinelReceiveIsMasterDownReply, NULL,
                    "SENTINEL is-master-down-by-addr %s %s %llu %s",
                    master->addr->ip, port,
                    sentinel.current_epoch,
                    //若大于SENTINEL_FAILOVER_STATE_NONE则说明执行故障切换,传入server.runid  
                    (master->failover_state > SENTINEL_FAILOVER_STATE_NONE) ?
                    server.runid : "*");
        if (retval == REDIS_OK) ri->pending_commands++;
    }
    dictReleaseIterator(di);
}

(4) 对端哨兵处理发起选举的投票结果

上述步骤发起投票的哨兵节点发起投票后,收到投票请求的哨兵实例就会进行如下检查:

  • master纪元小于发起投票请求的哨兵纪元req_epoch。
  • 当前哨兵纪元小于req_epoch。

如果符合要求则说明发起投票请求的哨兵可以作为leader,当前实例将leader 设置为该节点,然后回复结果给发送结果的实例:

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);
    }
    //如果master纪元小于发起选举的纪元且当前哨兵纪元小于等于发起选举的纪元
    if (master->leader_epoch < req_epoch && sentinel.current_epoch <= req_epoch)
    {
        sdsfree(master->leader);
        //设置当前的master为candidate的runid
        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;
}

(5) 处理投票结果

收到响应后sentinelReceiveIsMasterDownReply回调函数就会解析出其他哨兵的leader_epoch 信息,作为后续选举leader的依据,如果半数以上的leader_epoch 为当前哨兵所设置的run_id,则说明当前哨兵作为leader进行故障转移:

void sentinelReceiveIsMasterDownReply(redisAsyncContext *c, void *reply, void *privdata) {
    sentinelRedisInstance *ri = c->data;
    redisReply *r;
    REDIS_NOTUSED(privdata);

    //......
        if (strcmp(r->element[1]->str,"*")) {//不为*则采集投票结果
           //......
            //基于返回结果更新当前哨兵维护的哨兵数组中leader的leader_epoch 信息(记录的是作为leader的哨兵的run_id),作为后续选举leader使用
            ri->leader = sdsnew(r->element[1]->str);
            ri->leader_epoch = r->element[2]->integer;
        }
    }
}
(6) 选举出leader并广播通告

最后基于状态机模式,根据当前master状态为SENTINEL_FAILOVER_STATE_WAIT_START于是调用sentinelFailoverWaitStart选举leader

void sentinelFailoverStateMachine(sentinelRedisInstance *ri) {
    redisAssert(ri->flags & SRI_MASTER);

    if (!(ri->flags & SRI_FAILOVER_IN_PROGRESS)) return;

    switch(ri->failover_state) {
        //如果状态为SENTINEL_FAILOVER_STATE_WAIT_START,则调用sentinelFailoverWaitStart选举出leader
        case SENTINEL_FAILOVER_STATE_WAIT_START:
            sentinelFailoverWaitStart(ri);
            break;
       //......
    }
}

步入sentinelFailoverWaitStart即可看到该方法调用sentinelGetLeader,如果发现是自己则发送广播告知自己为leader进行故障转移:

void sentinelFailoverWaitStart(sentinelRedisInstance *ri) {
   //......
    //获取leader
    leader = sentinelGetLeader(ri, ri->failover_epoch);
    isleader = leader && strcasecmp(leader,server.runid) == 0;
    sdsfree(leader);

    //......
    //告知当选的leader是自己
    sentinelEvent(REDIS_WARNING,"+elected-leader",ri,"%@");
    ri->failover_state = SENTINEL_FAILOVER_STATE_SELECT_SLAVE;
    ri->failover_state_change_time = mstime();
    sentinelEvent(REDIS_WARNING,"+failover-state-select-slave",ri,"%@");
}

对此我们也给出选举哨兵leader的核心方法sentinelGetLeader,核心步骤为:

  • 如果投票结果给出的leader值不为空(这个leader记录的是其他哨兵投票的实例的run_id)且纪元和当前选举纪元一致,则给对应的leader票数+1。
  • 将这个投票结果存入counter这个字典中。
  • 遍历counter如果这个值大于配置的quorum或哨兵的半数以上,则将其设置为winner,即最后的leader,由此让这个leader哨兵进行故障转移:

对应的我们也给出这段代码的实现:

char *sentinelGetLeader(sentinelRedisInstance *master, uint64_t epoch) {
  //......
   //设置voters 为哨兵数+1
    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);
        //如果其他哨兵投票的leader值不为空,且纪元和当前投票纪元一致,则给这个leader的对应的run_id对应的投票数做个自增
        if (ri->leader != NULL && ri->leader_epoch == sentinel.current_epoch)
            sentinelLeaderIncr(counters,ri->leader);
    }

//......
//找到得票最多的
    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);

   //......
   
   //如果票数大于一半+1或大于配置的quorum则设置为leader
    voters_quorum = voters/2+1;
    if (winner && (max_votes < voters_quorum || max_votes < master->quorum))
        winner = NULL;

    winner = winner ? sdsnew(winner) : NULL;
    //......
    return winner;
}

小结

自此我们来小结一下哨兵选举与故障转移的大体过程:

  • 当前哨兵主观认定下线之后,通过异步连接询问其它哨兵是否客观认定master下线。
  • 超过半数的哨兵认为下线则当前哨兵就认为master下线于是开启发起投票选举。
  • 更新自己的纪元并携带runid到其它哨兵节点上拉票。
  • 基于回调函数获取其它哨兵选票结果进行遍历汇总,用以一个字典以哨兵runid为key,投票值为value进行维护。
  • 汇总后通知全局哨兵leader。
  • leader进行故障转移。
责任编辑:赵宁宁 来源: 写代码的SharkChili
相关推荐

2025-02-17 11:07:10

2020-04-21 22:59:50

Redis搭建选举

2022-05-17 22:20:41

哨兵Redis机制

2018-11-01 14:50:01

RedisNoSQL数据库

2009-02-03 17:50:03

服务器虚拟化VMware

2012-07-03 11:38:32

FacebookHadoop

2019-12-05 10:00:03

架构Redis服务器

2015-07-23 13:43:43

vSphereHA虚拟化

2011-05-26 13:07:29

数据库切换故障转移

2022-07-06 13:48:24

RedisSentinel机制

2024-07-16 08:38:06

2023-12-01 08:49:29

哨兵模式自动恢复

2023-04-09 19:54:52

2024-12-09 00:00:09

2024-04-29 08:06:19

Redis分布式系统

2023-03-15 08:30:37

2021-04-01 08:50:54

SentinelRedis 集群原理

2023-05-26 08:24:17

短信渠道模型

2010-07-08 10:53:09

Windows Ser故障转移群集

2022-12-21 09:50:41

点赞
收藏

51CTO技术栈公众号