这道题在面试里出现的频率不低,但绝大多数候选人的回答停留在「脑裂就是两个 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 事件,而不是死连一个地址。
故障复盘时如何判定「到底丢了哪一段」
面试里如果被追问「你怎么证明丢的是这段时间的数据」,可以用以下几个证据链:
- 旧 master 的日志:网络恢复瞬间会被 Sentinel 发送
REPLICAOF,日志里出现REPLICAOF <new-master-ip> <port> enabled,以及随后的MASTER <-> REPLICA sync started/Full resync from master。从「分区开始」到这条Full resync之间的写命令就是候选丢失集。 - offset 对比:故障前后记录
master_repl_offset和slave_repl_offset。切换后新 master 的 offset 如果小于旧 master 的最后 offset,差值部分即丢失量(字节级)。 - AOF / RDB 时间戳:如果旧 master 开了 AOF,
appendonly.aof中在降级前的最后一段时间戳写入的命令,在新拓扑里就不存在了。 - 业务侧幂等日志:这是最实用的——用业务主键去新 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 卡顿是触发误判和主从延迟的常见根因。