Redis 哨兵模式

Redis 哨兵核心实现

哨兵需要具有发现节点的功能,发现主节点,从节点,哨兵节点,即,哨兵需要知道当前的 Redis 部署网络视图。

哨兵需要判断节点运行状态,判断主节点是否正常工作。

哨兵需要就主节点下线达成共识并进行故障转移,选举一个 leader 节点进行故障转移。

image

sentinel 初始化

哨兵的初始化,主要是建立与主节点以及新发现节点的连接,以及订阅主节点从节点 hello 通道,启动定时任务。定时任务中会定期进行节点有效性检测,判断节点是否主观下线,客观下线,以及判断是否进行故障转移,进入故障转移状态机流程。

哨兵初始化流程如下:

main(int argc, char **argv)
{
    // 判断是否是 sentinel 模式
    server.sentinel_mode = checkForSentinelMode(argc,argv, exec_name);
    ACLInit();  // 初始化 ACL

    if (server.sentinel_mode) {
        initSentinelConfig();
        initSentinel();
    }    

    if (argc >= 2) {
        loadServerConfig(server.configfile, config_from_stdin, options);
        if (server.sentinel_mode) loadSentinelConfigFromQueue();    
    }

    // 哨兵节点必须有可写的配置文件,会将发现的从节点以及哨兵节点等节点信息写入配置文件中
    if (server.sentinel_mode) sentinelCheckConfigFile();
    server.supervised = redisIsSupervised(server.supervised_mode);

    initServer();

    if (server.sentinel_mode) {
        ACLLoadUsersAtStartup();
        InitServerLast();
        sentinelIsRunning(){  // 生成哨兵 ID,写入配置文件
            sentinelGenerateInitialMonitorEvents(){
                sentinelEvent(LL_WARNING,"+monitor",ri,"%@ quorum %d",ri->quorum);
            }
        }
    }
    
    aeMain(server.el);  // 启动事件循环
}

sentinelIsRunning() 中生成哨兵 ID,写入配置文件,生成一个 +monitor 事件,哨兵在启动时广播自身监控的 master 列表。

205502:X 27 Jan 2026 19:43:07.403 * Sentinel new configuration saved on disk
205502:X 27 Jan 2026 19:43:09.212 # Sentinel ID is b3461fcadb4a23ebc9534c08228612c72e13d3a8
当前 Sentinel 节点开始监控名为 mymaster 的主服务器,且需要至少 2 个 Sentinel 节点同意才能判定其故障
205502:X 27 Jan 2026 19:47:39.470 # +monitor master mymaster 192.168.232.128 6379 quorum 2

哨兵必须配置配置文件,且可写,当哨兵发现从节点以及其他哨兵节点时,会将该信息写入配置文件,做持久化存储。

初始化流程中还会调用 serverCron 函数:

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) {
    sentinelCheckTiltCondition();
    sentinelHandleDictOfRedisInstances(sentinel.masters);
    sentinelRunPendingScripts();
    sentinelCollectTerminatedScripts();
    sentinelKillTimedoutScripts();

    /* We continuously change the frequency of the Redis "timer interrupt"
     * in order to desynchronize every Sentinel from every other.
     * This non-determinism avoids that Sentinels started at the same time
     * exactly continue to stay synchronized asking to be voted at the
     * same time again and again (resulting in nobody likely winning the
     * election because of split brain voting). */
    server.hz = CONFIG_DEFAULT_HZ + rand() % CONFIG_DEFAULT_HZ;
}

哨兵节点发现与失效检测

节点发现

哨兵的核心功能之一就是判断出哪个节点故障了,并做出故障转移。而判断节点故障,首要的是要知道当前所有集群节点信息。

节点发现很容易就想到 gossip 协议,其实 Redis 中节点发现的思想就是 gossip 协议,哨兵节点的配置文件中会配置主节点信息。即哨兵是通过主节点发现其他节点信息,并保存在配置文件中。所有的哨兵都会配置其监控的主节点信息,通过主节点就能够获取所有节点信息。

sentinel monitor mymaster 127.0.0.1 6379 2  # 哨兵配置文件,监控主节点

哨兵在初始状态,仅知道主节点信息,需要后续自动获取:

  • 从节点信息(通过定期执行 INFO 命令从主节点获取从节点列表信息)
  • 哨兵节点信息(通过 Pub/Sub 订阅机制,哨兵节点会定期向该频道发布自己的信息 hello 消息,相当于 gossip 协议中的 push 消息,其他节点收到 hello 消息更新节点信息)

哨兵节点发现

Sentinel 使用 Pub/Sub(发布/订阅)机制实现哨兵节点间的自动发现。所有监控同一个 Master 的 Sentinel 节点都会订阅一个特殊的频道 __sentinel__:hello,并定期向该频道发布自己的信息,从而实现相互发现和信息同步。

#define SENTINEL_HELLO_CHANNEL "__sentinel__:hello"

哨兵节点发现流程如下:

sequenceDiagram
    participant S1 as Sentinel 1
    participant M as Master
    participant S2 as Sentinel 2
    participant S3 as Sentinel 3

    Note over S1,S3: 所有 Sentinel 订阅 __sentinel__:hello 频道
    
    S1->>M: SUBSCRIBE __sentinel__:hello
    S2->>M: SUBSCRIBE __sentinel__:hello
    S3->>M: SUBSCRIBE __sentinel__:hello
    
    Note over S1,S3: 每 2 秒发布一次 Hello 消息
    
    S1->>M: PUBLISH __sentinel__:hello<br/>"IP,Port,RunID,Epoch,..."
    M->>S2: 转发 S1 的消息
    M->>S3: 转发 S1 的消息
    
    Note over S2: 发现 Sentinel 1
    Note over S3: 发现 Sentinel 1
    
    S2->>M: PUBLISH __sentinel__:hello<br/>"IP,Port,RunID,Epoch,..."
    M->>S1: 转发 S2 的消息
    M->>S3: 转发 S2 的消息
    
    Note over S1: 发现 Sentinel 2
    Note over S3: 发现 Sentinel 2
    
    Note over S1,S3: 所有 Sentinel 相互发现完成

从节点信息发现

哨兵节点会定期向主节点发送 INFO 命令,获取从节点信息。主节点是知道所有的从节点信息的,执行 INFO 命令:

127.0.0.1:6379> info
# Replication
role:master
connected_slaves:1
slave0:ip=192.168.232.137,port=6379,state=online,offset=524642,lag=0

通过解析 info 命令的执行结果,就可以获取从节点信息,更新配置文件。

// 哨兵节点发现从节点信息
58221:X 29 Jan 2026 11:37:12.703 * +slave slave 192.168.232.137:6379 192.168.232.137 6379 @ mymaster 192.168.232.128 6379

周期性的发送 INFO、PING 命令,Hello 消息。

void sentinelSendPeriodicCommands(sentinelRedisInstance *ri) {

    // 向主节点,从节点发送 INFO 命令
    if ((ri->flags & SRI_SENTINEL) == 0 &&
        (ri->info_refresh == 0 ||
        (now - ri->info_refresh) > info_period))
    {
        // 接收 INFO 命令处理回调函数
        retval = redisAsyncCommand(ri->link->cc,
            sentinelInfoReplyCallback, ri, "%s",
            sentinelInstanceMapCommand(ri,"INFO"));
        if (retval == C_OK) ri->link->pending_commands++;
    }

    // 向主节点,从节点,哨兵节点发送 PING 命令
    if ((now - ri->link->last_pong_time) > ping_period &&
               (now - ri->link->last_ping_time) > ping_period/2) {
        sentinelSendPing(ri);
    }

    // 发布 hello 消息
    if ((now - ri->last_pub_time) > sentinel_publish_period) {
        sentinelSendHello(ri);
    }
}

INFO 命令结果处理:

void sentinelInfoReplyCallback(redisAsyncContext *c, void *reply, void *privdata) {
    // 获取 INFO 命令结果
    if (r->type == REDIS_REPLY_STRING)
        sentinelRefreshInstanceInfo(ri,r->str);
}

建立连接

当 Sentinel 与 Master 或 Slave 建立连接时,会创建两个独立的连接:

  • 命令连接:用于发送 PING、INFO 等命令,哨兵节点与其他所有节点建立命令连接(主节点,从节点,哨兵节点)
  • 消息通道: 订阅 __sentinel__:hello 通道。哨兵节点仅与主节点和从节点建立消息通道,订阅 __sentinel__:hello 通道。
// serverCron 会调用此函数
void sentinelTimer(void) {
    // 递归地对 Sentinel 监控的所有 Redis 实例(主、从、其他 Sentinel)执行周期性任务
    sentinelHandleDictOfRedisInstances(sentinel.masters);
}

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. */
    di = dictGetIterator(instances);
    while((de = dictNext(di)) != NULL) {
        sentinelRedisInstance *ri = dictGetVal(de);

        // 对当前实例执行周期性任务
        sentinelHandleRedisInstance(ri);
        if (ri->flags & SRI_MASTER) { // 当前实例是主节点
            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);
}

// 哨兵的核心定时器处理函数
void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {
    /* ========== MONITORING HALF ============ */
    /* Every kind of instance */
    sentinelReconnectInstance(ri); // 如果与实例连接中断,则发起连接
    sentinelSendPeriodicCommands(ri); // 定期向实例发送命令以探测状态

    /* ============== ACTING HALF ============= */
    /* We don't proceed with the acting half if we are in TILT mode.
     * TILT happens when we find something odd with the time, like a
     * sudden change in the clock. */
    if (sentinel.tilt) {
        if (mstime()-sentinel.tilt_start_time < sentinel_tilt_period) return;
        sentinel.tilt = 0;
        sentinelEvent(LL_WARNING,"-tilt",NULL,"#tilt mode exited");
    }

    // 检测实例是否主观下线
    sentinelCheckSubjectivelyDown(ri);

    /* Masters and slaves */
    if (ri->flags & (SRI_MASTER|SRI_SLAVE)) {
        /* Nothing so far. */
    }

    // 哨兵节点周期性检测主节点是否客观下线
    if (ri->flags & SRI_MASTER) {
        sentinelCheckObjectivelyDown(ri);
        if (sentinelStartFailoverIfNeeded(ri)) // 是否需要开启故障转移
            sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);
        sentinelFailoverStateMachine(ri);
        // 哨兵节点向其他哨兵节点询问主节点状态
        sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
    }
}

具体会建立两个连接:

void sentinelReconnectInstance(sentinelRedisInstance *ri) {
    // 如果命令连接断开,则重新建立连接
    if (link->cc == NULL) {
        link->cc = redisAsyncConnectBind(ri->addr->ip,ri->addr->port,server.bind_source_addr);
    
        redisAeAttach(server.el,link->cc);
        // 发送认证命令, 哨兵作为客户端,需要认证
        sentinelSendAuthIfNeeded(ri,link->cc); 
        // 发送 PING
        sentinelSendPing(ri);
    }

    /* Pub / Sub */
    if ((ri->flags & (SRI_MASTER|SRI_SLAVE)) && link->pc == NULL) {
        link->pc = redisAsyncConnectBind(ri->addr->ip,ri->addr->port,server.bind_source_addr);
        if (link->pc && !link->pc->err) anetCloexec(link->pc->c.fd);
        if (!link->pc) {
            sentinelEvent(LL_DEBUG,"-pubsub-link-reconnection",ri,"%@ #Failed to establish connection");
        } else if (!link->pc->err && server.tls_replication &&
                (instanceLinkNegotiateTLS(link->pc) == C_ERR)) {
            sentinelEvent(LL_DEBUG,"-pubsub-link-reconnection",ri,"%@ #Failed to initialize TLS");
        } else if (link->pc->err) {
            sentinelEvent(LL_DEBUG,"-pubsub-link-reconnection",ri,"%@ #%s",
                link->pc->errstr);
            instanceLinkCloseConnection(link,link->pc);
        } else {
            int retval;
            link->pc_conn_time = mstime();
            link->pc->data = link;
            redisAeAttach(server.el,link->pc);
            redisAsyncSetConnectCallback(link->pc,
                    sentinelLinkEstablishedCallback);
            redisAsyncSetDisconnectCallback(link->pc,
                    sentinelDisconnectCallback);
            sentinelSendAuthIfNeeded(ri,link->pc);
            sentinelSetClientName(ri,link->pc,"pubsub");
   
            // 订阅 __sentinel__:hello 通道
            // 设置订阅通道消息回调处理函数,处理 hello 消息
            retval = redisAsyncCommand(link->pc,
                sentinelReceiveHelloMessages, ri, "%s %s",
                sentinelInstanceMapCommand(ri,"SUBSCRIBE"),
                SENTINEL_HELLO_CHANNEL);
            if (retval != C_OK) {
                /* If we can't subscribe, the Pub/Sub connection is useless
                 * and we can simply disconnect it and try again. */
                instanceLinkCloseConnection(link,link->pc);
                return;
            }
        }
    }
}

发送 Hello

建立命令连接,并订阅 __sentinel__:hello 通道后,哨兵节点会定时(默认 2s)向 __sentinel__:hello 通道发送 Hello 消息,Hello 消息中包含自身信息及监控的主节点配置。

哨兵会从订阅的通道 __sentinel__:hello 中接收到其他 Sentinel 节点的 Hello 消息,并解析消息,更新本地集群视图。

hello 消息格式:

sentinel_ip,sentinel_port,sentinel_runid,current_epoch, master_name,master_ip,master_port,master_config_epoch

字段说明作用
0Sentinel IP节点网络地址
1Sentinel 端口节点通信端口
2Sentinel runid节点唯一标识
3current_epoch逻辑时钟(用于故障转移)
4master_name监控的主节点名称
5master_ip主节点当前 IP
6master_port主节点当前端口
7master_config_epoch主节点配置纪元

epoch 用来防止脑裂问题,确保最新配置优先。

  • 每次故障转移增加 epoch
  • 哨兵仅当收到更高的 config_epoch 时才更新本地配置

更高 config_epoch 的配置才是新主节点配置,防止旧配置覆盖新配置。

失效检测

哨兵需要能够检测出失效节点,如何检测呢?通过周期性的 PING 命令,向所有(主,从,哨兵)节点发送 PING 命令,并检查 PING 的返回结果 PONG,记录最后一次发送 ping 的时间以及最后一次收到 pong 的时间。

sentinelTimer
--> sentinelHandleDictOfRedisInstances
    --> sentinelHandleRedisInstance
        --> sentinelSendPeriodicCommands
            --> sentinelSendPing

默认间隔 1s 发送 PING 命令:

    /* Send PING to all the three kinds of instances. */
    if ((now - ri->link->last_pong_time) > ping_period &&
               (now - ri->link->last_ping_time) > ping_period/2) {
        sentinelSendPing(ri);
    }

间隔 1s:

#define SENTINEL_PING_PERIOD 1000
static mstime_t sentinel_info_period = 10000;
static mstime_t sentinel_ping_period = SENTINEL_PING_PERIOD;

仅当前节点判断则为主观下线,超过 quorum 个节点认为实例失效,则为主观下线。

判定主观下线的逻辑:

  • 未收到 PONG 响应时间 > down-after-milliseconds 配置的时间
  • 对于主节点,还需要检测 INFO 命令响应(确保不是临时网络抖动)
void sentinelCheckSubjectivelyDown(sentinelRedisInstance *ri) {
    mstime_t elapsed = 0;
    if (ri->link->act_ping_time)  // 距离上一次 PING 过去了多久
        elapsed = mstime() - ri->link->act_ping_time;
    else if (ri->link->disconnected)
        elapsed = mstime() - ri->link->last_avail_time;
    /* Update the SDOWN flag. We believe the instance is SDOWN if:
     *
     * 1) It is not replying.
     * 2) We believe it is a master, it reports to be a slave for enough time
     *    to meet the down_after_period, plus enough time to get two times
     *    INFO report from the instance. */
    if (elapsed > ri->down_after_period ||
        (ri->flags & SRI_MASTER &&
         ri->role_reported == SRI_SLAVE &&
         mstime() - ri->role_reported_time >
          (ri->down_after_period+sentinel_info_period*2)) ||
          (ri->flags & SRI_MASTER_REBOOT && 
           mstime()-ri->master_reboot_since_time > ri->master_reboot_down_after_period))
    {
        /* Is subjectively down */
        if ((ri->flags & SRI_S_DOWN) == 0) {
            sentinelEvent(LL_WARNING,"+sdown",ri,"%@");
            ri->s_down_since_time = mstime();
            ri->flags |= SRI_S_DOWN;
        }
    } else {
        /* Is subjectively up */
        if (ri->flags & SRI_S_DOWN) {
            sentinelEvent(LL_WARNING,"-sdown",ri,"%@");
            ri->flags &= ~(SRI_S_DOWN|SRI_SCRIPT_KILL_SENT);
        }
    }
}

配置节点主观下线超时时间:

sentinel down-after-milliseconds mymaster 30000  # 哨兵节点配置文件,设置实例 mymaster 失效时间

哨兵节点会定时检测主节点是否客观下线,当本哨兵节点认为主节点已经主观下线时,会向其他哨兵节点发送 SENTINEL IS-MASTER-DOWN-BY-ADDR 请求,询问其他哨兵节点当前主节点的状态。

sentinelHandleRedisInstance()
--> sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS) {
        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 : "*");    
    }

其他哨兵节点在收到 SENTINEL IS-MASTER-DOWN-BY-ADDR 请求后,回复自己认为主节点是否主观下线:

    addReply(c, isdown ? shared.cone : shared.czero);

哨兵节点周期性调用 sentinelCheckObjectivelyDown 函数检测主节点是否客观下线。

void sentinelCheckObjectivelyDown(sentinelRedisInstance *master) {
    dictIterator *di;
    dictEntry *de;
    unsigned int quorum = 0, odown = 0;

    if (master->flags & SRI_S_DOWN) {
        /* Is down for enough sentinels? */
        quorum = 1; /* the current sentinel. */
        /* Count all the other sentinels. */
        di = dictGetIterator(master->sentinels);
        while((de = dictNext(di)) != NULL) {
            sentinelRedisInstance *ri = dictGetVal(de);
            // 收集哨兵节点中关于主节点的状态
            if (ri->flags & SRI_MASTER_DOWN) quorum++;
        }
        dictReleaseIterator(di);
        if (quorum >= master->quorum) odown = 1;
    }

    /* Set the flag accordingly to the outcome. */
    if (odown) {
        if ((master->flags & SRI_O_DOWN) == 0) {
            sentinelEvent(LL_WARNING,"+odown",master,"%@ #quorum %d/%d",
                quorum, master->quorum);
            master->flags |= SRI_O_DOWN; // 标记主节点客观下线
            master->o_down_since_time = mstime();
        }
    } else {
        if (master->flags & SRI_O_DOWN) {
            sentinelEvent(LL_WARNING,"-odown",master,"%@");
            master->flags &= ~SRI_O_DOWN;
        }
    }
}

检测完成主节点是否客观下线后,判断是否需要进行故障转移,判断条件:

  • 主节点处于客观下线状态
  • 当前没有正在进行的故障转移
  • 最近是否已经尝试过故障转移(防止过于频繁的故障转移尝试)
void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {
    /* Only masters */
    if (ri->flags & SRI_MASTER) {
        // 检测主节点是否客观下线
        sentinelCheckObjectivelyDown(ri);
        // 判断是否需要进行故障转移
        if (sentinelStartFailoverIfNeeded(ri))
            // 带有 SENTINEL_ASK_FORCED 标识,要求哨兵节点选举产生 leader 哨兵节点投票
            sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);
        // 故障转移状态机
        sentinelFailoverStateMachine(ri);
        sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
    }
}

如果通过判断条件,则调用 sentinelStartFailover 进行故障转移:

/* Setup the master state to start a failover. */
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; // epoch 加 1
    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();
}

故障转移

故障转移主要分两个阶段:选举 leader 阶段,以及具体执行故障转移任务。

节点选举

在哨兵进入 SENTINEL_FAILOVER_STATE_WAIT_START 状态后,即确认主节点客观下线后,并且确认需要进行故障转移后,就进入哨兵节点 leader 节点选举阶段。第一个进入该状态的哨兵节点开始向其他哨兵请求投票。

void sentinelAskMasterStateToOtherSentinels(sentinelRedisInstance *master, int flags) {
        // * 表示请求投票给自己
        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 : "*");    
}

投票格式:

SENTINEL is-master-down-by-addr <ip> <port> <current-epoch> <runid>
  • runid 为 * 时表示”我请求投票”
  • runid 为具体 ID 时表示”我投票给该 ID”

其他节点收到请求后,判断是否进行投票:

void sentinelCommand(client *c) {
        /* Vote for the master (or fetch the previous vote) if the request
         * includes a runid, otherwise the sender is not seeking for a vote. */
        if (ri && ri->flags & SRI_MASTER && strcasecmp(c->argv[5]->ptr,"*")) {
            leader = sentinelVoteLeader(ri,(uint64_t)req_epoch,
                                            c->argv[5]->ptr,
                                            &leader_epoch);
        }
}

哨兵节点之间进行投票,选举 leader 节点,其他哨兵节点作为 observer 节点。为什么要进行选举呢?防止脑裂。只有大于本节点的 epoch,才会投票,所以对于同一个逻辑值 epoch(即,本轮次的故障转移),只能投给 1 个哨兵节点。

char *sentinelVoteLeader(sentinelRedisInstance *master, uint64_t req_epoch, char *req_runid, uint64_t *leader_epoch) {
    // 同步全局纪元到本地,更新 sentinel.conf 中的 current_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);
    }

    // 判断是否需要投票
    //  1. 本哨兵节点未在该纪元投票
    //  2. 投票 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;
        sentinelFlushConfig();
        sentinelEvent(LL_WARNING,"+vote-for-leader",master,"%s %llu",
            master->leader, (unsigned long long) master->leader_epoch);
        // 如果不是投票给自己,则设置一个随机延迟,减少竞争性故障转移。
        // 避免多个哨兵同时认为自己应该当领导者,类似 raft 算法中的领导者选举
        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;
}

流程图:

sequenceDiagram
    participant S1 as Sentinel A (检测到 O_DOWN)
    participant S2 as Sentinel B (被请求投票)
    participant M as Redis Master

    S1->>S2: SENTINEL is-master-down-by-addr<br/>+ req_epoch + req_runid
    S2->>S2: sentinelVoteLeader(master, req_epoch, req_runid, &epoch)
    alt 允许投票
        S2-->>S1: +vote-for-leader (返回新 leader)
        S2->>S2: 设置 failover_start_time (若非自投票)
    else 已投票/纪元过期
        S2-->>S1: 返回当前 leader (NULL 或旧值)
    end
    S1->>S1: 统计票数 ≥ quorum?
    alt 是
        S1->>M: 启动故障转移 (sentinelStartFailover)
    end

这里还涉及到一个问题,本节点哨兵自己投票给谁?

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);

    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. */
    // 判断当前票数最领先的候选者
    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_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;
}

经过投票阶段后:故障转移状态机判断当前谁为 leader,并开始故障转移。

void sentinelFailoverWaitStart(sentinelRedisInstance *ri) {
    char *leader;
    int isleader;

    // 获取当前 leader
    leader = sentinelGetLeader(ri, ri->failover_epoch);
    // 判断是否为 leader
    isleader = leader && strcasecmp(leader,sentinel.myid) == 0;
    sdsfree(leader);

    /* If I'm not the leader, and it is not a forced failover via
     * SENTINEL FAILOVER, then I can't continue with the failover. */
    if (!isleader && !(ri->flags & SRI_FORCE_FAILOVER)) {
        mstime_t election_timeout = sentinel_election_timeout;

        /* The election timeout is the MIN between SENTINEL_ELECTION_TIMEOUT
         * and the configured failover timeout. */
        if (election_timeout > ri->failover_timeout)
            election_timeout = ri->failover_timeout;
        /* Abort the failover if I'm not the leader after some time. */
        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();
    // leader 节点选举完成后,进入下一状态
    ri->failover_state = SENTINEL_FAILOVER_STATE_SELECT_SLAVE;
    ri->failover_state_change_time = mstime();
    sentinelEvent(LL_WARNING,"+failover-state-select-slave",ri,"%@");
}

故障转移任务执行

投票产生 leader 哨兵节点后,开始故障转移状态机:

  • SENTINEL_FAILOVER_STATE_WAIT_START: 等待开始故障转移
  • SENTINEL_FAILOVER_STATE_SELECT_SLAVE: 选择一个从节点作为新主节点
  • SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE: 发送 slaveof no one 命令给提升为新主节点的从节点
  • SENTINEL_FAILOVER_STATE_WAIT_PROMOTION: 等待新主节点的 promotion
  • SENTINEL_FAILOVER_STATE_RECONF_SLAVES: 配置新主节点的从节点,原有的从节点需要更新为新的主节点
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:
            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;
    }
}

选择一个从节点作为新主节点

leader 节点选举完成后,第一件事情就是从目前已有的从节点中选择一个作为新的主节点。

void sentinelFailoverSelectSlave(sentinelRedisInstance *ri) {
    sentinelRedisInstance *slave = sentinelSelectSlave(ri);
    // ...
}

那么选择的原则是什么呢?

  • 首先获取所有从节点
  • 筛选节点健康状态,排除处于主观下线和客观下线的从节点
  • 排除连接异常的节点(例如命令连接断开等)
  • 排除 replica-priority=0 的节点
  • 排除与原主节点断联时间超过一定阈值的节点
  • 排除 INFO 信息过旧的节点
  • 剩下的节点作为候选节点进行排序
  • 排序为三级排序:
    • 首要为从节点优先级(replica-priority)
    • 其次为复制偏移量,优先选数据落后主节点最少的从节点
    • 最后按 runid 进行排序,兜底

将从节点提升为主节点

选择好待提升的从节点后,执行 sentinelFailoverSendSlaveOfNoOne 函数,将从节点提升为主节点。

void sentinelFailoverSendSlaveOfNoOne(sentinelRedisInstance *ri) {
    int retval;

    /* We can't send the command to the promoted slave if it is now
     * disconnected. Retry again and again with this state until the timeout
     * is reached, then abort the failover. */
    if (ri->promoted_slave->link->disconnected) {
        if (mstime() - ri->failover_state_change_time > ri->failover_timeout) {
            sentinelEvent(LL_WARNING,"-failover-abort-slave-timeout",ri,"%@");
            sentinelAbortFailover(ri);
        }
        return;
    }

    /* Send SLAVEOF NO ONE command to turn the slave into a master.
     * We actually register a generic callback for this command as we don't
     * really care about the reply. We check if it worked indirectly observing
     * if INFO returns a different role (master instead of slave). */
    retval = sentinelSendSlaveOf(ri->promoted_slave,NULL);
    if (retval != C_OK) return;
    sentinelEvent(LL_NOTICE, "+failover-state-wait-promotion",
        ri->promoted_slave,"%@");
    ri->failover_state = SENTINEL_FAILOVER_STATE_WAIT_PROMOTION;
    ri->failover_state_change_time = mstime();
}

提升从节点执行过程:

通过事务命令:

  • MULTI 开启事务
  • SLAVEOF NO ONE 从节点提升为主节点
  • CONFIG REWRITE 将新配置重写到配置文件中
  • CLIENT KILL TYPE normal 断开普通客户端连接
  • CLIENT KILL TYPE pubsub 断开发布订阅客户端连接
  • EXEC 执行事务

执行提升命令后,等待从节点被提升为主节点完成。通过 INFO 命令间接等待从节点提升为主节点。

// 检测故障转移是否超时
void sentinelFailoverWaitPromotion(sentinelRedisInstance *ri) {
    /* Just handle the timeout. Switching to the next state is handled
     * by the function parsing the INFO command of the promoted slave. */
    if (mstime() - ri->failover_state_change_time > ri->failover_timeout) {
        sentinelEvent(LL_WARNING,"-failover-abort-slave-timeout",ri,"%@");
        sentinelAbortFailover(ri);
    }
}

哨兵节点会周期性的向主节点以及从节点发送 INFO 命令。

void sentinelSendPeriodicCommands(sentinelRedisInstance *ri) {
    // 向主节点和从节点发送 INFO 命令,sentinelInfoReplyCallback 处理响应
    if ((ri->flags & SRI_SENTINEL) == 0 &&
        (ri->info_refresh == 0 ||
        (now - ri->info_refresh) > info_period))
    {
        retval = redisAsyncCommand(ri->link->cc,
            sentinelInfoReplyCallback, ri, "%s",
            sentinelInstanceMapCommand(ri,"INFO"));
        if (retval == C_OK) ri->link->pending_commands++;
    }
}

哨兵节点响应主节点、从节点的 INFO 回应。当检测到从节点 role 字段变为 master 时,触发提升确认逻辑。

void sentinelInfoReplyCallback(redisAsyncContext *c, void *reply, void *privdata) {
    if (r->type == REDIS_REPLY_STRING)
        sentinelRefreshInstanceInfo(ri,r->str);
}

void sentinelRefreshInstanceInfo(sentinelRedisInstance *ri, const char *info) {
    /* Handle slave -> master role switch. */
    if ((ri->flags & SRI_SLAVE) && role == SRI_MASTER) {
        /* If this is a promoted slave we can change state to the
         * failover state machine. */
        if ((ri->flags & SRI_PROMOTED) &&
            (ri->master->flags & SRI_FAILOVER_IN_PROGRESS) &&
            (ri->master->failover_state ==
                SENTINEL_FAILOVER_STATE_WAIT_PROMOTION))
        {
            /* Now that we are sure the slave was reconfigured as a master
             * set the master configuration epoch to the epoch we won the
             * election to perform this failover. This will force the other
             * Sentinels to update their config (assuming there is not
             * a newer one already available). */
            // 确认从节点已被提升为新主节点,重写配置
            // 触发哨兵 leader 执行客户端重配置脚本
            ri->master->config_epoch = ri->master->failover_epoch;
            ri->master->failover_state = SENTINEL_FAILOVER_STATE_RECONF_SLAVES; // 进入下一个状态
            ri->master->failover_state_change_time = mstime();
            sentinelFlushConfig();
            sentinelEvent(LL_WARNING,"+promoted-slave",ri,"%@");
            if (sentinel.simfailure_flags &
                SENTINEL_SIMFAILURE_CRASH_AFTER_PROMOTION)
                sentinelSimFailureCrash();
            sentinelEvent(LL_WARNING,"+failover-state-reconf-slaves",
                ri->master,"%@");
            sentinelCallClientReconfScript(ri->master,SENTINEL_LEADER,
                "start",ri->master->addr,ri->addr);
            sentinelForceHelloUpdateForMaster(ri->master);
        } else {
            /* A slave turned into a master. We want to force our view and
             * reconfigure as slave. Wait some time after the change before
             * going forward, to receive new configs if any. */
            mstime_t wait_time = sentinel_publish_period*4;
            // 不是预期的从节点变为主节点,强制它变为从节点
            if (!(ri->flags & SRI_PROMOTED) &&
                 sentinelMasterLooksSane(ri->master) &&
                 sentinelRedisInstanceNoDownFor(ri,wait_time) &&
                 mstime() - ri->role_reported_time > wait_time)
            {
                int retval = sentinelSendSlaveOf(ri,ri->master->addr);
                if (retval == C_OK)
                    sentinelEvent(LL_NOTICE,"+convert-to-slave",ri,"%@");
            }
        }
    }
}

其他从节点重配置

前面处理完待提升的从节点提升为新主节点后,其他从节点需要变更配置,主要变更其 slaveof 的主节点信息,将旧主信息变更为新主节点:SLAVE OF <new master address>

void sentinelFailoverReconfNextSlave(sentinelRedisInstance *master) {
        // 向其他从节点发送 slaveof newmaster 命令
        /* Send SLAVEOF <new master>. */
        retval = sentinelSendSlaveOf(slave,master->promoted_slave->addr);
        if (retval == C_OK) {
            slave->flags |= SRI_RECONF_SENT;
            slave->slave_reconf_sent_time = mstime();
            sentinelEvent(LL_NOTICE,"+slave-reconf-sent",slave,"%@");
            in_progress++;
        }

    // 检测从节点是否重配置完成
    sentinelFailoverDetectEnd(master);    
}
58221:X 29 Jan 2026 13:56:24.668 # +sdown master mymaster 192.168.232.128 6379
58221:X 29 Jan 2026 13:56:24.682 * Sentinel new configuration saved on disk
58221:X 29 Jan 2026 13:56:24.682 # +new-epoch 1
58221:X 29 Jan 2026 13:56:24.686 * Sentinel new configuration saved on disk
58221:X 29 Jan 2026 13:56:24.686 # +vote-for-leader fe6e146804ee6df95a04afa50d27a7d651ab33d2 1
58221:X 29 Jan 2026 13:56:24.745 # +odown master mymaster 192.168.232.128 6379 #quorum 3/2
58221:X 29 Jan 2026 13:56:24.745 # Next failover delay: I will not start a failover before Thu Jan 29 13:56:45 2026
58221:X 29 Jan 2026 13:56:45.133 * Sentinel new configuration saved on disk
58221:X 29 Jan 2026 13:56:45.133 # +new-epoch 2
58221:X 29 Jan 2026 13:56:45.135 * Sentinel new configuration saved on disk
58221:X 29 Jan 2026 13:56:45.135 # +vote-for-leader 556372190acd0c77c0bc6760178830ed66ddac08 2

哨兵节点的角色

哨兵节点有两种角色:

  • leader: 通过选举产生
  • observer: 如果不是 leader,则只能作为观察者
#define SENTINEL_LEADER (1<<17)
#define SENTINEL_OBSERVER (1<<18)

哨兵节点需要的命令

哨兵需要执行的命令:

  • INFO 获取从节点以及获取复制偏移量等信息
  • PING 节点探测
  • MULTI、SLAVEOF、CONFIG REWRITE、EXEC、CLIENT 从节点提升为主节点,将配置修改重写配置文件