Redis 哨兵模式
Redis 哨兵核心实现
哨兵需要具有发现节点的功能,发现主节点,从节点,哨兵节点,即,哨兵需要知道当前的 Redis 部署网络视图。
哨兵需要判断节点运行状态,判断主节点是否正常工作。
哨兵需要就主节点下线达成共识并进行故障转移,选举一个 leader 节点进行故障转移。

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
| 字段 | 说明 | 作用 |
|---|---|---|
| 0 | Sentinel IP | 节点网络地址 |
| 1 | Sentinel 端口 | 节点通信端口 |
| 2 | Sentinel runid | 节点唯一标识 |
| 3 | current_epoch | 逻辑时钟(用于故障转移) |
| 4 | master_name | 监控的主节点名称 |
| 5 | master_ip | 主节点当前 IP |
| 6 | master_port | 主节点当前端口 |
| 7 | master_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 从节点提升为主节点,将配置修改重写配置文件