编号: 136 分类: 09 Redis 与缓存体系 预计阅读时间: 40-50 分钟
1. 引言:为什么需要 Sentinel?
在单机 Redis 时代,一个 Redis 实例挂了,服务就停了。引入主从复制后,我们有了数据冗余——主节点负责写,从节点负责读和备份。但主从复制解决了一个问题,又带来了新问题:主节点挂了怎么办?
手动切换的流程大约是这样的:运维同学收到告警 → 登录机器确认主节点确实挂了 → 挑选一个数据最新的从节点 → 执行 SLAVEOF NO ONE 将其提升为主 → 修改其他从节点指向新主 → 修改应用配置。整个过程快则几分钟,慢则十几分钟,期间服务不可写。
Redis Sentinel 就是为自动化这个流程而生的。它是一个分布式的高可用解决方案,核心能力包括:
| 能力 | 说明 |
|---|---|
| 监控(Monitoring) | 持续检查主节点和从节点是否正常运行 |
| 通知(Notification) | 通过 API 或 Pub/Sub 将故障事件推送给运维和应用程序 |
| 自动故障转移(Automatic Failover) | 主节点不可用时,自动选举新主并完成切换 |
| 配置提供者(Configuration Provider) | 客户端连接 Sentinel 获取当前的主节点地址 |
理解 Sentinel 的关键在于认识到:Sentinel 本身也是一个分布式系统。如果只有一个 Sentinel 进程,它自己挂了谁来保证高可用?所以 Sentinel 必须部署多个实例(至少 3 个),它们之间通过共识来判定故障状态和决策。
2. Sentinel 架构总览
2.1 物理部署拓扑
一个典型的 Sentinel 部署架构如下:
- Redis 节点:1 主 + N 从
- Sentinel 节点:至少 3 个,部署在不同物理机或可用区
- 客户端:通过 Sentinel 连接获取当前主节点信息
┌─────────────────────────────────────────────────────┐
│ 客户端 (Client) │
│ 连接 Sentinel 获取主节点地址 │
└──────────┬──────────┬──────────┬────────────────────┘
│ │ │
┌──────▼──┐ ┌───▼────┐ ┌──▼──────┐
│Sentinel1│ │Sentinel2│ │Sentinel3│
└────┬─────┘ └────┬───┘ └────┬────┘
│ │ │
┌────▼────┐ ┌───▼────┐ ┌───▼─────┐
│ Master │◄───│ Slave1 │ │ Slave2 │
└─────────┘ └────────┘ └─────────┘

2.2 Sentinel 内部视角
每个 Sentinel 进程内部维护着它所监控的所有实例的状态,包括:
- 主节点状态:IP、端口、运行状态、角色、配置纪元(configEpoch)等
- 从节点状态:IP、端口、运行状态、所属主节点、复制偏移量等
- 其他 Sentinel 状态:IP、端口、最后通信时间、对该主节点的投票等
这些状态信息通过 Sentinel 之间的 Gossip 协议和 与 Redis 节点的 INFO 命令 来持续更新。
2.3 核心数据结构
在 Redis 源码中,Sentinel 的核心数据结构是 sentinelRedisInstance(src/sentinel.c),它用一个 int flags 字段的低位来标识实例类型:
#define SRI_MASTER (1<<0) // 主节点
#define SRI_SLAVE (1<<1) // 从节点
#define SRI_SENTINEL (1<<2) // 哨兵节点
每个 Sentinel 实例中有一个 sentinel.masters 字典,以主节点名称为 key,存储了该主节点及其所有从节点、其他 Sentinel 的状态树。
3. 监控机制:Sentinel 如何发现和感知
3.1 三个信息通道
Sentinel 通过三条通道获取 Redis 集群的信息:
通道一:配置文件中的主节点地址
Sentinel 启动时,从配置文件中读取要监控的主节点:
sentinel monitor mymaster 192.168.1.100 6379 2
参数 2 是 quorum 值,表示至少需要多少个 Sentinel 同意才能判定主节点客观下线。
通道二:**INFO**命令发现从节点
Sentinel 每 10 秒向主节点发送 INFO 命令,从返回结果中解析出:
- 所有从节点的 IP 和端口
- 主节点的
runid(runid变化说明主节点重启过) - 从节点的复制偏移量(
slave_repl_offset)和优先级(slave_priority)
发现的从节点会被加入 sentinel.masters[master_name].slaves 字典中,Sentinel 然后每 10 秒向这些从节点也发送 INFO 命令。
通道三:Pub/Sub**__sentinel__:hello**频道发现其他 Sentinel
Sentinel 每 2 秒向所有监控的 Redis 节点的 __sentinel__:hello 频道发布一条消息,内容包含自己的 IP、端口、runid、当前配置纪元等。同时,Sentinel 也订阅这个频道,从其他 Sentinel 发布的消息中获知它们的存在。
这种机制非常巧妙:Sentinel 之间不需要互相配置,只要它们监控同一个主节点,通过 Pub/Sub 就能自动发现彼此。

3.2 三个定时任务
Sentinel 有三个核心定时任务驱动整个监控循环:
| 任务 | 频率 | 目标 | 作用 |
|---|---|---|---|
| PING 检查 | 每秒 | 所有已知实例 | 检测节点是否存活 |
| INFO 获取 | 每 10 秒 | 主节点和从节点 | 发现拓扑变化、复制状态 |
| Hello 消息 | 每 2 秒 | 通过 Pub/Sub | 发现其他 Sentinel,交换信息 |
Sentinel 的事件循环基于 aeEventLoop(Redis 的事件驱动框架),它不是一个传统的周期性定时器,而是通过 serverCron 中调用 sentinelTimer() 来实现周期性任务的分发。sentinelTimer() 内部通过 sentinel.last_ping_time、sentinel.last_info_time 等时间戳变量来判断是否到了执行时间。
3.3 PING 检查的细节
PING 检查看似简单,但有几个关键参数:
**down-after-milliseconds**:Sentinel 在多少毫秒内没有收到实例的有效回复,就将其标记为主观下线。这个值对主节点和从节点都适用,但建议主节点的值设得比从节点小一些,以便更快发现主节点故障。**sentinel.ping_period**:PING 发送频率,默认 1 秒。
PING 的回复可以是 +PONG、-LOADING(正在加载数据)、-MASTERDOWN(主节点已下线)等。只要收到合法回复,就认为实例"存活"。
4. 主观下线(SDOWN)
4.1 什么是主观下线
主观下线(Subjectively Down,简称 SDOWN)是指单个 Sentinel 实例基于自己的检测结果,判断某个 Redis 实例不可达。
之所以叫"主观",是因为这是单点视角的判断。可能只是 Sentinel 和 Redis 之间的网络抖动,而不是 Redis 真的挂了。所以 SDOWN 的阈值设得不能太敏感——一般设为 down-after-milliseconds 默认 30000ms(30 秒)。
4.2 SDOWN 的判定流程
从源码角度,SDOWN 的判定在 sentinelCheckSubjectivelyDown() 函数中:
void sentinelCheckSubjectivelyDown(sentinelRedisInstance *ri) {
mstime_t elapsed = 0;
if (ri->link->act_ping_time)
elapsed = mstime() - ri->link->act_ping_time;
else if (ri->link->disconnected)
elapsed = mstime() - ri->link->last_avail_time;
if (elapsed > ri->down_after_period ||
(ri->flags & SRI_MASTER &&
ri->role_reported == SRI_SLAVE &&
mstime() - ri->role_reported_time >
(ri->down_after_period * 2)))
{
// 标记为主观下线
if ((ri->flags & SRI_S_DOWN) == 0) {
ri->flags |= SRI_S_DOWN;
ri->s_down_since_time = mstime();
}
} else {
// 清除主观下线
if (ri->flags & SRI_S_DOWN) {
ri->flags &= ~SRI_S_DOWN;
}
}
}
这里有两条判定路径:
- PING 超时:最后一次 PING 回复的时间距今超过
down-after-milliseconds。 - 主节点角色变化:如果主节点自己报告说它变成了从节点(说明发生了切换),且持续时间超过
down-after-milliseconds * 2,也标记为 SDOWN。
4.3 SDOWN 的陷阱
陷阱一:网络分区导致的误判
如果 Sentinel 所在机器和 Redis 所在机器之间的网络断了,但 Redis 本身正常运行,Sentinel 仍会标记 SDOWN。这就是为什么需要多个 Sentinel 来确认——单个 Sentinel 的视角不可靠。
陷阱二:**down-after-milliseconds**设得太小
如果设为 5000ms(5 秒),一个短暂的 GC 停顿或网络抖动就可能导致误判。建议生产环境设为 30 秒以上。
陷阱三:主节点主观下线后的特殊处理
当 Sentinel 标记主节点为 SDOWN 后,会触发 sentinelCheckObjectivelyDown() 来与其他 Sentinel 商议,进入客观下线的判定流程。
5. 客观下线(ODOWN)
5.1 什么是客观下线
客观下线(Objectively Down,简称 ODOWN)是指多个 Sentinel 达成共识,认为主节点确实不可用。
ODOWN 只适用于主节点。从节点和 Sentinel 节点只需要 SDOWN 判定,不需要 ODOWN。
5.2 ODOWN 的判定流程
当某个 Sentinel 将主节点标记为 SDOWN 后,它会执行 sentinelCheckObjectivelyDown():
void sentinelCheckObjectivelyDown(sentinelRedisInstance *master) {
dictIterator *di;
dictEntry *de;
int quorum = 0, odown = 0;
// 如果我自己认为主节点 SDOWN
if (master->flags & SRI_S_DOWN) {
quorum = 1; // 自己算一票
di = dictGetIterator(master->sentinels);
while ((de = dictNext(di)) != NULL) {
sentinelRedisInstance *ri = dictGetVal(de);
// 其他 Sentinel 也认为主节点 SDOWN
if (ri->flags & SRI_MASTER_DOWN)
quorum++;
}
dictReleaseIterator(di);
// 达成票数 >= quorum 配置值
if (quorum >= master->quorum) {
odown = 1;
}
}
if (odown) {
if ((master->flags & SRI_O_DOWN) == 0) {
master->flags |= SRI_O_DOWN;
master->o_down_since_time = mstime();
// 触发故障转移
sentinelEvent(LL_WARNING, "+odown", master, "%@");
}
} else {
if (master->flags & SRI_O_DOWN) {
master->flags &= ~SRI_O_DOWN;
}
}
}
关键点:
- 当前 Sentinel 会向其他 Sentinel 发送
SENTINEL is-master-down-by-addr命令,询问它们是否也认为主节点下线。 - 收集投票,如果赞同票数 >= quorum,则标记为 ODOWN。
- 标记 ODOWN 后,立即触发故障转移流程。
5.3 quorum 的配置策略

quorum 值的设置需要权衡:
- quorum 太小:容易因为网络抖动而产生误判,导致不必要的故障转移。
- quorum 太大:实际的故障无法被及时发现,可能因为少数 Sentinel 不可达而无法达成共识。
经典配置建议:
| Sentinel 数量 | quorum 建议 | 原因 |
|---|---|---|
| 3 | 2 | 容忍 1 个 Sentinel 网络分区 |
| 5 | 3 | 容忍 2 个 Sentinel 故障 |
| 7 | 4 | 容忍 3 个 Sentinel 故障 |

注意:quorum 只决定"是否判定 ODOWN",但故障转移的执行还需要多数 Sentinel 选举出一个 Leader。所以即使 quorum=2(3 个 Sentinel),选举 Leader 也需要至少 2 票(N/2+1)。
5.4 SDOWN 与 ODOWN 的关系
flowchart TD
A[Sentinel 定期 PING] --> B{PING 超时?}
B -->|否| A
B -->|是| C[标记 SDOWN]
C --> D{是主节点?}
D -->|否| E[仅 SDOWN<br/>等待恢复或遗忘]
D -->|是| F[向其他 Sentinel 询问]
F --> G{赞同票 >= quorum?}
G -->|否| H[保持 SDOWN<br/>继续等待]
G -->|是| I[标记 ODOWN]
I --> J[触发 Leader 选举]
J --> K[执行故障转移]
6. Sentinel 选举:谁来做故障转移?
6.1 为什么需要选举
ODOWN 判定后,不是所有 Sentinel 都去执行故障转移。如果多个 Sentinel 同时执行,就会产生多个新主节点,导致数据分裂。因此,Sentinel 之间需要先选举出一个 Leader,由 Leader 来执行故障转移。
6.2 选举算法:Raft 的简化版
Sentinel 的选举算法类似于 Raft 共识算法的 Leader 选举,但做了简化:
核心流程:
- 发现 ODOWN 的 Sentinel 发起投票,给自己投一票,并向其他 Sentinel 发送
SENTINEL is-master-down-by-addr命令(带runid参数表示投票请求)。 - 收到投票请求的 Sentinel 按照规则决定是否投票:
- 规则一:每个纪元只能投一票(
leader_epoch机制)。 - 规则二:先到先得——先收到谁的请求就给谁投票。
- 规则三:不投给自己认为主节点健康的 Sentinel——如果收到请求的 Sentinel 没有把主节点标记为 SDOWN,它不会投票。
- 规则一:每个纪元只能投一票(
- 发起投票的 Sentinel 统计得票数,如果票数 >= N/2+1(多数派)且 >= quorum,则当选为 Leader。
- 如果在规定时间内(
sentinel.failover_timeout的 2 倍)没有选举出 Leader,则纪元 +1,重新选举。
6.3 配置纪元(Config Epoch)

configEpoch 是 Sentinel 用来保证故障转移一致性的关键机制:
- 每次发起新的故障转移时,
configEpoch会递增。 - 投票时,Sentinel 会携带自己的
configEpoch,只有 epochs 匹配的投票才有效。 - 故障转移成功后,Leader 会把新的
configEpoch广播给所有 Sentinel。
这个机制类似于 Raft 的 term 概念,防止过期的投票干扰当前的选举。
6.4 选举的源码要点
// 发送投票请求给其他 Sentinel
char *sentinelVoteLeader(sentinelRedisInstance *master,
uint64_t req_epoch,
char *req_runid,
uint64_t *leader_epoch) {
// 如果请求的纪元比当前纪元更大,更新当前纪元
if (req_epoch > sentinel.current_epoch) {
sentinel.current_epoch = req_epoch;
// 重置本纪元的投票
sentinel.leader = NULL;
}
// 如果本纪元已经投过票,返回已投票的 Leader
if (sentinel.leader && sentinel.leader_epoch == req_epoch) {
*leader_epoch = sentinel.leader_epoch;
return sentinel.leader;
}
// 如果请求的纪元等于当前纪元,投票给自己
sentinel.leader = sdsnew(req_runid);
sentinel.leader_epoch = sentinel.current_epoch;
*leader_epoch = sentinel.leader_epoch;
return sentinel.leader;
}
6.5 选举失败的场景
场景一:Sentinel 数量不足
如果只有 2 个 Sentinel,多数派是 2。1 个 Sentinel 挂了,另一个永远无法获得多数票,故障转移无法执行。所以生产环境至少部署 3 个 Sentinel。
场景二:网络分区
如果 5 个 Sentinel 被分成 3 和 2 两个分区,关联主节点的一侧有 3 个 Sentinel,能够达成多数派并执行故障转移。另一侧只有 2 个,无法达成多数派,故障转移不会执行。
7. 故障转移:如何选出新主?
7.1 故障转移的整体流程
Leader 当选后,故障转移分为以下步骤:
1. 从当前从节点中选出新的主节点
2. 让选中的从节点执行 SLAVEOF NO ONE
3. 让其他从节点指向新主节点
4. 当旧主节点恢复后,让它成为新主节点的从节点
5. 通知客户端新主节点地址

7.2 从节点筛选和排序
不是所有从节点都有资格成为新主节点。Leader 按以下规则筛选:
第一步:过滤掉不合格的从节点
- 与主节点断开连接超过
down-after-milliseconds * 10的从节点(数据可能太旧) - 标记为 SDOWN 的从节点
- 有
SRI_SCRIPT_KILL_SENT标记的从节点
第二步:按优先级排序
redis.conf 中的 slave-priority 参数(值越小越优先,0 表示永不提升):
slave-priority 100 # 默认值,数值越小优先级越高
slave-priority 0 # 永远不参与选举
第三步:同优先级按复制偏移量排序
复制偏移量越大,说明数据越新,越接近主节点。
第四步:同偏移量按 runid 字典序排序
runid 在字典序上更小的优先。这是一个确定的排序,确保所有 Sentinel 在同一条件下选出相同的结果。
7.3 执行 SLAVEOF NO ONE
Leader 选定新主节点后,向它发送 SLAVEOF NO ONE 命令,该从节点会:
- 关闭与旧主节点的复制连接
- 清空复制状态
- 将自己切换为主节点角色
- 可以接受写操作
7.4 重新配置其他从节点
Leader 向其他从节点发送 SLAVEOF <new_master_ip> <new_master_port> 命令,让它们开始复制新主节点。这里有一个优化:如果从节点已经有新主节点的数据(复制偏移量在范围内),可以进行部分同步而不是全量同步。
7.5 处理旧主节点恢复
当旧主节点恢复后,Sentinel 会检测到它,并发送 SLAVEOF 命令让它成为新主节点的从节点。这是安全的,因为:
- 旧主节点在故障期间可能写入了一些数据,但这些数据在切换到新主节点后已经丢失了。
- 让旧主节点成为从节点,它会从新主节点全量同步,丢弃自己作为旧主节点期间的写入。
这就引出了 Redis 主从复制的一个核心特性:Redis 的复制是异步的,故障转移可能会导致数据丢失。如果旧主节点在故障前有未同步到从节点的写入,这些数据将会丢失。
7.6 故障转移的配置参数
| 参数 | 说明 | 默认值 |
|---|---|---|
sentinel failover-timeout |
故障转移超时时间 | 180000ms(3 分钟) |
sentinel parallel-syncs |
同时向新主节点同步的从节点数量 | 1 |
sentinel client-reconfig-script |
故障转移后执行的脚本 | 无 |
parallel-syncs 设为 1 是为了避免 RDB 全量同步时多个从节点同时请求导致主节点 CPU 和 I/O 压力过大。

8. 客户端发现与服务发现
8.1 Sentinel 作为配置提供者
客户端不应直接连接 Redis 主节点,而应通过 Sentinel 获取当前主节点地址。这是 Sentinel 作为"配置提供者"的角色。
客户端连接 Sentinel 的流程:
- 客户端连接任意一个 Sentinel,发送
SENTINEL get-master-addr-by-name <master-name>获取当前主节点地址。 - 客户端连接主节点,通过
ROLE命令验证自己确实连接到了主节点。
3. 客户端订阅 Sentinel 的 +switch-master 频道,当主节点发生切换时,Sentinel 会发布通知,客户端收到后更新连接。

8.2 客户端库的实现
以 Jedis(Java Redis 客户端)为例:
Set<String> sentinels = new HashSet<>();
sentinels.add("sentinel1:26379");
sentinels.add("sentinel2:26379");
sentinels.add("sentinel3:26379");
JedisSentinelPool pool = new JedisSentinelPool("mymaster", sentinels);
Jedis jedis = pool.getResource(); // 自动获取当前主节点连接
JedisSentinelPool 内部维护了一个 Sentinel 监听器,当收到 +switch-master 消息后,会自动重建连接池。
8.3 Pub/Sub 通知频道
Sentinel 通过以下频道发布事件:
| 频道 | 事件 |
|---|---|
+sdown |
实例被标记为主观下线 |
-sdown |
实例主观下线已恢复 |
+odown |
主节点被标记为客观下线 |
-odown |
主节点客观下线已恢复 |
+switch-master |
主节点发生切换(携带新主节点信息) |
+slave |
新从节点被识别 |
9. 配置与部署实践
9.1 最小生产配置
# sentinel.conf
# 监控主节点:名称 mymaster,地址 192.168.1.100:6379,quorum=2
sentinel monitor mymaster 192.168.1.100 6379 2
# 主观下线判定时间:30 秒
sentinel down-after-milliseconds mymaster 30000
# 故障转移超时时间:3 分钟
sentinel failover-timeout mymaster 180000
# 同时向新主同步的从节点数量:1
sentinel parallel-syncs mymaster 1
# 认证密码(如果 Redis 设置了密码)
sentinel auth-pass mymaster your_secure_password
9.2 部署拓扑建议
| 部署策略 | 说明 |
|---|---|
| 3 个 Sentinel | 最小推荐配置,部署在 3 台不同物理机或 3 个可用区 |
| 奇数个 Sentinel | 确保能达成多数派(N/2+1) |
| Sentinel 与 Redis 分离 | Sentinel 不要和 Redis 部署在同一台机器,避免单点故障 |
| 独立网络 | Sentinel 之间的通信链路应独立于业务流量 |
9.3 运维注意事项
Sentinel 只读配置文件
Sentinel 启动后,会重写自己的配置文件,将发现的从节点和其他 Sentinel 写入 sentinel.conf。但不要手动修改 Sentinel 运行时的配置,修改后需要执行 SENTINEL RESET 或重启 Sentinel 才能生效。
切换中的数据一致性
故障转移过程中,客户端可能短暂地连接到旧主节点并写入数据。这些写入在切换完成后会丢失。如果你的业务对数据一致性要求极高,需要在上层做幂等和补偿。
脑裂场景

如果网络分区导致两个 Sentinel 组各自选出一个 Leader 并执行故障转移,就会出现两个主节点。这称为"脑裂"。Redis 的 min-slaves-to-write 和 min-slaves-max-lag 配置可以缓解这个问题:主节点检测到从节点不足时,拒绝写入。
10. 常见问题与排查
问题一:故障转移不执行
现象:主节点已经挂了,但 Sentinel 没有执行故障转移。
排查:
- 检查 quorum 是否满足:
SENTINEL masters查看num-other-sentinels和quorum。 - 检查是否有 Leader 选举成功:
SENTINEL sentinels mymaster查看各 Sentinel 的状态。 - 检查
failover-timeout是否过长。
问题二:故障转移后客户端连接失败
现象:故障转移完成后,客户端报连接错误。
排查:
- 确认客户端是否通过 Sentinel 获取地址(而非直连 Redis)。
- 确认客户端库是否正确订阅了
+switch-master频道。 - 检查客户端是否缓存了旧的主节点地址。
问题三:频繁切换
现象:主节点在短时间内反复切换。
原因:
down-after-milliseconds设得太小- 网络不稳定
- 主节点负载过高导致 PING 超时
解决:增大 down-after-milliseconds,排查网络和主机性能问题。
11. 与 Redis Cluster 的对比
| 维度 | Sentinel | Cluster |
|---|---|---|
| 数据分片 | 不支持(单主) | 支持(16384 槽) |
| 高可用 | 自动故障转移 | 自动故障转移 |
| 扩展性 | 垂直扩展 | 水平扩展 |
| 复杂度 | 较低 | 较高 |
| 适用场景 | 中小规模、数据量可控 | 大规模、数据量超内存 |
选型建议:如果你的数据量超过单机内存,或者需要水平扩展,应该选择 Cluster。如果数据量可控,只需要高可用,Sentinel 是更简单的选择。
12. 总结
Redis Sentinel 是一个完整的分布式高可用方案,核心机制层层递进:
- 监控:通过 INFO/PING/Pub-Sub 三条通道感知整个集群状态。
- SDOWN:单 Sentinel 视角的故障判定,可能误判。
- ODOWN:多 Sentinel 共识的故障判定,触发故障转移。
- 选举:类 Raft 算法的 Leader 选举,确保只有一个 Sentinel 执行故障转移。
- 故障转移:按优先级和偏移量选出新主,重配置所有从节点。
理解 Sentinel 不仅是为了应对面试,更是为了在生产环境中正确配置和运维 Redis 高可用集群。记住它的设计哲学:一个分布式系统本身的可靠性,不应该由单点来保证。
面试提问与参考答案
以下问题从面试官视角设计,考察对 Sentinel 原理的深度理解,而非简单记忆文档内容。
Q1:Sentinel 的 quorum 和故障转移的多数派投票有什么区别?为什么需要两个不同的阈值?
参考答案:
这两个概念在 Sentinel 中服务于不同的阶段,混淆它们会导致生产事故。
quorum 用于判定 ODOWN(客观下线),即多少个 Sentinel 同意主节点不可用,才能触发故障转移流程。quorum 通过 sentinel monitor mymaster 192.168.1.100 6379 2 中的最后一个参数配置(此例中为 2)。
多数派投票(majority) 用于 Leader 选举,即 Sentinel 之间选出一个 Leader 来执行故障转移,得票数必须超过 Sentinel 总数的半数(N/2+1)。
为什么需要两个不同的阈值?
这两个阈值服务于不同的目标:
| 维度 | quorum(ODOWN 判定) | majority(Leader 选举) |
|---|---|---|
| 目标 | 检测故障,触发流程 | 选举唯一执行者,防止脑裂 |
| 取值 | 可配置,可小于 majority | 固定为 N/2+1 |
| 失败后果 | 故障不被发现,服务中断 | 无人执行故障转移(不会脑裂) |
举例:5 个 Sentinel,quorum=2,majority=3。如果网络分区导致 3 个 Sentinel 可见主节点故障,满足 quorum=2 即可判定 ODOWN,但 Leader 选举必须获得至少 3 票。这保证了即使 quorum 设得较低、故障能被快速发现,执行者也必须获得多数派认可,不会出现两个 Leader 同时执行故障转移。
面试官追问:如果 quorum=2 但只剩 2 个 Sentinel 存活,ODOWN 满足但 majority 不满足(需要 3),故障转移能执行吗?
答案:不能。这是 Sentinel 的安全性优先原则——宁可牺牲可用性(不执行故障转移),也不能在无法达成共识时冒险执行可能导致脑裂的操作。
Q2:如果网络出现分区,两个 Sentinel 组各自认为自己是多数派,会发生什么?Redis 如何防止脑裂?
参考答案:
场景分析:假设 5 个 Sentinel 被分成两组——3 个 Sentinel + 旧主在分区 A,2 个 Sentinel 在分区 B。
分区 A 的情况:3 个 Sentinel 检测到旧主不可达(如果旧主也在分区 A 则可达),满足 quorum 和 majority,可以选举 Leader 并执行故障转移,从分区 A 的从节点中选出新主。
分区 B 的情况:只有 2 个 Sentinel,无法满足 majority=3,因此无法选举 Leader,不会执行故障转移。旧主如果不在分区 B,分区 B 的客户端无法写入,但也不会出现两个主节点。
Redis 防止脑裂的机制:
- majority 选举:Leader 选举必须获得超过半数的选票,网络分区后至多只有一个分区满足此条件。
**min-slaves-to-write**和**min-slaves-max-lag**:配置旧主在检测到可用从节点不足时拒绝写入。如果旧主在分区 B 中,它发现从节点数量不足,会拒绝所有写请求,防止数据在两个分区中产生分歧。
min-slaves-to-write 1
min-slaves-max-lag 10
这表示:如果从节点数量少于 1 个,或者从节点延迟超过 10 秒,主节点拒绝写入。
3. 配置纪元(configEpoch):新 Leader 执行故障转移后,会递增 configEpoch 并广播。旧主恢复后收到更高的纪元,发现自己被降级,自动成为从节点。
面试官追问:如果 min-slaves-to-write 没配置,脑裂期间写入旧主的数据会怎样?
答案:旧主继续接受写入,但这些数据无法被新主感知。当旧主恢复并发现自己被降级后,会从新主全量同步,脑裂期间写入旧主的所有数据将永久丢失。这就是为什么生产环境必须配置 min-slaves-to-write。
Q3:Sentinel 选举算法和 Raft 的 Leader 选举有何异同?为什么 Sentinel 不直接使用 Raft?
参考答案:
相同点:
| 特性 | Sentinel | Raft |
|---|---|---|
| 纪元机制 | configEpoch 递增 |
term 递增 |
| 投票规则 | 先到先得,每纪元一票 | 先到先得,每 term 一票 |
| 多数派获胜 | 需要 N/2+1 票 | 需要 N/2+1 票 |
| 选举失败处理 | 纪元 +1,重试 | term +1,随机超时后重试 |
不同点:
| 维度 | Sentinel | Raft |
|---|---|---|
| 随机超时 | 无,使用固定超时 | 有,随机选举超时减少冲突 |
| Leader 职责 | 执行一次故障转移后即结束 | 持续心跳 + 日志复制 |
| 日志复制 | 无 | 有,Leader 负责日志同步 |
| 提交规则 | 无 | 需要多数派确认日志提交 |
| 状态机 | 无 | 每个节点维护状态机 |
为什么 Sentinel 不直接使用 Raft?
Sentinel 只需要选举一个 Leader 来执行一次性的故障转移操作,不需要 Raft 的完整功能(日志复制、状态机、提交规则等)。引入完整 Raft 会显著增加代码复杂度,而收益有限。Sentinel 的选举算法是 Raft 选举的最小化子集——只保留了保证唯一性所需的核心机制。
面试官追问:Sentinel 不使用随机选举超时,如果多个 Sentinel 同时发现 ODOWN 并同时发起选举,会不会永远选不出 Leader?
答案:理论上可能发生冲突,但概率很低,原因有三:1) 不同 Sentinel 的 PING 周期有微小的时间差,检测到 ODOWN 的时刻通常不完全相同;2) 网络延迟造成消息到达顺序不同,先到先得的投票规则自然产生倾斜;3) 即使本轮冲突,下一轮纪元 +1 后重新选举,无法按时选出 Leader 只是延迟故障转移,不会导致系统不可用。Redis 官方认为引入随机超时的收益(降低冲突概率)不值得增加代码复杂度。
Q4:客户端通过 Sentinel 获取主节点地址的流程中,如何保证不会读到旧的主节点地址?这个过程中是否存在时间窗口导致的数据不一致?
参考答案:
客户端获取主节点地址的机制:
- 主动获取:客户端发送
SENTINEL get-master-addr-by-name <master-name>获取主节点 IP 和端口。 - 被动通知:客户端订阅 Sentinel 的
+switch-master频道,故障转移完成后 Sentinel 发布通知。 - 角色验证:连接后客户端执行
ROLE命令,确认连接到的节点确实为主节点角色。
存在的时间窗口:
| 时间窗口 | 阶段 | 风险 |
|---|---|---|
| T1:Sentinel 完成故障转移 | 新主已就位,旧主仍在运行 | 客户端可能仍连接旧主 |
T2:+switch-master 消息到达客户端 |
通知在网络中传输 | 客户端短暂连接旧主 |
| T3:客户端收到通知并重建连接 | 客户端重新获取地址 | 旧主仍接受写入 |
在 T1 到 T3 之间(通常 100ms~几秒),客户端可能仍在向旧主写入数据,这些数据在旧主被降级为从节点后可能丢失。
生产环境最佳实践:
- 客户端定期轮询:即使有 Pub/Sub 通知,也定期(如每 5 秒)向 Sentinel 查询最新主节点地址,作为兜底。
- 写入前验证角色:每次获取连接后执行
ROLE命令验证角色,不是主节点则拒绝写入。 - 配置
**min-slaves-to-write**:旧主在故障转移后发现自己从节点不足,拒绝写入。 - 业务层幂等 + 补偿:写入失败时重试新主,并记录未确认的写入用于后续对账。
面试官追问:如果客户端使用的是 Redis Cluster 模式而非 Sentinel,客户端如何感知主节点切换?
答案:Redis Cluster 的客户端使用 CLUSTER SLOTS 命令获取槽位分布,当收到 MOVED 重定向响应时自动更新本地路由表。Cluster 的故障转移由集群内节点通过 Gossip 协议自动完成,客户端不需要额外订阅通知频道。但 Sentinel 和 Cluster 都存在类似的短暂时间窗口问题。
Q5:在生产环境中,你如何设计 Sentinel 的部署拓扑来保证跨可用区的高可用?需要考虑哪些网络因素?
参考答案:
推荐部署拓扑:
| 组件 | 部署策略 | 原因 |
|---|---|---|
| Sentinel | 3 或 5 个,分布至少 3 个 AZ | 保证任意 AZ 故障后剩余多数派 |
| Redis 主节点 | 1 个 AZ | 主节点只有一个,无跨 AZ 复制延迟 |
| Redis 从节点 | 至少 2 个 AZ(含主节点 AZ) | 跨 AZ 备份,故障转移时有候选节点 |
| 客户端 | 所有 AZ 均衡部署 | 就近访问减少延迟 |
具体拓扑示例(3 个 AZ):
AZ-A: Sentinel-1, Sentinel-2, Master, Slave-1
AZ-B: Sentinel-3, Slave-2
AZ-C: Sentinel-4, Sentinel-5, Slave-3
关键网络因素:
**down-after-milliseconds**:跨 AZ 网络延迟通常 5-20ms,必须考虑网络抖动。建议设为 30000-60000ms,避免因短暂网络延迟误判。**failover-timeout**:跨 AZ 复制数据的时间可能更长,需要适当增大,建议 180000-300000ms。- Sentinel 间通信链路:确保 Sentinel 之间的网络独立于业务流量(使用独立 NIC 或 VLAN),避免业务流量拥塞导致 Sentinel 之间通信失败引发误判。
**parallel-syncs**:跨 AZ 的全量同步消耗大量带宽,建议设为 1,避免多个从节点同时同步导致跨 AZ 链路拥塞。- BGP 路由收敛:AZ 故障时,BGP 路由收敛可能需要数十秒,Sentinel 的超时设置需考虑此因素。
面试官追问:如果只有 2 个 AZ 可用,如何部署 Sentinel?
答案:2 个 AZ 时建议部署 3 个 Sentinel(AZ-A 部署 2 个,AZ-B 部署 1 个),主节点在 AZ-A。如果 AZ-A 故障,AZ-B 的 1 个 Sentinel 无法满足 majority=2,故障转移不会自动执行——需要人工介入。这就是为什么跨 3 个 AZ 是生产环境的标准配置。如果预算有限只能部署 2 个 AZ,可以为每个 AZ 内部署奇数个 Sentinel 并配合负载均衡,或使用 Redis Cluster 的故障转移机制。
Q6:故障转移过程中,客户端正在写入的数据会怎样?如果你正在做一个支付系统,如何保证切换过程中的数据不丢失?
参考答案:
故障转移过程中的数据流转:
- 故障转移前:主节点接受写入,异步复制到从节点。尚未同步到从节点的数据处于风险中。
- 故障转移中:旧主停止服务,新主尚未接管。所有写操作失败,客户端收到连接错误。
- 故障转移后:新主开始接受写入。旧主恢复后被降级为从节点,丢弃自己作为旧主期间的未同步数据。
数据丢失的两个来源:
- 异步复制窗口:故障前已写入旧主但未同步到从节点的数据
- 切换空窗期:故障转移过程中客户端无法写入
支付系统的多层防护:
┌──────────────┐ 写入 ┌──────────────┐
│ 支付业务层 │ ────────→ │ MySQL (主) │ ← 持久化保证
└──────┬───────┘ └──────┬───────┘
│ 同步落库 │ binlog 订阅
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Redis (缓存) │ │ Canal/订阅 │
└──────────────┘ └──────┬───────┘
│ │
│ 故障转移丢失 │ 回源修复
▼ ▼
┌──────────────┐ ┌──────────────┐
│ 数据从 MySQL │ ◄────────── │ 缓存回源刷新 │
│ 回源恢复 │ └──────────────┘
└──────────────┘
具体措施:
- Redis 作为缓存而非主存储:支付系统的核心数据(订单、流水)落 MySQL,Redis 只做热点缓存。切换后从 MySQL 回源。
- AOF 持久化:配置
appendonly yes和appendfsync everysec,即使 Redis 崩溃,重启后也能恢复最近 1 秒的数据。 **min-slaves-to-write**:确保至少 N 个从节点确认后才返回写入成功。代价是写入延迟增加。- 客户端重试 + 幂等:写入失败时自动重试新主节点,每次写入携带幂等键(如订单号)。
- 对账补偿:定时任务对比 MySQL 和 Redis 的数据,发现不一致时从 MySQL 回源修复。
- WAIT 命令:Redis 3.0+ 的
WAIT numslaves timeout命令可以同步等待从节点确认,但会增加延迟,支付核心链路可以接受。
面试官追问:WAIT 命令和 min-slaves-to-write 有什么区别?什么时候用哪个?
答案:min-slaves-to-write 是主节点级别的配置,所有写入都受限制,粒度粗但全局生效。WAIT 是命令级别的同步确认,可以按需使用——核心写入(如支付扣款)调用 WAIT,非核心写入(如浏览量)不调用。WAIT 的灵活性更高,但需要客户端配合。
Q7:如果 Redis 主节点因为 CPU 满载导致 PING 超时,Sentinel 误判为 SDOWN 并触发故障转移,这个过程会导致什么后果?如何避免?
参考答案:
后果链:
CPU 满载
→ PING 响应延迟 > down-after-milliseconds
→ Sentinel 标记 SDOWN
→ 其他 Sentinel 同意(因为它们也 PING 超时)
→ ODOWN 判定
→ Leader 选举
→ 故障转移执行
→ 新主从旧主同步最后的数据
→ 旧主被降级为从节点
具体后果:
- 不必要的故障转移:主节点本身没挂,只是 CPU 忙,但因误判触发了完整的切换流程,期间服务中断数秒到数十秒。
- 数据丢失风险:旧主 CPU 满载期间仍在处理写入请求,但部分写入可能尚未同步到从节点。故障转移后,这些写入丢失。
- 旧主被降级后清空数据:旧主 CPU 恢复后,发现自己被降级为从节点,从新主全量同步,清除自己作为旧主期间的所有数据。
- 级联故障:CPU 满载的旧主在恢复后还要执行全量同步(RDB 生成),进一步增加 CPU 负载,可能导致反复故障。
- 客户端混乱:部分客户端可能仍在连接旧主(因为旧主没挂),部分已切换到新主,数据在两个节点间产生分歧。
避免措施:
| 措施 | 说明 |
|---|---|
合理设置 down-after-milliseconds |
生产环境建议 30-60 秒,容忍短暂 CPU 尖峰 |
| CPU 监控告警 | 在 CPU 使用率 > 80% 时提前告警,不要等到 PING 超时 |
| CPU 隔离(cgroups) | 限制 Redis 进程的 CPU 使用上限,防止单个进程耗尽 CPU |
| 绑定 CPU 核心 | taskset 将 Redis 绑定到专用核心,避免与其他进程争抢 |
| 慢查询监控 | 监控 Redis 慢查询日志,及时发现耗时命令 |
| 惰性过期调优 | hz 参数控制过期键扫描频率,避免集中过期占用 CPU |
禁用 THP(透明大页) |
透明大页可能导致 Redis 在 fork 子进程时 CPU 飙升 |
面试官追问:如果你发现生产环境 Redis 主节点 CPU 使用率持续 95%,同时在 Sentinel 日志中看到 +sdown 事件,你会怎么处理?
答案:第一步,立即检查 Redis 的慢查询日志和 CLIENT LIST,确认是否有慢命令或异常连接数;第二步,如果确认是业务流量导致的 CPU 满载,紧急扩容(增加从节点读分流,或升级主节点规格);第三步,临时增大 down-after-milliseconds 防止误判触发故障转移;第四步,事后排查根因:是否热 Key 集中、是否有大 Key 操作、是否有批量过期、是否配置了不当的持久化策略。
Q8:假设你有 3 个 Sentinel,quorum=2。如果其中 2 个 Sentinel 同时重启,剩余的 1 个 Sentinel 能否单独完成故障转移?为什么?
参考答案:
不能。 原因逐层分析:
第一层:ODOWN 判定
判定 ODOWN 需要至少 quorum=2 个 Sentinel 同意。只剩 1 个 Sentinel 运行,即使它检测到主节点 SDOWN 并询问其他 Sentinel,也收不到任何回复(其他 2 个正在重启),赞同票只有 1,无法满足 quorum=2。因此,ODOWN 判定失败,故障转移流程根本不会触发。
第二层:Leader 选举
即使 ODOWN 判定成功(假设某种极端情况),Leader 选举需要 majority。3 个 Sentinel 的 majority 是 3/2+1=2 票。只剩 1 个 Sentinel,它最多只能获得 1 票(自己的一票),无法满足 majority=2。因此,Leader 选举也不会成功。
第三层:设计哲学
这是 Sentinel 有意为之的安全设计,体现了安全性优先于可用性的原则:
| 如果 Sentinel 允许... | 会导致... |
|---|---|
| 1 个 Sentinel 判定 ODOWN | 单个 Sentinel 的网络问题导致误判,触发不必要的故障转移 |
| 1 个 Sentinel 当选 Leader | 多个分区中各自有 Sentinel 执行故障转移,产生多个主节点(脑裂) |
Sentinel 宁愿在存活的 Sentinel 不足时不执行故障转移(牺牲可用性),也绝不允许在无法达成共识时冒险执行可能破坏数据一致性的操作(保证安全性)。
生产环境启示:
- 至少部署 3 个 Sentinel 且分布在不同物理机/可用区。
- 不要将多个 Sentinel 部署在同一台机器上(否则一挂全挂)。
- Sentinel 的优先级与 Redis 本身同样重要——如果所有 Sentinel 同时不可用,即使 Redis 主从都正常,也会失去自动故障转移能力。
- 监控 Sentinel 的健康状态,确保数分钟内恢复宕机的 Sentinel 实例。
面试官追问:如果生产环境只有 3 台物理机,每台机器上部署了 1 个 Redis + 1 个 Sentinel。某台机器宕机了,上面的 Redis 和 Sentinel 都挂了。剩余的 2 个 Sentinel 能否完成故障转移?
答案:如果宕机的是从节点机器,不影响——主节点仍在,2 个 Sentinel 存活,quorum=2 满足,majority 也满足(剩余 2 个 Sentinel,majority=2/2+1=2)。如果宕机的是主节点机器,需要具体情况分析:quorum=2 满足(2 个 Sentinel 同意),但 majority=2 也满足(2 个 Sentinel 存活),故障转移可以执行。结论:3 台机器各部署 1 Sentinel + 1 Redis,任意一台宕机(包括主节点所在的机器),剩余 2 个 Sentinel 都能完成故障转移。这是经典的最小生产配置。
阅读时间说明:本文正文约 40-50 分钟,面试问答模块约 15-20 分钟(不计入正文阅读时间),总计约 60-70 分钟。建议先通读正文理解原理,再以问答形式自我检验。