【Redis进阶系列】:从主从复制到 Sentinel,一文建立故障检测、Leader 选举与 Failover 的完整心智模型

🔥 本文专栏:Redis

🌸作者主页:努力努力再努力wz

💪 今日博客励志语录:你可以暂时没有结果,但不能长期没有积累。


思维导图

text 复制代码
Redis 单节点
   ↓
单节点故障会导致服务不可用
   ↓
主从复制
   ↓
Master 负责写入并产生复制流
Replica 跟随 Master
   ↓
replid + offset 标识复制历史中的位置
   ↓
backlog 保存一段复制流历史
   ↓
断线后 Replica 自动重连
   ↓
能接上 backlog → Partial Resync
接不上 → Full Resync
   ↓
但是:Master 真正挂掉以后
Replica 只会重连旧 Master,不会自己选新 Master
   ↓
需要高可用控制层
   ↓
Redis Sentinel
   ↓
监控 Master / Replica
自动发现 Replica / 其他 Sentinel
   ↓
SDOWN:单个 Sentinel 的主观下线判断
   ↓
quorum 达标
   ↓
ODOWN:多个 Sentinel 确认 Master 不可用
   ↓
进入新的 epoch
   ↓
Sentinel Leader Election
   ↓
多数派授权
   ↓
唯一 Leader 获得 Failover 执行权
   ↓
筛选新的 Master 候选 Replica
   ↓
失联时间资格过滤
   ↓
replica-priority
   ↓
replication offset
   ↓
runid
   ↓
选出新 Master
   ↓
REPLICAOF NO ONE
   ↓
其余 Replica 改为复制新 Master
   ↓
新配置向其他 Sentinel 传播
   ↓
旧 Master 恢复
   ↓
被 Sentinel 降级为新 Master 的 Replica
   ↓
PSYNC 重新加入当前复制拓扑

引入:主从复制已经有了,为什么还需要 Sentinel

在前面对 Redis 主从复制的学习中,我们已经认识到,Redis 可以通过 Master 和 Replica 构造一套复制拓扑。

最简单的结构如下:

text 复制代码
            Master
           /      \
          ↓        ↓
      Replica1  Replica2

Master 接收写请求,并将这些写操作形成复制流发送给下游 Replica。Replica 则不断跟随 Master,使自己的数据状态尽量接近 Master。

如果只是某个 Replica 和 Master 之间的复制连接暂时断开,其实问题并不大,因为 Replica 会不断尝试重新连接原来的上游,然后通过 PSYNC 尝试恢复复制。

真正的问题在于:

如果挂掉的是 Master 本身,Replica 虽然拥有数据副本,但是它并不会因为 Master 挂掉就自动把自己提升为新的 Master。

此时如果没有额外机制,就只能人工完成:

text 复制代码
确认 Master 故障
      ↓
挑选一个 Replica
      ↓
将 Replica 提升为 Master
      ↓
让其他 Replica 改为复制新的 Master
      ↓
让客户端找到新的 Master

这一整套操作如果依赖人工完成,不但恢复速度慢,而且非常容易因为判断错误或者操作错误导致更严重的问题。

因此,Redis 在普通主从复制之上,又增加了一层专门负责高可用控制的机制:

text 复制代码
Redis Sentinel

要真正理解 Sentinel,不能一上来就记:

text 复制代码
SDOWN
ODOWN
quorum
majority
epoch
Leader

而应该先从主从复制本身开始,把 Sentinel 为什么会出现以及它接管了哪些事情串起来。


一、先重新建立 Redis 主从复制的底层模型

1. Master / Replica 与 Upstream / Downstream 是两套不同概念

在 Redis 复制中,经常会看到两组词:

text 复制代码
Master / Replica

以及:

text 复制代码
Upstream / Downstream

这两组概念不能完全混为一谈。

Master / Replica 描述的是 Redis 节点当前所承担的角色。

Upstream / Downstream 描述的是某一条复制连接中的方向关系。

例如:

text 复制代码
A Master
   ↓
B Replica
   ↓
C Replica

对于 A 和 B:

text 复制代码
A = B 的 upstream
B = A 的 downstream

对于 B 和 C:

text 复制代码
B = C 的 upstream
C = B 的 downstream

这里 B 虽然自身角色是 Replica,但是它仍然可以成为 C 的 upstream。

因此更准确的理解是:

Master / Replica 是节点角色,upstream / downstream 是复制链路上的相对关系。


2. Redis 主从复制本质上是在复制一条"数据变化历史"

假设客户端向 Master 连续写入:

text 复制代码
SET a 1
SET b 2
INCR count
DEL temp

Master 并不是简单地告诉 Replica:

text 复制代码
"我现在内存里有哪些 key。"

而是不断向 Replica 发送一条复制流,使 Replica 重放 Master 上发生的数据变化。

因此可以把整个复制过程理解为:

text 复制代码
客户端写请求
    ↓
Master 修改自身数据
    ↓
产生 replication stream
    ↓
发送给 Replica
    ↓
Replica 重放这些变化
    ↓
逐渐达到与 Master 一致的状态

Redis 默认采用异步复制。

也就是说,正常情况下并不是:

text 复制代码
Master 发送一批数据
↓
停下来等待 Replica ACK
↓
再发送下一批

而是 Master 可以继续处理请求、继续产生复制流,而 Replica 会异步向 Master 汇报自己已经处理到了哪个位置。

所以 ACK 更准确的作用是:

text 复制代码
"我已经处理到 replication offset = X 了。"

而不是每次都作为 Master 继续发送数据之前必须等待的确认信号。


3. replid + offset:给复制历史建立坐标系

如果 Redis 只是不断发送字节流,那么 Replica 断线以后会遇到一个问题:

我之前已经复制到哪里了?重新连接以后应该从哪里继续?

因此 Redis 给复制历史建立了两个非常关键的标识:

text 复制代码
replication ID(replid)
replication offset

可以把它们理解成:

text 复制代码
replid
→ 当前是哪一条复制历史

offset
→ 我在这条复制历史上已经走到了哪里

例如:

text 复制代码
replid = AAA

offset = 1000

表示:

当前节点处在 AAA 这条复制历史上,并且已经处理到逻辑位置 1000。

如果另一个 Replica 是:

text 复制代码
replid = AAA
offset = 1200

那么两者属于同一条复制历史,只是后者走得更靠前。

因此:

text 复制代码
replid + offset

共同描述了某一条复制历史上的一个确定位置。

这里一定要注意:

text 复制代码
replid 相同
≠ 当前数据一定完全一样

因为 offset 可能不同。

只有:

text 复制代码
replid 相同
+
offset 相同

才可以认为它们处在同一条复制历史上的同一位置。


4. replication backlog:为什么断线以后可以只补一部分数据

Master 还会在内存中维护一段有限大小的复制历史:

text 复制代码
replication backlog

可以把它理解成一个有限长度的历史窗口:

text 复制代码
较旧的数据                    较新的数据
     ↓                            ↓
|------------------------------------|
              backlog

其中保存最近一段 replication stream。

假设 Replica 原本已经复制到:

text 复制代码
offset = 10000

然后网络断开了一段时间。

Master 此时已经继续前进到:

text 复制代码
offset = 11000

Replica 重连以后会携带自己记住的:

text 复制代码
replid + offset

向 Master 发起 PSYNC

如果:

text 复制代码
replid 能对应当前复制历史
+
10001 ~ 11000 这段数据仍然在 backlog 中

那么 Master 没必要重新把整个数据集传一次,只需要把中间缺失的复制流补给 Replica。

这就是:

text 复制代码
Partial Resynchronization
部分重同步

整个过程可以理解为:

text 复制代码
Replica:
我属于 AAA 历史
我已经到 10000 了
        ↓
PSYNC AAA 10000
        ↓
Master 检查:
AAA 能对应
缺失数据还在 backlog
        ↓
只发送 10001 ~ 11000

如果 backlog 中已经没有 Replica 所缺失的那一段数据,或者 Replica 携带的复制历史已经无法对应,那么就无法部分同步,只能进入:

text 复制代码
Full Resynchronization
全量同步

5. 全量同步为什么成本更高

传统的全量同步过程可以粗略理解为:

text 复制代码
Master 内存数据
      ↓
fork() 创建后台子进程
      ↓
生成 RDB 快照
      ↓
RDB 传给 Replica
      ↓
Replica 建立完整基准数据集
      ↓
再继续接收后续复制流

这里 fork() 创建的是:

text 复制代码
子进程

父子进程通过 Copy-On-Write 的方式共享原有内存页面,Master 父进程继续处理业务,子进程负责生成 RDB。

如果使用传统磁盘式复制,可以将过程继续细化为:

text 复制代码
Master 内存
↓
RDB
↓
Master 磁盘文件
↓
读取 RDB
↓
网络发送
↓
Replica 接收
↓
建立完整基准状态

Redis 也支持 diskless replication,可以让后台子进程直接通过网络发送 RDB,减少中间磁盘步骤。

无论采用哪一种方式,全量同步的核心特点都没有变化:

它需要重新建立整个数据基准,因此成本远高于只补一小段 backlog 的部分同步。


6. Replica 与上游断开后,并不会自动换一个上游

这里还有一个非常重要的认识。

假设:

text 复制代码
A Master
   ↓
B Replica

B 和 A 的复制连接断开。

B 默认的动作不是:

text 复制代码
"既然 A 连不上,我去其他节点里重新选一个 upstream。"

而是:

text 复制代码
继续记住 A
↓
反复尝试重连 A
↓
重连成功后 PSYNC

也就是说,Redis 基础复制机制负责的是:

text 复制代码
"我怎样继续跟随原来的 upstream。"

它本身并没有解决:

text 复制代码
"原来的 Master 永远回不来了,我应该找谁接班?"

这正是 Sentinel 出现的根本原因。


二、Sentinel 本质上是主从复制之上的"控制层"

1. Sentinel 不是一个新的 KV 数据节点

普通 Redis Server 主要负责:

text 复制代码
存储业务 KV
处理读写请求
维护主从复制

而 Sentinel 的职责完全不同:

text 复制代码
监控 Redis 节点
判断 Master 是否故障
协调 Failover
选择新的 Master
重构复制拓扑
向客户端提供当前 Master 地址

因此可以建立这样一个分层模型:

text 复制代码
             Sentinel 控制层
      S1        S2        S3
       \         |         /
        \        |        /
         -----------------
                 ↓
             Redis 数据层

             Master
            /      \
           ↓        ↓
       Replica1  Replica2

普通 Redis 节点属于数据层。

Sentinel 属于高可用控制层。


2. Sentinel 仍然来自 redis-server

Sentinel 并不是另一套完全独立的二进制程序。

Redis 可以通过类似:

bash 复制代码
redis-server sentinel.conf --sentinel

让同一个 redis-server 以 Sentinel 模式运行。

Sentinel 默认监听:

text 复制代码
26379

普通 Redis 默认监听:

text 复制代码
6379

所以 Sentinel 本质上仍然是一个网络服务进程,也可以通过:

bash 复制代码
redis-cli -p 26379

去查询它的状态。


3. Sentinel 与 Redis 节点之间也是 TCP + RESP

Sentinel 需要不断向 Master / Replica 发送命令获取状态。

所以从网络角色上看:

text 复制代码
Sentinel
   ↓ TCP
Redis Server

此时 Sentinel 本身就是 Redis Server 的一个特殊客户端。

它可以发送类似:

text 复制代码
PING
INFO
ROLE
SENTINEL ...

Redis 的通信仍然基于 RESP 协议。

这里要注意,RESP 虽然很多时候看起来像文本协议,但是其 Bulk String 等结构本身是 binary-safe 的,所以不能简单理解成"Redis 网络协议就是纯文本协议"。


三、Sentinel 是如何知道整个 Redis 拓扑的

1. 配置 Sentinel 时只需要先告诉它 Master

一个 Sentinel 最基本的配置可以是:

conf 复制代码
sentinel monitor mymaster 172.18.0.10 6379 2

这里包含:

text 复制代码
mymaster
→ 当前监控的逻辑 Master 名称

172.18.0.10 6379
→ 当前 Master 地址

2
→ quorum

注意:

初始配置中并不要求手工把所有 Replica 都写进去。

Sentinel 会自己从 Master 获取复制拓扑信息。


2. Sentinel 如何发现 Replica

Sentinel 与 Master 建立连接以后,可以周期性获取 Master 的状态信息。

Master 本身知道哪些 Replica 正在复制自己,因此 Sentinel 可以通过 Master 提供的复制信息发现:

text 复制代码
Master
├── Replica1
├── Replica2
└── Replica3

随后 Sentinel 会直接连接这些 Replica,并分别监控它们的状态。

因此流程可以理解为:

text 复制代码
Sentinel
↓
先知道 Master
↓
向 Master 查询 replication 信息
↓
发现 Replica 地址
↓
再直接连接各个 Replica

这就是为什么 Sentinel 配置文件最开始只写 Master,也能够最终维护完整的 Redis 主从拓扑。


3. Sentinel 之间如何互相发现

只知道数据节点还不够。

如果有三个 Sentinel:

text 复制代码
S1
S2
S3

它们还必须知道彼此的存在。

Redis Sentinel 并不要求我们在每个 Sentinel 配置文件中手工写:

text 复制代码
S1 的地址
S2 的地址
S3 的地址

而是借助 Redis 自己的 Pub/Sub 机制,使用一个特殊频道:

text 复制代码
__sentinel__:hello

Sentinel 会周期性向被监控的 Master 和 Replica 上的这个频道发布 hello 信息,其中包含自己的:

text 复制代码
IP
port
runid
当前 Master 配置等信息

其他 Sentinel 同时订阅这个频道,就能够发现新的 Sentinel。

这里一定要避免一个误区:

text 复制代码
__sentinel__:hello

不是一条 TCP 连接,也不是 Unix 管道。

它是 Redis Pub/Sub 中的一个逻辑频道名称。

整个过程更像:

text 复制代码
S1 PUBLISH __sentinel__:hello ...
        ↓
Redis Pub/Sub
        ↓
S2 / S3 SUBSCRIBE
        ↓
获得 S1 的信息

当 S2 已经知道 S1 的 IP、端口等信息以后,它们之间还可以建立直接 TCP 通信。

所以 Sentinel 之间不是 RIP 那种"只能把消息一跳一跳向邻居扩散"的路由协议模型,而是:

一旦知道对方地址,就可以直接与对方通信。


四、为什么 Sentinel 不能只部署一个

假设只有一个 Sentinel:

text 复制代码
S1
↓
Master

如果 S1 自己所在机器故障,那么自动故障转移能力也一起消失。

更麻烦的是,如果 S1 自己发生网络异常:

text 复制代码
S1 ×── Master

S1 可能认为:

text 复制代码
"Master 挂了。"

但真实情况可能只是:

text 复制代码
S1 自己到 Master 的网络出了问题

因此不能让单个观察者的判断直接决定整个 Redis 拓扑。

更合理的做法是使用多个 Sentinel:

text 复制代码
S1      S2      S3
 \       |       /
  \      |      /
       Master

它们分别观察 Master,然后交换判断。

实际部署中通常至少使用 3 个 Sentinel,并尽量放在不同机器上。

这里 Sentinel 数量和 Redis 数据节点数量并没有 1:1 的强绑定关系。

例如:

text 复制代码
3 个 Redis 数据节点
3 个 Sentinel

可以为了部署方便放在三台机器中一一对应,也可以让 Sentinel 部署在其他独立机器节点上。

重点不是"必须一台 Redis 配一个 Sentinel",而是:

Sentinel 本身也应该避免全部依赖同一台机器,否则那台机器一挂,多个 Sentinel 会一起消失。


五、Master 是否故障:先从 SDOWN 到 ODOWN

1. SDOWN:单个 Sentinel 的主观判断

每个 Sentinel 都会独立监控 Master。

例如配置:

conf 复制代码
sentinel down-after-milliseconds mymaster 5000

意味着如果某个 Sentinel 在足够长的时间内没有从 Master 获得可接受的响应,它会先得到本地判断:

text 复制代码
SDOWN
Subjectively Down

可以理解为:

"站在我这个 Sentinel 的视角下,我认为 Master 不可用了。"

例如:

text 复制代码
S1 → Master ×

此时只能说明:

text 复制代码
S1 认为 Master 有问题

不能说明整个 Sentinel 系统已经确认 Master 故障。


2. 为什么 SDOWN 不能直接 Failover

假设只是:

text 复制代码
S1 和 Master 之间网络抖动

而:

text 复制代码
S2 → Master 正常
S3 → Master 正常

如果 S1 一判断 SDOWN 就立即提升 Replica,就可能因为一个局部网络问题制造出两个 Master。

因此 Sentinel 还要继续询问其他 Sentinel:

text 复制代码
"你们是不是也认为这个 Master 不可达?"

Sentinel 内部会使用类似:

text 复制代码
SENTINEL is-master-down-by-addr

的机制交换判断。


3. ODOWN:多个 Sentinel 达到 quorum

如果足够多 Sentinel 都认为 Master 不可用,那么 Master 才会进入:

text 复制代码
ODOWN
Objectively Down

这里关键参数就是:

text 复制代码
quorum

例如:

conf 复制代码
sentinel monitor mymaster 172.18.0.10 6379 2

其中最后的:

text 复制代码
2

就是 quorum。

对于三个 Sentinel:

text 复制代码
S1
S2
S3

如果至少有两个 Sentinel 对 Master 的故障判断满足条件,就可以把 Master 标记为 ODOWN。

所以可以先建立:

text 复制代码
SDOWN
→ 一个 Sentinel 自己认为 Master 挂了

ODOWN
→ 足够多 Sentinel 的故障判断达到 quorum

六、quorum 与 majority:Redis Sentinel 中最容易混淆的两个门槛

很多时候会把 quorum 和多数派混成一个东西,但实际上它们解决的是两个不同问题。

1. quorum:先确认"故障是否成立"

quorum 是人为配置的。

例如:

text 复制代码
Sentinel 数量 = 3
quorum = 2

那么:

text 复制代码
至少 2 个 Sentinel 认为 Master 不可达
↓
Master 可以进入 ODOWN

所以 quorum 主要控制:

text 复制代码
故障判断有多敏感

如果 quorum 设置得比较小:

text 复制代码
更容易达到 ODOWN

如果 quorum 设置得比较大:

text 复制代码
需要更多 Sentinel 同意
故障判断更加保守

2. majority:决定是否真的允许执行 Failover

Master 被标记为 ODOWN,并不意味着任何 Sentinel 都能直接去改拓扑。

接下来真正执行 Failover 的 Sentinel 还需要得到足够多 Sentinel 的授权。

对于 N 个 Sentinel:

text 复制代码
majority = floor(N / 2) + 1

例如:

text 复制代码
3 个 Sentinel → majority = 2
4 个 Sentinel → majority = 3
5 个 Sentinel → majority = 3

这也是为什么实际部署中经常使用奇数个 Sentinel。

例如:

text 复制代码
3 个 Sentinel
允许失去 1 个仍然拥有多数派

4 个 Sentinel
同样最多只能失去 1 个后仍然拥有 3 票多数派

因此从多数派利用率来看,奇数通常更加划算。


3. Failover 真正需要的授权门槛

可以把真正执行 Failover 所需要的授权数量理解为:

text 复制代码
max(quorum, majority)

例如:

text 复制代码
Sentinel = 5
quorum = 2
majority = 3

那么:

text 复制代码
2 个 Sentinel
→ 可以触发 ODOWN

但真正执行 Failover
→ 仍然至少需要 3 个 Sentinel 授权

反过来:

text 复制代码
Sentinel = 5
quorum = 5
majority = 3

那么 Failover 实际上需要:

text 复制代码
5

因此可以把两个参数这样记:

text 复制代码
quorum
→ 故障确认门槛

majority
→ 系统为了避免少数派独自修改拓扑而提供的安全底线

七、为什么必须选一个 Sentinel Leader

达到 ODOWN 以后,还有一个问题:

text 复制代码
S1、S2、S3 都知道 Master 挂了

那到底谁来执行:

text 复制代码
选 Replica
提升 Master
修改其他 Replica
传播新配置

如果三个 Sentinel 同时执行,很容易出现不同 Sentinel 同时修改拓扑的问题。

因此 Redis Sentinel 在每轮 Failover 中,会先选出一个 Sentinel Leader。

这里一定要把两个"选举"区分开:

text 复制代码
Sentinel Leader Election
→ 选谁来执行 Failover

Replica Selection
→ 选哪个 Replica 成为新的 Master

它们不是一回事。


八、epoch:给每一轮故障转移建立"轮次编号"

Sentinel Leader Election 不能只靠一句:

text 复制代码
"我投给 S1。"

因为网络中可能存在延迟消息。

假设上一轮投票是:

text 复制代码
epoch = 10

某个旧投票由于网络延迟,一直到下一轮:

text 复制代码
epoch = 11

才送到。

如果没有轮次编号,就可能把旧投票误当成当前轮次的有效投票。

因此 Sentinel 使用:

text 复制代码
epoch

标识当前 Failover / Leader Election 所属的逻辑轮次。

可以这样区分几个 ID:

text 复制代码
runid
→ 当前是哪一个 Redis / Sentinel 进程实例

replid
→ 当前是哪一条 Redis 复制历史

epoch
→ 当前是哪一轮 Sentinel 故障转移 / 选举

九、Sentinel Leader Election 到底是怎么投票的

假设三个 Sentinel:

text 复制代码
S1
S2
S3

都进入同一轮故障转移。

一个准备竞选 Leader 的 Sentinel 会向其他 Sentinel 发送内部请求。

可以将请求形式理解为:

text 复制代码
SENTINEL is-master-down-by-addr
    <master-ip>
    <master-port>
    <epoch>
    <candidate-runid>

这里一个容易写错的地方是:

请求中的 IP 和端口描述的是正在判断的 Master,不是候选 Sentinel 自己的 IP 和端口。

候选 Sentinel 的身份主要通过:

text 复制代码
candidate-runid

表达。

每一个 Sentinel 在同一个 epoch 中只能对同一个 Master 的 Failover 授权给一个候选者。

可以先用一个直观模型理解:

text 复制代码
S1 和 S2 几乎同时竞选

S1 → 自己投 S1
S2 → 自己投 S2

S3 先处理到 S1 的有效请求
→ S3 投 S1

最终:

text 复制代码
S1 = 2 票
S2 = 1 票

对于三个 Sentinel:

text 复制代码
majority = 2

所以 S1 获得 Leader 执行权。

这里的安全性不依赖:

text 复制代码
"大家运气好,刚好没有打平。"

而是来自:

text 复制代码
一个 Sentinel 每个 epoch 只能授权一个候选者
+
Leader 必须获得严格多数派

因此在同一个 epoch 中,不可能同时存在两个都获得严格多数派授权的合法 Leader。

例如四个 Sentinel 出现:

text 复制代码
S1 = 2 票
S2 = 2 票

当然可以发生。

但:

text 复制代码
majority = 3

于是两边都不能成为 Leader。

也就是说:

Sentinel 并不要求"投票数量永远不会打平",而是要求"没有达到合法门槛的人不能执行 Failover"。


1. Sentinel 不需要维护一个全局实时票数表

Leader Election 也不是:

text 复制代码
所有 Sentinel
↓
先把所有投票同步到一个中央节点
↓
计算全局排行榜

候选 Sentinel 只需要收集自己收到的授权回复,然后判断:

text 复制代码
我的有效授权数量
是否已经达到需要的门槛

所以一个候选者不需要实时知道:

text 复制代码
"另外一个候选者现在究竟有几票。"

它真正关心的是:

text 复制代码
"我自己的票够不够。"

十、Leader 选出来以后,如何选择新的 Master

Leader 获得 Failover 执行权以后,才进入另一个问题:

text 复制代码
哪个 Replica 最适合接班?

这个过程和 Sentinel Leader Election 完全不同。

Leader Election 解决的是:

text 复制代码
谁来执行

Replica Selection 解决的是:

text 复制代码
谁最适合成为新的 Master

Replica 的选择是真正会认真比较节点状态的。

整体可以先记成四步:

text 复制代码
Replica 候选集合
      ↓
① 失联时间资格过滤
      ↓
② replica-priority
      ↓
③ replication offset
      ↓
④ runid
      ↓
选出新的 Master

1. 第一关:根据与旧 Master 的失联时间淘汰不可靠 Replica

所谓失联时间,本质上就是:

text 复制代码
Replica 与旧 Master 的复制连接
已经断开了多久

如果一个 Replica 很早就已经和旧 Master 断开:

text 复制代码
Master offset = 100000

Replica-A offset ≈ 99900
Replica-B offset = 60000

那么 B 的数据显然可能已经落后很多。

Sentinel 不会简单地把所有 Replica 按:

text 复制代码
断联 1 秒
断联 2 秒
断联 3 秒

排序后直接选最短的。

失联时间首先是一个:

text 复制代码
资格过滤条件

官方规则可以近似表示为:

text 复制代码
Replica 失联时间
>
(down-after-milliseconds * 10)
+
当前 Sentinel 认为 Master 处于 SDOWN 的持续时间

那么这个 Replica 会被直接认为不适合 Failover。

所以第一关应该理解成:

text 复制代码
所有 Replica
↓
淘汰失联过久的
↓
留下仍然足够可靠的候选者

2. 第二关:比较 replica-priority

对于通过第一关的 Replica,接下来比较:

text 复制代码
replica-priority

这是一个人为配置项。

例如:

text 复制代码
Replica-A priority = 10
Replica-B priority = 100

数字越小:

text 复制代码
优先级越高

因此 A 更优先。

可以把它理解为管理员提前告诉 Sentinel:

如果以后需要 Failover,我更希望哪些 Replica 优先成为 Master。

还有一个特殊值:

text 复制代码
replica-priority = 0

表示:

这个 Replica 永远不要被 Sentinel 提升为 Master。

但是它仍然可以在 Failover 以后被重新配置为新 Master 的 Replica。


3. 第三关:比较 replication offset

如果:

text 复制代码
priority 相同

那么 Sentinel 会继续比较:

text 复制代码
replication offset

例如:

text 复制代码
Replica-A offset = 100000
Replica-B offset = 105000

那么优先选择 B。

原因非常直接:

text 复制代码
offset 越大
↓
说明从旧 Master 获取到的复制流越多
↓
数据通常更接近旧 Master 故障前的最新状态
↓
潜在数据损失更少

因此这一关才是真正意义上的:

text 复制代码
谁的数据更新

4. 第四关:offset 也相同,就比较 runid

如果多个 Replica:

text 复制代码
失联时间资格都通过
priority 一样
offset 也一样

那么已经没有明显业务意义上的优劣差别了。

Redis 最后会比较:

text 复制代码
runid

runid 是 Redis 进程实例启动时生成的随机标识,通常表现为一串 40 位十六进制字符串。

它表示的是:

text 复制代码
"当前这个 Redis / Sentinel 进程实例是谁"

并不代表节点性能。

最后选择:

text 复制代码
字典序更小的 runid

作为确定性的兜底规则。

所以:

text 复制代码
runid 更小
≠ 节点更优秀

这里只是为了:

text 复制代码
避免前面完全打平以后随机选择

让最终结果更加稳定、确定。


十一、Failover 真正执行时发生了什么

假设 Leader 最终选中了:

text 复制代码
Replica-B

那么整体过程可以理解为:

text 复制代码
Leader Sentinel
      ↓
选择 Replica-B
      ↓
发送 REPLICAOF NO ONE
      ↓
Replica-B 解除原 upstream
      ↓
Replica-B → Master
      ↓
等待 INFO 中确认角色真的已经变成 Master
      ↓
修改其他 Replica 的 upstream
      ↓
让它们全部复制 Replica-B
      ↓
发布新的 Master 配置

这里不是 Leader 一发命令就立刻认为成功。

Sentinel 会等待真正观察到 Replica 已经变成 Master,然后才继续推进 Failover 状态机。


十二、新 Master 为什么必须生成新的 replid

这里是 Sentinel Failover 和 Redis 基础复制真正连接起来的地方。

假设原来:

text 复制代码
A Master
replid = AAA
   /       \
  ↓         ↓
B Replica  C Replica

A 发生网络分区以后,并不一定真的死了。

可能出现:

text 复制代码
A 仍然活着
但是 B、C、足够多 Sentinel 已经无法访问 A

Sentinel 最终将 B 提升为新的 Master。

如果 B 还继续沿用:

text 复制代码
AAA

就可能出现:

text 复制代码
A:仍然认为自己是 Master
AAA + offset 10001
写入 SET x 1

B:已经被提升成新 Master
AAA + offset 10001
写入 SET x 2

此时:

text 复制代码
replid 相同
offset 相同

但数据却不同。

这样就破坏了:

text 复制代码
replid + offset
唯一确定复制历史位置

这一基本规则。

因此 Replica 一旦被提升为新的 Master,就必须产生新的 replication ID:

text 复制代码
AAA
↓ Failover
BBB

表示:

从这一刻开始,产生了一条新的复制历史。


十三、为什么新 Master 还要保留旧 replid

如果 B 晋升以后只保留:

text 复制代码
master_replid = BBB

把原来的:

text 复制代码
AAA

完全忘掉,会发生什么?

C 原本属于旧 Master A 的复制历史:

text 复制代码
C:
replid = AAA
offset = 9800

Failover 后 Sentinel 告诉 C:

text 复制代码
你的新 upstream 是 B

于是 C 去连接 B,并发起:

text 复制代码
PSYNC AAA 9800

如果 B 只认识:

text 复制代码
BBB

那么 B 会认为:

text 复制代码
你的复制历史和我完全不一样

于是只能:

text 复制代码
FULLRESYNC

但实际上 B 和 C 在故障发生前,本来都是 A 的 Replica,它们的数据通常非常接近。

例如:

text 复制代码
B = AAA + 10000
C = AAA + 9800

差距可能只有 200 个 offset 对应的数据。

因为角色切换就让 C 重新接收整个数据集,明显没有必要。

因此 B 晋升时会做一件非常关键的事情:

text 复制代码
master_replid  = BBB
master_replid2 = AAA

同时保存旧历史的有效边界:

text 复制代码
second_repl_offset

可以理解成:

text 复制代码
当前历史:BBB
上一条历史:AAA
AAA 在我这里有效到某个历史切换位置

于是 C 来:

text 复制代码
PSYNC AAA 9800

B 可以判断:

text 复制代码
AAA == master_replid2
↓
这条旧历史我认识

9800 仍然位于旧历史可衔接范围内
↓
并且需要的数据还在 backlog
↓
可以 Partial Resync

所以:

新 replid 用来明确"复制历史已经分叉";旧 replid2 则给故障转移前的 Replica 留了一座连接旧历史和新历史的桥。

它不是"假装新旧历史还是同一条",而是:

text 复制代码
明确承认已经进入新历史
+
仍然记住上一段历史

这样其他 Replica 才有机会避免昂贵的全量同步。


1. 用 Git 分支理解 replid2

可以把共同历史理解成:

text 复制代码
A ─ B ─ C
         \
          D ─ E

当前新 Master 已经走到了:

text 复制代码
D → E

但是它没有忘记:

text 复制代码
A → B → C

这一段祖先历史。

所以一个旧 Replica 说:

text 复制代码
"我走到 C 这里了。"

新 Master 仍然知道:

text 复制代码
"C 是我过去历史上的位置,你从这里继续往后追即可。"

而不是直接回答:

text 复制代码
"C 是什么?我完全不认识,重新传整个数据集。"

十四、旧 Master 恢复以后会发生什么

假设原来:

text 复制代码
A Master
├── B Replica
└── C Replica

A 挂掉或者被网络隔离以后,B 被提升:

text 复制代码
B New Master
└── C Replica

那么 A 在新拓扑中的身份已经不再是 Master。

即使 A 之后重新启动,它本地仍然可能暂时认为:

text 复制代码
role:master

因为它自己的启动配置并不知道这段时间 Sentinel 已经完成了一轮 Failover。

但是 Sentinel 保存的最新逻辑拓扑已经变成:

text 复制代码
B = Master
A = 应该成为 B 的 Replica
C = B 的 Replica

这里连接方向一定要搞清楚。

不是:

text 复制代码
A 重启
↓
A 主动寻找 Sentinel
↓
向 Sentinel 报到

而是:

text 复制代码
Sentinel 一直知道 A 的地址
↓
不断尝试监控 A
↓
发现 A 又可达
↓
Sentinel 主动连接 A

随后 Sentinel 会把当前正确拓扑重新施加到 A 身上,例如让 A:

text 复制代码
REPLICAOF B

于是:

text 复制代码
A:Master
↓
A:Replica of B

然后才轮到 A 作为 Replica:

text 复制代码
A
↓ 主动建立复制连接
B New Master
↓
PSYNC replid offset

最终 A 重新加入当前复制拓扑。

所以 Sentinel 的一个重要思想就是:

它会不断尝试把自己所知道的最新逻辑配置重新施加到实际 Redis 节点上。

旧 Master Failover 后重新回来,也会被重新配置成新 Master 的 Replica。


十五、旧 Master 恢复后一定全量同步吗

不一定。

这里还要分场景。

1. 旧 Master 真正宕机,没有继续产生自己的写入

假设 A 最后停在:

text 复制代码
AAA + offset 9800

B 晋升以后保留:

text 复制代码
master_replid  = BBB
master_replid2 = AAA

并且 backlog 仍然能够覆盖 A 所需要的数据。

那么 A 被降级为 Replica 后,有机会:

text 复制代码
PSYNC AAA 9800
↓
Partial Resync

所以:

text 复制代码
旧 Master 恢复
≠ 必然 Full Resync

2. 旧 Master 在网络隔离期间继续接受写请求

这个情况就更危险。

假设在分叉点:

text 复制代码
AAA + offset 10000

之后形成两条历史:

text 复制代码
               共同历史 AAA
                    ↓
                offset 10000
                 /       \
                /         \
               ↓           ↓
        旧 Master A      新 Master B
        继续接受写        replid = BBB
        AAA 自己向前      新历史继续向前

A 隔离期间可能走到了:

text 复制代码
AAA + offset 12000

但 B 只知道旧 AAA 历史在 Failover 切换点附近有效。

所以:

text 复制代码
A 的 offset 更大

并不代表:

text 复制代码
A 的数据更正确

因为 A 后面的数据属于另一条已经分叉的历史。

这时候就可能无法进行 Partial Resync,而需要重新以新 Master B 的数据为准建立基准。

A 隔离期间自己接受的那些写入不会自动和 B 做 merge,而可能在重新同步时被丢弃。


十六、网络隔离时 Sentinel 到底会不会 Failover

这里要分两种完全不同的网络问题。

1. 只有 Master ↔ Replica 复制链路断开

例如:

text 复制代码
        Sentinel
            │
            │ 正常
            ↓
         Master
        ×      ×
       /        \
Replica1      Replica2

也就是:

text 复制代码
Sentinel → Master:正常
Replica → Master:失败

站在 Replica 的视角:

text 复制代码
我的 upstream 不可达

它们会不断重连 Master。

但是站在 Sentinel 的视角:

text 复制代码
Master 仍然正常响应 PING / INFO

那么 Sentinel 不会形成 Master 的 SDOWN / ODOWN,也就不会触发 Failover。

这并不是系统"卡死了只能人工处理",而是一个合理选择。

因为真正故障的是:

text 复制代码
复制链路

而不是 Master 本身。

如果网络稍后恢复:

text 复制代码
Replica
↓
重新连接 Master
↓
PSYNC
↓
恢复复制

通常不需要人工换 Master。


2. 足够多 Sentinel 也无法访问 Master

另一种情况:

text 复制代码
网络分区 A:
旧 Master + 少数 Sentinel

网络分区 B:
Replica + 多数 Sentinel

如果多数 Sentinel 无法访问旧 Master,达到:

text 复制代码
quorum
+
majority

那么它们可能在分区 B 中完成 Failover,把一个 Replica 提升为新的 Master。

但旧 Master 本身可能仍然活着,并继续服务自己这一侧的客户端。

于是短时间内会出现:

text 复制代码
旧 Master A
继续接受写

新 Master B
也开始接受写

这就是网络分区中最危险的历史分叉场景之一。

分区恢复后,Sentinel 会把旧 Master A 降级为 B 的 Replica。

A 在隔离期间自己接受的写入并不会自动 merge 到 B,而可能被丢弃。

所以 Redis Sentinel 虽然提供高可用,但 Redis 基础复制本身仍然是异步复制,Failover 场景下不能简单理解成"绝对零数据丢失"。


十七、使用 Docker 搭建 3 Redis + 3 Sentinel 实验环境

前面的机制全部理解以后,下面通过一次真实实验把整个流程串起来。

需要先说明:

这里 6 个容器全部运行在同一台云服务器上,只适合验证机制,不是真正意义上的高可用部署。

因为物理宿主机一旦挂掉,6 个容器会一起消失。


1. 创建 Docker 网络

为了让多个容器处在同一个虚拟局域网中,首先创建:

bash 复制代码
docker network create redis-sentinel-net

这里可以把 Docker bridge network 理解成一个虚拟交换网络:

text 复制代码
redis-master      172.18.0.10
redis-replica1    172.18.0.11
redis-replica2    172.18.0.12
redis-sentinel1   172.18.0.21
redis-sentinel2   172.18.0.22
redis-sentinel3   172.18.0.23

每个容器都有自己的 Network Namespace,因此虽然多个容器内部都监听:

text 复制代码
6379

也不会冲突。


2. 创建 Master

实验中先启动:

bash 复制代码
docker run -d \
  --name redis-master \
  --network redis-sentinel-net \
  --ip 172.18.0.10 \
  redis:7-alpine \
  redis-server \
  --bind 0.0.0.0 \
  --port 6379 \
  --protected-mode no

随后查看:

bash 复制代码
docker exec -it redis-master redis-cli INFO replication

刚启动时 Master 没有任何 Replica,因此:

text 复制代码
role:master
connected_slaves:0
repl_backlog_active:0

实验截图如下:

这里还能看到当前 Master 已经拥有自己的:

text 复制代码
master_replid

但由于还没有 Replica:

text 复制代码
repl_backlog_active:0

3. 创建 Replica1

接下来创建第一个 Replica:

bash 复制代码
docker run -d \
  --name redis-replica1 \
  --network redis-sentinel-net \
  --ip 172.18.0.11 \
  redis:7-alpine \
  redis-server \
  --bind 0.0.0.0 \
  --port 6379 \
  --protected-mode no \
  --replicaof 172.18.0.10 6379

查看 Replica1:

bash 复制代码
docker exec -it redis-replica1 redis-cli INFO replication

可以看到:

text 复制代码
role:slave
master_host:172.18.0.10
master_port:6379
master_link_status:up

说明 Replica1 已经成功连接到 Master。

再回头查看 Master:

text 复制代码
connected_slaves:1
slave0:ip=172.18.0.11,port=6379,state=online

同时:

text 复制代码
repl_backlog_active:1

说明建立复制关系以后,Master 的 replication backlog 已经开始工作。


4. 创建 Replica2

继续创建第二个 Replica:

bash 复制代码
docker run -d \
  --name redis-replica2 \
  --network redis-sentinel-net \
  --ip 172.18.0.12 \
  redis:7-alpine \
  redis-server \
  --bind 0.0.0.0 \
  --port 6379 \
  --protected-mode no \
  --replicaof 172.18.0.10 6379

检查:

bash 复制代码
docker exec -it redis-replica2 redis-cli INFO replication

可以看到 Replica2 同样已经:

text 复制代码
role:slave
master_host:172.18.0.10
master_link_status:up

并且其 master_replid 与整个复制组当前使用的复制历史一致。

至此数据层完成:

text 复制代码
             172.18.0.10
                Master
              /        \
             ↓          ↓
      172.18.0.11    172.18.0.12
        Replica1       Replica2

十八、创建三个 Sentinel

1. Sentinel1 配置

创建 Sentinel1 的配置:

conf 复制代码
port 26379
bind 0.0.0.0
protected-mode no

sentinel monitor mymaster 172.18.0.10 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
sentinel parallel-syncs mymaster 1

这里:

text 复制代码
monitor ... 2
→ quorum = 2

down-after-milliseconds = 5000
→ 为了实验快速观察 SDOWN,将检测时间设置为 5 秒

failover-timeout = 60000
→ Failover 相关超时窗口

parallel-syncs = 1
→ Failover 后一次允许一个 Replica 与新 Master 并行重同步

然后启动:

bash 复制代码
docker run -d \
  --name redis-sentinel1 \
  --network redis-sentinel-net \
  --ip 172.18.0.21 \
  -v ~/redis-sentinel-lab/sentinel1:/etc/redis \
  redis:7-alpine \
  redis-server /etc/redis/sentinel.conf --sentinel

日志中可以观察到:

text 复制代码
+monitor master mymaster 172.18.0.10 6379 quorum 2
+slave ... 172.18.0.11:6379
+slave ... 172.18.0.12:6379

这直接验证了:

text 复制代码
我们只配置了 Master
↓
Sentinel 自动发现两个 Replica

2. Sentinel2:验证 Sentinel 之间的自动发现

Sentinel2 使用相同监控配置,地址为:

text 复制代码
172.18.0.22:26379

启动以后,它除了发现两个 Replica,还会出现类似:

text 复制代码
+sentinel sentinel <S1-runid> 172.18.0.21 26379 @ mymaster ...

这说明 S2 自动发现了 S1。

查看 Sentinel2 日志,可以观察到:

text 复制代码
+slave ... 172.18.0.11
+slave ... 172.18.0.12
+sentinel sentinel ... 172.18.0.21 26379

这正是:

text 复制代码
Replica 自动发现
+
__sentinel__:hello Sentinel 自动发现

两套机制同时工作。


3. 创建 Sentinel3

第三个 Sentinel 地址设置为:

text 复制代码
172.18.0.23:26379

启动以后最终得到:

text 复制代码
S1 = 172.18.0.21
S2 = 172.18.0.22
S3 = 172.18.0.23

此时整个实验环境为:

text 复制代码
                    Redis 数据层

                   172.18.0.10
                      Master
                   /          \
                  ↓            ↓
          172.18.0.11      172.18.0.12
             Replica1          Replica2


                   Sentinel 控制层

      172.18.0.21   172.18.0.22   172.18.0.23
          S1             S2             S3

                   quorum = 2
                  majority = 2

十九、先验证 Sentinel 集群具备 Failover 条件

通过:

bash 复制代码
docker exec -it redis-sentinel1 \
  redis-cli -p 26379 SENTINEL CKQUORUM mymaster

得到:

text 复制代码
OK 3 usable Sentinels. Quorum and failover authorization can be reached

这句话其实正好对应前面的两层机制:

text 复制代码
3 个 Sentinel 当前都可用
↓
quorum = 2 可以达到
↓
majority = 2 也可以达到
↓
当前具备自动 Failover 条件

随后在 Master 写入测试数据:

bash 复制代码
docker exec -it redis-master \
  redis-cli SET sentinel:test before-failover

两个 Replica 都能读取:

text 复制代码
"before-failover"

说明 Failover 前三个数据节点处于一致的基准状态。


二十、真正制造 Master 故障

直接杀掉 Master:

bash 复制代码
docker kill redis-master

等待 Sentinel 完成判断与切换以后,通过:

bash 复制代码
docker exec -it redis-sentinel1 \
  redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster

得到:

text 复制代码
172.18.0.12
6379

这说明:

text 复制代码
旧 Master:172.18.0.10
↓
新 Master:172.18.0.12

随后查看两个 Redis:

text 复制代码
172.18.0.12:
role:master
connected_slaves:1
slave0:ip=172.18.0.11

172.18.0.11:
role:slave
master_host:172.18.0.12
master_link_status:up

所以最终拓扑真正变成:

text 复制代码
172.18.0.10
   ×

      172.18.0.12
          Master
            │
            ↓
      172.18.0.11
          Replica

这说明 Sentinel 并不只是修改了自己内部"Master 地址"的记录,而是真的完成了:

text 复制代码
Replica2 → Master
Replica1 → 改为复制 Replica2

二十一、通过真实日志完整还原一次 Failover

这次实验最有价值的部分就是 Sentinel1 日志。

核心日志如下:

text 复制代码
+sdown master mymaster 172.18.0.10 6379
+odown master mymaster 172.18.0.10 6379 #quorum 3/2
+new-epoch 1
+try-failover master mymaster 172.18.0.10 6379
+vote-for-leader aabb... 1
...
+elected-leader master mymaster 172.18.0.10 6379
+failover-state-select-slave master mymaster ...
+selected-slave slave 172.18.0.12:6379 ...
+failover-state-send-slaveof-noone slave 172.18.0.12:6379 ...
+failover-state-wait-promotion slave 172.18.0.12:6379 ...
+promoted-slave slave 172.18.0.12:6379 ...
+failover-state-reconf-slaves master mymaster ...
+slave-reconf-sent slave 172.18.0.11:6379 ...
+slave-reconf-inprog slave 172.18.0.11:6379 ...
+slave-reconf-done slave 172.18.0.11:6379 ...
+failover-end master mymaster ...
+switch-master mymaster 172.18.0.10 6379 172.18.0.12 6379
+slave slave 172.18.0.11:6379 ... @ mymaster 172.18.0.12 6379
+slave slave 172.18.0.10:6379 ... @ mymaster 172.18.0.12 6379

现在可以把它和理论逐行对应。


1. +sdown:单个 Sentinel 首先认为旧 Master 不可达

text 复制代码
+sdown master mymaster 172.18.0.10 6379

表示:

text 复制代码
当前 Sentinel
↓
超过 down-after-milliseconds
没有获得正常响应
↓
SDOWN

三个 Sentinel 的 SDOWN 时间非常接近,但不是完全一致。

这很正常,因为它们是三个独立进程,各自维护自己的定时器和网络状态。


2. +odown ... #quorum 3/2

text 复制代码
+odown ... #quorum 3/2

可以理解成:

text 复制代码
当前获得 3 个 Sentinel 的故障判断
配置要求 quorum = 2
↓
3 >= 2
↓
ODOWN

另一个 Sentinel 日志中也可能出现:

text 复制代码
#quorum 2/2

因为各 Sentinel 接收到其他节点回复的具体时刻并不完全相同。

它们并不需要在同一时刻拥有完全同步的局部状态。


3. +new-epoch 1

text 复制代码
+new-epoch 1

表示开始新的故障转移逻辑轮次。

由于这是实验环境中的第一次 Failover,因此:

text 复制代码
epoch = 1

4. +try-failover 与真实 Leader 投票

这次日志非常有意思,因为 S1 和 S2 几乎同时尝试参与 Failover。

S1:

text 复制代码
+vote-for-leader aabb... 1

表示 S1 在 epoch=1 给自己授权。

S2:

text 复制代码
+vote-for-leader 2cfd... 1

表示 S2 也给自己授权。

然后 S3:

text 复制代码
c8df... voted for aabb... 1

于是形成:

text 复制代码
S1:S1 + S3 = 2
S2:S2      = 1

对于三个 Sentinel:

text 复制代码
majority = 2

所以 S1 最终出现:

text 复制代码
+elected-leader

这次真实实验正好证明了:

text 复制代码
多个 Sentinel 可以同时尝试竞选
↓
但是只有获得合法授权门槛的 Sentinel
↓
才能真正执行 Failover

5. +selected-slave 172.18.0.12

S1 成为 Leader 后进入:

text 复制代码
+failover-state-select-slave

随后:

text 复制代码
+selected-slave slave 172.18.0.12:6379

说明最终选择 Replica2 作为新 Master。

在这次实验里两个 Replica:

text 复制代码
replica-priority = 100

都相同。

它们故障前的 replication offset 也非常接近。

如果在 Leader 做最终比较时前面的条件都一致,就会继续使用 runid 的字典序规则做最终确定性选择。


6. send-slaveof-noone:真正提升 Replica

日志:

text 复制代码
+failover-state-send-slaveof-noone

含义非常直观。

Leader 要让:

text 复制代码
172.18.0.12

不再复制任何上游,也就是执行与:

text 复制代码
REPLICAOF NO ONE

等价的角色切换。

随后进入:

text 复制代码
+failover-state-wait-promotion

表示 Sentinel 正在等待 .12 真正完成角色变化。

直到看到:

text 复制代码
+promoted-slave

才说明 .12 已经真正成为 Master。


7. slave-reconf-*:让其他 Replica 改上游

新 Master 已经产生以后:

text 复制代码
172.18.0.12 = Master

剩下的 .11 还在复制旧 .10

所以 Leader 开始:

text 复制代码
+failover-state-reconf-slaves

随后:

text 复制代码
+slave-reconf-sent
+slave-reconf-inprog
+slave-reconf-done

最终 .11 的配置变成:

text 复制代码
master_host:172.18.0.12

于是:

text 复制代码
原来:
.11 → .10

现在:
.11 → .12

8. +switch-master:新的拓扑正式成立

最后出现:

text 复制代码
+switch-master mymaster
172.18.0.10 6379
172.18.0.12 6379

意味着 mymaster 的逻辑地址正式从:

text 复制代码
172.18.0.10:6379

切换为:

text 复制代码
172.18.0.12:6379

因此客户端以后通过 Sentinel 查询:

text 复制代码
SENTINEL get-master-addr-by-name mymaster

就会得到新的 Master 地址。


9. 为什么旧 .10 已经被记成 Replica

Failover 日志后面还会看到:

text 复制代码
+slave slave 172.18.0.10:6379 ... @ mymaster 172.18.0.12 6379

这非常关键。

虽然 .10 此时仍然被 docker kill,根本没有恢复,但 Sentinel 在新的逻辑拓扑中已经认定:

text 复制代码
172.18.0.10
以后恢复
↓
应该成为 172.18.0.12 的 Replica

所以后续才会出现:

text 复制代码
+sdown slave 172.18.0.10:6379

注意此时它已经不是:

text 复制代码
"不可达的 Master"

而是:

text 复制代码
"当前新拓扑中暂时不可达的 Replica"

这也为后面旧 Master 恢复后被自动降级重新接入新拓扑做了铺垫。


二十二、Failover 后的 replid 实验结果

新 Master .12INFO replication 出现:

text 复制代码
master_replid:f835620d...
master_replid2:136e432c...
second_repl_offset:126533

其中旧 Master .10 原来的 replication ID 正是:

text 复制代码
136e432c...

也就是说实验真实验证了:

text 复制代码
旧 Master replid
      ↓
Replica2 晋升
      ↓
新 master_replid = 新 ID
master_replid2    = 旧 ID

这正是前面所讨论的:

text 复制代码
新的 replid
→ 表示已经进入新的复制历史

replid2
→ 保留故障转移前的旧复制历史身份

second_repl_offset
→ 记录旧历史可以安全匹配的边界

从而让其他旧 Replica 在重新连接新 Master 时,有机会继续使用旧 replid + offset 进行部分同步,而不是因为 Master 身份发生切换就全部 Full Resync。


二十三、把整个 Sentinel 心智模型最终串起来

到这里,可以把 Redis 主从复制与 Sentinel 的关系完整地整理成下面这条逻辑链。

text 复制代码
Redis Master
负责接收写请求
↓
产生 replication stream
↓
Replica 跟随 Master
↓
replid 标识复制历史
offset 标识历史位置
backlog 保存最近一段历史
↓
普通复制链路断开
↓
Replica 自动重连原 upstream
↓
PSYNC replid offset
↓
能接 backlog
→ Partial Resync

不能接
→ Full Resync

但如果:

text 复制代码
Master 真正不可用

基础复制机制不会自己重新选 Master,因此需要 Sentinel:

text 复制代码
Sentinel 持续监控 Master
↓
单个 Sentinel 判断 Master 不可达
↓
SDOWN
↓
向其他 Sentinel 询问
↓
达到 quorum
↓
ODOWN
↓
进入新的 epoch
↓
多个 Sentinel 可能同时尝试竞选
↓
一个 Sentinel 每个 epoch 只能授权一个候选者
↓
候选者达到多数派授权门槛
↓
成为 Leader

Leader 获得执行权以后:

text 复制代码
先检查 Replica 与旧 Master 的失联时间
↓
失联过久
→ 淘汰
↓
比较 replica-priority
↓
优先级一样
→ 比 replication offset
↓
offset 一样
→ 比 runid
↓
选出一个 Replica

然后:

text 复制代码
REPLICAOF NO ONE
↓
Replica → New Master
↓
产生新的 replid
↓
旧 replid 保存到 replid2
↓
修改其他 Replica 的 upstream
↓
向其他 Sentinel 传播新配置
↓
客户端通过 Sentinel 获得新 Master

旧 Master 后续恢复:

text 复制代码
Sentinel 再次发现旧 Master 可达
↓
发现它与当前逻辑拓扑不一致
↓
将旧 Master 配置成新 Master 的 Replica
↓
旧 Master 主动连接新 Master
↓
PSYNC
↓
能够利用旧历史 + backlog
→ Partial Resync

无法继续旧历史
→ Full Resync

二十四、几个最容易混淆的概念

最后再把整个学习过程中最容易混淆的几个概念集中区分一下。

1. runid / replid / epoch

text 复制代码
runid
→ 一个 Redis / Sentinel 进程实例是谁

replid
→ 当前属于哪一条复制历史

epoch
→ 当前是哪一轮 Sentinel Failover / Leader Election

2. quorum / majority

text 复制代码
quorum
→ 人工配置
→ Master 从 SDOWN 推进到 ODOWN 所需要的故障判断数量

majority
→ floor(N/2)+1
→ Failover Leader 至少需要的多数派安全门槛

真正执行 Failover 的授权数量可以理解为:

text 复制代码
max(quorum, majority)

3. Sentinel Leader Election / Replica Selection

text 复制代码
Sentinel Leader Election
→ 决定"谁来执行 Failover"
→ 核心是 epoch + 投票授权 + 多数派

Replica Selection
→ 决定"哪个 Replica 接班"
→ 核心是节点状态排序

Replica Selection 的顺序:

text 复制代码
失联时间资格过滤
↓
replica-priority
↓
replication offset
↓
runid

4. Sentinel 与 Redis 节点的连接 / Redis 主从复制连接

这是两套不同用途的 TCP 连接。

控制连接:

text 复制代码
Sentinel
↓
Redis 数据节点

用于:

text 复制代码
监控
PING / INFO
拓扑修改

复制连接:

text 复制代码
Replica
↓
Master / upstream

用于:

text 复制代码
PSYNC
复制流传输

所以旧 Master 恢复以后不是主动寻找 Sentinel,而是 Sentinel 重新发现它,再将它配置成 Replica。随后旧 Master 才以 Replica 身份主动连接新的 upstream。


5. __sentinel__:hello 不是"管道"

它是:

text 复制代码
Redis Pub/Sub Channel

也就是一个逻辑消息主题。

Sentinel 使用它:

text 复制代码
发布自己的 IP / port / runid / 配置信息
↓
其他 Sentinel 订阅
↓
完成自动发现和配置传播

总结

Redis 主从复制首先解决的是:

text 复制代码
如何让多个 Redis 节点拥有相近的数据副本

它通过:

text 复制代码
replid
+
offset
+
replication backlog
+
PSYNC

使 Replica 在复制连接中断后能够尽量进行部分重同步,而不是每次都重新传整个数据集。

但是主从复制本身不会在 Master 永久不可用时自动重构拓扑。

因此 Sentinel 在主从复制之上增加了一层控制逻辑:

text 复制代码
监控
↓
故障判断
↓
ODOWN
↓
Leader Election
↓
Replica Selection
↓
Failover
↓
配置传播
↓
旧节点重新加入

真正理解 Sentinel 的关键,不是分别记住这些名词,而是理解它们为什么必须依次出现。

单个 Sentinel 的判断可能出错,所以需要:

text 复制代码
SDOWN → quorum → ODOWN

多个 Sentinel 不能同时改拓扑,所以需要:

text 复制代码
epoch + majority + Leader Election

新的 Master 不能随便选,所以需要:

text 复制代码
失联时间
→ priority
→ offset
→ runid

Replica 被提升以后复制历史可能和旧 Master 分叉,所以必须:

text 复制代码
生成新的 replid

但是其他 Replica 本来和新 Master 的数据非常接近,又没有必要全部重新做全量同步,所以还需要:

text 复制代码
master_replid2
+
second_repl_offset
+
backlog

最终,Sentinel 真正解决的是:

在普通 Redis 主从复制已经能够"复制数据"的基础上,再增加一套分布式控制机制,让系统能够自动判断 Master 故障、唯一确定 Failover 执行者、挑选新的 Master,并把整个复制拓扑重新收敛到一个新的稳定状态。


相关推荐
达梦数据1 小时前
DMDRS搭建部署简介与运行环境
数据库
Omics Pro1 小时前
1个月2轮融资!长寿虚拟细胞
数据库·人工智能·算法·机器学习·自然语言处理
zcn1262 小时前
row_number()函数与group by性能比较
数据库·sql优化改写
Wang's Blog2 小时前
Java 项目实战: 外卖平台-后台系统登录功能开发
java·服务器·redis
小K讲AI营销2 小时前
AI基础设施融资路径的中美比较:资本市场模式与政策金融模式
大数据·数据库·人工智能
净水深流2 小时前
中国冷链冷库行业数据分析:从规模扩张到技术驱动
大数据·数据库·人工智能·冷库冷链
李兆龙的博客2 小时前
从一到无穷大 #94:ClickHouse TimeSeries——时间线组织与 Prometheus 兼容性
数据库
冰暮流星2 小时前
事务的四个特性(ACID)详解
数据库
byte轻骑兵3 小时前
时序数据库选型全指南|大数据工业场景Apache IoTDB落地实操
大数据·数据库·人工智能·apache iotdb