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 ONERedis 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 是 nnnPSYNC (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 ONE→REPLICAOF切换从节点
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 │
# └─ 旧主节点端口 │
# └─ 旧主节点 IP3.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_offset 与 slave 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 差值 |
异步复制延迟、网络问题 |