Redis 主从切换时脑裂导致的数据丢失窗口判定

这道题在面试里出现的频率不低,但绝大多数候选人的回答停留在「脑裂就是两个 master,会丢数据」这个层面,再往下问「丢多少、什么时候丢、窗口多长、怎么算」就答不上来了。实际上 Redis 本身并不具备自动的脑裂防护(Sentinel 的 quorum 只能降低概率,不能消除),数据丢失窗口是有明确数学边界、可以被量化和判定的。这篇文章把窗口的构成拆开算清楚

这道题在面试里出现的频率不低,但绝大多数候选人的回答停留在「脑裂就是两个 master,会丢数据」这个层面,再往下问「丢多少、什么时候丢、窗口多长、怎么算」就答不上来了。实际上 Redis 本身并不具备自动的脑裂防护(Sentinel 的 quorum 只能降低概率,不能消除),数据丢失窗口是有明确数学边界、可以被量化和判定的。这篇文章把窗口的构成拆开算清楚,顺带给出可落地的检测手段和参数的取舍依据。

先明确:Redis 脑裂到底是怎么发生的

Redis 的经典部署是 master + N 个 replica + 一个 Sentinel 集群(或 Redis Cluster)。脑裂不是 Redis 独有的,它的触发条件非常具体,必须同时满足三条:

  • master 与 所有 replica 之间的网络中断,或者 master 所在机器负载过高导致心跳超时;
  • master 与 Sentinel 集群之间的网络也中断(否则 Sentinel 能感知到 master 失联并推进故障转移);
  • Sentinel 集群内部仍有多数派存活,能够达成 SDOWN → ODOWN 的判定并选出新 master。

此时旧 master 依然认为自己是从前的身份,继续接受客户端写入;新 master 被 Sentinel 提升,也在接受写入。这就是脑裂。等网络恢复后,Sentinel 会把旧 master 降级为 replica 并执行 REPLICAOF,旧 master 上那段时间写入的数据会被全量同步(full resync)直接抹掉——这部分就是丢失的数据。

一个容易忽略的点:Redis 的复制是异步的。repl-backlog 只能解决「短暂断连后部分重同步(PSYNC)」,解决不了「旧 master 已经写入了、新 master 没收到」的问题。所以丢失是结构性的,不是配置能消除的。

丢失窗口的构成:三段相加,不是拍脑袋的「几秒」

数据丢失窗口 = 故障检测与转移耗时 + 旧 master 继续写入的时长(含网络恢复后到降级前的残余时间)

把它拆成可计算的几段:

阶段 关键判定参数 时间量级
T1:Sentinel 判定主观下线 down-after-milliseconds 默认 30000 ms
T2:达成客观下线并选 leader quorum、Sentinel 数量、failover-timeout 通常数百 ms ~ 数秒
T3:选 replica、提升为 master、广播配置 failover-timeout(默认 180000 ms,是上限不是耗时) 通常 1~5 s
T4:旧 master 被降级前的残余写入 Sentinel 发现旧 master 恢复的时间 + 客户端连接重建 取决于网络恢复时刻,可能很长

注意 T4 才是真正的风险大头。很多事故报告里写「切换花了 20 秒」,但丢掉的是几分钟甚至更久的写入——因为旧 master 的网络分区一直没恢复,它一直在收写请求。只要旧 master 还活着、客户端还能连上它、且 Sentinel 还没把它降级,写入就是持续发生的。

T2 的耗时需要额外说明:Sentinel 的 quorum 只用来判断客观下线,选 leader 需要多数派授权(majority),所以如果只部署 2 个 Sentinel 且 quorum=2,一旦有一个挂了就无法完成故障转移——节点数必须是奇数且大于等于 3。

用算法估算:能丢多少条、多少秒

区分两个维度看:时间窗口和条数窗口。

条数维度:由 replication backlog 决定上限

断开瞬间旧 master 尚未同步给新 master 的数据量,理论上限是当时的 master_repl_offset 与 replica 的 slave_repl_offset 之差。可以通过 INFO replication 实时观测:

redis-cli -p 6379 INFO replication
# 关注以下字段
# role:master
# connected_slaves:2
# slave0:ip=10.0.0.12,port=6379,state=online,offset=189273645,lag=0
# master_repl_offset:189274001
# repl_backlog_active:1
# repl_backlog_size:1048576
# repl_backlog_first_byte_offset:188225426

差值 master_repl_offset - slave0:offset 就是当前未确认的字节数(不是命令条数,Redis 的 offset 单位是字节)。真正丢多少条命令取决于这些字节里含多少命令,只能估算。如果这个差值已经超过 repl_backlog_size,说明一旦断开就必然触发全量同步,此时丢失窗口没有上限——只要旧 master 还在写,写多少丢多少。

所以一个可判定的判据是:

若 master_repl_offset - slave_offset > repl_backlog_size,则本次故障的丢失上界 = ∞(不可界定),必须按 T4 时长 × 写入 QPS 估算。
若差值小于 backlog,则上界 ≈(T1 + T2 + T3 + T4)× 写入 QPS。

时间维度:用 lag 判断「已经不可信」

INFO replication 里每个 replica 都有 lag 字段,单位是秒,表示该 replica 上次与 master 交互距今多久。但要注意 lag 为 0 不代表数据一致——异步复制下,replica 收到的是 master 的「历史」,只是延迟极小。真正可以用作判据的是切换前的 lag 值:如果故障发生时某个 replica 的 lag 已经达到 30 s,那么被提升后它天然就少了 30 s 的数据,这与脑裂叠加,损失更大。

Sentinel 在选新 master 时有 replica-priority 和 offset 两个参考,优先选 offset 最大的(数据最新)。所以正确的降损思路不是「防止脑裂」,而是保证被选中的 replica 尽可能新。

降低丢失窗口的四个真实可调项

1. min-replicas-to-write 与 min-replicas-max-lag

这是 Redis 官方给出的脑裂缓解手段(2.8 起提供):master 在写入前检查「健康 replica 数量」,不足则直接拒绝写入并返回错误。配置示例:

# 至少有 1 个 replica 的 lag 不超过 10 秒,否则拒绝写入
min-replicas-to-write 1
min-replicas-max-lag 10

触发时客户端会收到明确报错:

(error) NOREPLICAS Not enough good replicas to write.

这正是面试官想听到的「真实报错」。它的作用是:让旧 master 在网络分区后尽快停止接受写入,从而把 T4 从「无限」压缩成「最多 min-replicas-max-lag 秒」。注意两个参数的坑:

  • 两个参数必须同时设置才生效,只配一个等于关闭该特性;
  • 设置后可用性下降——所有 replica 同时抖动时 master 会拒绝写入,业务侧必须有降级或重试策略;
  • 该检查发生在命令执行前,MULTI/EXEC 事务和 EVAL 脚本不受逐条保护,需要业务层评估。

2. repl-backlog-size 与 repl-backlog-ttl

backlog 越大,短时断连越倾向于走部分重同步,能显著缩小「切换后 replica 追不上、被迫全量同步」的窗口。默认 repl-backlog-size 1mb,在高写入场景下偏小。按峰值写入速率 × 期望容忍的断连秒数估算:

# 假设峰值 10 MB/s,容忍 30 秒断连
repl-backlog-size 300mb
repl-backlog-ttl 3600

backlog 是每个 master 一份的内存开销,repl-backlog-ttl 控制最后一个 replica 断开后 backlog 保留多久,默认 3600 秒。

3. down-after-milliseconds 的权衡

这个值直接决定 T1。默认 30000 ms 意味着至少 30 秒才开始怀疑。调小能缩短窗口,但会误判——网络抖动、慢查询导致的阻塞、fork 导致的延迟尖峰(AOF rewrite / RDB 保存时 fork 可能造成几百毫秒到数秒的停顿)都可能触发主观下线。实践中的取值区间是 5000~10000 ms,前提是 slowlog 和 fork 耗时(INFO stats 里的 latest_fork_usec)都在可控范围。

4. min-replicas 之外:客户端与代理层

如果前面挂了 Proxy(如 Codis、Twemproxy)或使用 Lettuce/Jedis 的 Sentinel 模式,切换瞬间的写入可能打到旧 master。客户端应配置合理的超时并监听 Sentinel 的 +switch-master 事件,而不是死连一个地址。

故障复盘时如何判定「到底丢了哪一段」

面试里如果被追问「你怎么证明丢的是这段时间的数据」,可以用以下几个证据链:

  1. 旧 master 的日志:网络恢复瞬间会被 Sentinel 发送 REPLICAOF,日志里出现 REPLICAOF <new-master-ip> <port> enabled,以及随后的 MASTER <-> REPLICA sync started / Full resync from master。从「分区开始」到这条 Full resync 之间的写命令就是候选丢失集。
  2. offset 对比:故障前后记录 master_repl_offset和 slave_repl_offset。切换后新 master 的 offset 如果小于旧 master 的最后 offset,差值部分即丢失量(字节级)。
  3. AOF / RDB 时间戳:如果旧 master 开了 AOF,appendonly.aof 中在降级前的最后一段时间戳写入的命令,在新拓扑里就不存在了。
  4. 业务侧幂等日志:这是最实用的——用业务主键去新 master 查询,比对请求时间戳,即可定位丢失区间。

典型日志长这样(Redis 6.x):

# 旧 master 侧
1:M 12 Aug 2023 10:21:03.114 * REPLICAOF 10.0.0.12:6379 enabled (user request from 'id=5 addr=10.0.0.15:53122')
1:M 12 Aug 2023 10:21:03.115 # Setting secondary replication ID: 8f2c...
1:M 12 Aug 2023 10:21:03.201 * Connecting to MASTER 10.0.0.12:6379
1:M 12 Aug 2023 10:21:03.203 * MASTER <-> REPLICA sync started
1:M 12 Aug 2023 10:21:03.205 * Full resync from master: 3a1b...:0

注意 Full resync 说明连部分重同步都没走通,旧 master 的内存数据会被清空后重建,这段时间涌入的写入直接消失。

面试回答的框架:先定性,再定量,最后给方案

一个完整的回答应该包含四层:

  • 定性:Redis 复制是异步的,脑裂下的丢失不可避免,Sentinel 的 quorum 只解决「选出新 master」,不解决「旧 master 停写」。
  • 定量:丢失窗口由「检测 + 转移 + 旧 master 残余写入」三段构成,关键变量是 down-after-milliseconds 和旧 master 的分区持续时长;条数上界受 repl-backlog-size 约束,超过则无界。
  • 判定:用 master_repl_offset 差值、INFO replication 的 lag、日志中的 Full resync 三者交叉确认丢失区间。
  • 缓解:min-replicas-to-write + min-replicas-max-lag 强制旧 master 停写;调大 backlog;down-after-milliseconds 适度调小;客户端接入 Sentinel 事件;关键业务用 WAIT 命令或外部一致性方案兜底。

补充一个常被追问的点:WAIT 命令。它可以让客户端在写入后阻塞等待 N 个 replica 确认,从而把「异步复制」变成「准同步」,但它不是强一致——WAIT 返回后如果 master 宕机,被选中的 replica 未必是那个已确认的 replica。WAIT 只能缩小窗口,不能消除。真正需要强一致就得走 Redis 的 WAITAOF(7.0+)或干脆换存储。

小结

脑裂导致的数据丢失窗口不是一个模糊的「几秒钟」,而是可以拆成故障检测、转移、旧 master 残余写入三段来估算的工程量。最危险的从来不是 Sentinel 切换慢,而是旧 master 在网络分区中持续接受写入,且它离被降级可能隔了几分钟。判定丢失量要看 offset 差值和日志中的 Full resync 标记,缓解的核心是让旧 master 主动停写(min-replicas-to-write),而不是指望复制机制自己变强。把这个逻辑讲清楚,这道题就过了。

FAQ

Q1:Sentinel 部署 3 个节点,quorum 设成 2,还会脑裂吗?

会。quorum 只决定「多少 Sentinel 认为 master 挂了就标记 ODOWN」,它约束的是判定,不约束旧 master 的行为。只要旧 master 与 replica、与 Sentinel 同时失联,它就会继续写入。quorum 提高只是降低误切换概率。

Q2:设置了 min-replicas-to-write,是不是就不会丢了?

不是。它只能保证「旧 master 在健康 replica 不足时停写」,但检查窗口内已写入的数据、以及断开瞬间尚未同步的数据依然会丢。报错 NOREPLICAS Not enough good replicas to write 是客户端侧能观测到的信号,业务需要处理这个错误。

Q3:Redis Cluster 会不会有同样的问题?

Cluster 的分片内同样是主从异步复制,脑裂发生在某个分片的 master 与其 replica 之间时,丢失机制与 Sentinel 模式一致。Cluster 的 cluster-node-timeout 承担了类似 down-after-milliseconds 的角色,默认 15000 ms。

Q4:为什么调大 repl-backlog-size 能减少丢失?

它不减少「写进旧 master 但没同步」的数据,而是让网络恢复后的 replica 有机会走 PSYNC 部分重同步,避免全量同步带来的长时间不可用窗口。全量同步期间该 replica 不可读且占用大量带宽,会放大故障影响。backlog 大小应按峰值写入速率 × 容忍的断连秒数配置。

Q5:怎么在线上提前发现「即将脑裂」?

监控三个指标:每个 replica 的 lag(超过阈值告警)、master_repl_offset - slave_offset 的差值趋势、以及 master 上 connected_slaves 的变化。另外监控 latest_fork_usec,fork 卡顿是触发误判和主从延迟的常见根因。