目录

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 个

  1. 避免误判:单个哨兵可能因为自己的网络问题而误认为主节点挂了。多个哨兵投票才能确认(客观下线需要达到 quorum);
  2. 哨兵自身的高可用:哨兵挂了就没人监控了。如果只有 1 个哨兵,它就是新的单点;
  3. 满足选举要求:故障转移需要超过半数的哨兵同意(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):网络抖动、主节点做 BGSAVE fork 阻塞、Lua 脚本执行慢,都可能被误判为下线,触发不必要的故障转移;
  • 设太大(如 60000):真的宕机了也要等 1 分钟才发现,故障恢复时间长。

建议同机房 5000~10000ms,跨机房 15000~30000ms。默认 30000 对大多数场景偏保守。

注意:这个值不只用于主节点,也用于判断从节点和其他哨兵的下线。

parallel-syncs

故障转移完成后,其他从节点要改为从新主节点复制,这会触发全量同步(虽然 PSYNC2 让大部分情况可以部分重同步,但不保证)。

parallel-syncs 限制同时进行全量同步的从节点数量

  • 设为 1(默认,推荐):从节点逐个同步。虽然整个过程慢一些,但保护了新主节点(避免同时给多个从节点生成和传输 RDB 把它压垮);
  • 设大了:同步快,但新主节点的 CPU、内存、带宽压力大,可能影响正常服务。

failover-timeout

这个参数控制多个方面(容易混淆):

  1. 同一个哨兵对同一主节点连续两次故障转移的间隔(实际是 failover-timeout × 2);
  2. 从节点向新主节点发起复制的超时时间
  3. 取消一个进行中的故障转移的超时时间
  4. 等待所有从节点重新配置完成的时间上限

默认 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

含义与影响

  1. sentinel monitor 那一行的 IP 会被改成当前的主节点(故障转移后自动更新)。所以你在配置文件里写的初始主节点地址只是"入口",之后会被哨兵自己维护;
  2. sentinel myid 是哨兵的唯一标识,不能重复用容器镜像批量部署时,如果配置文件里已经有 myid,所有哨兵会有相同的 ID,导致它们互相认为是同一个哨兵,故障转移永远无法达到 majority。这是容器化部署最经典的坑;
  3. 配置文件必须可写,否则哨兵启动失败(Sentinel config file ... is not writable);
  4. 不能用同一个配置文件启动多个哨兵

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 这个频道,所以能收到其他哨兵的消息

这个设计非常巧妙,它实现了两件事:

  1. 哨兵自动发现:新加入的哨兵只要监控同一个主节点,就能通过这个频道被其他哨兵发现(不需要在配置里写死其他哨兵的地址);
  2. 交换主节点的配置版本(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 哨兵按以下顺序逐层过滤和排序候选从节点:

第一步:过滤掉不健康的从节点

排除以下从节点:

  1. 已经 SDOWN 或 ODOWN 的
  2. disconnected 状态的(哨兵与它的连接断了);
  3. 最近 5 秒内没有回复过哨兵 INFO
  4. 与主节点断开连接太久的:断开时间 > down-after-milliseconds × 10。这条是为了排除数据严重落后的从节点;
  5. 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 之后就写不进去了,避免了数据丢失。

局限(必须知道)

  1. 不能完全避免丢失——在 M 判定"从节点不健康"之前(即 min-replicas-max-lag 这个窗口,如 10 秒),写入仍然成功,这部分数据还是会丢;
  2. 牺牲了可用性——从节点真的全挂了(不是网络分区)时,主节点也不可写;
  3. 要正确设置 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 频繁误切换

现象:明明主节点是好的,却频繁发生故障转移。

常见原因

  1. down-after-milliseconds 太小(如 1000~3000),网络抖动就触发;

  2. 主节点有阻塞操作

    • BGSAVE 的 fork 阻塞(大实例可能 1 秒以上);
    • 慢命令(KEYS *、大 key 的 HGETALL);
    • Lua 脚本执行时间长;
    • AOF fsync 被慢磁盘阻塞;
    • 内存 swap;

    这些都会让主节点在一段时间内无法响应哨兵的 PING,被误判为下线;

  3. 哨兵所在机器负载过高,自己的定时任务都跑不准;

  4. 跨机房部署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 < 期望值 告警
master0status != ok 告警
sentinel_tilt == 1 告警
+switch-master 事件 每次发生都要告警(记录切换历史)
+sdown / +odown 告警

7.6 TILT 模式

哨兵有一个鲜为人知的自我保护机制:TILT 模式

触发条件:哨兵发现"两次定时任务执行的时间间隔"异常(比如本该 100ms 却过了 2 秒,或者时间倒流了)。这说明:

  • 机器负载极高,进程被长时间调度不到;
  • 系统时钟被大幅调整;
  • 进程被 SIGSTOP 暂停过。

进入 TILT 模式后:哨兵仍然继续监控和收集信息,但不再执行任何故障转移动作(因为它认为自己的时间感知不可靠,做出的判断可能是错的)。

30 秒后(SENTINEL_TILT_PERIOD)如果一切正常就自动退出 TILT 模式。

排查INFO sentinelsentinel_tilt:1 就是在 TILT 模式。这时要检查机器负载、时钟同步(NTP)配置。


8. 哨兵的局限

明确哨兵解决不了什么,才知道什么时候该上 Cluster。

局限 说明
不能扩容写入/存储 所有写入还是集中在一个主节点,数据量受单机内存限制
故障转移期间不可写 10~35 秒的写不可用窗口
异步复制会丢数据 无法避免(AP 系统)
脑裂风险 只能缓解不能根除
客户端要支持哨兵协议 老客户端或自研客户端可能不支持
只有一个主节点承担写压力 单点性能瓶颈
运维组件更多 3 个哨兵 + N 个 Redis 节点,配置项多、坑多

什么时候该从哨兵升级到 Cluster

  1. 单实例数据量超过 10GB(fork 慢、恢复慢、全量同步压力大);
  2. 写 QPS 达到单实例瓶颈(几万以上);
  3. 需要更快的故障恢复(Cluster 的故障检测通常更快);
  4. 需要在线水平扩容

9. 高频面试题

Q1:哨兵的作用是什么?

四个核心能力:

  1. 监控:持续检查主节点、从节点是否正常(每秒 PING);
  2. 通知:节点出问题时通过 pub/sub 事件或脚本通知管理员;
  3. 自动故障转移:主节点失效时自动选一个从节点提升为主,并让其他从节点跟随新主;
  4. 配置提供者(服务发现):客户端向哨兵查询"当前主节点是谁"。

最容易被忽略的关键点:哨兵不是代理,客户端不通过哨兵转发数据,而是从哨兵获取主节点地址后直连主节点。哨兵只在控制面工作,不在数据面。

Q2:为什么哨兵至少要 3 个,且推荐奇数?

至少 3 个的三个理由

  1. 避免误判:单个哨兵可能因为自己的网络问题误判主节点下线,需要多个哨兵投票确认(quorum);
  2. 哨兵自身高可用:只有 1 个哨兵它就是新的单点;
  3. 满足选举要求:故障转移需要超过半数哨兵授权。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 → 【开始故障转移】

两个重要细节

  1. 只有主节点有 ODOWN 概念。从节点和其他哨兵只有 SDOWN——因为从节点下线不需要任何自动处理(只是标记为不可用、不作为候选),不需要投票确认;
  2. is-master-down-by-addr 有双重作用:既询问"你觉得主节点挂了吗",又顺便拉票(第 4 个参数传自己的 runid 就是请求投票给自己当 Leader,传 * 则只是询问)。

Q5:哨兵是怎么互相发现的?

不是通过配置文件写死,而是借道被监控的 Redis 节点的 pub/sub 频道

频道:__sentinel__:hello

每个哨兵每 2 秒向所有被监控的主/从节点 PUBLISH 一条消息:
<哨兵IP>,<哨兵端口>,<哨兵runid>,<哨兵current_epoch>,
<主节点名>,<主节点IP>,<主节点端口>,<主节点config_epoch>

同时每个哨兵都 SUBSCRIBE 这个频道

这个设计实现了两件事:

  1. 哨兵自动发现:新哨兵只要监控同一个主节点,就能被其他哨兵发现,不用在配置里写死彼此的地址;
  2. 交换主节点的配置版本(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:哨兵怎么选新的主节点?(从节点选主规则)

第一步:过滤掉不健康的从节点

  1. 已 SDOWN/ODOWN 的;
  2. disconnected 状态的;
  3. 最近 5 秒没回复过 INFO 的;
  4. 与主节点断开时间 > down-after-milliseconds × 10 的(数据落后太多);
  5. 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> <新端口>

客户端侧的五个注意点

  1. 必须配多个哨兵地址(单个哨兵挂了就拿不到地址);
  2. 切换期间有 10~35 秒写不可用,要有带退避的重试;
  3. 不要缓存主节点地址(会一直连旧主);
  4. 切换后连接池里所有旧连接都要丢弃重建
  5. 收到 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(一致性)牺牲了,两个原因:
    1. 复制是异步的——主节点执行完立即返回客户端,不等从节点确认。主节点宕机时未同步的写入永久丢失;
    2. 故障转移选 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?

哨兵解决不了的

  1. 不能扩容写入和存储——所有写还是集中在一个主节点,数据量受单机内存限制;
  2. 故障转移期间不可写(10~35 秒);
  3. 异步复制丢数据(无法避免,AP 系统);
  4. 脑裂(只能缓解);
  5. 单主节点的性能瓶颈

该升级到 Cluster 的信号

  1. 单实例数据量超过 10GB(fork 慢、恢复慢、全量同步压力大——见第 7、10 篇);
  2. 写 QPS 达到单实例瓶颈(几万以上);
  3. 需要在线水平扩容(Cluster 支持 reshard);
  4. 需要更快的故障恢复

注意 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-priorityoffsetrunid 排序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-sentinelsnum-slavessentinel_tilt+switch-master 事件。SENTINEL SET 只改当前哨兵,必须逐个执行
  • 哨兵不能扩容写入和存储;单实例超 10GB 或写 QPS 到瓶颈时应升级到 Cluster