Redis-11 哨兵 Sentinel 高可用
1. 为什么需要哨兵
第 10 篇讲的主从复制解决了数据冗余和读扩展,但留下一个致命缺口:主节点挂了怎么办?
纯主从架构下需要人工介入:
1. 运维收到告警(可能已经过了几分钟)
2. 登录服务器确认主节点真的挂了
3. 选一个从节点,执行 REPLICAOF NO ONE 提升为主
4. 把其他从节点指向新主节点
5. 修改应用配置里的 Redis 地址,或者切换 VIP / DNS
6. 重启或热加载应用
整个过程可能十几分钟到几小时,期间服务完全不可写。
Sentinel(哨兵)就是把这套流程自动化的组件。它提供四个核心能力:
| 能力 | 说明 |
|---|---|
| 监控(Monitoring) | 持续检查主节点、从节点是否正常 |
| 通知(Notification) | 节点出问题时通过 API/脚本通知管理员 |
| 自动故障转移(Automatic Failover) | 主节点失效时自动选一个从节点提升为主,并让其他从节点跟随 |
| 配置提供者(Configuration Provider) | 客户端向哨兵询问"当前主节点是谁",哨兵充当服务发现 |
最后一点常被忽略但非常重要:哨兵不是代理,客户端不通过哨兵转发数据,而是向哨兵查询主节点地址,然后直连主节点。
2. 哨兵架构
2.1 拓扑
┌──────────┐ ┌──────────┐ ┌──────────┐
│Sentinel 1│ │Sentinel 2│ │Sentinel 3│ ← 哨兵集群(奇数个)
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
└──────────────┼──────────────┘
监控 + 互相通信
┌──────────────┼──────────────┐
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Master │────▶│Replica1│ │Replica2│
└────────┘ └────────┘ └────────┘
▲
│ 1. 询问主节点地址
│ 2. 直连主节点读写
┌────────┐
│ Client │
└────────┘
2.2 为什么哨兵至少要 3 个且是奇数
为什么至少 3 个:
- 避免误判:单个哨兵可能因为自己的网络问题而误认为主节点挂了。多个哨兵投票才能确认(客观下线需要达到 quorum);
- 哨兵自身的高可用:哨兵挂了就没人监控了。如果只有 1 个哨兵,它就是新的单点;
- 满足选举要求:故障转移需要超过半数的哨兵同意(
majority)。2 个哨兵时,1 个挂了剩下 1 个无法达到2/2+1 = 2的多数,故障转移永远无法进行。
为什么是奇数:
和所有基于多数派(quorum)的分布式协议一样——偶数个节点在容错能力上没有提升,但成本更高:
| 哨兵数 | majority(需要的多数) | 最多容忍宕机 |
|---|---|---|
| 2 | 2 | 0 |
| 3 | 2 | 1 |
| 4 | 3 | 1(和 3 一样!) |
| 5 | 3 | 2 |
| 6 | 4 | 2(和 5 一样) |
4 个哨兵和 3 个哨兵的容错能力完全相同(都只能容忍 1 个宕机),但多了一台机器的成本。所以标配是 3 个,对可用性要求极高的用 5 个。
2.3 哨兵部署位置
哨兵应该部署在与客户端相同的网络环境里,而不是和 Redis 节点放在一起。
原因:哨兵判断"主节点是否可达",应该从客户端的视角来判断。如果哨兵和主节点在同一台机器/同一个机架,它们同时故障,哨兵就无法工作;反之,如果客户端到主节点的网络断了但哨兵到主节点是通的,哨兵不会触发切换,客户端就一直不可用。
推荐部署:3 个哨兵分布在三个不同的可用区/机架,与应用服务器部署在一起(或者独立的小规格机器)。
3. 部署哨兵
3.1 配置文件
# sentinel.conf
port 26379 # 哨兵默认端口
daemonize yes
dir /data/redis/sentinel
logfile "/data/redis/sentinel/sentinel-26379.log"
# ---------- 核心配置 ----------
# 格式:sentinel monitor <主节点名> <IP> <端口> <quorum>
sentinel monitor mymaster 192.168.1.10 6379 2
# 主节点的密码(如果 Redis 设了 requirepass)
sentinel auth-pass mymaster mypassword
# 6.0+ ACL
sentinel auth-user mymaster myuser
# 判定主观下线的超时时间(毫秒),默认 30000
sentinel down-after-milliseconds mymaster 30000
# 故障转移超时(毫秒),默认 180000(3 分钟)
sentinel failover-timeout mymaster 180000
# 故障转移后,同时有多少个从节点向新主节点发起全量同步
# 设为 1 表示【逐个同步】,避免同时全量同步把新主节点压垮
sentinel parallel-syncs mymaster 1
# ---------- 可选配置 ----------
# 哨兵自己的密码(哨兵之间通信要用)
requirepass sentinelpassword
# 容器/NAT 环境必配
sentinel announce-ip 1.2.3.4
sentinel announce-port 26379
# 事件通知脚本(节点上下线时调用)
sentinel notification-script mymaster /opt/scripts/notify.sh
# 故障转移完成后调用的脚本(可用来更新 DNS/VIP/配置中心)
sentinel client-reconfig-script mymaster /opt/scripts/reconfig.sh
# 6.2+ 拒绝在没有明确 IP 的情况下解析主机名
sentinel resolve-hostnames no
sentinel announce-hostnames no
# 5.0+ 是否允许在配置文件中使用 hostname
# SENTINEL SIMULATE-FAILURE 用于测试
3.2 关键配置解析
sentinel monitor <name> <ip> <port> <quorum>
quorum 是判定客观下线所需的最少哨兵数,不是选举需要的多数。
极其重要的区分(面试高频):
| 概念 | 值 | 用途 |
|---|---|---|
| quorum | 配置指定 | 判定客观下线需要多少个哨兵认为主节点主观下线 |
| majority | 哨兵总数/2 + 1(自动计算) |
实际执行故障转移需要多少个哨兵授权 |
3 个哨兵,quorum = 2:
├─ 需要 2 个哨兵认为主节点下线 → 判定客观下线
└─ 需要 2 个哨兵(majority = 3/2+1 = 2)授权 → 才能执行故障转移
3 个哨兵,quorum = 1(不推荐):
├─ 只需 1 个哨兵认为下线 → 判定客观下线(容易误判!)
└─ 但仍需 2 个哨兵授权才能真正执行故障转移
关键点:即使 quorum 设得很小,故障转移仍然需要 majority 授权。 这是 Redis 的一个安全设计——防止在网络分区的少数派一侧发生故障转移(那会造成双主脑裂)。
推荐设置:quorum = majority = 哨兵数/2 + 1(3 个哨兵设 2,5 个设 3)。
down-after-milliseconds
哨兵向主节点发 PING,如果连续 down-after-milliseconds 毫秒都没收到有效回复,就判定为主观下线。
权衡:
- 设太小(如 3000):网络抖动、主节点做
BGSAVEfork 阻塞、Lua 脚本执行慢,都可能被误判为下线,触发不必要的故障转移; - 设太大(如 60000):真的宕机了也要等 1 分钟才发现,故障恢复时间长。
建议:同机房 5000~10000ms,跨机房 15000~30000ms。默认 30000 对大多数场景偏保守。
注意:这个值不只用于主节点,也用于判断从节点和其他哨兵的下线。
parallel-syncs
故障转移完成后,其他从节点要改为从新主节点复制,这会触发全量同步(虽然 PSYNC2 让大部分情况可以部分重同步,但不保证)。
parallel-syncs 限制同时进行全量同步的从节点数量:
- 设为 1(默认,推荐):从节点逐个同步。虽然整个过程慢一些,但保护了新主节点(避免同时给多个从节点生成和传输 RDB 把它压垮);
- 设大了:同步快,但新主节点的 CPU、内存、带宽压力大,可能影响正常服务。
failover-timeout
这个参数控制多个方面(容易混淆):
- 同一个哨兵对同一主节点连续两次故障转移的间隔(实际是
failover-timeout × 2); - 从节点向新主节点发起复制的超时时间;
- 取消一个进行中的故障转移的超时时间;
- 等待所有从节点重新配置完成的时间上限。
默认 180000(3 分钟)通常够用。设太小可能导致故障转移还没完成就被判定超时。
3.3 启动与验证
# 两种启动方式(完全等价)
redis-sentinel /path/to/sentinel.conf
redis-server /path/to/sentinel.conf --sentinel
# 连接哨兵
redis-cli -p 26379
# 查看监控的主节点信息
127.0.0.1:26379> SENTINEL MASTER mymaster
1) "name"
2) "mymaster"
3) "ip"
4) "192.168.1.10"
5) "port"
6) "6379"
7) "runid"
8) "8371b4fb..."
9) "flags"
10) "master"
...
"num-slaves" → 2
"num-other-sentinels" → 2 ← 发现了另外 2 个哨兵
"quorum" → 2
# 其他常用命令
SENTINEL MASTERS # 所有被监控的主节点
SENTINEL REPLICAS mymaster # 该主节点的所有从节点(旧名 SLAVES)
SENTINEL SENTINELS mymaster # 监控同一主节点的其他哨兵
SENTINEL GET-MASTER-ADDR-BY-NAME mymaster # 【客户端最常用】查询主节点地址
SENTINEL CKQUORUM mymaster # 检查当前哨兵数量是否足够做故障转移
SENTINEL FAILOVER mymaster # 【强制】手动触发故障转移(不需要 quorum 同意)
SENTINEL RESET mymaster # 重置该主节点的状态(清空已发现的从节点和哨兵,重新发现)
SENTINEL MONITOR <name> <ip> <port> <quorum> # 运行时新增监控
SENTINEL REMOVE <name> # 取消监控
SENTINEL SET mymaster down-after-milliseconds 5000 # 运行时改配置
SENTINEL IS-MASTER-DOWN-BY-ADDR ... # 哨兵之间互相询问用的内部命令
SENTINEL SIMULATE-FAILURE crash-after-election # 测试用,模拟故障
SENTINEL CKQUORUM 是很实用的巡检命令:
127.0.0.1:26379> SENTINEL CKQUORUM mymaster
OK 3 usable Sentinels. Quorum and failover authorization can be reached
如果哨兵数量不足,会返回错误,说明此时故障转移无法进行(相当于高可用已经失效),应该立即告警。
3.4 哨兵会改写配置文件
这是一个必须知道的行为:哨兵会自动修改自己的配置文件,把发现的拓扑信息持久化下来。
启动后配置文件会多出这些内容:
# Generated by CONFIG REWRITE
sentinel myid 7f8b3c2a9d1e4f5a6b7c8d9e0f1a2b3c4d5e6f7a
sentinel config-epoch mymaster 3
sentinel leader-epoch mymaster 3
sentinel known-replica mymaster 192.168.1.11 6379
sentinel known-replica mymaster 192.168.1.12 6379
sentinel known-sentinel mymaster 192.168.1.21 26379 a1b2c3d4...
sentinel known-sentinel mymaster 192.168.1.22 26379 e5f6a7b8...
sentinel current-epoch 3
含义与影响:
sentinel monitor那一行的 IP 会被改成当前的主节点(故障转移后自动更新)。所以你在配置文件里写的初始主节点地址只是"入口",之后会被哨兵自己维护;sentinel myid是哨兵的唯一标识,不能重复。用容器镜像批量部署时,如果配置文件里已经有myid,所有哨兵会有相同的 ID,导致它们互相认为是同一个哨兵,故障转移永远无法达到 majority。这是容器化部署最经典的坑;- 配置文件必须可写,否则哨兵启动失败(
Sentinel config file ... is not writable); - 不能用同一个配置文件启动多个哨兵。
4. 哨兵的工作机制
4.1 三个定时任务
哨兵的所有工作都由三个定时任务驱动(在 sentinelTimer 里):
任务一:每 10 秒向所有主/从节点发 INFO
目的:获取拓扑信息
├─ 从主节点的 INFO replication 输出里【自动发现所有从节点】
│ (所以你只需要配置主节点地址,从节点是自动发现的)
├─ 确认每个节点的 role(master/slave)
├─ 获取从节点的复制偏移量、优先级、连接状态
└─ 故障转移后,通过 role 变化确认新拓扑已生效
这就是为什么容器环境必须配 replica-announce-ip——哨兵靠主节点 INFO 里的从节点地址去连接从节点,如果那个地址不可达,哨兵就发现不了从节点,故障转移时没有候选者。
发现频率的特殊情况:如果主节点处于客观下线状态(正在故障转移),INFO 的频率会提高到每秒一次,以便尽快感知拓扑变化。
任务二:每 2 秒通过 pub/sub 交换信息
哨兵之间不直接互相连接来交换主节点状态,而是借道被监控的 Redis 节点的 pub/sub 频道:
频道名:__sentinel__:hello
每个哨兵每 2 秒向所有被监控的主/从节点 PUBLISH 一条消息:
<哨兵IP>,<哨兵端口>,<哨兵runid>,<哨兵current_epoch>,<主节点名>,<主节点IP>,<主节点端口>,<主节点config_epoch>
同时每个哨兵都 SUBSCRIBE 这个频道,所以能收到其他哨兵的消息
这个设计非常巧妙,它实现了两件事:
- 哨兵自动发现:新加入的哨兵只要监控同一个主节点,就能通过这个频道被其他哨兵发现(不需要在配置里写死其他哨兵的地址);
- 交换主节点的配置版本(config-epoch):哨兵通过比较对方消息里的
config_epoch,判断自己的主节点信息是否过期。如果对方的 epoch 更大,就更新自己的认知。这是故障转移后所有哨兵能达成一致的关键。
任务三:每 1 秒向所有节点(含其他哨兵)发 PING
目的:故障检测
├─ 对主节点、从节点、其他哨兵都发 PING
├─ 有效回复:+PONG / -LOADING / -MASTERDOWN
│ (注意 -LOADING 和 -MASTERDOWN 也算"活着",只是暂时不可用)
└─ 连续 down-after-milliseconds 没有有效回复 → 判定【主观下线】
4.2 主观下线(SDOWN)
Subjectively Down:单个哨兵认为某个节点下线了。
哨兵 S1 在 down-after-milliseconds 内没收到主节点 M 的有效 PING 回复
→ S1 把 M 标记为 SDOWN(在 S1 自己的视角里)
特点:
- 这只是一个哨兵的个人判断,可能是误判(比如 S1 自己的网络有问题);
- 对从节点和其他哨兵,只有 SDOWN 没有 ODOWN——因为从节点下线不需要做任何自动处理(只是标记为不可用,不作为故障转移候选),不需要投票确认;
- 只有主节点的 SDOWN 会升级为 ODOWN。
4.3 客观下线(ODOWN)
Objectively Down:足够多的哨兵(>= quorum)都认为主节点下线了。
1. S1 判定主节点 M 为 SDOWN
2. S1 向其他所有哨兵发送:
SENTINEL is-master-down-by-addr <M的IP> <M的端口> <S1的current_epoch> <runid或*>
3. 其他哨兵回复三个信息:
├─ down_state:1 表示我也认为它下线了,0 表示我认为它还活着
├─ leader_runid:我投票给谁做 leader(或 * 表示不投票)
└─ leader_epoch:投票的纪元
4. S1 统计认为 M 下线的哨兵数(含自己)
5. 数量 >= quorum → 把 M 标记为 ODOWN,【开始故障转移流程】
注意 is-master-down-by-addr 命令的双重作用:它既询问"你觉得主节点挂了吗",又顺便拉票(如果第 4 个参数是自己的 runid 而不是 *,就是在请求对方投票给自己当 leader)。
只有 ODOWN 才会触发故障转移,这防止了单个哨兵的误判造成不必要的切换。
4.4 选举哨兵 Leader(Raft)
主节点被判定 ODOWN 后,不是所有哨兵都去做故障转移(那会乱套),而是先选出一个 Leader,由它独自执行整个故障转移流程。
选举用的是 Raft 算法的领导者选举部分(简化版):
1. 每个发现 ODOWN 的哨兵都想成为 Leader,于是:
├─ 把自己的 current_epoch + 1(epoch 就是 Raft 里的 term,任期)
└─ 向其他哨兵发送 is-master-down-by-addr 请求,
第 4 个参数带上【自己的 runid】= "请投票给我"
2. 每个哨兵在【每个 epoch 里只能投一票】,遵循【先到先得】:
├─ 收到第一个请求 → 投票给它,记录 leader_runid 和 leader_epoch
└─ 收到后续请求(同一 epoch)→ 拒绝(回复之前投给的那个 runid)
3. 某个哨兵收到的票数 >= max(quorum, majority) → 成为 Leader
↑ 注意这里是【两者取最大】,即至少要 majority 票
4. 如果本轮没人达到多数(票被分散了):
├─ 等待一个【随机时间】(避免再次同时发起,这是 Raft 的关键设计)
├─ epoch + 1,进入下一轮选举
└─ 直到选出 Leader 或超过 failover-timeout
为什么需要 majority 而不只是 quorum:这是防止网络分区导致双主的关键。
5 个哨兵,网络分区成 2 + 3:
├─ 少数派(2 个):即使 quorum=2 让它们判定了 ODOWN,
│ 也最多只能拿到 2 票 < majority(3),【无法成为 Leader】
│ → 无法执行故障转移 ✅
└─ 多数派(3 个):能拿到 3 票 >= majority(3),
→ 正常执行故障转移 ✅
结果:只有多数派一侧会产生新主节点,避免了双主。
4.5 选择新主节点(从节点选主)
Leader 哨兵按以下顺序逐层过滤和排序候选从节点:
第一步:过滤掉不健康的从节点
排除以下从节点:
- 已经 SDOWN 或 ODOWN 的;
disconnected状态的(哨兵与它的连接断了);- 最近 5 秒内没有回复过哨兵
INFO的; - 与主节点断开连接太久的:断开时间 >
down-after-milliseconds × 10。这条是为了排除数据严重落后的从节点; replica-priority为 0 的(明确声明永不做主节点)。
第二步:按四个条件依次排序
1. 【replica-priority 更小的优先】
配置项,默认 100,数字越小优先级越高,0 表示永不被选
→ 用途:让性能好的机器/同机房的节点优先成为主节点
2. priority 相同 → 【复制偏移量 offset 更大的优先】
offset 越大说明它从主节点复制到的数据越多,数据最完整
→ 这是【最重要】的实质性条件,决定了丢多少数据
3. offset 也相同 → 【runid 字典序更小的优先】
纯粹为了让所有哨兵能做出【确定性的、一致的】选择
(避免不同哨兵选出不同的节点)
记忆口诀:优先级 → 偏移量 → runid。
注意 offset 最大 ≠ 数据完整。因为复制是异步的,即使选了 offset 最大的从节点,它仍可能落后于原主节点。故障转移必然可能丢数据(这也是为什么要配 min-replicas-to-write)。
4.6 执行故障转移
Leader 哨兵选定新主节点后,执行以下步骤:
1. 向选中的从节点 S 发送 REPLICAOF NO ONE
→ S 从从节点变为主节点
2. Leader 用【每秒一次的 INFO】确认 S 的 role 已变成 master
(确认切换真正生效,而不是命令发出去就认为成功)
3. 更新自己维护的主节点配置(config-epoch + 1,指向 S),
并通过 __sentinel__:hello 频道广播新配置
→ 其他哨兵收到后,因为 epoch 更大,会更新自己的认知
4. 向其他所有从节点发送 REPLICAOF <S的IP> <S的端口>
├─ 按 parallel-syncs 限制并发数(默认 1,逐个进行)
└─ 等每个从节点开始同步后再处理下一个
5. 把【原主节点 M】也标记为从节点
├─ 如果 M 此时还没恢复,先记下来
└─ 等 M 恢复上线后,哨兵立即向它发送 REPLICAOF <S>,
让它变成新主节点的从节点
6. 通过 pub/sub 发布 +switch-master 事件,通知客户端
└─ 如果配了 client-reconfig-script,调用它
整个流程的耗时:down-after-milliseconds(发现故障,如 5~30 秒)+ 选举(通常 < 1 秒)+ 切换(通常 1~2 秒)。所以典型的故障恢复时间是 10~35 秒,主要由 down-after-milliseconds 决定。
5. 客户端如何感知主节点切换
这是实际使用中最容易出问题的部分。
5.1 客户端的正确工作流程
客户端不能把 Redis 地址写死,而必须通过哨兵获取。
1. 客户端启动时连接【任意一个哨兵】
(配置里给一组哨兵地址,逐个尝试直到连上一个)
2. 执行 SENTINEL GET-MASTER-ADDR-BY-NAME mymaster
→ 得到当前主节点的 IP 和端口
3. 连接主节点,正常读写
4. 【关键】同时 SUBSCRIBE 哨兵的 +switch-master 频道
→ 主节点切换时会收到通知,重新执行步骤 2 并重建连接池
5. 如果与主节点的连接失败,也要重新执行步骤 2
(因为可能刚发生了切换但通知还没到)
5.2 哨兵发布的关键事件
客户端可以订阅这些频道来感知集群状态变化:
| 频道 | 含义 |
|---|---|
+switch-master |
主节点已切换(最重要)。格式:<name> <旧IP> <旧端口> <新IP> <新端口> |
+sdown |
某节点被判定主观下线 |
-sdown |
某节点恢复(主观下线解除) |
+odown |
主节点被判定客观下线 |
-odown |
客观下线解除 |
+slave |
发现了一个新的从节点 |
+sentinel |
发现了一个新的哨兵 |
+try-failover |
开始尝试故障转移 |
+elected-leader |
本哨兵被选为 Leader |
+failover-state-select-slave |
正在选择新主节点 |
+selected-slave |
已选定新主节点 |
+promoted-slave |
从节点已提升为主 |
+failover-end |
故障转移完成 |
-failover-abort-not-elected |
故障转移中止(没被选为 leader) |
+slave-reconf-sent / +slave-reconf-done |
从节点重配置进度 |
# 实时观察哨兵事件(排查故障转移问题的利器)
redis-cli -p 26379 PSUBSCRIBE '*'
5.3 各语言客户端的用法
Go(go-redis):
import "github.com/redis/go-redis/v9"
rdb := redis.NewFailoverClient(&redis.FailoverOptions{
MasterName: "mymaster",
SentinelAddrs: []string{
"192.168.1.21:26379",
"192.168.1.22:26379",
"192.168.1.23:26379",
},
Password: "mypassword", // Redis 节点的密码
SentinelPassword: "sentinelpassword", // 哨兵的密码(如果哨兵设了 requirepass)
DB: 0,
PoolSize: 50,
// 读写分离:把读请求发到从节点(谨慎使用,见第 10 篇)
// ReplicaOnly: true,
// RouteByLatency: true, // 按延迟路由到最快的节点
// RouteRandomly: true, // 随机路由到从节点
})
// go-redis 内部会自动:
// 1. 向哨兵查询主节点地址
// 2. 订阅 +switch-master 事件
// 3. 切换时自动重建连接
Java(Jedis):
Set<String> sentinels = new HashSet<>(Arrays.asList(
"192.168.1.21:26379", "192.168.1.22:26379", "192.168.1.23:26379"));
JedisSentinelPool pool = new JedisSentinelPool(
"mymaster", sentinels, poolConfig, 2000, "mypassword");
try (Jedis jedis = pool.getResource()) {
jedis.set("k", "v");
}
Java(Lettuce / Spring Boot):
spring:
redis:
sentinel:
master: mymaster
nodes:
- 192.168.1.21:26379
- 192.168.1.22:26379
- 192.168.1.23:26379
password: sentinelpassword
password: mypassword
lettuce:
pool:
max-active: 50
Python(redis-py):
from redis.sentinel import Sentinel
sentinel = Sentinel([
('192.168.1.21', 26379),
('192.168.1.22', 26379),
('192.168.1.23', 26379),
], socket_timeout=0.5, sentinel_kwargs={'password': 'sentinelpassword'})
master = sentinel.master_for('mymaster', socket_timeout=0.5, password='mypassword')
replica = sentinel.slave_for('mymaster', socket_timeout=0.5, password='mypassword')
master.set('k', 'v')
print(replica.get('k'))
5.4 客户端侧的注意点
(1)必须配置多个哨兵地址
只配一个哨兵的话,那个哨兵挂了客户端就无法获取主节点地址(虽然已建立的连接还能用,但重连时会失败)。
(2)故障转移期间会有一段不可写的时间
从主节点故障到新主节点就绪,期间所有写请求都会失败(10~35 秒)。客户端必须:
- 有合理的重试机制(带退避,避免重试风暴);
- 业务层要能容忍这段时间的写失败(降级、写队列、返回友好提示)。
(3)不要在代码里缓存主节点地址
有些实现为了"性能"把主节点地址缓存很久甚至写进配置。切换后这些客户端会一直连着旧主节点(如果旧主还活着,它已经被降级为从节点,写入会报 READONLY)。
(4)连接池要能感知切换
切换后连接池里所有指向旧主的连接都是无效的,必须全部丢弃重建。成熟客户端会自动处理,自己封装时容易漏掉这一点。
(5)注意 READONLY 错误的处理
如果客户端连到了一个已被降级的旧主节点,写命令会返回:
-READONLY You can't write against a read only replica.
客户端收到这个错误应该立即重新向哨兵查询主节点地址,而不是简单重试(重试还是会失败)。
6. 脑裂问题
6.1 什么是脑裂
脑裂(split-brain):网络分区导致同时存在两个主节点,客户端向不同的主节点写入,数据发生分叉。
时刻 T0:正常状态
哨兵 S1,S2,S3 → Master M ← Client A, Client B
↓
Replica R1, R2
时刻 T1:网络分区!M 与哨兵、从节点都断开,但 Client A 到 M 还通
┌──── 分区 1 ───┐ ┌──────── 分区 2 ────────┐
│ Master M │ │ 哨兵 S1,S2,S3 │
│ ← Client A │ │ Replica R1, R2 │
└───────────────┘ │ ← Client B │
└────────────────────────┘
时刻 T2:哨兵判定 M 客观下线,把 R1 提升为新主节点 M'
┌──── 分区 1 ───┐ ┌──────── 分区 2 ────────┐
│ Master M │ │ Master M' (原 R1) │
│ ← Client A │ │ Replica R2 │
│ 【仍在写入!】 │ │ ← Client B 【也在写入】│
└───────────────┘ └────────────────────────┘
【两个主节点,数据分叉】
时刻 T3:网络恢复
哨兵发现 M 又活了,但现在主节点是 M'
→ 哨兵向 M 发送 REPLICAOF M'
→ M 变成从节点,【清空自己的所有数据】,从 M' 全量同步
→ 【T1~T2 期间 Client A 写入 M 的所有数据永久丢失】
6.2 缓解手段
(1)min-replicas-to-write + min-replicas-max-lag(第 10 篇讲过)
在 Redis 主节点上配置:
min-replicas-to-write 1
min-replicas-max-lag 10
效果:分区后,M 发现自己没有任何健康的从节点(超过 10 秒没收到 ACK),主动拒绝所有写入:
(error) NOREPLICAS Not enough good replicas to write.
这样 Client A 从 T1 之后就写不进去了,避免了数据丢失。
局限(必须知道):
- 不能完全避免丢失——在 M 判定"从节点不健康"之前(即
min-replicas-max-lag这个窗口,如 10 秒),写入仍然成功,这部分数据还是会丢; - 牺牲了可用性——从节点真的全挂了(不是网络分区)时,主节点也不可写;
- 要正确设置
min-replicas-max-lag:设太大(如 60 秒)保护窗口太长基本无效;设太小(如 3 秒)容易在正常的网络抖动时误拒写入。
(2)业务侧幂等与补偿
接受"可能丢数据"的现实,在业务层做保障:
- 重要数据双写(Redis + 数据库),以数据库为准;
- 关键操作可重试且幂等;
- 对账机制:定期比对 Redis 和数据源,修复不一致。
(3)合理的 down-after-milliseconds
设太小会导致网络抖动时频繁误切换,每次切换都是一次潜在的数据丢失。同机房 5~10 秒是合理值。
(4)从架构上规避
- 如果对一致性要求极高,不要用 Redis 做主存储;
- 或者用支持强一致的方案(如基于 Raft 的 Redis 变体,或 etcd/ZooKeeper 这类 CP 系统)。
6.3 Redis 的 CAP 定位
Redis(主从 + 哨兵)是 AP 系统,不是 CP 系统。
- A(可用性):优先保证可用——主节点挂了快速切换继续服务;
- P(分区容错):网络分区时多数派继续工作;
- C(一致性):牺牲了强一致性——异步复制 + 故障转移必然可能丢数据。
面试时如果被问"Redis 是 CP 还是 AP",答案是 AP,并要能说出理由:异步复制(主节点写完就返回,不等从节点确认)+ 故障转移选 offset 最大的从节点但不保证它数据完整。
配了 min-replicas-to-write 后可以说是"在一致性方向做了一些倾斜的 AP",但依然不是 CP(不像 Raft/Paxos 那样保证已提交的数据绝不丢失)。
7. 常见问题排查
7.1 故障转移不执行
排查清单:
# 1. 检查哨兵数量是否足够
redis-cli -p 26379 SENTINEL CKQUORUM mymaster
# 应该返回 OK。返回错误说明哨兵不够,无法达到 majority
# 2. 检查哨兵之间是否互相发现
redis-cli -p 26379 SENTINEL SENTINELS mymaster
# num-other-sentinels 应该等于(哨兵总数 - 1)
redis-cli -p 26379 SENTINEL MASTER mymaster | grep -A1 num-other-sentinels
# 3. 检查是否所有哨兵的 myid 相同(容器化最经典的坑)
for p in 26379 26380 26381; do
redis-cli -p $p SENTINEL MYID
done
# 三个值必须【不同】!相同说明配置文件被复制了
# 4. 检查哨兵能否发现从节点
redis-cli -p 26379 SENTINEL REPLICAS mymaster
# 如果为空,检查主节点的 replica-announce-ip 配置
# 5. 看哨兵日志
tail -100 /data/redis/sentinel/sentinel-26379.log
grep -E '\+sdown|\+odown|\+try-failover|\+elected-leader|-failover-abort' sentinel.log
# 6. 实时观察事件
redis-cli -p 26379 PSUBSCRIBE '*'
常见原因:
| 原因 | 表现 | 解决 |
|---|---|---|
| 哨兵 myid 重复 | num-other-sentinels 为 0 或偏小 |
删除配置文件里的 sentinel myid 行,重启 |
| 哨兵数量不足 majority | CKQUORUM 报错 |
增加哨兵数量到 3 个以上 |
| 哨兵之间网络不通 | 只发现部分哨兵 | 检查 26379 端口的防火墙 |
sentinel auth-pass 没配 |
哨兵日志报 NOAUTH | 配置密码 |
| 从节点全部不健康 | +failover-abort-no-good-slave |
检查从节点状态和 replica-priority |
| 没有从节点 | 同上 | 哨兵无法凭空造出主节点,必须有至少一个健康从节点 |
replica-priority 全为 0 |
所有从节点被排除 | 至少留一个非 0 |
7.2 频繁误切换
现象:明明主节点是好的,却频繁发生故障转移。
常见原因:
-
down-after-milliseconds太小(如 1000~3000),网络抖动就触发; -
主节点有阻塞操作:
BGSAVE的 fork 阻塞(大实例可能 1 秒以上);- 慢命令(
KEYS *、大 key 的HGETALL); - Lua 脚本执行时间长;
- AOF
fsync被慢磁盘阻塞; - 内存 swap;
这些都会让主节点在一段时间内无法响应哨兵的
PING,被误判为下线; -
哨兵所在机器负载过高,自己的定时任务都跑不准;
-
跨机房部署但
down-after-milliseconds按同机房设置。
排查与解决:
# 看主节点是否有阻塞
redis-cli INFO stats | grep latest_fork_usec # fork 是否很慢
redis-cli SLOWLOG GET 20 # 有没有慢命令
redis-cli INFO persistence | grep aof_delayed_fsync # 磁盘是否慢
redis-cli --latency -h master # 实测延迟
redis-cli --intrinsic-latency 100 # 机器固有延迟
# 解决
# 1. 调大 down-after-milliseconds(同机房 5000~10000)
redis-cli -p 26379 SENTINEL SET mymaster down-after-milliseconds 10000
# 2. 消除主节点的阻塞源(控制实例大小、禁用慢命令、关 THP)
# 3. 把持久化转移到从节点
7.3 切换后旧主节点的数据没了
这是正常行为,不是 bug:旧主节点恢复后被降级为从节点,必须清空自己的数据从新主节点全量同步(否则主从数据不一致)。
如果旧主节点上有新主节点没有的数据(脑裂期间的写入),这些数据就永久丢失了。
如果需要保留这些数据做分析:在把旧主接回集群之前,先手动导出:
# 在旧主节点恢复后、接入集群前
redis-cli -p 6379 --rdb /tmp/old-master.rdb # 先备份数据
# 或者用 SCAN 导出关键 key
但要注意:哨兵会自动把恢复的旧主设为从节点(在 INFO 发现它上线后立即执行 REPLICAOF),所以留给你手动干预的时间只有几秒。要保数据必须先断开网络或改配置阻止它被哨兵管理。
7.4 哨兵配置文件的运维
# 运行时修改配置(会自动写入配置文件)
redis-cli -p 26379 SENTINEL SET mymaster down-after-milliseconds 10000
redis-cli -p 26379 SENTINEL SET mymaster failover-timeout 60000
redis-cli -p 26379 SENTINEL SET mymaster parallel-syncs 1
redis-cli -p 26379 SENTINEL SET mymaster quorum 2
redis-cli -p 26379 SENTINEL SET mymaster auth-pass newpassword
# 重要:SENTINEL SET 只改【当前这个】哨兵,必须对每个哨兵都执行!
# 重置状态(当拓扑信息混乱时)
redis-cli -p 26379 SENTINEL RESET mymaster
# 会清空该哨兵记录的所有从节点和其他哨兵,重新从主节点的 INFO 发现
# 用途:从节点/哨兵的 IP 变了、有僵尸节点记录时
# 新增监控(不用重启)
redis-cli -p 26379 SENTINEL MONITOR othermaster 192.168.1.30 6379 2
redis-cli -p 26379 SENTINEL SET othermaster auth-pass xxx
# 取消监控
redis-cli -p 26379 SENTINEL REMOVE othermaster
注意 SENTINEL SET 只影响当前哨兵,必须对所有哨兵都执行一遍,否则会出现配置不一致(比如某个哨兵的 down-after-milliseconds 特别小,它总是第一个发起故障转移)。
7.5 监控指标
# 哨兵侧
SENTINEL CKQUORUM mymaster # 【最重要】能否执行故障转移
SENTINEL MASTER mymaster # 主节点状态、num-slaves、num-other-sentinels
INFO sentinel # 监控的主节点数、状态
# 从 INFO sentinel 输出
sentinel_masters:1
sentinel_tilt:0 # 【重要】TILT 模式标志,见下
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=192.168.1.10:6379,slaves=2,sentinels=3
告警项:
| 指标 | 阈值 |
|---|---|
SENTINEL CKQUORUM |
非 OK 立即告警(高可用已失效) |
num-other-sentinels |
< 期望值-1 告警 |
num-slaves |
< 期望值 告警 |
master0 的 status |
!= ok 告警 |
sentinel_tilt |
== 1 告警 |
+switch-master 事件 |
每次发生都要告警(记录切换历史) |
+sdown / +odown |
告警 |
7.6 TILT 模式
哨兵有一个鲜为人知的自我保护机制:TILT 模式。
触发条件:哨兵发现"两次定时任务执行的时间间隔"异常(比如本该 100ms 却过了 2 秒,或者时间倒流了)。这说明:
- 机器负载极高,进程被长时间调度不到;
- 系统时钟被大幅调整;
- 进程被
SIGSTOP暂停过。
进入 TILT 模式后:哨兵仍然继续监控和收集信息,但不再执行任何故障转移动作(因为它认为自己的时间感知不可靠,做出的判断可能是错的)。
30 秒后(SENTINEL_TILT_PERIOD)如果一切正常就自动退出 TILT 模式。
排查:INFO sentinel 里 sentinel_tilt:1 就是在 TILT 模式。这时要检查机器负载、时钟同步(NTP)配置。
8. 哨兵的局限
明确哨兵解决不了什么,才知道什么时候该上 Cluster。
| 局限 | 说明 |
|---|---|
| 不能扩容写入/存储 | 所有写入还是集中在一个主节点,数据量受单机内存限制 |
| 故障转移期间不可写 | 10~35 秒的写不可用窗口 |
| 异步复制会丢数据 | 无法避免(AP 系统) |
| 脑裂风险 | 只能缓解不能根除 |
| 客户端要支持哨兵协议 | 老客户端或自研客户端可能不支持 |
| 只有一个主节点承担写压力 | 单点性能瓶颈 |
| 运维组件更多 | 3 个哨兵 + N 个 Redis 节点,配置项多、坑多 |
什么时候该从哨兵升级到 Cluster:
- 单实例数据量超过 10GB(fork 慢、恢复慢、全量同步压力大);
- 写 QPS 达到单实例瓶颈(几万以上);
- 需要更快的故障恢复(Cluster 的故障检测通常更快);
- 需要在线水平扩容。
9. 高频面试题
Q1:哨兵的作用是什么?
四个核心能力:
- 监控:持续检查主节点、从节点是否正常(每秒 PING);
- 通知:节点出问题时通过 pub/sub 事件或脚本通知管理员;
- 自动故障转移:主节点失效时自动选一个从节点提升为主,并让其他从节点跟随新主;
- 配置提供者(服务发现):客户端向哨兵查询"当前主节点是谁"。
最容易被忽略的关键点:哨兵不是代理,客户端不通过哨兵转发数据,而是从哨兵获取主节点地址后直连主节点。哨兵只在控制面工作,不在数据面。
Q2:为什么哨兵至少要 3 个,且推荐奇数?
至少 3 个的三个理由:
- 避免误判:单个哨兵可能因为自己的网络问题误判主节点下线,需要多个哨兵投票确认(quorum);
- 哨兵自身高可用:只有 1 个哨兵它就是新的单点;
- 满足选举要求:故障转移需要超过半数哨兵授权。2 个哨兵时 majority = 2,一个挂了剩下 1 个永远无法达到 2,故障转移彻底失效。
为什么奇数:偶数在容错能力上没有提升但成本更高:
| 哨兵数 | majority | 最多容忍宕机 |
|---|---|---|
| 2 | 2 | 0 |
| 3 | 2 | 1 |
| 4 | 3 | 1(和 3 一样) |
| 5 | 3 | 2 |
| 6 | 4 | 2(和 5 一样) |
标配 3 个,可用性要求极高用 5 个。
Q3:quorum 和 majority 有什么区别?
这是最容易混淆的概念:
| quorum | majority | |
|---|---|---|
| 来源 | 配置指定(sentinel monitor 的第 4 个参数) |
自动计算:哨兵总数/2 + 1 |
| 用途 | 判定主节点**客观下线(ODOWN)**需要多少个哨兵认为它主观下线 | 实际执行故障转移需要多少个哨兵授权(选 Leader 的票数) |
关键点:即使 quorum 设得很小(如 1),故障转移仍然需要 majority 授权。
这是 Redis 的安全设计,用来防止网络分区的少数派一侧发生故障转移:
5 个哨兵分区成 2 + 3,quorum = 2:
├─ 少数派(2 个):可以判定 ODOWN(满足 quorum=2),
│ 但最多拿 2 票 < majority(3) → 【无法成为 Leader,无法故障转移】✅
└─ 多数派(3 个):拿到 3 票 >= majority(3) → 正常故障转移 ✅
→ 只有多数派产生新主节点,避免了双主
选 Leader 需要的票数实际是 max(quorum, majority)。
推荐设置:quorum = majority = 哨兵数/2 + 1。
Q4:什么是主观下线和客观下线?
主观下线(SDOWN,Subjectively Down):单个哨兵在 down-after-milliseconds 内没收到某节点的有效 PING 回复,就在自己的视角里把它标记为 SDOWN。这只是个人判断,可能是误判。
客观下线(ODOWN,Objectively Down):
1. 哨兵 S1 判定主节点 M 为 SDOWN
2. S1 向其他哨兵发 SENTINEL is-master-down-by-addr <M的IP> <端口> <epoch> <runid或*>
3. 其他哨兵回复:down_state(我是否也认为它下线)、leader_runid、leader_epoch
4. 认为 M 下线的哨兵数(含自己)>= quorum → 标记为 ODOWN → 【开始故障转移】
两个重要细节:
- 只有主节点有 ODOWN 概念。从节点和其他哨兵只有 SDOWN——因为从节点下线不需要任何自动处理(只是标记为不可用、不作为候选),不需要投票确认;
is-master-down-by-addr有双重作用:既询问"你觉得主节点挂了吗",又顺便拉票(第 4 个参数传自己的 runid 就是请求投票给自己当 Leader,传*则只是询问)。
Q5:哨兵是怎么互相发现的?
不是通过配置文件写死,而是借道被监控的 Redis 节点的 pub/sub 频道:
频道:__sentinel__:hello
每个哨兵每 2 秒向所有被监控的主/从节点 PUBLISH 一条消息:
<哨兵IP>,<哨兵端口>,<哨兵runid>,<哨兵current_epoch>,
<主节点名>,<主节点IP>,<主节点端口>,<主节点config_epoch>
同时每个哨兵都 SUBSCRIBE 这个频道
这个设计实现了两件事:
- 哨兵自动发现:新哨兵只要监控同一个主节点,就能被其他哨兵发现,不用在配置里写死彼此的地址;
- 交换主节点的配置版本(config-epoch):哨兵比较对方消息里的 epoch,如果对方更大就更新自己的认知。这是故障转移后所有哨兵能达成一致的关键机制。
从节点则是通过每 10 秒向主节点发 INFO,从 INFO replication 的输出里自动发现的。所以配置里只需要写主节点地址。
这也是容器环境必须配 replica-announce-ip 的原因——哨兵靠主节点 INFO 里的从节点地址去连接,地址不可达就发现不了从节点,故障转移时没有候选者。
Q6:哨兵的 Leader 是怎么选出来的?
用 Raft 算法的领导者选举部分:
1. 每个发现 ODOWN 的哨兵都想当 Leader:
├─ 自己的 current_epoch + 1(epoch 就是 Raft 的 term/任期)
└─ 向其他哨兵发 is-master-down-by-addr,第 4 个参数带【自己的 runid】= 拉票
2. 每个哨兵在【每个 epoch 里只能投一票】,【先到先得】:
├─ 第一个请求 → 投给它,记录 leader_runid 和 leader_epoch
└─ 后续请求(同一 epoch)→ 拒绝,回复之前投给的 runid
3. 得票 >= max(quorum, majority) → 成为 Leader
4. 本轮无人达到多数(票被分散):
├─ 等待一个【随机时间】(Raft 的关键设计,避免再次同时发起)
├─ epoch + 1,下一轮选举
└─ 直到选出 Leader 或超过 failover-timeout
只有 Leader 执行故障转移,避免多个哨兵同时操作造成混乱。
为什么要 majority 而不只是 quorum:见 Q3,这是防止网络分区导致双主的核心机制。
Q7:哨兵怎么选新的主节点?(从节点选主规则)
第一步:过滤掉不健康的从节点
- 已 SDOWN/ODOWN 的;
disconnected状态的;- 最近 5 秒没回复过
INFO的; - 与主节点断开时间 >
down-after-milliseconds × 10的(数据落后太多); replica-priority为 0 的(声明永不做主)。
第二步:按三个条件依次排序
1. 【replica-priority 更小的优先】(默认 100,越小越优先,0 = 永不)
2. priority 相同 → 【复制偏移量 offset 更大的优先】(数据最完整,最重要的实质条件)
3. offset 相同 → 【runid 字典序更小的优先】(纯粹为了让所有哨兵做出确定性一致的选择)
口诀:优先级 → 偏移量 → runid。
重要补充:offset 最大 ≠ 数据完整。 因为复制是异步的,即使选了 offset 最大的从节点,它仍可能落后于原主节点。故障转移必然可能丢数据——这也是为什么要配 min-replicas-to-write。
Q8:故障转移的完整流程是什么?大概要多久?
1. Leader 哨兵向选中的从节点 S 发 REPLICAOF NO ONE → S 变成主节点
2. Leader 用【每秒一次的 INFO】确认 S 的 role 真的变成了 master
(确认切换生效,而不是命令发出就认为成功)
3. Leader 更新主节点配置(config-epoch + 1),通过 __sentinel__:hello 广播
→ 其他哨兵因 epoch 更大而更新认知
4. 向其他从节点发 REPLICAOF <S>,按 parallel-syncs(默认 1)限制并发
5. 把原主节点 M 也标记为从节点;M 恢复上线后立即发 REPLICAOF <S> 让它降级
6. 发布 +switch-master 事件通知客户端;调用 client-reconfig-script
耗时:down-after-milliseconds(发现故障,5~30 秒)+ 选举(通常 < 1 秒)+ 切换(1~2 秒)。
典型故障恢复时间 10~35 秒,主要由 down-after-milliseconds 决定。 这期间所有写请求失败,客户端必须有重试机制、业务要能容忍。
Q9:客户端如何知道主节点切换了?
客户端绝不能把 Redis 地址写死,必须通过哨兵获取:
1. 连接【任意一个哨兵】(配置里给一组地址,逐个尝试)
2. SENTINEL GET-MASTER-ADDR-BY-NAME mymaster → 得到主节点地址
3. 直连主节点读写
4. 【关键】同时 SUBSCRIBE +switch-master 频道
→ 收到通知时重新执行步骤 2 并【重建整个连接池】
5. 与主节点连接失败时也要重新执行步骤 2
+switch-master 消息格式:<主节点名> <旧IP> <旧端口> <新IP> <新端口>。
客户端侧的五个注意点:
- 必须配多个哨兵地址(单个哨兵挂了就拿不到地址);
- 切换期间有 10~35 秒写不可用,要有带退避的重试;
- 不要缓存主节点地址(会一直连旧主);
- 切换后连接池里所有旧连接都要丢弃重建;
- 收到
READONLY You can't write against a read only replica错误时,要立即重新向哨兵查询地址而不是简单重试(说明连到了被降级的旧主)。
用 NewFailoverClient(go-redis)、JedisSentinelPool(Jedis)、Sentinel(redis-py)这些封装,不要自己实现。
Q10:什么是脑裂?怎么解决?
脑裂:网络分区导致同时存在两个主节点,客户端向不同主节点写入,数据分叉。
1. 主节点 M 与哨兵、从节点网络断开,但部分客户端仍能连到 M
2. 哨兵判定 M 客观下线,把从节点 R1 提升为新主 M'
3. 【两个主节点】:Client A 还在往 M 写,Client B 往 M' 写
4. 网络恢复,哨兵让 M 变成 M' 的从节点
5. M 作为从节点【清空自己所有数据】从 M' 全量同步
→ 【步骤 3 期间写入 M 的所有数据永久丢失】
缓解手段:
(1)min-replicas-to-write 1 + min-replicas-max-lag 10(配在 Redis 主节点上)
分区后 M 发现没有健康从节点(10 秒没收到 ACK),主动拒绝写入(返回 NOREPLICAS),从而避免数据丢失。
局限:① 在判定"从节点不健康"之前(min-replicas-max-lag 窗口内)的写入仍会丢;② 牺牲可用性(从节点真挂了主节点也不可写);③ 参数难设(太大无效、太小容易误拒)。
(2)业务侧:重要数据双写(以 DB 为准)、操作幂等可重试、定期对账修复。
(3)合理的 down-after-milliseconds:太小会频繁误切换,每次切换都是潜在的数据丢失。
(4)架构上:一致性要求极高就不要用 Redis 做主存储。
Q11:Redis 主从 + 哨兵是 CP 还是 AP?
AP 系统。
- A(可用性):优先保证可用——主节点挂了快速自动切换继续服务;
- P(分区容错):网络分区时多数派继续工作;
- C(一致性)牺牲了,两个原因:
- 复制是异步的——主节点执行完立即返回客户端,不等从节点确认。主节点宕机时未同步的写入永久丢失;
- 故障转移选 offset 最大的从节点,但不保证它数据完整——它仍可能落后于原主节点。
配了 min-replicas-to-write 后可以说是"在一致性方向做了倾斜的 AP",但依然不是 CP——它不像 Raft/Paxos 那样保证"已向客户端确认的写入绝不丢失"。
Q12:down-after-milliseconds 设太小或太大分别有什么问题?
太小(如 1000~3000)→ 频繁误切换:
主节点在以下情况会短暂无法响应 PING,被误判为下线:
BGSAVE的 fork 阻塞(大实例可能 1 秒以上);- 慢命令(
KEYS *、大 key 的HGETALL); - Lua 脚本执行时间长;
- AOF fsync 被慢磁盘阻塞;
- 内存 swap;
- 网络抖动。
每次误切换都是一次潜在的数据丢失 + 一段服务不可写,代价很高。
太大(如 60000)→ 故障恢复慢:真的宕机了也要等 1 分钟才发现。
建议:同机房 5000~10000ms,跨机房 15000~30000ms。默认 30000 偏保守。
同时要消除主节点的阻塞源(控制实例大小 <= 10GB、禁用慢命令、关闭 THP、把持久化转移到从节点、用 SSD)。
Q13:哨兵会修改配置文件吗?容器化部署要注意什么?
会。哨兵把发现的拓扑信息自动写入自己的配置文件:
sentinel myid 7f8b3c2a... # 哨兵唯一标识
sentinel config-epoch mymaster 3
sentinel leader-epoch mymaster 3
sentinel known-replica mymaster 192.168.1.11 6379
sentinel known-sentinel mymaster 192.168.1.21 26379 a1b2c3d4...
sentinel current-epoch 3
同时 sentinel monitor 那行的 IP 会被改成当前主节点(故障转移后自动更新)。所以配置文件里写的初始地址只是"入口"。
容器化最经典的坑:sentinel myid 重复。
如果把已经运行过的配置文件打进镜像(里面已有 myid),所有哨兵实例会有相同的 myid,导致它们互相认为是同一个哨兵,num-other-sentinels 为 0,永远无法达到 majority,故障转移彻底失效。
解决:镜像里的配置文件不能包含 sentinel myid(用干净的模板),让每个实例启动时自己生成。
其他容器化注意点:
- 配置文件必须可写(挂载可写卷),否则哨兵启动失败;
- 不能用同一个配置文件启动多个哨兵;
- 必配
sentinel announce-ip/sentinel announce-port和 Redis 侧的replica-announce-ip; - 用 StatefulSet 保证稳定的网络标识;
- K8s 环境更推荐 Redis Cluster 或 Operator,哨兵在 K8s 的网络模型下坑很多(Pod IP 漂移、哨兵记录的是 IP 而非 hostname)。
Q14:SENTINEL SET 修改配置需要注意什么?
SENTINEL SET 只修改当前连接的那一个哨兵,必须对每个哨兵都执行一遍:
for p in 26379 26380 26381; do
redis-cli -p $p SENTINEL SET mymaster down-after-milliseconds 10000
done
如果只改了部分哨兵会怎样:出现配置不一致。比如某个哨兵的 down-after-milliseconds 特别小,它会总是第一个判定 SDOWN 并发起故障转移,导致切换行为难以预测,也更容易误切换。
好消息是 SENTINEL SET 会自动触发配置文件重写,所以改完就是持久化的(不需要额外操作)。
另外 SENTINEL RESET mymaster 会清空该哨兵记录的所有从节点和其他哨兵、重新发现——当拓扑信息混乱时(节点 IP 变了、有僵尸节点记录)很有用,但要注意它会短暂地让哨兵"失忆"。
Q15:哨兵解决不了什么问题?什么时候该上 Cluster?
哨兵解决不了的:
- 不能扩容写入和存储——所有写还是集中在一个主节点,数据量受单机内存限制;
- 故障转移期间不可写(10~35 秒);
- 异步复制丢数据(无法避免,AP 系统);
- 脑裂(只能缓解);
- 单主节点的性能瓶颈。
该升级到 Cluster 的信号:
- 单实例数据量超过 10GB(fork 慢、恢复慢、全量同步压力大——见第 7、10 篇);
- 写 QPS 达到单实例瓶颈(几万以上);
- 需要在线水平扩容(Cluster 支持 reshard);
- 需要更快的故障恢复。
注意 Cluster 也有自己的限制(不支持跨 slot 的多 key 操作、只能用 db0、客户端要支持重定向),见第 12 篇。
小结
- 哨兵提供四个能力:监控、通知、自动故障转移、配置提供者(服务发现)。它不是代理——客户端从哨兵拿到主节点地址后直连主节点。
- 哨兵至少 3 个且为奇数:避免误判、哨兵自身高可用、2 个哨兵时 majority=2,一个挂了故障转移就彻底失效。偶数在容错上无提升但成本更高。
- quorum(配置的,判定 ODOWN 用)≠ majority(自动算的,执行故障转移用)。即使 quorum 很小,故障转移仍需 majority 授权——这是防止分区少数派产生双主的核心设计。选 Leader 实际需要
max(quorum, majority)票。 - 三个定时任务:每 10 秒
INFO(发现拓扑,从主节点INFO replication自动发现从节点)、每 2 秒通过__sentinel__:hello频道交换信息(哨兵自动发现 + 交换 config-epoch)、每 1 秒PING(故障检测)。 - SDOWN(单个哨兵的主观判断)→ 收集 >= quorum 个哨兵的确认 → ODOWN → 触发故障转移。只有主节点有 ODOWN 概念。
is-master-down-by-addr兼具"询问"和"拉票"双重作用。 - Leader 选举用 Raft:epoch+1 → 拉票 → 每个 epoch 每个哨兵只投一票且先到先得 → 得票 >=
max(quorum, majority)当选 → 失败则随机等待后下一轮。 - 选新主的规则:过滤不健康的(断开 >
down-after × 10、priority=0 等)→ 按replica-priority→offset→runid排序。offset 最大 ≠ 数据完整,故障转移必然可能丢数据。 - 故障转移流程:
REPLICAOF NO ONE→ 用 INFO 确认 role 变化 → 更新 config-epoch 并广播 → 让其他从节点跟随(按parallel-syncs限流,默认 1)→ 旧主恢复后自动降级 → 发布+switch-master。典型耗时 10~35 秒,主要由down-after-milliseconds决定。 - 客户端必须通过哨兵获取地址 + 订阅
+switch-master,不能写死地址、不能缓存地址、切换后要重建整个连接池、收到READONLY错误要重新查询地址。用NewFailoverClient/JedisSentinelPool这类现成封装。 - 脑裂只能缓解不能根除:
min-replicas-to-write+min-replicas-max-lag让失去从节点的旧主主动拒写,但在判定窗口内的写入仍会丢。 - Redis 主从+哨兵是 AP 系统:异步复制 + 故障转移不保证数据完整 → 牺牲了强一致性。
down-after-milliseconds同机房设 5000~10000;太小会因 fork 阻塞、慢命令、Lua、AOF fsync、swap 而误切换。- 容器化两大坑:
sentinel myid重复(配置文件打进镜像导致所有哨兵 ID 相同 → 互相认为是同一个 → 永远达不到 majority)、没配announce-ip(哨兵发现不了从节点)。 - 巡检必备:
SENTINEL CKQUORUM(非 OK 说明高可用已失效)、num-other-sentinels、num-slaves、sentinel_tilt、+switch-master事件。SENTINEL SET只改当前哨兵,必须逐个执行。 - 哨兵不能扩容写入和存储;单实例超 10GB 或写 QPS 到瓶颈时应升级到 Cluster。
xingliuhua