Skip to content
Redis 高可用架构:主从复制与哨兵机制

Redis 高可用架构:主从复制与哨兵机制

Redis 单节点部署存在单点故障风险、读写吞吐量瓶颈和数据安全隐患。主从复制 (Master-Slave Replication) 与哨兵机制 (Redis Sentinel) 是 Redis 内置的高可用方案,提供了数据冗余、读写分离和自动故障转移能力。本文全面覆盖两者原理、配置、运维和常见陷阱。


1. 主从架构概述

1.1 为什么需要主从

问题 单节点瓶颈 主从方案
单点故障 宕机即服务不可用 主节点故障后可手动/自动切换至从节点
数据安全 磁盘损坏数据全部丢失 多个副本分散在不同机器上
读吞吐量 单机 CPU/内存撑死 读请求分流至从节点,水平扩展读能力
备份影响 BGSAVE 期间性能抖动 在从节点上执行 BGSAVE/RDB 备份

1.2 架构全景图

┌──────────────────────────────────────────────────────────────────┐
│                         Client / Application                      │
└────────────┬──────────────┬──────────────┬────────────────────────┘
             │ 写入         │ 读取         │ 读取
             ▼              ▼              ▼
       ┌─────────┐    ┌─────────┐    ┌─────────┐
       │ Master  │───▶│ Slave 1 │    │ Slave 2 │
       │  (RW)   │    │  (RO)   │    │  (RO)   │
       └─────────┘    └─────────┘    └─────────┘
             │              │              │
             │     RDB / AOF 命令流        │
             └──────────────┴──────────────┘

1.3 核心能力与局限性

能力 说明
读写分离 写操作走主节点,读操作分流到从节点,降低主节点压力
数据冗余 多份副本分布在不同物理机,提升数据安全性
负载均衡 多个从节点分摊读请求
故障恢复基础 主节点宕机后可从从节点中提升新主节点
局限性 说明
手动故障转移 纯主从模式下需要人工介入切换
写能力未扩展 所有写操作仍集中在主节点,无法水平扩展写入
数据一致性 异步复制,存在数据丢失窗口
客户端感知 切换后客户端需更新连接地址

2. 主从复制

2.1 搭建主从

命令行方式(临时,重启失效)

# 让当前节点成为指定主节点的从节点(Redis 5.0+)
REPLICAOF 192.168.1.100 6379

# 旧版本使用 SLAVEOF(仍兼容,但不推荐)
SLAVEOF 192.168.1.100 6379

# 断开复制,从节点提升为独立主节点
REPLICAOF NO ONE

Redis 5.0 将 SLAVEOF 重命名为 REPLICAOF,旧命令仍然可用,但建议使用新命令。

配置文件方式(永久生效)

# redis.conf
replicaof 192.168.1.100 6379        # 主节点 IP 和端口
masterauth your_master_password     # 主节点认证密码
replica-read-only yes               # 从节点只读(默认 yes,强烈不建议改为 no)

💡 最佳实践:从节点应始终保持 replica-read-only yes,避免意外写入导致主从数据不一致。

2.2 复制流程总览

Slave                                     Master
  │                                          │
  │──── PSYNC ? -1 ────────────────────────▶│  ① 建立连接,请求全量同步
  │                                          │
  │◀─── FULLRESYNC runid offset ───────────│  ② 返回 runid + 偏移量
  │                                          │
  │                                          │
  │◀─── RDB 文件 ──────────────────────────│  ③ 发送 RDB 快照
  │    (replication buffer 缓存期间新写命令)  │
  │                                          │
  │◀─── 缓冲命令 ──────────────────────────│  ④ 发送增量命令
  │                                          │
  │◀═══ 持续命令传播 ═══════════════════════│  ⑤ 异步复制后续写命令
  │                                          │

整个复制生命周期分为四个阶段:建立连接 → 权限验证 → 同步数据 → 命令传播。其中同步数据阶段根据场景不同分为全量同步 (Full Resynchronization) 和增量同步 (Partial Resynchronization)。

2.3 🔬 全量同步 (Full Resynchronization)

触发条件

  • 从节点第一次连接到主节点(发送 PSYNC ? -1
  • 从节点断连时间过长,复制偏移量 (offset) 已不在主节点的 repl_backlog_buffer 范围内
  • 主节点重启导致 runid 发生变化

详细流程(三阶段)

第一阶段:建立连接协商

Slave → Master:  PSYNC ? -1
                  │ 表示:我是新来的,没有 runid,没有 offset
Master → Slave:   FULLRESYNC <runid> <offset>
                  │ 表示:必须全量同步,我的 runid 是 xxx,当前 offset 是 nnn

PSYNC (Partial Synchronization) 是 Redis 2.8 引入的复制同步命令,替代了旧版的 SYNC? 表示未知 runid,-1 表示无偏移量。

第二阶段:主库生成并发送 RDB

┌──────────────┐                          ┌──────────────┐
│    Master    │                          │    Slave     │
│              │                          │              │
│ 1. BGSAVE    │                          │  等待数据     │
│    ↓         │                          │              │
│ 2. 写 RDB 文件│                          │              │
│    │         │                          │              │
│    │ 期间新写入的命令                      │              │
│    │ 暂存在                              │              │
│    │ replication buffer 中              │              │
│    │                                    │              │
│ 3. 发送 RDB ─────────────────────────▶   │ 4. 清空旧数据 │
│                                         │ 5. 加载 RDB   │
└──────────────┘                          └──────────────┘
  • Master 执行 BGSAVE 生成 RDB 快照,fork 子进程写磁盘
  • 生成 RDB 期间所有新的写命令暂存在 replication buffer
  • RDB 生成完毕后,先将 RDB 文件发送给 Slave
  • Slave 收到后清空自身所有旧数据,然后加载 RDB 文件

第三阶段:发送缓冲命令

Master 将 replication buffer 中缓存的命令发送给 Slave,Slave 逐条执行,使状态追上主节点。之后进入正常的命令传播阶段。

无盘复制 (Diskless Replication)

🔬 传统全量同步需要主节点先把 RDB 写到磁盘再发送。当磁盘 I/O 慢而网络快时,磁盘成为瓶颈。无盘复制直接将 RDB 数据通过 socket 发送给从节点,跳过写磁盘步骤。

repl-diskless-sync yes              # 开启无盘复制
repl-diskless-sync-delay 5          # 等待 N 秒,让更多从节点一起接收

性能提示

  • 磁盘慢、网络快 → 开 repl-diskless-sync
  • 磁盘快(SSD/NVMe)、网络慢 → 保持默认(写磁盘方式)
  • repl-diskless-sync-delay 设为几秒,可让多个从节点同时接收同一份 RDB,减少 BGSAVE 次数
无盘复制流程:

Master                                     Slave1  Slave2
  │                                           │       │
  │ BGSAVE(仅 fork,不写磁盘)                 │       │
  │                                           │       │
  │ 等待 repl-diskless-sync-delay 秒           │       │
  │                                           │       │
  ├── socket ────────────────────────────────▶│       │
  ├── socket ─────────────────────────────────────────▶│
  │  (并发发送同一份 RDB 到多个从节点)         │       │
  │                                           │       │

2.4 🔬 增量同步 (Partial Resynchronization)

触发条件

从节点短暂断连后重连,且断连期间的写入量未超过 repl_backlog_buffer 的容量。如果 offset 仍在缓冲区内,即可增量同步;否则退化为全量同步。

核心三要素

要素 作用 获取位置
runid 主节点唯一标识,用于判断断连前后是否为同一主节点 主节点重启会变,可从 INFO replication 查看
offset 复制偏移量,记录主从之间的同步进度 主从各自维护,通过 INFO replication 查看
repl_backlog_buffer 主节点维护的环形缓冲区,存放最近的写命令 仅主节点维护,配置项 repl-backlog-size

增量同步流程

Slave                                     Master
  │                                          │
  │── PSYNC <runid> <offset> ──────────────▶│  ① 带上已知的 runid 和 offset
  │                                          │
  │                                      ┌───┴──────────────┐
  │                                      │ 检查 runid 是否匹配│
  │                                      │ 检查 offset 是否  │
  │                                      │ 在 backlog 范围内  │
  │                                      └───┬──────────────┘
  │                                          │
  │◀── CONTINUE ───────────────────────────│  ② 同意增量同步
  │                                          │
  │◀── backlog 中 offset 之后的命令 ────────│  ③ 发送差异数据
  │                                          │
  │◀═══ 持续命令传播 ═══════════════════════│  ④ 恢复正常复制
  │                                          │

🔬 repl_backlog_buffer 详解

环形缓冲区示意(默认 1MB):

         ┌─────────────────────────┐
         │  cmd_1 │ cmd_2 │ cmd_3 │
         ▼        │        │        │
  ┌──────┴──────┐ │ ┌──────┴──────┐ │
  │             │ │ │             │ │
  │    ...      │ │ │   已发送     │ │
  │             │ │ │   命令...    │ │
  │             │ ▼ │             │ ▼
  │    尚未发送  │   └─────────────┘
  │    的命令    │
  │             │  ────── offset 移动方向 ──────▶
  └─────────────┘
  
  写入指针 ↑
  └─ 主节点每产生一条写命令,offset+1,写入缓冲区当前位置
  
  如果写入速度过快,新数据会覆盖旧数据(环形覆盖)
  从节点 offset 对应的数据被覆盖 → 无法增量同步 → 回退到全量同步

大小计算公式

repl-backlog-size = 断连秒数 × 每秒写入量 × 2

例如:
  预期最大断连时间 60 秒
  每秒写入 1000 条命令(平均每条 256 字节)
  → size = 60 × 1000 × 256 × 2 = 30,720,000 ≈ 30MB

×2 的余量用于应对写入突发,避免断连瞬间就因缓冲区溢出而触发全量同步。

2.5 复制拓扑

一主多从(星型拓扑)

              ┌─────────┐
              │  Master  │
              └────┬─────┘
          ┌────────┼────────┐
          ▼        ▼        ▼
     ┌─────────┐┌─────────┐┌─────────┐
     │ Slave 1 ││ Slave 2 ││ Slave 3 │
     └─────────┘└─────────┘└─────────┘
  • 所有从节点直接从主节点复制,结构简单
  • 从节点数量较多时主节点复制压力大(同时 BGSAVE 多份 RDB)

🔬 链式复制(主-从-从)

              ┌─────────┐
              │  Master  │
              └────┬─────┘
                   │ 复制
                   ▼
              ┌─────────┐
              │ Slave 1 │ ← 直接从主节点复制
              └────┬─────┘
                   │ 复制
                   ▼
              ┌─────────┐
              │ Slave 2 │ ← 从 Slave 1 复制
              └────┬─────┘
                   │ 复制
                   ▼
              ┌─────────┐
              │ Slave 3 │ ← 从 Slave 2 复制
              └─────────┘
对比维度 一主多从 链式复制
主节点压力 大(每个从节点占用主节点资源) 小(只有一个下游从节点)
数据延迟 低(所有从节点直连主节点) 高(逐层传递,每层增加延迟)
故障影响 主节点挂则全部不可写 中间节点挂则下游全部脱节
适用场景 从节点数少、对延迟敏感 从节点数多(10+)、主节点资源有限

💡 最佳实践:大多数场景使用一主多从即可。仅当从节点数量特别多(如 10+)且主节点资源紧张时考虑链式复制。使用链式复制时务必配合哨兵监控每一层。

2.6 配置参数全解

# ─── 基础复制配置 ───
replicaof <master_ip> <master_port>   # 指定主节点
masterauth <password>                 # 主节点认证密码
replica-read-only yes                 # 从节点只读(默认 yes,生产环境必须 yes)

# ─── 同步期间行为 ───
replica-serve-stale-data yes          # 同步中是否响应读请求
                                      # yes: 可用旧数据响应(可能过期)
                                      # no:  仅回复 SYNC with master(除 INFO/REPLICAOF 外)

# ─── 无盘复制 ───
repl-diskless-sync yes                # 开启无盘复制
repl-diskless-sync-delay 5            # 无盘复制等待延迟(秒)

# ─── 复制缓冲区 ───
repl-backlog-size 64mb                # 复制 backlog 缓冲区大小
repl-backlog-ttl 3600                 # 无从节点连接时 backlog 存活时间(秒)
                                      # 设为 0 表示永不过期

# ─── 哨兵相关 ───
replica-priority 100                  # 从节点选举优先级(越小越优先,0 表示永不参选)

# ─── 写入保护(半同步语义) ───
min-replicas-to-write 1               # 最少有多少个从节点在线时才接受写入
min-replicas-max-lag 10               # 从节点延迟超过此秒数即视为不在线

# ─── 其他 ───
replica-announce-ip <ip>              # 向主节点宣告的 IP(Docker/NAT 环境必配)
replica-announce-port <port>          # 向主节点宣告的端口(Docker/NAT 环境必配)
repl-ping-replica-period 10           # 主节点向从节点发送 PING 的间隔(秒)
repl-timeout 60                       # 复制超时时间(秒),超过此时间无通信即视为断连

2.7 🚨 常见问题与排查

主从数据不一致

# 诊断:在主节点查看复制状态
redis-cli -h master_host INFO replication

# 关键指标
# master_repl_offset    主节点当前 offset
# slave0:offset         从节点上报的 offset
# 两个 offset 差值 = 从节点的延迟字节数

原因与解决

  • 异步复制天然存在延迟窗口 → 接受最终一致性
  • 从节点网络带宽不足 → 升级网络或减少从节点数量
  • 主节点写入突发过大 → 增大 repl-backlog-size

复制延迟过大

# 在从节点查看延迟
redis-cli -h slave_host INFO replication
# 关注 master_link_status: up/down
# 关注 master_last_io_seconds_ago 上次与主节点通信已过多少秒

常见原因

  • 从节点正在加载 RDB(全量同步阶段)
  • 从节点磁盘 I/O 繁忙(写 RDB 慢)
  • 网络带宽不足或丢包
  • 主节点写入量超过了从节点处理能力

主节点重启导致全量同步

🚨 主节点重启后 runid 必然变化,从节点重连后发现 runid 不匹配,只能触发全量同步。即使数据完全一样,从节点也要重新接收完整的 RDB。

缓解方案

  • 使用哨兵机制自动故障转移(从新主节点增量同步)
  • 增大 repl-backlog-size,在主节点计划重启前手动 REPLICAOF NO ONEREPLICAOF 切换从节点

repl_backlog_buffer 太小导致频繁全量同步

症状:从节点偶尔短暂断连(几秒),但重连后总是触发全量同步而非增量同步。

# 查看当前 backlog 使用情况
redis-cli INFO replication | grep repl_backlog
# repl_backlog_active:1
# repl_backlog_size:1048576     ← 当前大小
# repl_backlog_first_byte_offset:  ← 缓冲区中最旧命令的 offset
# repl_backlog_histlen:          ← 缓冲区实际使用量

# 如果 histlen 接近 size,说明缓冲区接近满

解决:根据写入量合理调整 repl-backlog-size,在 redis.conf 中配置或运行时动态调整:

# 在线调大(立即生效,但不持久化,需同步改配置文件)
CONFIG SET repl-backlog-size 67108864   # 64MB

过期键在主从间的处理

🔬 关键机制:从节点不会主动删除过期键,而是等待主节点同步 DEL 命令。

主节点处理过期键的方式:
  1. 惰性删除:客户端访问过期键时删除
  2. 定期删除:每 100ms 随机抽取键检查并删除

  → 无论哪种方式,主节点都会在删除后生成 DEL 命令
  → DEL 命令被写入 AOF 并传播给所有从节点
  → 从节点执行 DEL 命令删除对应键

🚨 陷阱:在从节点执行 TTL key 时,如果主节点已经惰性删除了该键但尚未同步 DEL 命令给从节点,从节点上的键仍存在但已过期。Redis 3.2+ 修复了此问题,从节点会返回 -2(键不存在)。


3. 哨兵机制 (Redis Sentinel)

3.1 架构概述

Redis Sentinel 是 Redis 官方提供的高可用方案,在 Redis 2.8 版本中正式引入。它是一个分布式系统,由多个哨兵进程共同协作,实现对主从架构的自动故障转移。

四大核心功能

┌─────────────────────────────────────────────────────────────┐
│                     Redis Sentinel 集群                       │
│                   (最少 3 个,推荐奇数个)                    │
├───────────────┬───────────────┬───────────────────────────────┤
│   ① 监控       │ ② 自动故障转移  │ ③ 配置提供者                  │
│   Monitoring  │  Auto Failover│  Config Provider             │
│               │               │                              │
│ 每 1s PING    │ 检测主节点下线  │ 客户端通过哨兵查询             │
│ 每 2s INFO    │ 选举新主节点   │ 当前主节点地址                 │
│ 哨兵间发现     │ 自动切换拓扑   │                              │
├───────────────┴───────────────┴───────────────────────────────┤
│                      ④ 通知 (Notification)                   │
│      故障转移结果通知客户端,支持 Pub/Sub 频道订阅              │
└─────────────────────────────────────────────────────────────┘

架构全景图

                  ┌──────────────────────────┐
                  │       Client / App        │
                  │   (Sentinel-aware 客户端)  │
                  └──────────┬───────────────┘
                             │ ① 问:当前 Master 在哪?
                             ▼
         ┌───────────────────────────────────────┐
         │          Sentinel 集群 (≥3)            │
         │                                       │
         │  ┌───────────┐  ┌───────────┐  ┌───────────┐
         │  │ Sentinel 1│  │ Sentinel 2│  │ Sentinel 3│
         │  │ :26379    │  │ :26380    │  │ :26381    │
         │  └─────┬─────┘  └─────┬─────┘  └─────┬─────┘
         │        │              │              │
         └────────┼──────────────┼──────────────┼────┘
                  │  监控/管理    │              │
         ┌────────┴──────────────┴──────────────┴────┐
         │              Redis 数据节点                  │
         │                                            │
         │  ┌──────────┐   ┌──────────┐  ┌──────────┐ │
         │  │  Master  │──▶│ Slave 1  │  │ Slave 2  │ │
         │  │  :6379   │   │  :6380   │  │  :6381   │ │
         │  └──────────┘   └──────────┘  └──────────┘ │
         └─────────────────────────────────────────────┘

3.2 核心功能详解

① 监控 (Monitoring)

哨兵通过三条心跳线维持对集群状态的感知:

心跳 频率 目标 用途
PING 每 1 秒 所有节点(主+从+哨兵) 检测节点存活状态
INFO 每 2 秒(对主从),每 10 秒(对哨兵) 主节点和从节点 获取复制拓扑(发现新从节点)
Pub/Sub 每 2 秒 主节点的 __sentinel__:hello 频道 哨兵间互相发现、交换信息
Sentinel 发现新从节点流程:

  Sentinel ── PING ──▶ Master
  Sentinel ── INFO ──▶ Master  → 返回 connected_slaves 列表
                                → Sentinel 自动添加对从节点的监控
                                → 无需手动配置从节点地址

哨兵间自动发现流程:

  Sentinel-1 ── PUBLISH __sentinel__:hello ──▶ Master
  Sentinel-2 ◀── SUBSCRIBE __sentinel__:hello ── Master
  Sentinel-2 从消息中得知 Sentinel-1 的 IP/端口
  两个哨兵建立直连关系
  
  → 无需手动配置其他哨兵的地址

② 自动故障转移 (Automatic Failover)

当哨兵集群判定主节点客观下线后,自动执行以下步骤:

故障转移全流程:

 1. 哨兵集群投票选出执行故障转移的 Leader Sentinel
 2. Leader Sentinel 从所有从节点中选举新主节点
 3. Leader Sentinel 向新主节点发送 REPLICAOF NO ONE
 4. Leader Sentinel 向其余从节点发送 REPLICAOF new_master_ip new_master_port
 5. 持续监控旧主节点,一旦恢复则将其变为新主节点的从节点

③ 配置提供者 (Configuration Provider)

客户端不直接硬编码 Redis 地址,而是向哨兵查询当前谁是主节点:

# 查询主节点地址
SENTINEL get-master-addr-by-name mymaster
# 返回: 1) "192.168.1.100"  2) "6379"

# 查询所有从节点
SENTINEL replicas mymaster
# SENTINEL slaves mymaster  (旧命令,仍兼容)

故障转移完成后,哨兵自动更新内部拓扑信息,客户端重新查询即可获得新主节点地址。

④ 通知 (Notification)

哨兵通过 Pub/Sub 机制向订阅者推送事件:

# 订阅所有哨兵事件(客户端)
SUBSCRIBE +sentinel

# 订阅特定事件频道
SUBSCRIBE +switch-master       # 主节点切换
SUBSCRIBE +sdown               # 主观下线
SUBSCRIBE +odown               # 客观下线
SUBSCRIBE +reboot              # 节点重启

# 事件消息格式
# +switch-master mymaster 192.168.1.100 6379 192.168.1.101 6379
#                      │                │         │               │
#                      │                │         └─ 新主节点端口  │
#                      │                └─ 新主节点 IP            │
#                      └─ 旧主节点端口                             │
#                       └─ 旧主节点 IP

3.3 🔬 故障判定机制

主观下线 (SDOWN — Subjectively Down)

判定流程:

  哨兵 ── PING ──▶ 节点
       ◀── 无响应 ── 持续 down-after-milliseconds 毫秒
       → 标记该节点为 SDOWN(该哨兵的主观判断,不代表集群共识)
  • 仅影响单个哨兵的观点,不会触发故障转移
  • 每个哨兵的 down-after-milliseconds 可以不一样(但应保持一致)

客观下线 (ODOWN — Objectively Down)

判定流程:

  Sentinel-1 发现 Master 为 SDOWN
     │
     │ 向其他哨兵发送: SENTINEL is-master-down-by-addr <ip> <port>
     │
     ├── Sentinel-2: "是的,我也认为 Master 下线了"
     ├── Sentinel-3: "是的,我也认为 Master 下线了"
     │
     └── 收集到 ≥ quorum 张赞成票
        → 标记该节点为 ODOWN
        → 启动故障转移流程

quorum 与 majority 的区别

概念 含义 典型值
quorum 判定客观下线所需的赞成票数 Sentinel 配置项,如 sentinel monitor mymaster IP PORT 2 中的 2
majority 执行故障转移所需的 Leader 选举票数 ceil(哨兵数 / 2),如 3 个哨兵 majority = 2

🚨 关键陷阱

情景:3 个哨兵,quorum = 1

  如果 Sentinel-1 与主节点网络不通(其他正常)
  Sentinel-1 认为主节点 SDOWN
  仅需 1 票即可达到 quorum → 标记 ODOWN
  Sentinel-1 发起故障转移,但无法获得 majority(需要 2 票)
  → 故障转移卡住,但集群已进入不稳定的中间状态

💡 最佳实践quorum 应设为 ceil(哨兵数 / 2) 以上,最保守设为 ceil(哨兵数 / 2) + 1。这样可以避免脑裂和误判。

3.4 新主节点选举规则

Leader Sentinel 按照以下优先级排序,选出最优从节点作为新主节点:

排序优先级(从高到低):

  ┌─ 1. 排除不健康节点
  │      ├── 处于 SDOWN / ODOWN 状态的节点
  │      ├── 断连时间超过 down-after-milliseconds * 10 的节点
  │      └── 与主节点断开连接超过一定时间的节点
  │
  ├─ 2. replica-priority(越小越优先,0 = 永不参选)
  │
  ├─ 3. 复制偏移量最大(数据最新)
  │
  ├─ 4. runid 字典序最小(相同条件下确定性选择)
  │
  └─ 选出新主节点
# 查看从节点优先级
redis-cli INFO replication | grep replica-priority

# 设置优先级(redis.conf 或运行时)
CONFIG SET replica-priority 50

💡 最佳实践

  • 性能最好的机器设低 replica-priority(如 10),让它优先成为主节点
  • 不希望成为主节点的从节点设 replica-priority 0
  • 跨机房部署时,同机房从节点优先级更低,避免主节点切到远程机房

3.5 故障转移完整流程

┌─ 阶段一:Leader 选举 ──────────────────────────────────────┐
│                                                             │
│  发现 ODOWN 的哨兵向其他哨兵发投票请求                        │
│  → 每个哨兵在同一个 epoch 只能投一票                          │
│  → 获得 majority 票的哨兵成为 Leader                         │
│  → 使用 Raft-like 协议,epoch 保证同一轮只有一位 Leader        │
│                                                             │
└─────────────────────────────────────────────────────────────┘
     │
     ▼
┌─ 阶段二:选举新主节点 ───────────────────────────────────────┐
│                                                             │
│  Leader Sentinel 按选举规则(优先级→偏移量→runid)选出最优从节点│
│                                                             │
└─────────────────────────────────────────────────────────────┘
     │
     ▼
┌─ 阶段三:升级新主节点 ────────────────────────────────────────┐
│                                                             │
│  Leader → 新主节点:  REPLICAOF NO ONE                        │
│  新主节点升级为独立节点,可接受读和写                          │
│                                                             │
└─────────────────────────────────────────────────────────────┘
     │
     ▼
┌─ 阶段四:重定向其他从节点 ────────────────────────────────────┐
│                                                             │
│  Leader → 其余从节点: REPLICAOF <new_master_ip> <port>       │
│  使用 parallel-syncs 控制同时同步的从节点数量                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘
     │
     ▼
┌─ 阶段五:监控旧主节点 ────────────────────────────────────────┐
│                                                             │
│  旧主节点恢复后:                                            │
│  Sentinel 检测到旧主节点重新上线                               │
│  → 向旧主节点发送 REPLICAOF <new_master_ip> <port>           │
│  → 旧主节点降级为新主节点的从节点                              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

3.6 配置详解

# sentinel.conf
# ─── 基础配置 ───
port 26379                                                    # 哨兵监听端口

# ─── 监控配置 ───
# sentinel monitor <master-name> <ip> <port> <quorum>
sentinel monitor mymaster 192.168.1.100 6379 2                # quorum 建议 ≥ ceil(N/2)

# ─── 超时配置 ───
sentinel down-after-milliseconds mymaster 30000               # 主观下线超时(毫秒)
                                                              # 建议 > 网络 RTT × 3

sentinel failover-timeout mymaster 180000                     # 故障转移总超时(毫秒)
                                                              # 超过此时间未完成则中止并重试

sentinel parallel-syncs mymaster 1                            # 故障转移后同时向新主节点同步的从节点数
                                                              # 建议设为 1,避免多从节点同时全量同步

# ─── 认证配置 ───
sentinel auth-pass mymaster your_password                     # 主节点密码
# Redis 6.0+ 支持 ACL,也可配置用户名
sentinel auth-user mymaster your_username

# ─── 脚本通知(可选) ───
sentinel notification-script mymaster /path/to/script.sh       # 故障转移等事件通知脚本
sentinel client-reconfig-script mymaster /path/to/script.sh    # 客户端重配置脚本

# ─── 工作目录 ───
dir /tmp                                                       # 哨兵状态文件和运行时配置

# ─── Docker/容器化环境必配 ───
sentinel announce-ip 10.0.0.1                                 # 哨兵对外宣告的 IP
sentinel announce-port 26379                                  # 哨兵对外宣告的端口

💡 配置要点

参数 建议值 说明
down-after-milliseconds 30000 (30s) 太短易误判,太长恢复慢
failover-timeout 180000 (3min) 需大于从节点完成全量同步的时间
parallel-syncs 1 避免新主节点压力过大
quorum ceil(N/2)ceil(N/2)+1 防止偶发网络抖动误判

3.7 哨兵集群通信

哨兵间通过主节点的 __sentinel__:hello 频道实现自动发现:

┌──────────────┐                          ┌──────────────┐
│  Sentinel 1  │                          │  Sentinel 2  │
│  192.168.1.1 │                          │  192.168.1.2 │
└──────┬───────┘                          └──────┬───────┘
       │                                         │
       │ 每 2 秒 PUBLISH                        │ 每 2 秒 PUBLISH
       │ __sentinel__:hello                     │ __sentinel__:hello
       │ "192.168.1.1, 26379, ..."              │ "192.168.1.2, 26379, ..."
       │                                         │
       ▼                                         ▼
┌─────────────────────────────────────────────────────────┐
│                    Master (:6379)                        │
│                   __sentinel__:hello                     │
│                        频道                              │
└─────────────────────────────────────────────────────────┘
       │                                         │
       │ SUBSCRIBE                              │ SUBSCRIBE
       │ __sentinel__:hello                     │ __sentinel__:hello
       │                                         │
       ▼ 收到 Sentinel 2 的信息                   ▼ 收到 Sentinel 1 的信息
  Sentinel 1 自动添加                     Sentinel 2 自动添加
  Sentinel 2 到已知哨兵列表                 Sentinel 1 到已知哨兵列表
  • 每个哨兵每 2 秒__sentinel__:hello 频道发布自身信息(IP、端口、runid、当前 epoch 等)
  • 每个哨兵订阅该频道,从而获知其他哨兵的存在
  • 发现新哨兵后建立直连,进行 PING / INFO / 投票等通信
  • 无需在配置文件中手动列出所有哨兵地址

3.8 🔬 Sentinel 内部机制

哨兵状态机

              ┌─────────────┐
              │  正常运行    │ 每 1s PING, 每 2s INFO
              └──────┬──────┘
                     │ 检测到 SDOWN
                     ▼
              ┌─────────────┐
              │  询问其他     │ 发送 SENTINEL is-master-down-by-addr
              │  哨兵意见    │
              └──────┬──────┘
                     │ 达到 quorum
                     ▼
              ┌─────────────┐
              │  Leader选举  │ Raft-like 投票
              └──────┬──────┘
                     │ 当选 Leader
                     ▼
              ┌─────────────┐
              │  执行故障     │ 选举新主节点、重配置拓扑
              │  转移       │
              └──────┬──────┘
                     │ 故障转移完成
                     ▼
              ┌─────────────┐
              │  恢复正常     │ 继续监控(包括旧主节点恢复)
              │  监控       │
              └─────────────┘

🔬 TILT 模式

TILT 模式是哨兵的自我保护机制。当哨兵检测到系统时钟发生异常(如时间跳变超过 2 秒)时,进入 TILT 模式:

触发条件:
  - 两次定时任务之间的实际时间差与预期差 > 2 秒
  - 常见原因:NTP 同步导致时钟跳变、虚拟机暂停/恢复

TILT 模式下的行为:
  - 继续收集信息(PING、INFO、Pub/Sub)但不做任何决策
  - 不发起故障转移
  - 不参与投票
  - 30 秒后退出 TILT 模式(如果时钟恢复正常)

查看 TILT 状态:
  redis-cli -p 26379 INFO sentinel | grep tilt
  # tilt:0 → 正常
  # tilt:1 → TILT 模式

🚨 运维提示:所有哨兵节点的时间必须保持同步,建议配置 NTP 服务。

Leader 选举(Raft-like 协议)

哨兵的 Leader 选举借鉴了 Raft 协议的核心思想:

选举过程:

  1. 发现 ODOWN 的哨兵增加自己的 current_epoch
  2. 向其他哨兵发送投票请求(包含新 epoch)
  3. 每个哨兵在每个 epoch 只能投一票(先到先得)
  4. 获得 ≥ majority 票数的哨兵成为 Leader
  5. 如果超时未选出 Leader,等待 failover-timeout 后重新选举

关键属性:
  - epoch 是单调递增的逻辑时钟
  - 每个 epoch 最多产生一个 Leader
  - 确保故障转移的唯一性

3.9 客户端使用哨兵

Go 示例(go-redis v9 哨兵模式)

package main

import (
    "context"
    "fmt"

    "github.com/redis/go-redis/v9"
)

func main() {
    rdb := redis.NewFailoverClient(&redis.FailoverOptions{
        MasterName:       "mymaster",                       // 哨兵监控的主节点名称
        SentinelAddrs:    []string{                          // 哨兵地址列表
            "192.168.1.1:26379",
            "192.168.1.2:26379",
            "192.168.1.3:26379",
        },
        SentinelPassword: "sentinel_password",               // 哨兵密码(如果有)
        Password:         "redis_password",                  // Redis 节点密码
        DB:               0,
        PoolSize:         10,
        MinIdleConns:     5,
        DialTimeout:      5 * time.Second,
        ReadTimeout:      3 * time.Second,
        WriteTimeout:     3 * time.Second,
    })

    ctx := context.Background()

    // 哨兵客户端自动处理主节点发现和切换
    err := rdb.Set(ctx, "key", "value", 0).Err()
    if err != nil {
        panic(err)
    }

    val, err := rdb.Get(ctx, "key").Result()
    if err != nil {
        panic(err)
    }
    fmt.Println("key:", val)
}

客户端工作流程

客户端启动
    │
    ▼
随机连接一个 Sentinel ────▶ SENTINEL get-master-addr-by-name "mymaster"
    │                                   │
    │                           返回 Master IP:Port
    │                                   │
    ▼                                   ▼
直接连接 Master 并执行命令 ◀────────────┘
    │
    │ 同时 SUBSCRIBE +switch-master 频道
    │
    ▼
收到主节点切换通知
    │
    ▼
断开旧 Master ──▶ 重新查询 Sentinel ──▶ 连接新 Master

💡 最佳实践

  • Sentinel 地址列表配全(3 个以上),避免单点
  • 客户端应同时订阅 +switch-master 频道以快速感知切换
  • 切换期间短暂不可用,调用方应实现重试机制
  • 不要混用哨兵模式和直连模式,选其一

3.10 部署建议

推荐的物理部署拓扑(6 台机器):

    ┌────────────────────────────────────────────────────────┐
    │  机器 1         机器 2         机器 3                   │
    │  Sentinel 1      Sentinel 2     Sentinel 3             │
    │  端口 26379      端口 26379     端口 26379              │
    └────────────────────────────────────────────────────────┘
    
    ┌────────────────────────────────────────────────────────┐
    │  机器 4         机器 5         机器 6                   │
    │  Redis Master   Redis Slave 1  Redis Slave 2           │
    │  端口 6379      端口 6379      端口 6379                │
    └────────────────────────────────────────────────────────┘
部署原则 说明
哨兵独立部署 哨兵不与 Redis 节点同机部署,避免单机故障同时影响两者
奇数个哨兵 3 或 5 个,保证 majority 投票
跨机架/可用区 哨兵和主从节点分布在不同的物理机架或可用区
时钟同步 所有节点配置 NTP,避免 TILT 模式
网络隔离 哨兵和 Redis 节点间网络延迟应低于 down-after-milliseconds 的 1/3

3.11 Sentinel 管理命令速查

# 连接哨兵
redis-cli -p 26379

# ─── 信息查询 ───

# 查看所有监控的主节点
SENTINEL masters

# 查看指定主节点的详细信息(IP、端口、从节点、哨兵列表、状态等)
SENTINEL master mymaster

# 查看指定主节点的所有从节点
SENTINEL replicas mymaster
# 旧命令(仍可用)
SENTINEL slaves mymaster

# 查看指定主节点的所有哨兵
SENTINEL sentinels mymaster

# 获取当前主节点地址
SENTINEL get-master-addr-by-name mymaster
# 返回: 1) "192.168.1.100" 2) "6379"

# ─── 管理操作 ───

# 强制故障转移(主动发起,无需等待故障)
SENTINEL failover mymaster

# 重置指定主节点的哨兵状态(清除已知信息,重新发现)
SENTINEL reset mymaster

# 重置所有主节点的哨兵状态
SENTINEL reset *

# 修改哨兵配置(运行时)
SENTINEL SET mymaster down-after-milliseconds 60000

# 移除指定主节点的监控
SENTINEL REMOVE mymaster

# 查看哨兵状态
redis-cli -p 26379 INFO sentinel

# ─── 哨兵状态监控 ───

# 检查哨兵对主节点的共识
SENTINEL ckquorum mymaster
# 返回: OK 3 usable Sentinels. Quorum and failover authorization can be reached

# 模拟故障转移(仅返回将选哪个从节点,不执行)
# 注意:此命令名是 SIMULATE_FAILOVER(需确认版本支持)

4. 🚨 常见陷阱

哨兵数量为偶数导致投票分裂

场景:4 个哨兵,majority = ceil(4/2) = 2

  如果网络分区将哨兵分成 2-2:
  - 两边的 Leader 候选人各得 2 票(自己的 + 另一票)
  - 但需要 > 2 票才能成为 Leader
  - 故障转移无法完成

结论:哨兵数量必须为奇数(3、5),不要用偶数。

quorum 设置不当导致误判或无效调度

quorum 值 哨兵数 3 效果
1 3 单个哨兵网络抖动即可标记 ODOWN,但无法获 majority 执行故障转移。集群进入不稳定中间状态。
2 3 正确:需 2 票标记 ODOWN,majority=2 可执行故障转移
3 3 过于严格:必须全部哨兵同意,任何一个哨兵故障都无法切换

down-after-milliseconds 设置太短导致频繁切换

设置过短(如 1 秒):
  → 偶发网络抖动被误判为节点故障
  → 触发不必要的故障转移
  → 故障转移期间可能有数据丢失(异步复制未完成)

设置过长(如 5 分钟):
  → 真实故障恢复慢
  → 服务不可用时间过长

建议:生产环境 15-30 秒

从节点过多导致主节点复制压力大

主节点每新增一个从节点:
  - fork 一个子进程进行 BGSAVE
  - fork 操作本身会阻塞主线程(fork 时间与内存成正比,大内存实例尤为严重)
  - CPU 和内存使用增加
  
缓解:
  - 使用 repl-diskless-sync 减少磁盘 I/O
  - 使用链式复制分摊压力
  - 多个从节点交错启动(避免同时全量同步)

fork 子进程时内存不足导致 OOM

🔬 Redis fork 使用 Copy-On-Write (COW) 机制。fork 期间内核为子进程创建页表映射,如果主进程在 BGSAVE 期间有大量写入,被修改的内存页会被复制,导致实际内存使用量可能短暂翻倍。

计算公式:
  安全内存 = Redis 数据大小 + fork 期间预期写入量 + 系统预留

风险场景:
  - Redis 使用 8GB 内存,宿主机共 16GB
  - BGSAVE 期间有 4GB 写入
  - 内存使用 = 8GB (现有) + 4GB (COW 复制) = 12GB
  - 加上系统开销,可能触发 OOM Killer

缓解:
  - 宿主机预留足够内存(至少 Redis 数据大小 × 1.5)
  - 避免在写入高峰时段执行 BGSAVE
  - 使用 `vm.overcommit_memory=1` 配合 swap

主节点重启后 runid 变化导致全量同步

原因:runid 是 Redis 启动时随机生成的,重启后必然变化。

影响:
  - 从节点发现 runid 变化 → 无法增量同步 → 触发全量同步
  - 全量同步可能耗时数十分钟(取决于数据量)
  - 期间从节点可能不可用(取决于 replica-serve-stale-data 设置)

缓解:
  - 计划内重启时先执行故障转移,让哨兵将主节点切走
  - 增大 repl-backlog-size 无济于事(因为 runid 变了)
  - 从 Redis 7.0 开始,可以通过 replication-pair-id 减少此问题

哨兵集群中哨兵节点时间不同步

# 检查哨兵 TILT 状态
redis-cli -p 26379 INFO sentinel | grep tilt
# tilt:0  正常
# tilt:1  异常,已进入 TILT 模式

# TILT 模式下哨兵不执行任何操作
# 可能导致集群在该哨兵视角下"瘫痪"

解决: 配置 NTP 确保时间同步

Docker/容器化环境未配置 announce-ip/announce-port

🚨 高频陷阱:容器/Docker 环境使用端口映射(如 -p 6380:6379)时,Redis 向哨兵和其他节点报告的仍是容器内部 IP 和端口(如 172.17.0.3:6379),导致哨兵无法正确连接。

# 主节点 redis.conf
replica-announce-ip 10.0.0.1       # 对外可访问的 IP
replica-announce-port 6379         # 对外映射的端口

# 哨兵 sentinel.conf
sentinel announce-ip 10.0.0.10     # 哨兵对外可访问的 IP
sentinel announce-port 26379       # 哨兵对外映射的端口

# 同时配置 sentinel monitor 使用可路由的 IP
sentinel monitor mymaster 10.0.0.1 6379 2

主从切换后客户端连接信息未更新

问题表现:
  - 故障转移后,客户端仍连旧主节点
  - 旧主节点回复 READONLY 错误(已降级为从节点)
  - 客户端写入失败

原因:
  - 客户端未使用哨兵模式
  - 客户端未订阅 +switch-master 频道
  - 客户端 DNS 缓存了旧地址

解决:
  - 使用 Sentinel-aware 客户端(go-redis FailoverClient)
  - 客户端需要 SUBSCRIBE +switch-master
  - 编写应用层重连逻辑

错误认为主从复制能替代备份

🚨 主从复制不能替代真正的数据备份。如果误删了主节点的数据(如 FLUSHALL),该命令会同步到所有从节点,所有副本的数据同时丢失。

为什么需要独立备份:
  - 误操作(DROP / FLUSHALL / DEL)会同步到从节点
  - 应用 Bug 导致的逻辑损坏会传播
  - 无法做到时间点恢复 (PITR)

正确的备份策略:
  - 定期在从节点上执行 BGSAVE / AOF 备份到外部存储
  - 保留多份历史备份(至少过去 7 天)
  - AOF + RDB 混合持久化
  - 异地/多可用区存放备份文件

parallel-syncs 设置过大导致服务雪崩

故障转移后,新主节点需要向所有从节点同步数据:

  parallel-syncs = 1:
    Slave 1 同步完成 → Slave 2 开始同步 → Slave 3 开始同步
    主节点负担可控,从节点逐个恢复
    
  parallel-syncs = N (如 10):
    10 个从节点同时发起全量同步
    主节点需要 fork 10 个子进程进行 BGSAVE
    → 主节点 CPU 飙升、内存耗尽
    → 可能引发新的故障转移
    → 雪崩

建议: parallel-syncs 设为 1,宁可慢一点,不能压垮新主节点

附录:常见问题排查速查表

症状 排查命令 可能原因
从节点反复全量同步 INFO replication 查看 master_repl_offsetslave offset repl-backlog-size 太小
从节点显示 master_link_status:down 在从节点执行 INFO replication 网络不通、认证失败、主节点宕机
哨兵未发起故障转移 SENTINEL master mymaster 查看 num-other-sentinels 哨兵数量不足、quorum 未达到
故障转移后客户端报错 客户端日志 客户端未连接哨兵、连接池未刷新
TILT 模式持续 INFO sentinel 查看 tilt 时钟不同步
fork 耗时长 INFO stats 查看 latest_fork_usec 内存过大、系统内存碎片
主从数据不一致 对比 master_repl_offset 差值 异步复制延迟、网络问题