Files
cs-note/hhs/Redis/06-主从与哨兵.md
T
2026-05-25 23:07:45 +08:00

611 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [Redis, 缓存, 高可用, 主从, Sentinel]
create time: 2026-05-15 18:13
---
# Redis 主从复制与 Sentinel
## 概述
### 从单机困境说起
假设你的 Redis 跑在一台服务器上,所有读写都走这一个实例。随着业务增长,两个现实问题会逐渐暴露:
1. **读性能瓶颈**:热点 key 被反复访问,单实例 QPS 顶到天花板(通常 10w 左右),请求开始排队
2. **单点故障风险**:服务器宕机 = Redis 全线不可用,数据甚至可能丢失
> [!QUESTION] 想一想:如果让你来设计解决方案,你会怎么做?
> - 瓶颈在"读"→ 能不能让**多个节点分摊读请求**?
> - 风险在"单点"→ 能不能让数据**自动备份到其他机器**?
> - 故障了靠人恢复太慢 → 能不能**自动检测并切换**?
这就是 **主从复制** 和 **Sentinel** 要解决的问题。
### 两大核心机制
| 机制 | 解决什么问题 | 一句话说明 |
|------|------------|----------|
| **主从复制(Replication)** | 数据冗余 + 读扩展 | Master 处理写操作,Replica 自动同步数据并承担读请求 |
| **Sentinel(哨兵)** | 人工恢复慢 | 自动检测 Master 故障,提升最佳 Replica 为新 Master |
通俗地说:主从复制是"抄作业"机制——Master 做完(写入),Replica 抄一份;Sentinel 是"监考老师"——发现 Master"交不了卷"(宕机),立刻指定一个 Replica"接替答题"。
> [!SUMMARY] 知识定位
> - ✅ 本方案适合 **读多写少** 的场景(8:2 ~ 9:1)
> - ⚠️ 写入仍然集中在单点,若需水平写入 → [[hhs/Redis/07-集群方案]]
> - 🔧 这是学习 Redis Cluster 的前置知识
```mermaid
flowchart TD
Master["Master<br/>写操作入口"]
R1["Replica-1"]
R2["Replica-2"]
R3["Replica-3"]
Master -->|"全量/增量同步"| R1
Master -->|"全量/增量同步"| R2
Master -->|"全量/增量同步"| R3
ClientRW["写请求"] --> Master
ClientR["读请求"] --> R1
ClientR --> R2
ClientR --> R3
```
## 一、主从复制原理
### 异步 vs 半同步
复制的核心矛盾是:**性能** 和 **数据安全** 不可兼得。我们先看两种策略的区别:
```mermaid
flowchart LR
subgraph async["异步复制(Redis 默认)"]
A1["Master 写入"] -->|"立即返回 OK"| A2["客户端"]
A1 -.->|"后台异步发送"| A3["Replica"]
end
subgraph semisync["半同步(手动启用)"]
B1["Master 写入"] -->|"等待 Replica 确认"| B2["Replica"]
B2 -->|"确认收到"| B3["客户端收到 OK"]
end
```
> [!QUESTION] 为什么 Redis 默认选择异步复制?
> 想象一下:你去超市买东西,收银员每收一笔钱都要先打电话给总部确认到账了才给你小票——你愿意等吗?对追求低延迟的 Redis 来说,**等 Replica 确认 = 强制每次写入多等一次网络往返**(通常 0.1~1ms),这对微秒级响应的场景是不可接受的。
**异步复制** 的工作方式:Master 写完就返回客户端,Replica 的事"后台慢慢同步"。代价是极端情况下可能丢数据:
```text
场景: 写 A=1 → Master 返回 OK → Master 突然宕机(数据还没来得及传到 Replica)
→ Replica 上没有 A=1 → 用 Replica 接管后,这个写入就丢了
```
> [!NOTE] 这个丢数据的概率高吗?
> 正常情况下极低——因为 Master 到 Replica 的同步几乎是实时的(毫秒级),只有在 Master 写入后**立刻宕机**的极短窗口内才会丢。这就像你刚存完钱、银行还没来得及记账就停电了——概率很小,但在金融场景下不能接受。
#### 如果业务要求"至少一份副本确认"呢?
Redis 提供了 `WAIT` 命令,可以让客户端**阻塞等待**指定数量的副本确认同步完成。这不算严格的半同步(因为 Master 已经先写入并返回了),但能大幅降低丢数据的概率:
```go
// Go: 写入后,确保至少 1 个副本已经同步了这条命令
client.Set(ctx, "order:12345", orderData, 0)
numReplicas := 1 // 至少 1 个 replica 收到
timeout := 100 // 最多等 100ms,超时就放弃
replicaCount := client.Do(ctx, "WAIT", numReplicas, timeout).Int()
if replicaCount >= 1 {
// 放心:就算 Master 立刻宕机,至少有 1 个 Replica 有这份数据
} else {
// 超时了,数据可能只在 Master 上 → 业务层面考虑重试或告警
}
```
> [!TIP] WAIT 的代价
> `WAIT` 会阻塞当前连接直到满足条件或超时。高频写场景慎用,建议仅在关键业务路径上使用(如订单创建、扣款等),普通缓存写入不需要。
### 全量同步 vs 增量同步
主从同步分为两种模式,理解它们的前提是知道一个关键概念:**Replication Offset(复制偏移量)**——你可以把它想象成一条数据"流水线"上的**刻度尺**。Master 每写入一条命令,刻度就往后移一点;Replica 同步到哪里了,也有自己的刻度。只要两个刻度对得上,就能"增量同步"。
> [!NOTE] PSync:一次连接,多次复用
> Redis 2.8+ 引入 PSYNC 协议替代旧版 SYNC,核心优势:**断线重连后可增量同步**。旧版 SYNC 每次断线都得全量传输(相当于每次请假都得抄一整本笔记),PSYNC 改为"只抄你缺的那几页"。
```mermaid
sequenceDiagram
participant R as Replica
participant M as Master
Note over R,M: "阶段 1: 全量同步(首次连接或断线过久)"
R->>M: PSYNC ? -1 (首次连接, 没有 offset 记录)
M->>M: 触发 BGSAVE, 生成 RDB 快照文件
M-->>R: +FULLRESYNC <runId> <offset>, 然后发送 RDB 文件
R->>R: 清空旧数据, 用 RDB 全量覆盖
Note over M: "RDB 生成期间, 新的写入命令不能丢"
loop "RDB 传输期间的新写入"
M->>M: 追加到 repl-backlog-buffer
end
M-->>R: 发送 backlog 中缓存的命令流
R->>R: 重放缓冲命令, 追上 Master 的进度
Note over R,M: "阶段 2: 增量同步(后续心跳, 每秒一次)"
loop "正常运行期间"
R->>M: PSYNC <masterRunId> <myOffset>
M->>R: 只发送 offset 之后的增量命令
end
```
> [!QUESTION] 全量同步的代价是什么?
> 全量同步需要 Master 执行 `BGSAVE`(fork 子进程生成 RDB 文件),然后通过网络传给 Replica。如果数据量大(比如 10GB),这个过程会**消耗大量 CPU、内存和网络带宽**。所以我们的目标是:**尽量避免全量同步,让增量同步成为常态**。
#### 什么情况下触发全量同步?
| 触发条件 | 为什么? | 能避免吗? |
|---------|------|------|
| **首次连接** | Replica 是空的,没有数据也没有 offset,只能全量下载 | ❌ 必然发生 |
| **Master 重启** | Master 重启后 Run ID 变了,Replica 认不出"老朋友" | ⚠️ 持久化可缓解(重启后 Run ID 不变) |
| **Replica 手动执行 SLAVEOF** | 相当于主动"认新主人",必须重新来过 | ❌ 手动触发,通常可控 |
| **Backlog 溢出** | 断线太久,Master 的"缓冲笔记本"已经翻页覆盖了断线前的内容 | ✅ **增大 `repl-backlog-size`** |
| **config-resetstat 执行** | 元数据被重置,offset 信息丢失 | ⚠️ 少用此命令 |
> [!WARNING] 生产环境最常见的"不必要全量同步"
> 绝大多数情况是 **Backlog 过小**导致的。网络抖动几秒,Backlog 不够用,Replica 被迫全量同步——这在网络不稳定的环境中会反复发生,严重拖垮 Master 性能。**加大 Backlog 是性价比最高的优化手段**。
### 复制背压(Replication Backlog)
每个 Master 内部维护一个固定大小的 **环形缓冲区**(默认仅 1MB),这是实现增量同步的关键:
```mermaid
flowchart LR
WriteHead["写入指针(新命令)"] -->|"追加写入"| Buffer["repl-backlog-buffer<br/>环形缓冲区"]
Buffer -->|"读取并发送"| ReadHead["读取指针(发给 Replica)"]
Buffer -->|"空间不足时"| Overwrite["覆盖最旧数据"]
```
> [!NOTE] 什么是"环形缓冲区"?
> 想象一个**圆形的笔记本**,只有固定的 100 页。你从第 1 页开始记笔记,记满了就回到第 1 页覆盖重写。Replica 落后得越多,它需要"回看"的内容就越远。如果它的位置已经被覆盖了,就只能全量重新抄一本——这就是全量同步。
>
> 所以 Backlog 的大小直接决定了:**Replica 最多能断线多久还能增量恢复**。
```conf
# 相关配置
repl-backlog-size 256mb # 缓冲区大小,增大允许更长断线恢复
repl-backlog-ttl 3600 # 无人订阅时多久自动释放(秒)
```
**经验估算公式**:假设业务 QPS = 10,000,每条命令平均 50 字节,则每秒消耗 ~500KB 缓冲区。
- backlog = 1MB → 约支持 **2 秒** 断线恢复
- backlog = 256MB → 约支持 **8 分钟** 断线恢复
### 大 Key 问题
> [!QUESTION] 一个 key 有 5MB 大小,会有什么问题?
> - **写入阻塞**:Redis 是单线程的,写入一个 5MB 的 Hash/Set 会阻塞其他所有请求几毫秒甚至更久
> - **同步瓶颈**:这条命令不仅要在 Master 上执行,还要通过网络传给每个 Replica,相当于同一条 5MB 的数据在内网里传 N 次
> - **内存尖峰**:`BGSAVE` 时 fork 子进程,大 key 会导致 COW(Copy-On-Write)产生大量内存拷贝
> [!SUMMARY] 如何发现大 Key?
> ```bash
> redis-cli --bigkeys # 在线扫描(谨慎!高 QPS 环境有性能影响)
> scan 0 MATCH * COUNT 100000 # 逐页扫描,用 type/hashlist/set 分别评估
> ```
**解决方案**:
- **写入侧**:拆分大 key 为多个小 key(例如一个存储用户全部订单的 Hash → 按月份拆分为 `user:orders:2026-05`、`user:orders:2026-06` 等多个小 Hash)
- **同步侧**:启用无盘复制(`repl-diskless-sync yes`),Master 直接通过 socket 把 RDB 发给所有 Replica,避免写磁盘再读磁盘的双重开销
## 二、级联复制(拓扑优化)
### 为什么要级联?
假设 Master 需要服务 10 个 Replica,每个 Replica 每秒同步 ~500KB 的命令流,那 Master 每秒要输出 **5MB 的复制流量**。而且每个 Replica 断线重连都可能触发全量同步——Master 的网络带宽和 CPU(BGSAVE 的 fork)会成为瓶颈。
> [!QUESTION] 怎么降低 Master 的压力?
> 答案:不让所有 Replica 都直接连 Master,而是让部分 Replica "转发"数据给其他 Replica,形成**树形拓扑**。
```mermaid
flowchart LR
Master["Master<br/>只服务 2 个 Replica"]
R1["Replica-1<br/>(同时作为下游的 Master)"]
R2["Replica-2"]
R3["Replica-3<br/>(从 R1 同步)"]
R4["Replica-4<br/>(从 R1 同步)"]
Master -->|"直接同步"| R1
Master -->|"直接同步"| R2
R1 -->|"级联同步"| R3
R1 -->|"级联同步"| R4
```
> [!TIP] 最佳实践
> - 只有 1~2 个 Replica 直连 Master,其余通过级联获取数据
> - 级联的 Replica 也能配置为接受下游连接(Redis 原生支持,无需额外设置)
> - **缺点**:级联层级越深,末端 Replica 的数据延迟越大。通常 **两层** 是合理的上限
## 三、只读副本注意事项
Replica 默认是 **只读** 的(`replica-read-only yes`),这不是 Redis 故意限制你,而是有充分的技术原因:
> [!WARNING] 为什么不应该在 Replica 上写数据?
> 想象一下:你在 Replica 上写入了 `SET foo bar`,但 Master 上没有这个写入。下一次同步发生时,Master 会把自己的数据集"覆盖"到 Replica 上——你写的数据**悄无声息地消失了**。注意 Redis 复制是**单向的**(Master → Replica),你在 Replica 上的写入不会反向传播到 Master,但这本身就意味着 Replica 和 Master 之间的数据已经不一致了。
>
> **结论**:在 Replica 上写数据,本质上是在制造**数据不一致的定时炸弹**。
### Replica 的关键配置详解
```conf
# --- 副本如何发现 Master ---
# 在 Docker/NAT 环境下,容器内部 IP 和宿主机 IP 不同。
# 如果不设置 announce-ip,Sentinel 和其他节点可能会记录容器内部 IP,
# 导致跨网络访问失败。
replica-announce-ip 192.168.1.20 # 对外宣告的 IP
replica-announce-port 6379 # 对外宣告的端口
# --- Replica 是否可被查询 ---
# 这组配置决定:当 Replica 和 Master 断开连接时,还能不能对外服务?
replica-serve-stale-data yes # yes=断线时仍返回旧数据(可用性优先)
# no=断线时直接报错(一致性优先)
replica-read-only yes # 只读保护,防止误写入导致数据不一致
replica-lazy-flush no # FULLRESYNC 时清空旧数据的策略:
# no=立即 flush(默认),yes=惰性清理
# --- 副本同步相关 ---
repl-diskless-sync no # 无盘复制:Master 不写 RDB 文件,
# 直接通过 socket 发给 Replica(适合磁盘 I/O 慢的环境,如云盘/HDD)
repl-diskless-sync-delay 5 # 开启无盘复制后,等几秒让多个 Replica 同时连接并接收 RDB 流,
# 减少 Master 重复生成 RDB 的次数(不是随机延迟,有明确优化意图)
repl-timeout 60 # 同步超时阈值(秒),超时则断开重连
```
> [!QUESTION] `replica-serve-stale-data` 应该设为 yes 还是 no?
> 这取决于你的业务对**可用性 vs 一致性**的取舍:
> - **电商首页**、**推荐列表**等场景:设为 `yes`,返回稍微过期的数据总比报错好
> - **库存扣减**、**余额查询**等场景:设为 `no`,宁可不可用也不能返回错误数据
## 四、核心参数调优参考
> [!NOTE] 根据业务特点选择合适的参数组合
### 方案 A:极致性能优先
```conf
# 适用于读 QPS > 50k,对一致性要求不高
repl-backlog-size 256mb # 大容量 backlog 减少全量同步
repl-diskless-sync yes # 无盘复制加速 RDB 传输
replica-lazy-flush no # 确保同步前快速清理旧数据
```
### 方案 B:一致性优先
```conf
# 适用于金融、账务等场景
WAIT 1 100 # 写入时确认至少 1 个副本成功
repl-backlog-size 512mb # 更大缓冲区容纳更多增量命令
```
### 常用调试命令
```bash
# 查看当前复制状态
redis-cli INFO replication
# 输出示例:
# role:master ← 或 replica
# connected_slaves:3
# master_repl_offset:12345678
# repl_backlog_active:1
# repl_backlog_size:268435456
```
## 五、Sentinel 哨兵机制
### 为什么需要 Sentinel?
上文介绍了主从复制,但有一个关键问题没解决:**Master 宕机了怎么办?**
> [!QUESTION] 没有 Sentinel 时,Master 故障的处理流程是什么?
> 1. 运维人员收到告警(可能是半夜 3 点)
> 2. 登录服务器,确认 Master 确实挂了
> 3. 手动挑一个数据最新的 Replica,执行 `SLAVEOF NO ONE` 提升为新 Master
> 4. 让其他 Replica 指向新 Master
> 5. 通知客户端切换连接地址
>
> 整个过程可能需要 **几分钟甚至更久**,而且容易出错。Sentinel 要做的就是把这整套流程**自动化**。
### 架构组成
Sentinel 本身也是一个 Redis 进程(只是不处理普通数据读写),它的工作就是**盯着 Master 和 Replica,必要时自动故障转移**。
```mermaid
flowchart TD
S1["Sentinel-1"]
S2["Sentinel-2"]
S3["Sentinel-3"]
MasterNode["Master"]
RepNode["Replica"]
S1 <-->|"互相探测心跳"| S2
S1 <-->|"互相探测心跳"| S3
S2 <-->|"互相探测心跳"| S3
S1 <-->|"监控 Master/Replica 状态"| MasterNode
S2 <-->|"监控 Master/Replica 状态"| MasterNode
S3 <-->|"监控 Master/Replica 状态"| MasterNode
MasterNode <-->|"主从同步"| RepNode
```
> [!QUESTION] 为什么需要奇数个 Sentinel?
> 故障转移需要**多数派投票**才能决策(和选举类似)。3 个 Sentinel 中有 2 个同意就能执行;但如果只有 2 个,一旦 1 个挂了,剩下的 1 个永远达不到多数,故障转移就无法执行。偶数个(比如 4 个)也浪费——4 个节点允许 1 个故障,和 3 个一样,却多消耗了一份资源。
>
> **记住这个公式:N 个 Sentinel 允许 (N-1)/2 个故障**。3 个允许 1 个故障,5 个允许 2 个故障。
### 核心功能
Sentinel 有三大核心职责:**监控**、**通知**和**故障转移**。我们逐一拆解。
#### 1. 监控(Monitoring)——"心跳检测"
每个 Sentinel 每隔 1 秒向 Master 和 Replica 发送 `PING` 命令,期待收到 `PONG` 回复。如果在 `down-after-milliseconds` 时间内没有收到回复,Sentinel 就认为这个节点"有问题了"。
但一个 Sentinel 的判断可能是误判(比如只是 Sentinel 和 Master 之间的网络抖动),所以需要多个 Sentinel 协同确认:
```mermaid
flowchart TD
A["Sentinel-1 向 Master 发 PING"] --> B{"收到 PONG?"}
B -->|"是"| C["一切正常, 继续监控"]
B -->|"否"| D["标记 Master 为 SDOWN<br/>(主观下线: 我觉得它挂了)"]
D --> E{"询问其他 Sentinel:<br/>你们觉得 Master 还活着吗?"}
E -->|"Quorum 个 Sentinel<br/>都认为挂了"| F["标记 Master 为 ODOWN<br/>(客观下线: 大家都确认它挂了)"]
E -->|"没达到 Quorum"| G["可能是网络局部问题<br/>保持 SDOWN, 继续观察"]
F --> H["进入故障转移流程"]
```
> [!NOTE] SDOWN vs ODOWN,一句话总结
> - **SDOWN**(Subjectively Down)= "我觉得它挂了"——单个 Sentinel 的主观判断
> - **ODOWN**(Objectively Down)= "大家都确认它挂了"——达到 Quorum 个 Sentinel 的客观共识
>
> 只有 ODOWN 才会触发真正的故障转移,SDOWN 只是一个"预警"状态。
#### 2. 选主(Leader Election)——"谁来操刀故障转移"
当 Master 被判定 ODOWN 后,**不能所有 Sentinel 都去执行故障转移**(否则会乱套)。需要先选举一个 **Sentinel Leader** 来统一操刀:
```mermaid
sequenceDiagram
participant S1 as Sentinel-1
participant S2 as Sentinel-2
participant S3 as Sentinel-3
Note over S1,S3: "Master 被判定 ODOWN, 需要选出一个 Leader"
S1->>S2: "我要当 Leader, 请投我一票"
S1->>S3: "我要当 Leader, 请投我一票"
S2->>S1: "同意, 投给你了"
S3->>S1: "同意, 投给你了"
Note over S1: "获得多数票, 我是 Leader 了"
S1->>S1: "开始执行故障转移..."
```
> [!TIP] 投票规则细节
> - **先到先得**:每个 Sentinel 在一轮投票中只能投一票,投给第一个来拉票的
> - **一轮只选一个**:如果多个 Sentinel 同时发起选举,票数不够就都不当选,等随机超时后重新发起(类似以太网的 CSMA/CD 避免冲突)
> - **任期制**:每次故障转移都有一个 `configEpoch`(类似 Raft 的 term),防止旧 Leader 误操作
### 故障转移步骤详解
Sentinel Leader 当选后,按以下顺序执行故障转移。整个过程通常在 **几秒到几十秒** 内完成:
```mermaid
flowchart TD
A["Step 1: 选出最佳 Replica"] --> B["Step 2: 提升为新 Master"]
B --> C["Step 3: 让其他 Replica 转向新 Master"]
C --> D["Step 4: 通知客户端"]
D --> E["Step 5: 旧 Master 回来后自动降级"]
```
**Step 1:选出最佳 Replica** — "谁来接班?"
从所有健康的 Replica 中选出数据最新的一个,选择标准按优先级排序:
| 优先级 | 评估维度 | 含义 | 举例 |
|--------|---------|------|------|
| 1(最高) | **复制偏移量最大** | 同步进度最接近 Master | Replica-A offset=10000, Replica-B offset=9998 → 选 A |
| 2 | **`replica-priority` 最小** | 配置中人为指定的优先级 | priority=10 优先于 priority=100;priority=0 表示"永不选我" |
| 3(最低) | **Run ID 字典序最小** | 纯粹的 tie-breaker | 两个 Replica 前两项完全相同,选 Run ID 字母序靠前的 |
> [!WARNING] 如果所有 Replica 都不健康怎么办?
> Sentinel **不会强行执行故障转移**。如果所有 Replica 都已断线、数据严重滞后或不可达,Sentinel 宁可让系统保持不可用状态,也不会选出一个数据严重落后的 Replica 当 Master——这体现了"宁缺毋滥"的设计哲学。此时运维需要介入,排查 Replica 为什么全部掉线。
**Step 2:提升为新 Master**
Sentinel 向选中的 Replica 发送 `SLAVEOF NO ONE`,使其断开与旧 Master 的复制关系,变成独立的 Master。
**Step 3:让其他 Replica 转向新 Master**
Sentinel 向其余 Replica 发送 `SLAVEOF <new-master-ip> <port>`,它们开始从新 Master 同步数据。
> [!NOTE] `parallel-syncs` 的作用
> Sentinel 配置中的 `parallel-syncs` 控制**同时转向新 Master 的 Replica 数量**。设为 1 表示一个接一个地切换,避免所有 Replica 同时中断服务去同步(同步期间可能短暂不可用)。
**Step 4:通知客户端**
Sentinel 通过 Pub/Sub 机制在 `+switch-master` 频道广播新 Master 的地址。支持 Sentinel 协议的客户端库(如 go-redis)会自动订阅并切换。
**Step 5:旧 Master 回来后怎么办?**
旧 Master 恢复上线后,Sentinel 会自动把它配置为新 Master 的 Replica——它从"领导"变成了"下属",不会再抢回 Master 身份。
### Sentinel 配置文件
```conf
# ── 核心配置:监控哪个 Master ──
sentinel monitor mymaster 192.168.1.10 6379 2
# ↑ Master地址 ↑ 端口 ↑ Quorum(至少几个 Sentinel 同意才判定 ODOWN)
# ── 认证 ──
sentinel auth-pass mymaster your-password # Master 设置了密码时必填
# ── 三个最重要的时间参数 ──
# 1. 主观下线阈值:Master 多久没回 PING 就标记 SDOWN
# 不要太低!网络抖动 1~2 秒在生产环境很常见
sentinel down-after-milliseconds mymaster 30000 # 30 秒(推荐 15~30 秒)
# 2. 故障转移超时:一次 Failover 最多花多长时间
# 超时则认为本次转移失败,等下次重试
sentinel failover-timeout mymaster 180000 # 3 分钟
# 3. 并行同步数:同时转向新 Master 的 Replica 个数
# 设为 1 = 逐个切换,保证至少有部分 Replica 始终可用
sentinel parallel-syncs mymaster 1
```
> [!WARNING] Sentinel 配置陷阱
> - **`down-after-milliseconds` 不要太低**(如 1s)——网络抖动会触发误判,导致不必要的故障转移。建议 15~30 秒
> - **`failover-timeout` 不宜太短**——如果 Master 数据量大,Replica 同步需要时间,太短会导致 Failover 反复超时失败
> - **`parallel-syncs` 设为 1**——虽然切换慢一点,但保证服务不停;设为 0 或很大则所有 Replica 同时中断服务去同步
> - **生产环境建议手动维护 Sentinel 配置文件**——虽然 Sentinel 有自动感知功能,但配置文件中的信息更可控
> - **Redis 6+ ACL 兼容**——启用 ACL 后,用 `sentinel user myuser >password` 替代 `auth-pass`
### Sentinel 下的客户端连接
> [!QUESTION] 故障转移后,客户端怎么知道新 Master 的地址?
> 传统方式是客户端硬编码 Redis 地址——Master 换了 IP,所有客户端都得改配置重启。Sentinel 方案下,客户端**直接连接 Sentinel**(而不是 Redis),由 Sentinel 告诉客户端"谁是当前的 Master"。
>
> 主流语言的 Redis 客户端库都内置了 Sentinel 支持,原理是:
> 1. 启动时向 Sentinel 查询当前 Master 地址
> 2. 连接 Master 进行读写
> 3. 如果连接失败,重新向 Sentinel 查询新地址
**Go(go-redis)示例**——客户端自动发现和切换:
```go
rdb, err := redis.NewFailoverClient(&redis.FailoverOptions{
MasterName: "mymaster", // Sentinel 中配置的 Master 名称
SentinelAddrs: []string{"sentinel1:26379", "sentinel2:26379", "sentinel3:26379"},
Password: "your-password",
MaxRetries: 3, // 连接失败重试次数
RetryDelay: 500 * time.Millisecond,
})
// 客户端自动感知 Master:读请求走 Replica(如果配置了读写分离),写请求走 Master
_ = rdb.Set(ctx, "order:12345", data, 0) // 写 → Master
val, _ := rdb.Get(ctx, "order:12345").Result() // 读 → 可能走 Replica
```
**Python(redis-py)示例**:
```python
from redis.sentinel import Sentinel
# 连接 Sentinel 节点
sentinel = Sentinel([
('sentinel1', 26379),
('sentinel2', 26379),
('sentinel3', 26379),
])
# 获取 Master 和 Slave 连接(自动发现 + 故障切换)
master = sentinel.master_for('mymaster', password='your-password') # 用于写
slave = sentinel.slave_for('mymaster', password='your-password') # 用于读
master.set('key', 'value') # 写入 Master
value = slave.get('key') # 从 Replica 读取
```
## 六、生产实践 checklist
### 部署前 Checklist
- [ ] **Sentinel 节点数 ≥ 3,且部署在不同机器/可用区** — 避免单机故障导致 Sentinel 丧失多数派
- [ ] **Master + Replica 数量建议 1:2 ~ 1:3** — 太多 Replica 会加重 Master 同步负担;太少则冗余不足
- [ ] **配置 `replica-priority`** — 不应被选为 Master 的 Replica 设为 `0`(如配置较低的从节点)
- [ ] **持久化策略保持一致** — Master 和 Replica 都应开启 RDB 或 AOF,否则故障转移后新 Master 无持久化可能导致数据丢失
### 监控项
日常巡检重点关注以下指标(建议每秒采样,配合 Prometheus/Grafana 看趋势):
```bash
# 复制状态 —— 最核心,异常时第一时间查这个
redis-cli INFO replication
# 关键字段:
# connected_slaves:3 ← 在线 Replica 数量,突然减少说明有 Replica 掉线
# master_repl_offset:12345678 ← Master 当前写入偏移量,持续增长说明正常
# repl_backlog_active:1 ← Backlog 是否活跃
# slave0:offset=12345600,lag=0 ← 每个 Replica 的同步偏移量和延迟
# 内存 —— BGSAVE 期间内存会临时翻倍(fork 子进程)
redis-cli INFO memory
# 关键字段:
# used_memory_human:2.50GB ← 同步期间如果骤增,可能触发 OOM
# 每秒操作数 —— 发现流量突增或突降
redis-cli INFO stats
# 关键字段:
# instantaneous_ops_per_sec:15000
```
### 故障排查流程
```mermaid
flowchart TD
A["发现 Master 异常"] --> B{"Sentinel 是否已 ODOWN?"}
B -->|"是"| C["自动 Failover 中..."]
B -->|"否"| D["检查网络 / CPU / 大 Key 阻塞"]
C --> E["Failover 成功?"]
E -->|"是"| F["客户端已自动切换 ✓"]
E -->|"否"| G["手动干预:<br/>SENTINEL FAILOVER mymaster"]
D --> H["问题解决后<br/>重新加入复制拓扑"]
```
### 常见坑点汇总
| 坑 | 现象 | 根因 | 解决方案 |
|---|------|------|------|
| **Docker 环境 IP 错乱** | Sentinel 把容器内部 IP(如 172.17.0.x)当成 Master 地址告诉客户端,客户端连不上 | Docker 网络与宿主机网络不同 | 设置 `replica-announce-ip` 为宿主机 IP |
| **脑裂(Split Brain)** | 网络分区后出现两个 Master 同时接受写入,分区恢复后一个 Master 的数据被覆盖丢失 | Master 与 Sentinel 网络断开,但与部分客户端仍连通 | 配置 `min-replicas-to-write` 保护 |
| **BGSAVE 时 OOM** | 同步期间 Redis 内存突然翻倍,被 OOM Killer 杀掉 | fork 子进程使用 COW 机制,内存紧张时几乎复制整个数据集 | 关闭 THP、增大 swap、预留 50% 内存空间 |
> [!NOTE] 脑裂是什么?怎么防?
> **脑裂**指的是:Master 实际还活着(只是和 Sentinel 之间网络断了),但 Sentinel 以为它挂了,选出了一个新 Master。这时候**两个 Master 同时接受写入**。网络恢复后,旧 Master 变成新 Master 的 Replica,它的数据会被覆盖——写入旧 Master 的数据就丢了。
>
> **防御措施**:在 Master 端配置"至少要有 N 个 Replica 在线才接受写入":
> ```conf
> # 至少 1 个 Replica 在线且延迟不超过 10 秒,Master 才接受写入
> min-replicas-to-write 1
> min-replicas-max-lag 10
> ```
> 这样当 Master 被隔离时(没有 Replica 在线),它会**拒绝写入**,而不是默默接受数据最终丢失的写操作。
## 七、局限性与替代方案
Sentinel 方案虽然解决了"高可用"问题,但它有明确的边界。理解这些边界才能选对方案:
| 场景 | Sentinel 能解决吗? | 为什么? | 应该用什么? |
|------|----------------|---------|-----------|
| Master 宕机自动恢复 | ✅ | 这正是 Sentinel 的核心功能 | — |
| 读请求太多,单节点扛不住 | ✅ | 增加 Replica 分摊读压力 | 主从复制 |
| **写请求太多,单 Master 瓶颈** | ❌ | 只有一个 Master 处理所有写入 | [[hhs/Redis/07-集群方案]] Cluster |
| **数据量超过单机内存** | ❌ | 每个节点都存全量数据 | Cluster(数据分片) |
| **跨机房容灾** | ❌ | 跨网段延迟太高,Sentinel 心跳会误判 | 异地多活架构 |
> [!QUESTION] 什么时候该从 Sentinel 升级到 Cluster?
> 当你遇到以下任一情况时,就该考虑 Cluster 了:
> - 单 Master 写 QPS 持续超过 5w,垂直扩展(换更强的机器)已到极限
> - 数据量超过单机内存(如 100GB+),且无法通过压缩优化
> - 需要水平扩展写入能力(多个 Master 分片处理不同 key 的写入)
>
> → 详见 [[hhs/Redis/07-集群方案]]
## 关联笔记
- [[hhs/Redis/07-集群方案]] — Cluster 架构(水平扩展)
- [[hhs/Redis/11-运维与性能调优]] — 监控与告警配置
- [[hhs/Redis/05-AOF持久化]] — AOF 与 RDB 结合的高可用策略
- [[hhs/EXAM/Week05]] — Docker Compose 中的部署示例