“Redis 主从复制原理”,这六个字在面试题里出现的频率有多高,不用我多说。但说实话,很多资料把它讲成一句话:主节点负责写,从节点负责读。等你真把一主两从的架构搭进生产环境,会发现这句话根本撑不住场面——为什么从节点只是断了几分钟,恢复后主节点 CPU 飙到90%?为什么明明网络没断,从节点的复制偏移量却长时间不动?为什么从节点连得好好的,读到的缓存还是旧值?“Redis 主从复制”背后是一整套状态机、缓冲区、异步协议和故障恢复机制,不是一句“读写分离”能带过的。这篇文章,我想把主从复制的原理从头串一遍,从握手、全量同步、部分重同步,到命令传播和数据一致性问题,最后落到生产环境的配置治理、监控指标和几个我实际踩过的复制坑。无论是准备 Redis 面试题,还是想在业务里做缓存治理、读写分离、高可用建设,这篇都能直接抄作业。
1. 单机 Redis 的短板:主从复制到底解决的是哪一类问题
先把问题摆清楚。单机 Redis 不是不能用,而是有几个硬伤:CPU 再强也只能用一个核处理命令,读 QPS 高到一定程度就顶不住;进程一挂,缓存直接冷启动,后端数据库瞬间被打满;磁盘坏了、机器被回收,内存里的数据连恢复的机会都没有。主从复制,本质上就是在回答这三个问题:分摊读请求、提供冗余副本、为故障转移铺路。
1.1 三个关键业务场景
第一类是读写分离。把写请求打到主节点,把读请求分给从节点,业务读多写少时效果立竿见影。我自己见过一个比较典型的数据看板系统,写量不大,读量是写的几十倍,主节点一个线程处理不过来,加了三个从节点之后,主节点 CPU 从 85% 降到 30%。这就是主从复制最朴素的价值:把单线程的 Redis 横向拉成多份读能力。
第二类是故障转移的基础。Redis 的哨兵(Sentinel)模式,就是靠监控主从节点的状态来做的自动切换。主节点挂了,哨兵从一堆从节点里挑一个提升为新主节点,整个过程依赖的就是“从节点手里有一份接近实时的数据副本”。没有复制,就没有高可用切换的基础数据。
第三类是数据备份与容灾。每天凌晨从节点做 BGSAVE 生成 RDB 快照,比直接在主节点上做备份要安全得多——至少不会因为备份期间的磁盘 IO 拖慢主节点响应。异地容灾场景也常见,主节点在一个可用区,从节点放到另一个可用区,主区整体断电时,至少还有一份完整数据能顶上。
1.2 主从复制解决不了的事
这里必须先给预期“降降温”。主从复制只解决“数据从主节点流向从节点”的问题,不解决以下问题:
- 脑裂后自动仲裁:主节点和从节点之间的网络分区了,业务还在写主节点,哨兵又把某个从节点提升成了新主,两边同时接受写入,旧主恢复后数据怎么合并?主从复制不管这事,这是哨兵与分布式一致性要处理的难题。
- 存储容量扩展:主从只是把同一份数据复制多份,不是分片。内存总容量没有任何增加,数据量超过单机内存,照样得靠 Redis Cluster 的分片机制。
- 强一致性保证:复制是异步的,主节点写成功不代表从节点立刻有,这中间存在天然延迟窗口。想靠纯主从做到“写主立即读从一致”,从机制上就不现实。
1.3 对“一致性”的预期校准
很多刚接触 Redis 的同学会下意识认为:主从复制了,两边就应该每分钟都一模一样。真实情况是,Redis 主从复制默认就是最终一致,而且是“松最终一致”——正常情况下延迟可以压到毫秒级,但网络抖一下、从节点在做 RDB 加载、或者主节点写吞吐突然暴涨时,延迟会被瞬间放大到秒级甚至分钟级。
我常用的一个类比是:主节点像个直播主播,从节点是观众;主播说话,观众通过直播流听到,正常情况下延迟一两秒,但网络一卡,画面就会卡住甚至重新缓冲。“重新缓冲”对应到 Redis 就是全量重同步。理解了这个模型,再去看后面的机制就不会跑偏。
2. 建立复制链路:从 REPLICAOF 到 PSYNC,握手时到底交换了什么
很多人一上来就敲命令,却不知道从节点和主节点建立连接时,背后其实是一套完整的握手协议。把这条链路的每个细节搞清楚,你才能真正理解后面所有故障现象。
2.1 最小可运行配置
先给一份最基础的一主一从配置,在从节点 redis.conf 里加上这几行:
# 声明主节点地址和端口 replicaof 10.0.0.5 6379 # 如果主节点开启了 requirepass,这里要填主节点的密码 masterauth your-redis-password # 从节点默认只读,强烈建议保持开启 replica-read-only yes也可以运行时临时生效,不用重启进程:
redis-cli -p 6380 replicaof 10.0.0.5 6379Redis 5.0 之前这个命令叫 slaveof,5.0 之后逐步换成了 replicaof,语义上就强调“副本”关系。如果你的代码里还在用 SLAVEOF,新版 Redis 依然兼容,但新项目建议直接用 replicaof。
配置完后,到主节点执行INFO replication,如果看到connected_slaves:1,说明链路已经建立。
2.2 握手协议拆解
从节点建立连接,不是发一条命令就完事的,完整握手过程是这样的:
- 从节点发起 TCP 连接到主节点的 6379 端口。
- 从节点发送
PING,确认主节点进程还活着、能正常响应命令。 - 如果主节点开启了密码认证,从节点发送
AUTH完成身份认证。这里的密码就是前面配置里的masterauth,不是从节点自己的 requirepass。 - 从节点发送
REPLCONF listening-port <port>,告诉主节点“我在哪个端口监听”,主节点会记录这个地址,用于 INFO 输出和后续哨兵发现副本。 - 从节点发送
REPLCONF capa eof capa psync2,这是能力协商,告诉主节点“我支持 EOF 格式的 RDB 传输、支持新版 PSYNC 协议”。 - 最后,从节点发送
PSYNC <replid> <offset>,正式请求同步。
这里面第 5 步很容易被忽略。capability协商的意义在于,老版本 Redis 和新版本 Redis 的复制协议细节有差异,如果不协商,老主节点会按老逻辑走,新从节点可能拿到无法识别的数据格式。生产环境主从版本不一致时,很多诡异问题都出在这里。我的建议很简单:主从版本尽量保持一致,至少也要保证 Redis 4.0 以上,因为 PSYNC2 带来的部分重同步能力是一个分水岭。
2.3 复制 ID 与复制偏移量的核心作用
握手最后一步里的replid和offset,是整个复制机制最核心的两个概念。
- 复制 ID(replid):相当于主节点复制历史的“身份证号”,一个 40 字符的随机十六进制字符串。主节点初次启动或从节点被提升为主节点时,会生成新的复制 ID。
- 复制偏移量(offset):从节点已经消费到的复制流字节数,主节点每写一条命令,偏移量就往后挪。它不是命令条数,是累计的字节数。
主从双方各自维护 offset,从节点会通过REPLCONF ACK <offset>周期性回报自己的进度。所以判断主从落后多少,最直接的办法就是比较两者的master_repl_offset和slave_repl_offset。
2.4 为什么需要 replid2:新老身份切换
Redis 4.0 引入 PSYNC2 之后,每个实例不再是单一复制 ID,而是有一主一备两个复制 ID:master_replid和master_replid2。这是为了解决一个非常现实的场景:主节点挂掉,某个从节点被哨兵提升为新主,老主节点网络恢复后又回来当从节点,此时新主节点手下的其他从节点可能还保留着老主节点的复制 ID。
新主节点会把自己的master_replid换成新的,同时把旧老主节点的复制 ID 存进master_replid2,这样老主节点回来请求部分重同步时,新主节点一看“你的 replid 是我记录的 replid2”,就知道你是我之前的同源兄弟,只要 offset 还在 backlog 里,就能走增量,不必让所有从节点从头再来一次全量同步。
这个机制很不起眼,但非常重要。没有它,每次主从切换都可能引发所有节点集体全量重同步,生产环境就是一次复制风暴。面试里如果能把 replid2 讲明白,基本能看出你是真用过而不是背过。
3. 全量同步:当从节点什么都没有时,主节点做了什么
从节点第一次挂上来、或者主从断开太久导致 backlog 丢失时,就会触发全量同步。这也是对主节点影响最大的一种同步方式,很多线上故障都发生在这一环节。
3.1 全量同步的完整生命周期
一次全量同步,拆开来看有六个关键阶段:
- 从节点发送
PSYNC ? -1,表示“我什么都不知道,给我来份全量”。 - 主节点返回
FULLRESYNC <replid> <offset>,把当前复制 ID 和偏移量发给从节点,同时触发后台保存。 - 主节点 fork 一个子进程,子进程把内存中的数据生成 RDB 快照。主进程继续服务正常读写。
- 在生成和传输 RDB 期间,主进程收到的新写入命令会不断累积到复制积压缓冲区(backlog)里。
- RDB 生成完成后,主节点把 RDB 文件发给从节点。从节点先清空自己原有的旧数据,再把 RDB 加载进内存。
- 从节点加载完 RDB 后,主节点把 backlog 里累积的增量命令继续发给从节点,从节点追上最新 offset,同步完成。
整个流程里,有一个细节特容易误解:从节点在加载 RDB 的这段时间,它自己是阻塞的,不会响应任何命令。如果从节点配置了replica-serve-stale-data yes(默认值),它会继续用旧数据对外服务;如果配了no,客户端会直接收到MASTERDOWN错误。很多业务把从节点当读缓存用,却没想到全量同步期间从节点要么给旧数据、要么直接不可用,这个窗口要提前做好预案。
3.2 fork 生成 RDB 的代价,为什么大实例全量同步会卡
RDB 保存不是主进程自己在那里写磁盘,而是 fork 一个子进程来做。看起来不影响主进程,但这里有两个隐藏代价:
第一,fork 瞬间需要复制父进程的页表,内存越大,fork 耗时越长。几十 GB 的 Redis 实例,fork 一次可能要几百毫秒甚至几秒,期间主进程短暂阻塞,慢查询日志里会出现一条阻塞时间特别长的记录。
第二,子进程做 RDB 期间,父进程的内存如果持续被修改,Linux 的写时复制机制会让物理内存翻倍增长。极端场景下,本来 20GB 的实例,做一次全量同步让内存冲到 40GB,直接把机器内存打满。
所以全量同步不是“从节点自己吃点数据”这么轻描淡写,而是会波及主节点的服务质量和整个机器的内存水位。这也是为什么我一直强调:监控 Redis 实例内存的时候,必须同时看 used_memory 和 RSS(实际物理内存占用),RSS 暴涨往往就是全量同步的征兆。
3.3 全量传输的两种模式:磁盘型与无盘同步
全量同步传输 RDB 的方式,在 Redis 2.8.18 之后有两种选择,由repl-diskless-sync控制:
| 配置 | RDB 生成位置 | 传输方式 | 适用场景 |
|---|---|---|---|
| repl-diskless-sync no | 子进程写磁盘 | 主节点读磁盘文件再发送 | 从节点少、网络慢、磁盘快 |
| repl-diskless-sync yes | 子进程直接写套接字 | RDB 不落盘,直接发给从节点 | 从节点多、磁盘 IO 慢、希望减少磁盘写 |
无盘同步的优势在于省掉了“写磁盘再读磁盘”两道 IO,但缺点也明显:从节点无法从磁盘上拿快照恢复,主节点如果中途断连,整个流程要重来。另外无盘同步默认有一个repl-diskless-sync-delay 5秒的等待窗口,目的是让多个从节点一起同步时能复用同一份 RDB,减少重复 fork。
如果你的主节点从节点数量比较多,我建议把无盘同步打开,并且根据实际业务调整 delay。默认 5 秒是“再等等看有没有别的从节点来”,如果你希望快速同步,可以把它调小到 1-2 秒。
3.4 全量同步风暴的成因
“同步风暴”是 Redis 运维里最典型的故障之一,它长这样:一个主节点下面挂了五个从节点,某个瞬间所有从节点几乎同时断开又重连,触发全量同步。主节点被 fork 了五次,RDB 同时生成五份,内存翻好几倍,网络带宽占满,结果主节点自己先被打挂,然后从节点再次重连,再次全量,整个集群雪崩。
形成风暴的常见原因有三个:
- 主节点重启,复制 ID 变化,所有从节点被迫全量重同步。
- 网络设备抖动,所有从节点同时断线,断线时间超过 backlog 容量,部分重同步全部失效。
- 批量发布或者批量重启从节点,没有做错峰。
对策也对应有三条:主节点升级重启前评估复制 ID 影响;调大repl-backlog-size,让断线恢复尽量走增量;从节点重启时手动错峰,不要在同一个维护窗口里全部一起重启。另外,主节点 maxmemory 触发大量淘汰时,淘汰命令也会沉重地压到复制链路上,这属于容易被忽略的隐性全量同步诱因——因为从节点处理不过来积压,offset 差距越拉越大,最终 backlog 被覆盖,又退化成全量。
4. 部分重同步:断网恢复后如何用 backlog“接着聊”
全量同步代价大,所以 Redis 设计了一个轻量级的恢复方式:部分重同步。理解它,你就理解了为什么断线几秒和断线几小时的结局完全不同。
4.1 断线恢复的两类结局
当从节点与主节点断开连接时,它本地不会丢失已经收到的数据,只是复制偏移量停在了断线前的位置。等网络恢复,从节点会重新发起握手,然后向主节点发送:
PSYNC <master_replid> <slave_repl_offset>主节点收到后做判断:如果你的复制 ID 和我的对得上,而且你要求的偏移量离我不算太远,我 backlog 里还有那段数据,我就回复CONTINUE,然后只把这期间的增量命令发给你,这就是部分重同步。如果主节点发现复制 ID 对不上,或者你的 offset 太旧、backlog 里的数据已经被覆盖了,就只能回复FULLRESYNC,老老实实再走一遍全量。
同一个故障,最终走的是增量还是全量,完全取决于 backlog 里有没有覆盖断线时段的写入。
4.2 backlog 缓冲区的环形结构
backlog 是主节点上的一个环形缓冲区,默认大小只有 1MB,由repl-backlog-size控制。每一条写命令在主节点执行后,会被追加进这个缓冲区,也会发给所有从节点。因为它是环形结构,新数据会覆盖旧数据,所以它只能保存最近一段时间内的复制流。
打个比方:backlog 就像一个容量固定的电视录像带,旧的节目会不断被覆盖。如果从节点断线太久,它想补看的时段已经被覆盖了,那对不起,只能重新看全量。
repl-backlog-size应该设多大?我的经验公式很简单:
期望容忍断线秒数 × 峰值写入流量(字节/秒) = 合理的 backlog 大小比如你期望断线 5 分钟内不要触发全量,峰值写入在 2MB/s,那 backlog 至少要300 × 2MB ≈ 600MB。当然这是理论值,实际还要留 1.5-2 倍余量。很多生产事故就是这么来的:默认 1MB 的 backlog,业务写几下就填满了,断线两分钟回来还想走增量,结果直接全量,主节点 CPU 瞬间被打满。
另外有个repl-backlog-ttl 3600的配置,含义是当没有任何从节点连接时,backlog 最多保留 3600 秒。这个参数的意义是省内存:如果所有从节点都断光了,主节点没必要一直留着积压数据。
4.3 部分重同步的完整判定过程
把判定逻辑拆得更细一点:
- 从节点上报的复制 ID 与主节点当前
master_replid一致;如果不一致,但刚好等于master_replid2,且 offset 匹配,也算一致——这就是主从切换后老从节点能接着续传的关键。 - 从节点上报的 offset 必须落在 backlog 的有效范围内:
offset >= repl_backlog_first_byte_offset。如果 offset 比 backlog 最老的数据还早,说明断线期间写入量太大,断点已经被覆盖。 - 以上两个条件同时满足,主节点返回
CONTINUE;否则返回FULLRESYNC <replid> <offset>。
从节点收到CONTINUE后,会接着从断点处消费主节点发来的增量数据。对业务侧来说,这次网络抖动几乎无感。
4.4 offset 差太多,为什么只能全量
这个问题我经常拿来反问自己:能不能让主节点再多存一些历史数据,总可以从某些持久化文件里补?答案是不能。因为主节点自己的复制流就只有 backlog 这么一份内存缓冲,没有外部存储记录更早的命令流。从节点主动发行数据去别的源头的机制是有的——比如从节点可以先从另一个从节点同步 RDB,但这是运维层面的手工方案,不改变协议本身的限制。
所以 offset 差太多=只能全量,这是机制决定的,不是配置没调好。能做的只有把 backlog 调大、把断线恢复时间压缩、把网络稳定性提升,尽量减少走到全量的概率。
5. 命令传播和数据一致性:为什么你会读到旧数据
主从之间不可能永远靠 RDB 同步,日常运行靠的是命令传播。理解了传播机制,才算真正理解了主从复制的数据一致性边界。
5.1 命令传播与 ACK 心跳
全量同步完成后,主节点会把每一条写命令(SET、DEL、EXPIRE 等)实时转发给所有从节点。从节点执行相同的命令后,就和主节点保持了数据一致。
这条链路上有一个重要的心跳机制:从节点默认每隔一秒向主节点发送一次REPLCONF ACK <offset>,告诉主节点“我已经执行到多少字节了”。主节点收到 ACK 后,就能在INFO replication里计算出每个从节点的滞后情况。
ACK 除了让主节点感知从节点状态,还有一个看似不起眼的作用:保证网络连接活跃,防止长时间没有写入时 TCP 连接被中间设备断开。你观察 Redis 主从连接,即使业务完全没有写流量,链路上也会每秒有 ACK 包在走。如果你发现master_last_io_seconds_ago大于 10,基本可以断定心跳出了问题。
5.2 主节点的写缓冲和从节点的执行阻塞
命令传播不是“主节点说一句话、从节点马上听到”这么简单。主节点对每个从节点都维护了一个独立的输出缓冲区,写命令先进缓冲区,再由网络层发送。如果从节点消费速度跟不上,这个缓冲区的数据会越积越多,最终可能触达上限:
client-output-buffer-limit replica 256mb 64mb 60这条配置的含义是:对从节点类型的客户端,如果缓冲持续超过 64MB 且在 60 秒内没有降到限制以下,或者硬限制超过 256MB,主节点会直接断开这个从节点。
从节点执行速度跟不上,最常见的原因是从节点上跑了慢命令。很多人觉得从节点只读就不会拖垮复制,但复制过来的命令也要在从节点单线程里执行。如果从节点同时被业务用KEYS *、SMEMBERS大集合、或者BLPOP长时间阻塞了事件循环,它处理复制命令的速度就会严重下降,ACK 发得慢,主节点输出缓冲区不断膨胀,最终把自己断开。这个坑我踩过不止一次,排查思路往往不是看网络,而是看从节点的慢查询日志。
5.3 过期键与驱逐策略:主从不是同时删的
这里有一个很容易被忽视的数据不一致来源:过期键。
主节点删除一个 key,原因无非两种:键到了 TTL 过期,或者内存达到 maxmemory 被淘汰。无论哪种,主节点在真正删除后,都会往复制流里写一条 DEL 命令,通知从节点删除。
但反向不对:从节点不会主动执行过期删除,哪怕它本地时钟已经超过了 key 的 TTL。它只会等待主节点的 DEL 命令到达。这样设计的初衷是为了严格跟随主节点的删除时序,避免主从各自删除导致的数据不一致。代价就是,如果你拿从节点做缓存读取,会读到一些“在主节点那边已经过期、但从节点还没来得及删掉”的旧数据。
另一个坑是:如果你给从节点也单独配了maxmemory,而每个从节点的淘汰策略和主节点不一致,就可能出现从节点自己淘汰了一些主节点还没淘汰的 key,而主节点的淘汰命令到达后,从节点会收到一个“我根本不存在的 key 的 DEL”,执行也没关系。真正需要担心的是从节点因为自己做淘汰,导致数据集的 key 分布和主节点产生偏差。所以生产环境我建议主从节点的 maxmemory 策略保持一致,不要各玩各的。
5.4 延迟度量:offset 差值不是万能的
常见的主从延迟度量方法,是比较主从节点各自的master_repl_offset。差值就是从节点落后的字节数。但它只能反映“复制流消费到哪了”,不等于“业务读到的数据有多旧”。更接近业务延迟的做法,是写一个带时间戳的 key,然后在从节点读出来对比:
# 主节点执行 redis-cli -p 6379 set probe:$(date +%s%N) 1 ex 60 # 从节点执行 redis-cli -p 6380 get probe:$(date +%s%N)当然这属于临时验证手段,不适合长期监控。长期来说,看INFO replication里的 offset 差值和master_last_io_seconds_ago就够用了。
如果业务对一致性很敏感,有两个方案:写后必须立即读到的请求强制走主节点;或者使用WAIT命令主动等待从节点确认:
WAIT 1 1000WAIT后面的参数含义是“等待至少 1 个从节点确认,最长等待 1000 毫秒”,返回确认的从节点数量。它可以把异步复制临时变成“半同步”,但代价是写延迟上升,而且只适用于非集群模式。我这里没有把 WAIT 当默认方案,因为大部分业务用不到,但它是一个一问出来就能加分的知识点。
6. 生产环境的复制治理:配置、监控与排障实录
前面讲了原理,最后落回生产。这里把我实际用下来的一套配置、监控方法和排障案例整理出来。
6.1 一组可以直接抄的复制相关配置
# 从节点 replicaof 10.0.0.5 6379 masterauth your-redis-password replica-read-only yes replica-serve-stale-data no # 主节点 repl-backlog-size 256mb repl-backlog-ttl 3600 repl-diskless-sync yes repl-diskless-sync-delay 5 client-output-buffer-limit replica 512mb 128mb 60 repl-timeout 60几个参数逐个说明:
replica-serve-stale-data no:从节点在断线或全量同步期间拒绝服务旧数据。如果你的从节点本来就是给非核心读业务用的,可以保持yes,但要让业务知道读到的可能是旧数据。我个人倾向在生产核心链路用no,宁可暂时不可用,也不能给脏数据。repl-diskless-sync yes:配合repl-diskless-sync-delay使用,适合从节点较多的场景,能省掉反复写盘的 IO。client-output-buffer-limit replica:我习惯把硬限制从默认的 256MB 提到 512MB,给网络抖动留出更大缓冲,但不会无限制放大,否则从节点卡死时主节点内存会被输出缓冲拖垮。repl-timeout 60:超过 60 秒没有收到从节点的 ACK 或 IO,主节点判定从节点超时断开。
这套配置不是银弹,不同业务要按流量模型调整,但至少不会一上来就被默认参数坑到。
6.2 INFO replication 关键字段解读
在主节点执行INFO replication,输出大概是这样:
# Replication role:master connected_slaves:2 slave0:ip=10.0.0.2,port=6379,state=online,offset=123456,lag=0 slave1:ip=10.0.0.3,port=6379,state=online,offset=123000,lag=1 master_replid:7b2f7c6a9e5d4f3a2b1c0d9e8f7a6b5c4d3e2f1a master_replid2:0000000000000000000000000000000000000000 master_repl_offset:123456 second_repl_offset:-1 repl_backlog_active:1 repl_backlog_size:268435456 repl_backlog_first_byte_offset:100000 repl_backlog_histlen:23456从节点上执行,重点看这些:
role:slave master_host:10.0.0.5 master_port:6379 master_link_status:up master_last_io_seconds_ago:0 master_sync_in_progress:0 slave_read_repl_offset:123456 slave_repl_offset:123456我把这些字段整理成一张速查表,方便排障时对照:
| 字段 | 含义 | 值得警惕的状态 |
|---|---|---|
| role | 当前节点角色 | master/slave 与预期不符 |
| master_link_status | 与主节点的连接状态 | down 说明复制链路断了 |
| master_last_io_seconds_ago | 距上次与主节点通信的秒数 | 大于 10 要查心跳和网络 |
| master_sync_in_progress | 是否正在进行全量同步 | 长期为 1 要查 RDB 大小和带宽 |
| slave_repl_offset | 从节点已消费的复制偏移量 | 长时间不变,查从节点慢命令 |
| master_repl_offset | 主节点当前复制偏移量 | 与从节点差值持续拉大要告警 |
| repl_backlog_histlen | backlog 里还有多少有效数据 | 接近 0,部分重同步随时失效 |
监控告警我建议至少覆盖三件事:master_link_status变 down、主从 offset 差值超过阈值(按字节数,比如落后 64MB 以上)、master_last_io_seconds_ago超过 10 秒。这三条能兜住绝大多数复制异常。
6.3 三个我实际遇见的复制故障
第一个故障:从节点 offset 长时间不动,但连接状态是 up。排查过程:先在从节点INFO replication看到master_last_io_seconds_ago: 35,说明心跳已经断了。再查网络,TCP 连接还在,但数据不通,典型的半开连接。Redis 自身的超时检测没触发,是因为repl-timeout还没到 60 秒。处理方式是调低repl-timeout到 30 秒左右,让断线被更快识别,从而触发重连和增量同步。这个案例给我的教训是:连接状态 up 不代表复制正常,必须结合master_last_io_seconds_ago一起看。
第二个故障:全量同步反复触发。主节点日志里不停出现从节点重连和 FULLRESYNC。排查后发现是主节点输出缓冲打满了从节点:某个从节点在执行耗时巨大的 Lua 脚本,事件循环被卡住几秒钟,ACK 发不回来,主节点输出缓冲膨胀,触发了client-output-buffer-limit replica限制,把从节点断开。从节点重连时,offset 已经落后到 backlog 之外,只能全量。处理方法是把那个 Lua 脚本拆小,同时适度调大输出缓冲区限制,并给从节点单独禁用耗时命令。负重前行的从节点撑不了大脚本,这个平衡要心里有数。
第三个故障:主节点重启后所有从节点同时全量。重启前主节点有四个从节点,重启后复制 ID 变化,四个从节点同时发起 PSYNC,主节点瞬间被 fork 四次,CPU 和内存全部报警。Redis 4.0 之后部分场景可以从持久化的 RDB 里恢复复制 ID,但在我们当时的版本,还是避免不了全量。此后我们的上线规范里加了一条:主节点重启前,先摘流量,再从节点错峰重连,避免集中风暴。
6.4 拓扑设计与落地建议
链式复制是主从架构里一个容易踩坑的设计。A 是主节点,B 是 A 的从节点,C 又是 B 的从节点,拓扑变成了 A → B → C。链路延长,C 的数据延迟天然会比 B 高出一截。如果 B 或 A 之间断线,C 的恢复路径更加复杂,部分重同步的成功率也会下降。我的建议是:能不用链式就不用,让所有从节点直连主节点。只有跨机房网络条件真的很差、或者主节点连接数受限时,才考虑中间层从节点作为转发。
再提一条实战经验:不要在主从复制环境中随意使用FLUSHALL或者DEBUG FLUSHALL。这个命令会在主节点执行,然后被复制到每一个从节点,所有副本数据一扫而光。操作前不仅要 double check,而且要想清楚是不是还要把从节点一个个恢复。Redis 客服从同步删除中恢复的方式是先断开复制、再本地全量重建,过程很痛苦。
如果你刚上手,我建议把一主两从在本地 Docker 环境搭一遍,然后手动做几个动作:停掉从节点几秒再启动,观察INFO replication里的 offset 变化;CONFIG SET repl-backlog-size 1故意把 backlog 调到极小,再断线重连,观察它退化成全量同步的过程;用DEBUG SLEEP模拟主节点阻塞,观察从节点延迟和输出缓冲的连锁反应。做过一轮,你对主从复制的理解会比看十篇文档都深。
我个人的体会是,主从复制真正难的不是配置本身,而是对“异步”这两个字的敬畏。所有高可用方案都在和延迟窗口赛跑,Redis 用 backlog、replid、offset 这套机制把成本尽量压低,但它终究不是强一致系统。所以线上设计时,永远要给复制中断留出退路:核心读请求能降级到主节点、从节点挂掉时读流量能快速切换、全量同步期间资源消耗能被监控提前发现。把最坏的情况想清楚,主从复制才真正成为了可靠的地基,而不是一个看起来能用、一抖就碎的玻璃板。