Files
cs-note/hhs/Redis/06-主从与哨兵.md
T
2026-05-27 00:12:00 +08:00

779 lines
38 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 增量同步
回到"抄作业"的类比:你缺了一整学期的笔记,那就得**全量抄一本**(全量同步);你只是昨天请了一天假,那就**只抄昨天那几页**(增量同步)。Redis 的主从同步也一样,分这两种模式:
- **全量同步(Full Resync)**:Master 把**整个数据集**打包成 RDB 文件发给 Replica。适用于首次连接、断线太久等场景。代价大,应尽量避免
- **增量同步(Partial Resync)**:Master 只发送 **Replica 缺失的那部分命令**。适用于正常运行期间的短暂断线。代价小,是常态
> [!QUESTION] 那 Redis 怎么判断应该走"全量"还是"增量"呢?
> 这就要说到两个关键的"身份标识"——Run ID 和 Replication Offset。
#### 前置概念:两个关键标识
要理解全量/增量的判断逻辑,需要先搞清楚两个东西:
**1. Run ID —— "你是哪个 Master?"**
每个 Redis 实例启动时会随机生成一个 40 字符的唯一标识符(如 `8b1f8a3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a`)。Replica 第一次连接 Master 时会记住它的 Run ID。之后每次重连,Replica 先报出"我记得的 Run ID",Master 对比:
- **Run ID 匹配** → "你还认识我" → 有可能增量同步
- **Run ID 不匹配** → "我不认识你"(Master 重启了 / 换了台机器)→ 只能全量同步
**2. Replication Offset —— "你同步到哪了?"**
你可以把它想象成一条**数据流水线上的刻度尺**。Master 和 Replica 各自维护一个 offset:
- Master 每写入一条命令,offset 就往后移(比如从 `1000` 到 `1050`)
- Replica 每同步一条命令,自己的 offset 也往后移
- 只要 Master 的 **backlog 缓冲区** 里还保留着 Replica 需要的那段数据(即 Replica 的 offset 在 backlog 覆盖范围内),就能增量同步
```mermaid
flowchart TD
subgraph Identity["两个身份标识"]
RunID["Run ID<br/>回答: 你是哪个 Master?"]
Offset["Replication Offset<br/>回答: 我同步到哪了?"]
end
RunID -->|"匹配"| CheckOffset["检查 Offset"]
RunID -->|"不匹配"| FullSync["只能全量同步"]
CheckOffset -->|"Offset 在 backlog 范围内"| PartialSync["增量同步"]
CheckOffset -->|"Offset 已被覆盖"| FullSync
```
> [!NOTE] PSYNC 协议:让增量同步成为可能
> Redis 2.8 之前用的是 `SYNC` 命令,**每次断线都全量传输**——相当于你每次请假都得抄一整本笔记,不管你只缺一天还是缺一个月。Redis 2.8+ 引入了 `PSYNC` 协议,Replica 重连时带上 Run ID 和自己的 Offset,Master 就能判断"你只缺了一点"还是"你缺太多了",从而决定增量还是全量。
#### 全量同步:逐步拆解
全量同步发生在 **首次连接** 或 **断线太久导致增量同步不可能** 的场景。整个过程分为三个阶段:
```mermaid
sequenceDiagram
participant R as Replica
participant M as Master
Note over R,M: "阶段 1: 握手与身份确认"
R->>M: PSYNC ? -1
Note right of R: "? = 我不知道你的 Run ID<br/>-1 = 我没有 offset 记录"
M->>M: 执行 BGSAVE 生成 RDB 快照
M-->>R: "+FULLRESYNC <runId> <offset>"
Note left of M: "意思是: 好, 全量同步,<br/>我的 Run ID 是 xxx,<br/>当前 offset 是 yyy"
M-->>R: 发送 RDB 文件
R->>R: 清空旧数据, 用 RDB 全量覆盖
Note over R,M: "阶段 2: RDB 传输期间的新写入不能丢"
Note over M: "BGSAVE 期间, 客户端仍在写入"
loop "RDB 传输期间的新写入"
M->>M: 新命令追加到 repl-backlog-buffer
end
Note over R,M: "阶段 3: 发送缓冲命令, 追赶进度"
M-->>R: 发送 backlog 中缓存的增量命令
R->>R: 逐条重放, 追上 Master 最新进度
```
> [!NOTE] 阶段 2 为什么不会丢数据?
> 你可能会担心:BGSAVE 是某一时刻的快照,那快照之后到 RDB 传输完成这段时间的写入怎么办?答案是 **backlog 缓冲区**。Master 在生成 RDB 的同时,会把所有新写入的命令追加到 backlog 里,等 RDB 传完后再把这些缓冲命令发给 Replica。所以 Replica 先拿到"截止到某个时间点的完整快照",再拿到"快照之后的增量命令",两者拼起来就是完整的最新数据。
#### 增量同步:逐步拆解
增量同步是 **正常运行时的常态**。Replica 短暂断线后重连,只需要补齐断线期间缺失的命令:
```mermaid
sequenceDiagram
participant R as Replica
participant M as Master
Note over R,M: "Replica 短暂断线后重连"
R->>M: PSYNC <runId> <myOffset>
Note right of R: "我认识你, Run ID 是 xxx,<br/>我的 offset 是 10000"
M->>M: 检查 runId 匹配<br/>检查 offset 10000 是否在 backlog 范围内
alt "offset 在 backlog 范围内 → 增量同步"
M-->>R: "+CONTINUE"
M-->>R: 发送 offset 10000 之后的增量命令
Note left of M: "只发你缺的 10001 ~ 10500,<br/>不用重传整个数据集"
R->>R: 重放增量命令, offset 追到 10500
else "offset 已被覆盖 → 退化为全量同步"
M-->>R: "+FULLRESYNC"
Note left of M: "你缺的太多了, 增量同步<br/>不可能了, 转为全量重传"
end
```
> [!QUESTION] 怎么理解"offset 在 backlog 范围内"?
> 回到"圆形笔记本"的比喻——backlog 是一个固定大小的环形缓冲区(默认仅 1MB)。Master 不断往里写新命令,写满了就从头覆盖最旧的内容。当 Replica 断线重连时,它报出自己的 offset,Master 检查:
>
> - 这个 offset 对应的数据**还在 backlog 里**(没被覆盖)→ 只需发送 backlog 中这段数据 → 增量同步
> - 这个 offset 对应的数据**已经被覆盖了**(断线太久,backlog 装不下)→ 只能全量同步
#### PSYNC 完整决策流程
把上面的逻辑串起来,PSYNC 的完整判断过程如下:
```mermaid
flowchart TD
Start["Replica 发送<br/>PSYNC <runId> <offset>"] --> CheckRunID{"runId 匹配?"}
CheckRunID -->|"不匹配<br/>(Master 重启或新 Master)"| FullSync["返回 +FULLRESYNC<br/>全量同步"]
CheckRunID -->|"匹配"| CheckOffset{"offset 对应的数据<br/>还在 backlog 里?"}
CheckOffset -->|"是"| PartialSync["返回 +CONTINUE<br/>增量同步"]
CheckOffset -->|"否<br/>(已被覆盖)"| FullSync
CheckRunID -->|"runId = '?'<br/>(首次连接)"| FullSync
```
> [!NOTE] 一句话总结 PSYNC 的决策逻辑
> 先看你"认不认识我"(Run ID),再看你"缺的那部分我还记不记得"(Offset 是否在 backlog 内)。两个条件都满足才走增量,否则全量。
#### 全量同步的代价
> [!QUESTION] 全量同步开销有多大?
> 全量同步需要 Master 执行 `BGSAVE`(fork 子进程生成 RDB 文件),然后通过网络传给 Replica。如果数据量大(比如 10GB),这个过程会**消耗大量 CPU、内存和网络带宽**。更糟糕的是,如果多个 Replica 同时触发全量同步,Master 会被拖垮。
>
> 所以我们的目标是:**尽量避免全量同步,让增量同步成为常态**。
#### 什么情况下触发全量同步?
| 触发条件 | 为什么? | 能避免吗? |
|---------|------|------|
| **首次连接** | Replica 是空的,没有 Run ID 也没有 offset,PSYNC 只能发 `? -1` | ❌ 必然发生 |
| **Master 重启** | 重启后 Run ID 变了,Replica 发的旧 Run ID 匹配不上 | ⚠️ 持久化可缓解(配置 `save` 后重启 Run ID 不变) |
| **Replica 执行 SLAVEOF 指向新 Master** | 新 Master 的 Run ID 和旧的不同,必须全量重来 | ❌ 手动触发,通常可控 |
| **Backlog 溢出** | 断线太久,Replica 需要的 offset 已被环形缓冲区覆盖 | ✅ **增大 `repl-backlog-size`** |
| **config-resetstat 执行** | 重置 INFO 统计计数器,可能干扰监控判断,但不会直接影响复制 offset | ⚠️ 少用此命令 |
> [!WARNING] 生产环境最常见的"不必要全量同步"
> 绝大多数情况是 **Backlog 过小**导致的。网络抖动几秒,Backlog 不够用,Replica 被迫全量同步——这在网络不稳定的环境中会反复发生,严重拖垮 Master 性能。**加大 Backlog 是性价比最高的优化手段**,具体配置和估算方法见下一节。
### 复制背压(Replication Backlog)
每个 Master 内部维护一个固定大小的 **环形缓冲区**(64-bit 系统上默认 1MB,Redis 7.0+ 默认 64MB),这是实现增量同步的关键:
```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 分钟** 断线恢复
### REPLCONF ACK:Master 如何感知 Replica 状态
> [!QUESTION] Master 一直在往 Replica 发数据,但它是单向发送的——Master 怎么知道 Replica 是否还活着、同步到哪了?
> 前面讲的都是 "Master → Replica" 的数据流。但复制是双向"互动"的:Master 需要知道每个 Replica 的实时状态,才能做运维决策(比如判断延迟、拒绝写入保护数据一致性)。这个感知能力靠的是 **Replica 主动向 Master 汇报**。
**工作原理**:Replica 每秒向 Master 发送一次 `REPLCONF ACK <offset>` 命令,告诉 Master 两件事:
1. **"我还活着"** — 心跳保活
2. **"我的同步进度是 offset=xxx"** — 用于计算延迟
Master 收到后更新该 Replica 的状态。通过 `INFO replication` 可以看到:
```text
connected_slaves:3
slave0:ip=192.168.1.21,port=6379,state=online,offset=12345600,lag=0
slave1:ip=192.168.1.22,port=6379,state=online,offset=12345600,lag=0
slave2:ip=192.168.1.23,port=6379,state=online,offset=12344800,lag=2
```
- **offset**:该 Replica 当前同步到哪里了(对比 Master 的 `master_repl_offset` 可知差距)
- **lag**:Master 有多久(秒)没收到这个 Replica 的 ACK,lag 越大说明 Replica 越"失联"
如果 Master 在 `repl-timeout`(默认 60 秒)内没有收到某 Replica 的 ACK,就将其标记为 `state=offline`。
```mermaid
sequenceDiagram
participant R as Replica
participant M as Master
Note over M: "Master 持续写入, offset 推进"
R->>M: REPLCONF ACK 10000
Note left of M: "记录: slave0 offset=10000, lag=0"
M->>M: 继续写入, offset 到 10100
R->>M: REPLCONF ACK 10050
Note left of M: "记录: slave0 offset=10050, lag=1"
Note over R: "Replica 断线..."
Note over M: "repl-timeout(60s) 内没收到 ACK"
Note left of M: "标记: slave0 state=offline"
```
> [!NOTE] REPLCONF ACK 的实际意义:写入保护
> 这个机制不只是"看看状态"——它直接支撑了**脑裂场景下的写入保护**。配合以下配置:
> ```conf
> min-replicas-to-write 1 # 至少 1 个 Replica 在线
> min-replicas-max-lag 10 # 且 lag 不超过 10 秒
> ```
> Master 根据 REPLCONF ACK 汇报的 lag 值判断:**在线且低延迟的 Replica 数量不满足条件时,Master 直接拒绝写入**。这样即使 Master 被网络隔离(脑裂),它也不会默默接受写入——因为没有 Replica 能同步这些数据,写入最终会丢失。
>
> 简单说:**没有 REPLCONF ACK → Master 对 Replica 状态一无所知 → 无法做写入保护 → 脑裂时必然丢数据**。
> [!QUESTION] REPLCONF ACK 和 Sentinel 的心跳有什么区别?
> 两者都是"心跳检测",但**方向和目的不同**:
>
> | 对比 | REPLCONF ACK | Sentinel PING |
> |------|-------------|---------------|
> | 方向 | Replica → Master | Sentinel → Master/Replica |
> | 频率 | 每秒 1 次 | 每秒 1 次 |
> | 目的 | Master 感知 Replica 存活和同步进度 | Sentinel 感知节点存活,触发故障转移 |
> | 判定失联后 | Master 标记 Replica 为 offline,触发 min-replicas 保护 | Sentinel 标记 SDOWN/ODOWN,触发选主和 Failover |
>
> 两者互补,缺一不可。
### 大 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
# 适用于金融、账务等场景
repl-backlog-size 512mb # 更大缓冲区容纳更多增量命令
```
```go
// WAIT 是运行时命令,不能写在 redis.conf 里,需在业务代码中调用
// 写入后等待至少 1 个副本确认同步,最多阻塞 100ms
client.Do(ctx, "WAIT", 1, 100)
```
### 常用调试命令
```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(最高) | **`replica-priority` 最小** | 配置中人为指定的优先级 | priority=10 优先于 priority=100;priority=0 表示"永不选我" |
| 2 | **复制偏移量最大** | 同步进度最接近 Master | Replica-A offset=10000, Replica-B offset=9998 → 选 A |
| 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 中的部署示例