2024-04-05  阅读(3)
原文作者:文先生的博客 原文地址: http://wenfh2020.com/2020/09/26/redis-sentinel-vote/

投票原理:”先到先得”,每个 sentinel 机会是对等的,都有投票权利。

当前 sentienl 节点,确认某个 master 客观下线后,它会主动开启故障转移的选举环节,进行拉票(选自己)和投票(选别人)。

经过票数统计,在最新一轮的选举过程中,超过法定数量(一般过半数)的 sentinel 投票给某个 sentinel 节点时,那么它就当选 leader,选举结束后,由它去执行其它剩余的故障转移步骤。


1. 选举整体流程

下面是伪代码的执行流程。

202404052229415951.png

 
    # 只有检查到 master 客观掉线了才会去询问,在确认客观下线了,马上进行多轮选举,直到选出 leader。
    |-- main
        # 定时检测处理故障。
        |-- sentinelTimer
            |-- server.hz = CONFIG_DEFAULT_HZ + rand() % CONFIG_DEFAULT_HZ;
            |-- sentinelHandleDictOfRedisInstances(sentinel.masters);
                |-- sentinelHandleRedisInstance(ri);
                    # 通过 PING/INFO/PUBLISH 协议定时给集群其它节点发送数据,使得集群紧密联系起来。
                    |-- sentinelSendPeriodicCommands(ri);
                        # 往 __sentinel__:hello 频道发布数据。
                        |-- sentinelSendHello(ri)
                            # 定时更新 master 的选举纪元 current_epoch,使得整个集群保持选举纪元数据一致。
                            |-- <ip>,<port>,<runid>,<current_epoch>,<master_name>,<master_ip>,<master_port>,<master_config_epoch>
                    # 检查节点是否已经主观下线(所有类型的节点 sentinel/master/slave)。
                    |-- sentinelCheckSubjectivelyDown(ri);
                    # 检测 master 节点是否客观下线。
                    |-- sentinelCheckObjectivelyDown(ri);
                        |-- if (master->flags & SRI_S_DOWN)
                            # 统计已检测到该 master 节点下线的 sentinel 节点个数,如果总数大于法定人数,就确认某个 master 节点客观下线。
                            |-- if (quorum >= master->quorum) odown = 1;
                            # 确认客观下线后,设置标识。
                            |-- odown ? master->flags |= SRI_O_DOWN : master->flags &= ~SRI_O_DOWN;
                    # 如果检测到 master 节点是客观下线,马上开启进入故障转移的选举投票环节。
                    |-- if (sentinelStartFailoverIfNeeded(ri))
                            # 满足选举投票条件,进入新一轮的投票环节。
                            |-- if ((master->flags & SRI_O_DOWN) && !(master->flags & SRI_FAILOVER_IN_PROGRESS))
                                    |-- sentinelStartFailover(master);
                                        # 进入投票环节。
                                        |-- master->failover_state = SENTINEL_FAILOVER_STATE_WAIT_START; 
                                        # 设置故障转移状态为开启状态(选举投票)。
                                        |-- master->flags |= SRI_FAILOVER_IN_PROGRESS; 
                                        # 设置当前投票纪元(标识当前 sentinel 发起的投票是第几轮投票)
                                        |-- master->failover_epoch = ++sentinel.current_epoch;
                            # 刚开启新一轮的选举,强制马上执行拉票动作。
                            |-- sentinelAskMasterStateToOtherSentinels(ri, SENTINEL_ASK_FORCED); 
                    # 故障转移流程状态机。
                    |-- sentinelFailoverStateMachine(ri);
                        # 投票环节。
                        |-- SENTINEL_FAILOVER_STATE_WAIT_START:
                            |-- sentinelFailoverWaitStart(ri);
                                # 定时统计选票。
                                |-- leader = sentinelGetLeader(ri, ri->failover_epoch); 
                                             # 先统计当前接收到的选票,暂时将获得票数最多的 sentinel 节点确定为 winner。
                                             |-- ...
                                             # 然后再统计自己的投票。
                                             |-- if (winner)
                                                 # 在新一轮的选举中将自己的选票投给获得票数最多的 winnder。
                                                 |-- myvote = sentinelVoteLeader(master, epoch, winner, &leader_epoch);
                                                              # 刷新最新选举纪元。
                                                              |-- if (req_epoch > sentinel.current_epoch)
                                                                  |-- sentinel.current_epoch = req_epoch;
                                                              # 在新一轮选举中,将票投给对应的 sentinel 节点(req_runid)
                                                              |-- if (master->leader_epoch < req_epoch && sentinel.current_epoch <= req_epoch)
                                                                  |-- master->leader = sdsnew(req_runid);
                                                                  |-- master->leader_epoch = sentinel.current_epoch;
                                                                  # 如果其它 sentinel 节点获得选票,那么当前 sentinel 节点需延后自己进入下一轮选举的时间。
                                                                  |-- if (strcasecmp(master->leader, sentinel.myid))
                                                                      master->failover_start_time = mstime() + rand() % SENTINEL_MAX_DESYNC;
                                                              |-- return master->leader ? sdsnew(master->leader) : NULL;
                                             |-- else
                                                 # 在新一轮选举中,如果当前 sentinel 节点暂时没发现别的 sentinel 节点获得选票,就将票投给自己。
                                                 |-- myvote = sentinelVoteLeader(master, epoch, sentinel.myid, &leader_epoch);
                                             # 如果 winner 获得超过法定人数的选票,那么它就获选 leader。
                                             |-- voters_quorum = voters/2+1;
                                             |-- if (winner && (max_votes < voters_quorum || max_votes < master->quorum))
                                                 |-- winner = NULL;
                                             |-- return winner;
                                |-- isleader = leader && strcasecmp(leader, sentinel.myid) == 0;
                                # 通过票数统计,选出 leader,如果这个 leader 是自己,那么马上进入下一个故障转移环节。
                                |-- if (isleader)
                                    |-- ri->failover_state = SENTINEL_FAILOVER_STATE_SELECT_SLAVE;
                                |-- else
                                    # 如果当前 sentinel 节点开启选举后,在预定的选举时间内,没有选出 leader,那准备进入下一轮投票。
                                    |-- if (mstime() - ri->failover_start_time > election_timeout)
                                        |-- sentinelAbortFailover(ri);
                    # 定时在当前一轮投票中,进行节点客观下线询问或者拉票。
                    |-- sentinelAskMasterStateToOtherSentinels(ri, SENTINEL_NO_FLAGS);
        |-- ...
            # 网络端接收信息。
            |-- sentinelCommand(client *c)
                # 接收到其它 sentinel 节点客观下线询问或者拉票。
                |-- 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; # 检测到询问的 master 节点已下线。
                    # 接收到其它 sentinel 节点的拉票,。
                    |-- if (ri && ri->flags & SRI_MASTER && strcasecmp(c->argv[5]->ptr,"*"))
                        |-- leader = sentinelVoteLeader(ri,(uint64_t)req_epoch, c->argv[5]->ptr, &leader_epoch);
                    |-- addReply(c, isdown ? shared.cone : shared.czero);
                    |-- addReplyBulkCString(c, leader ? leader : "*");
            # 接收到其它 sentinel 的回复,设置客观下线,或者选举。
            |-- sentinelReceiveIsMasterDownReply
                # 根据别的 sentinel 节点的回复,设置它是否确认 master 已经下线。
                |-- if (r->element[0]->integer == 1)
                    |-- ri->flags |= SRI_MASTER_DOWN;
                |-- else
                    |-- ri->flags &= ~SRI_MASTER_DOWN;
                |-- if (strcmp(r->element[1]->str, "*"))
                    # 接收到别的 sentinel 节点的拉票,将票投给拉票者。
                    |-- ri->leader = sdsnew(r->element[1]->str);
                    |-- ri->leader_epoch = r->element[2]->integer;
            # 接收其它节点的发布信息并处理。
            |-- sentinelProcessHelloMessage
                |-- sentinelProcessHelloMessage(r->element[2]->str, r->element[2]->len);
                    # 刷新当前选举纪元,争取每次选举都在最新一轮的选举上进行。
                    |-- if (current_epoch > sentinel.current_epoch)
                        # 更新当前的选举纪元。
                        |-- sentinel.current_epoch = current_epoch;
                        # 将当前选举纪元持久化到本地配置文件。
                        |-- sentinelFlushConfig();

2. 拉票

2.1. 发送拉票命令

拉票命令:SENTINEL IS-MASTER-DOWN-BY-ADDR

 
    # is-master-down-by-addr 命令格式。
    SENTINEL is-master-down-by-addr <masterip> <masterport> <sentinel.current_epoch> <sentinel_runid>
  1. 参数传输 master 的 ip 和 端口(注意:不是传 mastername,因为每个 sentinel 上配置的 name 有可能不一样)。

  2. <sentinel.current_epoch> 选举纪元,可以理解为选举计数器,每次 sentinel 之间选举,不一定成功,有可能会进行多次,所以每次选举计数器会加 1,表示第几轮选举。

  3. <sentinel_runid> 当前 sentinel 的 runid,因为选举投票原理是 “先到先得”。当其它 sentinel 在一轮选举中,先接收到拉票信息的,会先投给它。

    例如:sentinel A,B,C 三个实例,当 A 向 B,C 进行拉票。如果 B 先接收到 A 的拉票信息,那么 B 就选 A 为 leader。如果 B 在接收到 A 的拉票信息前,已接收到 C 的拉票,那么 B 已将票投给了 C,B 将会回复 “已投票给 C” 给 A。


 
    void sentinelAskMasterStateToOtherSentinels(sentinelRedisInstance *master, int flags) {
        dictIterator *di;
        dictEntry *de;
    
        di = dictGetIterator(master->sentinels);
        while ((de = dictNext(di)) != NULL) {
            sentinelRedisInstance *ri = dictGetVal(de);
            ...
            /* Only ask if master is down to other sentinels if:
             *
             * 1) We believe it is down, or there is a failover in progress.
             * 2) Sentinel is connected.
             * 3) We did not receive the info within SENTINEL_ASK_PERIOD ms. */
            if ((master->flags & SRI_S_DOWN) == 0) continue;
            if (ri->link->disconnected) continue;
            if (!(flags & SENTINEL_ASK_FORCED) &&
                mstime() - ri->last_master_down_reply_time < SENTINEL_ASK_PERIOD)
                continue;
    
            /* 当 sentinel 检测到 master 主观下线,那么参数发送 "*",等待确认客观下线,
             * 当确认客观下线后,再进入选举环节。sentinel 再向其它 sentinel 发送自己的 runid,去拉票。*/
            ll2string(port, sizeof(port), master->addr->port);
            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 : "*");
            if (retval == C_OK) {
                ri->link->pending_commands++;
            }
        }
        dictReleaseIterator(di);
    }

2.2. 接收拉票

其它 sentinel 节点,接收到拉票信息,进行投票 sentinelVoteLeader

 
    void sentinelCommand(client *c) {
        ...
        else if (!strcasecmp(c->argv[1]->ptr, "is-master-down-by-addr")) {
            ...
            /* 其它 sentinel 接收到询问命令,根据 ip 和 端口查找对应的 master。 */
            ri = getSentinelRedisInstanceByAddrAndRunID(
                sentinel.masters, c->argv[2]->ptr, port, NULL);
    
            /* 当前 sentinel 如果没有处于异常保护状态,而且也检测到询问的 master 已经主观下线了。 */
            if (!sentinel.tilt && ri && (ri->flags & SRI_S_DOWN) && (ri->flags & SRI_MASTER))
                isdown = 1;
    
            /* 询问 master 主观下线命令参数是 *,选举投票参数是请求的 sentinel 的 runid。*/
            if (ri && ri->flags & SRI_MASTER && strcasecmp(c->argv[5]->ptr, "*")) {
                /* 投票 */
                leader = sentinelVoteLeader(
                         ri, (uint64_t)req_epoch, c->argv[5]->ptr, &leader_epoch);
            }
    
            /* 投票选举业务确定回复的内容参数。 */
            addReplyArrayLen(c, 3);
            addReply(c, isdown ? shared.cone : shared.czero);
            addReplyBulkCString(c, leader ? leader : "*");
            addReplyLongLong(c, (long long)leader_epoch);
            if (leader) {
                sdsfree(leader);
            }
        }
        ...
    }

2.3. 拉票回复

根据拉票回复,更新对方 sentinel 节点的投票结果。

 
    /* Receive the SENTINEL is-master-down-by-addr reply, see the
     * sentinelAskMasterStateToOtherSentinels() function for more information. */
    void sentinelReceiveIsMasterDownReply(
        redisAsyncContext *c, void *reply, void *privdata) {
        ...
        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) {
                /* ri sentinel 回复:它也检测到该 master 节点已经下线。 */
                ri->flags |= SRI_MASTER_DOWN;
            } else {
                ri->flags &= ~SRI_MASTER_DOWN;
            }
            if (strcmp(r->element[1]->str, "*")) {
                /* 当前 sentinel 向 ri 拉选票,ri 回复:它已将票投给某个 sentinel(runid)。*/
                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);
                ri->leader = sdsnew(r->element[1]->str);
                ri->leader_epoch = r->element[2]->integer;
            }
        }
    }

3. 投票

sentinel 的投票(sentinelVoteLeader)有两种方式:

  1. 被动:接收到别人的拉票请求,给对方进行投票。
  2. 主动:在最新一轮的选举过程中,经过票数统计,主动给票数最多的 winner 投票,因为它当选几率最大,如果其它 sentinel 节点还没获得选票,那么就把票投给自己。
  • 投票。
 
    char *sentinelVoteLeader(sentinelRedisInstance *master, 
        uint64_t req_epoch, char *req_runid, uint64_t *leader_epoch) {
        /* 同步 epoch,保证多个 sentinel 数据一致性。 */
        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);
        }
    
        /* 有可能出现多次选举,投票给最大一轮选举的 sentinel。*/
        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);
            /* 如果别人是 leader,那么先设置故障转移开始时间,使得在一个时间段内,
             * 别人在故障转移的时候,自己不能开启故障转移。 */
            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;
    }
  • 被动投票。
 
    void sentinelCommand(client *c) {
        ...
        else if (!strcasecmp(c->argv[1]->ptr,"is-master-down-by-addr")) {
            ...
            if (ri && ri->flags & SRI_MASTER && strcasecmp(c->argv[5]->ptr,"*")) {
                leader = sentinelVoteLeader(ri,(uint64_t)req_epoch,
                                                c->argv[5]->ptr,
                                                &leader_epoch);
            }
            ...
            addReplyBulkCString(c, leader ? leader : "*");
            addReplyLongLong(c, (long long)leader_epoch);
            ...
        }
        ...
    }
  • 主动投票。
 
    void sentinelFailoverWaitStart(sentinelRedisInstance *ri) {
        ...
        /* 统计选举结果。 */
        leader = sentinelGetLeader(ri, ri->failover_epoch);
        ...
    }
    
    /* 统计选票结果。*/
    char *sentinelGetLeader(sentinelRedisInstance *master, uint64_t epoch) {
        ...
        /* 如果没人来拉票,我也没有投过票,那么可以投自己,否则自己投票数多的人。 */
        if (winner)
            myvote = sentinelVoteLeader(master, epoch, winner, &leader_epoch);
        else
            myvote = sentinelVoteLeader(master, epoch, sentinel.myid, &leader_epoch);
        ...
        return winner;
    }

4. 统计票数

定时检查已经连接的 sentinel,统计选举情况,选出票数最多的 sentinel 为 leader。

 
    char *sentinelGetLeader(sentinelRedisInstance *master, uint64_t epoch) {
        ...
        counters = dictCreate(&leaderVotesDictType, NULL);
        voters = dictSize(master->sentinels) + 1; /* All the other sentinels and me.*/
    
        /* 统计别人的 sentinel 投票结果。 */
        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. */
        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);
    
        /* 前面是统计其它人的投票,现在轮到我投票,如果其它人已经投票了,那么就将自己的票投给 winner,
         * 否则自己的票就投给自己。 */
        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;
            }
        }
    
        /* 选出的 winner 最少要 >= 已知 sentinel 个数的 (50% + 1),
         * 而且 winner 票数也不能少于法定投票数量。 */
        voters_quorum = voters / 2 + 1;
        if (winner && (max_votes < voters_quorum || max_votes < master->quorum))
            winner = NULL;
    
        winner = winner ? sdsnew(winner) : NULL;
        sdsfree(myvote);
        dictRelease(counters);
        return winner;
    }

5. 差异化

5.1. 随机时间

服务端的网络一般比较流畅,当一个 master 下线,各个 sentinel 节点会很快就会感知到。

因为 sentinel 都是通过时钟定时工作,为了提高选举的成功率,需要对 sentinel 开启选举进行差异化,因此对时钟频率会引入随机数。

 
    int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
        ...
        /* Run the Sentinel timer if we are in sentinel mode. */
        if (server.sentinel_mode) sentinelTimer();
        ...
    }
    
    void sentinelTimer(void) {
        ...
        /* 定时器刷新频率添加随机数,添加投票的差异化。 */
        server.hz = CONFIG_DEFAULT_HZ + rand() % CONFIG_DEFAULT_HZ;
    }
 
    /* 故障转移开始时间也会添加一个随机时间因子。*/
    master->failover_start_time = mstime() + rand() % SENTINEL_MAX_DESYNC;

5.2. 先到先得

投票的原理是 “先到先得”,如何操作才能保证 “先到先得”?如何保证在同一个时间段内,只有一个 sentinel 在进行故障转移?我们看看这个变量的巧妙利用 master->failover_start_time

  • 主动。
 
    /* 是否满足故障转移条件,开启故障转移。 */
    int sentinelStartFailoverIfNeeded(sentinelRedisInstance *master) {
        /* master 客观下线。 */
        if (!(master->flags & SRI_O_DOWN)) return 0;
    
        /* 当前 master 没有处在故障转移过程中。 */
        if (master->flags & SRI_FAILOVER_IN_PROGRESS) return 0;
    
        /* 两次故障转移,需要有一定的时间间隔。如果别人已经开始了,
         * 那么你也需要等待一段时间,让别人在这个时间段内先跑完流程。否则自己可以开启故障转移流程。 */
        if (mstime() - master->failover_start_time < master->failover_timeout * 2) {
            ...
            return 0;
        }
    
        /* 满足故障转移条件,开启故障转移。 */
        sentinelStartFailover(master);
        return 1;
    }
    
    void sentinelStartFailover(sentinelRedisInstance *master) {
        ...
        /* 设定故障转移的选举纪元,标识第几轮选举。*/
        master->failover_epoch = ++sentinel.current_epoch;
        ...
        /* 延缓下一轮选举时间。*/
        master->failover_start_time = mstime() + rand() % SENTINEL_MAX_DESYNC;
        ...
    }
  • 被动。当别人向你拉票的时候,说明故障转移已经开始,结合上面分析,那么当你要开启故障转移的时候,你必须等待一段时间。
 
    char *sentinelVoteLeader(sentinelRedisInstance *master,
        uint64_t req_epoch, char *req_runid, uint64_t *leader_epoch) {
        ...
        /* 有可能出现多次选举,投票给最大一轮选举的 sentinel。*/
        if (master->leader_epoch < req_epoch && sentinel.current_epoch <= req_epoch) {
            ...
            if (strcasecmp(master->leader, sentinel.myid))
                /* 如果别人已获选,那么它在该轮选举中当选 leader 的几率非常高,
                 * 那么自己要延缓开启下一轮选举的时间,不要与它发生冲突。*/
                master->failover_start_time = mstime() + rand() % SENTINEL_MAX_DESYNC;
        }
        ...
    }

6. 参考


Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文