【Redis】哨兵集群-客观下线

在sentinelHandleRedisInstance函数中,如果是主节点,需要做如下处理:

void sentinelHandleRedisInstance(sentinelRedisInstance *ri) { 
    // 省略...
    
    // 如果是主节点
    if (ri->flags & SRI_MASTER) {
        // 检查是否主观下线
        sentinelCheckObjectivelyDown(ri);
        // 是否需要开始故障切换
        if (sentinelStartFailoverIfNeeded(ri))
            // 获取其他哨兵实例对节点状态的判断
            sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);
        // 故障切换状态机
        sentinelFailoverStateMachine(ri);
        // 获取其他哨兵实例对节点状态的判断
        sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
    }
}

节点的状态定义

#define SRI_MASTER  (1<<0) /* master主节点 */
#define SRI_SLAVE   (1<<1) /* slave从节点 */
#define SRI_SENTINEL (1<<2) /* 哨兵节点 */
#define SRI_S_DOWN (1<<3)   /* 主观下线 */
#define SRI_O_DOWN (1<<4)   /* 客观下线 */
#define SRI_MASTER_DOWN (1<<5) /* 节点下线 */
#define SRI_FAILOVER_IN_PROGRESS (1<<6) /* 正在执行master节点的故障切换
换 */

客观下线

sentinelCheckObjectivelyDown

sentinelCheckObjectivelyDown函数用于判断master节点是否客观下线:

  1. 首先确认主节点是否已经被哨兵标记为主观下线,如果已经主观下线,quorum的值置为1,表示当前已经有1个哨兵认为master主观下线,进行第2步
  2. master->sentinels存储了监控当前master的其他哨兵节点,遍历其他哨兵节点,通过flag标识中是否有SRI_MASTER_DOWN状态判断其他哨兵对MASTER节点下线的判断,如果有则认为MASTER下线,对quorum数量加1
  3. 遍历结束后判断quorum的数量是否大于master->quorum设置的数量,也就是是否有过半的哨兵认为主节点下线,如果是将odown置为1,认为主节点客观下线
  4. 根据odown的值判断主节点是否客观下线
    • 如果客观下线,确认master->flags是否有SRI_O_DOWN状态,如果没有,发布+odown客观下线事件并将master->flags置为SRI_O_DOWN状态
    • 如果没有客观下线,校验master->flags是否有SRI_O_DOWN状态,如果有,需要发布-odown事件,取消master的客观下线标记(master->flags的SRI_O_DOWN状态取消)

可以看到,master节点客观下线需要根据其他哨兵实例对主节点的判断来共同决定,具体是通过其他哨兵实例的flag中是否有SRI_MASTER_DOWN状态来判断的,如果认为master下线的哨兵个数超过了master节点中的quorum设置,master节点将被认定为客观下线,发布+odown客观下线事件,关于SRI_MASTER_DOWN状态是在哪里设置的在后面会讲到。

void sentinelCheckObjectivelyDown(sentinelRedisInstance *master) {
    dictIterator *di;
    dictEntry *de;
    unsigned int quorum = 0, odown = 0;
    // 判断主节点是否被标记为主观下线SRI_S_DOWN
    if (master->flags & SRI_S_DOWN) {
        /* quorum初始化为1 */
        quorum = 1; 
        /* 遍历监听主节点的其他哨兵实例. */
        di = dictGetIterator(master->sentinels);
        while((de = dictNext(di)) != NULL) {
            // 获取哨兵实例
            sentinelRedisInstance *ri = dictGetVal(de);
            // 判断其他哨兵实例是否把master节点标记为下线
            if (ri->flags & SRI_MASTER_DOWN) quorum++;
        }
        dictReleaseIterator(di);
        // 如果大于master->quorum,也就是过半的哨兵认为主节点已经下线,标记客观下线
        if (quorum >= master->quorum) odown = 1;
    }

    // 如果客观下线
    if (odown) {
        if ((master->flags & SRI_O_DOWN) == 0) {
            // 发布+odown客观下线事件
            sentinelEvent(LL_WARNING,"+odown",master,"%@ #quorum %d/%d",
                quorum, master->quorum);
            // 将MASTER节点标记为SRI_O_DOWN
            master->flags |= SRI_O_DOWN;
            // 标记客观下线的时间
            master->o_down_since_time = mstime();
        }
    } else {
        // 非客观下线,判断master是否有客观下线标识
        if (master->flags & SRI_O_DOWN) {
            // 发布-odown事件
            sentinelEvent(LL_WARNING,"-odown",master,"%@");
            // 取消master的客观下线标识
            master->flags &= ~SRI_O_DOWN;
        }
    }
}

是否需要执行故障切换

sentinelStartFailoverIfNeeded

sentinelStartFailoverIfNeeded用于判断是否需要执行故障切换,可以开始故障切换的条件有三个:

  1. master节点被认为客观下线(SRI_O_DOWN)
  2. 当前没有在进行故障切换(状态不是SRI_FAILOVER_IN_PROGRESS)
  3. 距离上次执行故障切换的时间,超过了故障切换超时时间设置的2倍,意味着上一次执行故障切换的时间已超时,可以重新进行故障切换

同时满足以上三个条件,达到执行故障切换的标准,调用sentinelStartFailover函数,将故障切换的状态改为待执行。

int sentinelStartFailoverIfNeeded(sentinelRedisInstance *master) {
    /* 如果MASTER不是客观下线,直接返回 */
    if (!(master->flags & SRI_O_DOWN)) return 0;

    /* 如果当前已经在执行故障切换直接返回 */
    if (master->flags & SRI_FAILOVER_IN_PROGRESS) return 0;

    /* 判断距离上次执行故障切换的时间,如果小于failover_timeout配置项的2倍,表示上次故障切换还未达到超时设置,所以本次不能执行*/
    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;
            serverLog(LL_WARNING,
                "Next failover delay: I will not start a failover before %s",
                ctimebuf);
        }
        return 0;
    }
    // 开始故障切换(只更改了故障切换状态)
    sentinelStartFailover(master);
    return 1;
}

sentinelStartFailover

可以看到sentinelStartFailover函数并没有直接进行故障切换,而是更改了一些状态

  1. 将failover_state置为了SENTINEL_FAILOVER_STATE_WAIT_START等待开始执行状态
  2. 将master节点的flags设置为SRI_FAILOVER_IN_PROGRESS故障切换执行中状态
  3. **将master的failover_epoch设置为当前哨兵的投票轮次current_epoch + 1 **,在选举leader时会用到
void sentinelStartFailover(sentinelRedisInstance *master) {
    serverAssert(master->flags & SRI_MASTER);
    // 将状态更改为等待开始执行故障切换
    master->failover_state = SENTINEL_FAILOVER_STATE_WAIT_START;
    // 设置为故障切换执行中状态
    master->flags |= SRI_FAILOVER_IN_PROGRESS;
    // 设置failover_epoch故障切换轮次
    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();
}

获取哨兵实例对主节点状态判断

在sentinelHandleRedisInstance函数中,可以看到sentinelStartFailoverIfNeeded条件成立时以及函数的最后都调用了sentinelAskMasterStateToOtherSentinels,接下来就去看看sentinelAskMasterStateToOtherSentinels里面都做了什么:

    if (ri->flags & SRI_MASTER) {
        sentinelCheckObjectivelyDown(ri);
        if (sentinelStartFailoverIfNeeded(ri))
            // 获取其他哨兵实例对主节点的状态判断,这里传入的参数是SENTINEL_ASK_FORCED
            sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);
        sentinelFailoverStateMachine(ri);
        // 获取其他哨兵实例对主节点的状态判断,这里传入的参数是SENTINEL_NO_FLAGS
        sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
    }

is-master-down-by-addr命令发送

sentinelAskMasterStateToOtherSentinels

sentinelAskMasterStateToOtherSentinels函数用于向其他哨兵实例发送is-master-down-by-addr命令获取其他哨兵实例对主节点状态的判断,它会遍历监听同一主节点的其他哨兵实例进行处理:

  1. 获取每一个哨兵实例
  2. 计算距离每个哨兵实例上一次收到IS-MASTER-DOWN-BY-ADDR命令回复时间的间隔
  3. 如果距离上次收到回复的时间已经超过了SENTINEL_ASK_PERIOD周期的5倍,清空哨兵节点flag中的SRI_MASTER_DOWN状态和leader
  4. 如果master节点已经是下线状态SRI_S_DOWN,不需要进行处理,回到第一步处理下一个哨兵
  5. 如果哨兵节点用于发送命令的link连接处于未连接状态,不处理,回到第一步处理下一个哨兵
  6. 如果不是强制发送命令(入参的flag是SENTINEL_ASK_FORCED时),并且距离上次收到回复命令的时间还在SENTINEL_ASK_PERIOD周期内,不处理,回到第一步处理下一个哨兵
  7. 通过redisAsyncCommand函数发送发送is-master-down-by-addr命令,sentinelReceiveIsMasterDownReply为处理函数,redisAsyncCommand函数有如下参数:
    • 用于发送请求的连接:ri->link->cc
    • 收到命令返回结果时对应的处理函数:sentinelReceiveIsMasterDownReply
    • master节点的ip:announceSentinelAddr(master->addr)
    • master节点端口:port
    • 当前哨兵的投票轮次:sentinel.current_epoch
    • 实例ID:master->failover_state > SENTINEL_FAILOVER_STATE_NONE时表示要执行故障切换,此时传入当前哨兵的myid,否则传入*
void sentinelAskMasterStateToOtherSentinels(sentinelRedisInstance *master, int flags) {
    dictIterator *di;
    dictEntry *de;
    // 遍历监听主节点的其他哨兵实例
    di = dictGetIterator(master->sentinels);
    while((de = dictNext(di)) != NULL) {
        // 获取每一个哨兵实例
        sentinelRedisInstance *ri = dictGetVal(de);
        // 计算距离上一次收到IS-MASTER-DOWN-BY-ADDR命令回复时间的间隔
        mstime_t elapsed = mstime() - ri->last_master_down_reply_time;
        char port[32];
        int retval;

        /* 如果距离上次收到回复的时间已经超过了SENTINEL_ASK_PERIOD周期的5倍,清空相关设置 */
        if (elapsed > SENTINEL_ASK_PERIOD*5) {
            // 取消SRI_MASTER_DOWN状态
            ri->flags &= ~SRI_MASTER_DOWN;
            sdsfree(ri->leader);
            // leader置为null
            ri->leader = NULL;
        }

        // 如果master已经是下线状态
        if ((master->flags & SRI_S_DOWN) == 0) continue;
        // 如果已经连接中断
        if (ri->link->disconnected) continue;
        // 如果不是强制发送命令状态SENTINEL_ASK_FORCED,并且距离上次收到回复命令的时间还在SENTINEL_ASK_PERIOD周期内,不处理,回到第一步处理下一个哨兵
        if (!(flags & SENTINEL_ASK_FORCED) &&
            mstime() - ri->last_master_down_reply_time < SENTINEL_ASK_PERIOD)
            continue;

        ll2string(port,sizeof(port),master->addr->port);
        // 发送is-master-down-by-addr命令,sentinelReceiveIsMasterDownReply为处理函数
        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);
}

is-master-down-by-addr命令处理

sentinelCommand

其他哨兵实例收到is-master-down-by-addr命令之后的处理逻辑在sentinelCommand函数中可以找到:

  1. 根据请求传入的ip和端口信息获取主节点的sentinelRedisInstance实例对象(在发送is-master-down-by-addr命令的redisAsyncCommand函数中传入了主节点的ip和端口)
  2. 如果不是TILT模式,校验sentinelRedisInstance对象是否是主节点并且主节点被标记为主观下线,如果条件都成立表示主节点已经主观下线,将isdown置为1
  3. 判断请求参数中的runid是否不为*,如果不为*表示当前需要进行leader选举,调用sentinelVoteLeader选举哨兵Leader
  4. 发送is-master-down-by-addr命令的回复,将对主节点主观下线的判断、选出的leader节点的runid、投票轮次leader_epoch返回给发送命令哨兵
void sentinelCommand(client *c) {
 if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"help")) {
 
 }
 // 省略其他else if...
 // 如果是is-master-down-by-addr命令
 else if (!strcasecmp(c->argv[1]->ptr,"is-master-down-by-addr")) {
        /* SENTINEL IS-MASTER-DOWN-BY-ADDR <ip> <port> <current-epoch> <runid>
         *
         * 参数说明:
         * ip和端口:哨兵检测的主节点的ip和端口
         * current-epoch:是故障切换中当前投票的轮次,每一个哨兵在一轮投票中只能投一次
         * runid:如果需要执行故障切换,传入的是哨兵的myid,否则传入的是 *
         */
        sentinelRedisInstance *ri;
        long long req_epoch;
        uint64_t leader_epoch = 0; // 默认为0
        char *leader = NULL;
        long port;
        int isdown = 0;

        if (c->argc != 6) goto numargserr;
        if (getLongFromObjectOrReply(c,c->argv[3],&port,NULL) != C_OK ||
            getLongLongFromObjectOrReply(c,c->argv[4],&req_epoch,NULL)
                                                              != C_OK)
            return;
        // 根据请求传入的ip和端口信息获取对应的哨兵实例,也就是监控的master节点
        ri = getSentinelRedisInstanceByAddrAndRunID(sentinel.masters,
            c->argv[2]->ptr,port,NULL);

        /* 如果不是TILT模式,校验是否是主节点并且主节点被标记为主观下线 */
        if (!sentinel.tilt && ri && (ri->flags & SRI_S_DOWN) &&
                                    (ri->flags & SRI_MASTER))
            isdown = 1;// 确定主观下线

        /* 如果是主节点并且传入的runid不为*,调用sentinelVoteLeader选举Leader执行故障切换 */
        if (ri && ri->flags & SRI_MASTER && strcasecmp(c->argv[5]->ptr,"*")) {
            // 选举leader
            leader = sentinelVoteLeader(ri,(uint64_t)req_epoch,
                                            c->argv[5]->ptr,
                                            &leader_epoch);
        }

        /* 发送回复包含三部分:
         * 下线状态, 选出的leader, leader的投票轮次leader_epoch */
        addReplyArrayLen(c,3);
        // 下线状态
        addReply(c, isdown ? shared.cone : shared.czero);
        // leader不为空传入leader否则传入*
        addReplyBulkCString(c, leader ? leader : "*");
        // 投票轮次
        addReplyLongLong(c, (long long)leader_epoch);
        if (leader) sdsfree(leader);
    }
    // 省略其他else if...
    else {
        addReplySubcommandSyntaxError(c);
    }
    return;
}

is-master-down-by-addr回复处理

sentinelReceiveIsMasterDownReply

在sentinelCommand中对命令处理之后发送了返回数据,数据里面包含主观下线的判断、leader的runid以及投票轮次leader_epoch,对返回数据的处理在sentinelReceiveIsMasterDownReply函数中:

  1. 如果回复者也标记了节点主观下线,将哨兵实例的flags状态置为SRI_MASTER_DOWN下线状态,SRI_MASTER_DOWN状态就是在这里设置的
  2. 如果返回的leader runid不是,意味着哨兵实例对leader进行了投票,需要更新哨兵实例中的leader和leader_epoch*
// 这里的privdata指向回复is-master-down-by-addr命令的那个哨兵实例
void sentinelReceiveIsMasterDownReply(redisAsyncContext *c, void *reply, void *privdata) {
    // 每个哨兵实例监控的master节点中,保存了其他监控该主节点的哨兵实例,这里的privdata就指向master节点存储的其他哨兵实例中回复了is-master-down-by-addr命令的那个哨兵实例
    sentinelRedisInstance *ri = privdata;
    instanceLink *link = c->data;
    redisReply *r;

    if (!reply || !link) return;
    link->pending_commands--;
    r = reply;
    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();
        // 如果回复主观下线
        if (r->element[0]->integer == 1) {
            // 将回复命令的哨兵节点的flags状态改为SRI_MASTER_DOWN
            ri->flags |= SRI_MASTER_DOWN;
        } else {
            ri->flags &= ~SRI_MASTER_DOWN;
        }
        // 如果runid不是*
        if (strcmp(r->element[1]->str,"*")) {
            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);
            // 更新回复命令的哨兵存储的leader
            ri->leader = sdsnew(r->element[1]->str);
            // 更新投票轮次
            ri->leader_epoch = r->element[2]->integer;
        }
    }
}

故障切换状态机

在sentinelHandleRedisInstance函数中,判断是否需要执行故障切换之后,就会调用sentinelFailoverStateMachine函数进入故障切换状态机,根据failover_state故障切换状态调用不同的方法,我们先关注以下两种状态:

  1. SENTINEL_FAILOVER_STATE_WAIT_START:等待执行状态,表示需要执行故障切换但还未开始,在sentinelStartFailoverIfNeeded函数中可以看到如果需要执行故障切换,会调用sentinelStartFailover函数将状态置为SENTINEL_FAILOVER_STATE_WAIT_START,对应的处理函数为sentinelFailoverWaitStart,sentinelFailoverWaitStart中会判断是当前哨兵节点是否是执行故障切换的leader,如果是将状态改为SENTINEL_FAILOVER_STATE_SELECT_SLAVE。

  2. SENTINEL_FAILOVER_STATE_SELECT_SLAVE:从SLAVE节点中选举Master节点的状态,处于这个状态意味着需要从Master的从节点中选举出可以替代Master节点的从节点,进行故障切换,对应的处理函数为sentinelFailoverSelectSlave

void sentinelFailoverStateMachine(sentinelRedisInstance *ri) {
    serverAssert(ri->flags & SRI_MASTER);
    // 如果已经在故障切换执行中,直接返回
    if (!(ri->flags & SRI_FAILOVER_IN_PROGRESS)) return;
    // 状态机
    switch(ri->failover_state) {
        case SENTINEL_FAILOVER_STATE_WAIT_START:// 等待执行
            sentinelFailoverWaitStart(ri);
            break;
        case SENTINEL_FAILOVER_STATE_SELECT_SLAVE: // 选举master节点
            sentinelFailoverSelectSlave(ri);
            break;
        case SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE:
            sentinelFailoverSendSlaveOfNoOne(ri);
            break;
        case SENTINEL_FAILOVER_STATE_WAIT_PROMOTION:
            sentinelFailoverWaitPromotion(ri);
            break;
        case SENTINEL_FAILOVER_STATE_RECONF_SLAVES:
            sentinelFailoverReconfNextSlave(ri);
            break;
    }
}

sentinelFailoverWaitStart

sentinelFailoverWaitStart函数的处理逻辑如下:

  1. 调用sentinelGetLeader获取执行故障切换的leader
  2. 对比当前哨兵是与获取到执行故障切换leader的myid是否一致,判断当前哨兵是否是执行故障切换的leader
  3. 如果当前哨兵不是故障切换leader, 并且不是强制执行状态SRI_FORCE_FAILOVER,当前哨兵不能执行故障切换
  4. 如果当前哨兵是故障切换的leader节点,将故障切换状态改为SENTINEL_FAILOVER_STATE_SELECT_SLAVE状态,在下一次执行故障切换状态机时会从slave节点选出master节点进行故障切换
void sentinelFailoverWaitStart(sentinelRedisInstance *ri) {
    char *leader;
    int isleader;

    /* 获取执行故障切换的leader */
    leader = sentinelGetLeader(ri, ri->failover_epoch);
    // leader不为空并且与当前哨兵的myid一致
    isleader = leader && strcasecmp(leader,sentinel.myid) == 0;
    sdsfree(leader);

    /* 如果当前哨兵不是leader, 并且不是强制执行状态SRI_FORCE_FAILOVER,当前哨兵不能执行故障切换 */
    if (!isleader && !(ri->flags & SRI_FORCE_FAILOVER)) {
        int election_timeout = SENTINEL_ELECTION_TIMEOUT;
        if (election_timeout > ri->failover_timeout)
            election_timeout = ri->failover_timeout;
        /* 在超时时终止故障切换 */
        if (mstime() - ri->failover_start_time > election_timeout) {
            sentinelEvent(LL_WARNING,"-failover-abort-not-elected",ri,"%@");
            sentinelAbortFailover(ri);
        }
        return;
    }
    sentinelEvent(LL_WARNING,"+elected-leader",ri,"%@");
    if (sentinel.simfailure_flags & SENTINEL_SIMFAILURE_CRASH_AFTER_ELECTION)
        sentinelSimFailureCrash();
    // 更改为SENTINEL_FAILOVER_STATE_SELECT_SLAVE状态,在下一次执行故障切换状态机时会从slave节点选出master节点
    ri->failover_state = SENTINEL_FAILOVER_STATE_SELECT_SLAVE;
    ri->failover_state_change_time = mstime();
    sentinelEvent(LL_WARNING,"+failover-state-select-slave",ri,"%@");
}

Leader选举

sentinelGetLeader

sentinelGetLeader函数用于从指定的投票轮次epoch中获取Leader节点,成为一个Leader节点需要获取大多数的投票,处理逻辑如下:

  1. 创建了一个counters字典,里面记录了每个哨兵得到的投票数,其中key为哨兵实例的id
    • counters的数据来源:遍历master->sentinels获取其他哨兵实例,判断哨兵实例记录的leader是否为空并且投票轮次与当前指定的epoch一致,如果一致加入counters中并将投票数增加一票
  2. 从counters中获取投票数最多的哨兵实例记为winner,最大投票数记为max_votes
  3. 判断winner是否为空
    • 如果不为空,在master节点中记录的leader节点和winner节点中,选出纪元(投票轮次)最新的节点记为myvote
    • 如果为空,在master节点中记录的leader节点和当前哨兵实例中,选出纪元(投票轮次)最新的节点记为myvote
  4. 经过上一步之后,如果myvote不为空并且leader_epoch与调用sentinelGetLeader函数时指定的epoch一致,当前哨兵给myvote增加一票,然后判断myvote得到的投票数是否大于max_votes,如果是将winner获胜者更新为myvote
  5. 到这里,winner中记录了本轮投票的获胜者,也就是得到票数最多的那个,max_votes记录了获得投票数,能否成为leader还需满足以下两个条件,,保证选举出的leader得到了过半哨兵的投票
    • 条件一:得到的投票数max_votes大于voters_quorum,voters_quorum为哨兵实例个数的一半+1,也就是需要有过半的哨兵实例
    • 条件二:得到的投票数max_votes大于master->quorum,这个值是在 sentinel.conf 配置文件中设置的,一般设置为哨兵总数的一半+1
  6. 选举结束,返回winner中记录的实例id作为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;

    /* 遍历哨兵实例 */
    di = dictGetIterator(master->sentinels);
    while((de = dictNext(di)) != NULL) {
        // 获取每一个哨兵节点
        sentinelRedisInstance *ri = dictGetVal(de);
        // 如果某个哨兵实例的leader不为空并且leader的轮次等于指定的轮次
        if (ri->leader != NULL && ri->leader_epoch == sentinel.current_epoch)
            sentinelLeaderIncr(counters,ri->leader); // ri->leader的投票数加1
    }
    dictReleaseIterator(di);
    di = dictGetIterator(counters);
    while((de = dictNext(di)) != NULL) {
        // 获取投票数
        uint64_t votes = dictGetUnsignedIntegerVal(de);
        // 获取投票数最多的节点的runid,记录在winner中
        if (votes > max_votes) {
            max_votes = votes;
            winner = dictGetKey(de);
        }
    }
    dictReleaseIterator(di);

    /* 如果winner不为空,在master节点和winner节点中获取epoch投票轮次最新的当做当前节点的投票者*/
    /* 如果winner为空,在master节点和当前哨兵节点中选取epoch投票轮次最新的节点*/
    if (winner)
        myvote = sentinelVoteLeader(master,epoch,winner,&leader_epoch);
    else
        myvote = sentinelVoteLeader(master,epoch,sentinel.myid,&leader_epoch);
    // 
    if (myvote && leader_epoch == epoch) {
        // 为myvote增加一票
        uint64_t votes = sentinelLeaderIncr(counters,myvote);
        // 如果超过了max_votes
        if (votes > max_votes) {
            max_votes = votes; // 更新max_votes
            winner = myvote; // 更新winner
        }
    }
    // voters_quorum,为哨兵实例个数的一半+1
    voters_quorum = voters/2+1;
    // 如果投票数量max_votes小于quorum,说明未达到过半的投票数
    if (winner && (max_votes < voters_quorum || max_votes < master->quorum))
        winner = NULL;

    winner = winner ? sdsnew(winner) : NULL;
    sdsfree(myvote);
    dictRelease(counters);
    // 返回获胜的节点,也就是leader节点
    return winner;
}

投票

sentinelVoteLeader

sentinelVoteLeader函数入参中可以看到传入了master节点,和候选节点的IDreq_runid以及候选节点的投票纪元(轮次)req_epoch。master节点中记录了当前哨兵节点选举的执行故障切换的leader节点,在master->leader中保存。sentinelVoteLeader就用于在这两个节点中选出获胜的那个节点:

  1. 如果候选节点的req_epoch大于当前sentinel实例的epoch,将当前哨兵实例的current_epoch置为请求轮次req_epoch
  2. 如果master节点记录的leader_epoch小于候选节点的req_epoch,并且当前实例的轮次小于等于候选节点轮次,将master节点中的leader改为候选节点
  3. 返回master节点中记录的leader
char *sentinelVoteLeader(sentinelRedisInstance *master, uint64_t req_epoch, char *req_runid, uint64_t *leader_epoch) {
    // 如果请求的轮次req_epoch大于当前sentinel实例的轮次
    if (req_epoch > sentinel.current_epoch) {
        // 将哨兵实例的current_epoch置为请求轮次
        sentinel.current_epoch = req_epoch;
        sentinelFlushConfig();
        sentinelEvent(LL_WARNING,"+new-epoch",master,"%llu",
            (unsigned long long) sentinel.current_epoch);
    }
    // 如果master节点记录的轮次leader_epoch小于请求轮次,并且当前实例的轮次小于等于请求轮次
    if (master->leader_epoch < req_epoch && sentinel.current_epoch <= req_epoch)
    {
        sdsfree(master->leader);
        // 将leader置为传入的runid
        master->leader = sdsnew(req_runid);
        // 更改master的leader_epoch
        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 (strcasecmp(master->leader,sentinel.myid))
            master->failover_start_time = mstime()+rand()%SENTINEL_MAX_DESYNC;
    }

    *leader_epoch = master->leader_epoch;
    return master->leader ? sdsnew(master->leader) : NULL;
}

总结

2612945-20220511234012381-60303521.png

参考

极客时间 - Redis源码剖析与实战(蒋德钧)

Redis版本:redis-6.2.5

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容