1. 从一道经典面试题看主从复制的核心价值
1.1 面试官到底在考察什么
"每日面试题分享209:Redis的主从复制原理是什么?"这个标题看着简单,实际上是我这些年面试里最喜欢用的开场题之一。原因很简单:问题本身只有一句话,但它能快速区分出候选人停留在"背过八股"还是"真正在线上用过"。
通常我会听到三种层次的说法。
第一种:主从复制就是搞个备机,master挂了从机顶上,顺便分担读流量。这个回答只能算及格,说明候选人知道有这回事,但没深入过同步协议。
第二种:能说出slaveof命令、全量同步RDB、增量同步命令传播、断线重连重新同步这些关键词。这种候选人大概率看过Redis官方文档或者几篇源码解析,算是有备而来。
第三种:能讲清楚PSYNC协议的完整流程,知道主从各维护了什么状态(replid、offset、backlog),能解释为什么网络抖动时部分重同步能兜住,还能结合自己的业务场景说出"如果主节点磁盘慢了,RDB生成会不会阻塞主线程""repl-backlog设置多大才不会频繁触发全量同步"。这种候选人我是愿意当场给通过信号的。
所以这道题表面问原理,实际考察的是一个工程师对分布式系统数据复制链路有没有系统性的认知,以及有没有踩过生产环境的坑。
1.2 主从复制在Redis技术体系里的位置
在讲实现原理之前,先把主从复制放在整个Redis架构里看明白。Redis是单进程内存数据库,一台机器扛所有读写请求,坏掉就彻底不可用,这个单点风险是任何业务都扛不住的。主从复制解决的就是这个问题:一台master负责写,多台replica副本同步数据,承担读请求或者作为故障切换的候选节点。
而哨兵和集群这两套高可用方案,都没有脱离主从复制的地基。哨兵做的事情是监控master心跳,挂了之后从replica里选一个新master,这个"选主"的过程依赖的就是replica已经通过复制拿到了数据;集群模式里每个分片其实也是一个小型的master-replica结构。所以可以说:搞懂主从复制,是理解Redis高可用的必经之路。
这里有个很关键的认知要提前立住:Redis的主从复制是异步的。master处理完一条写命令返回客户端OK,不代表replica已经落盘。这带来的直接后果是,任何时刻都可能存在一个"复制延迟窗口",master和replica的数据在严格意义上是有短暂偏差的。搞不清楚这一点,后面排查数据不一致问题时就会走弯路。
2. 一次完整的全量同步:从快照到命令传播的链路拆解
2.1 一条SLAVEOF命令触发了什么
先看最简单的场景:一台全新的replica第一次连上master。无论你是执行SLAVEOF <master-ip> <master-port>还是REPLICAOF,本质上都是让replica发起一次同步请求。
replica连上master之后,会发送一条PSYNC命令,格式大致是PSYNC ? -1,意思是"我没有历史数据,请求全量同步"。master拿到这条请求后,发现replica是第一次来,没有对应replid,于是进入全量同步模式。
整个链路按时间顺序是固定的,我用一个真实运维视角给你拆。
首先,master开启一个后台子进程执行BGSAVE,把当前内存里的全部数据快照成一份RDB文件。为什么不用命令行传输?因为Redis这种内存库,一个实例动辄几十GB,如果通过逐条命令把写入操作回放给replica,网络包数量会爆炸,速度也极慢。RDB是经过压缩的二进制快照,体积小、加载快,是跨节点复制全量数据的最高效方式。
其次,在BGSAVE执行期间,master可能仍然在持续接到写命令。这些新写入的命令不能被丢,得另外存到一份复制缓冲区里,等RDB传输完成后再发给replica。所以这里其实有两条并行的数据通道:一条走的是历史数据快照,另一条走的是快照生成后的增量命令。
然后,master把生成的RDB文件通过socket直接推给replica。replica收到RDB文件后,会先把本地现有的数据清空,然后加载这个RDB文件。这里有个容易被忽略的点:replica在加载RDB的这段时间里是阻塞的,对外响应不了任何读写请求。实例越大,加载时间越长,这个窗口越明显。
最后,RDB加载完成后,master把之前积攒在缓冲区里的增量命令一条条发给replica,并进入持续的命令传播状态。之后master每执行一条写命令,都会以某种协议格式转发给所有连着的replica。至此一次全量同步才算真正结束。
下表是整个过程中的关键阶段和状态变化:
| 阶段 | master侧动作 | replica侧动作 | 可访问性 |
|---|---|---|---|
| 连接建立 | 等待PSYNC命令 | 发起PSYNC请求 | 均正常 |
| RDB生成 | BGSAVE子进程写快照 | 空闲等待 | 均正常 |
| RDB传输 | 推送RDB文件 | 接收并暂存文件 | 均正常 |
| RDB加载 | 持续缓冲增量命令 | 清空旧数据并加载RDB | replica阻塞 |
| 命令补发 | 发送缓冲的增量命令 | 执行补发命令 | 均正常 |
| 稳态复制 | 实时传播写命令 | 实时执行写命令 | 均正常 |
2.2 为什么RDB加载前要清空旧数据
刚刚提到replica收到全量RDB后会清空本地数据,这一步很多新人会问:能不能不清空直接合并?答案是不能。
RDB是一个时间点上的完整快照,它假设接收方本身没有数据,加载完成后replica的状态应该和master在那个时间点完全一致。如果replica本地还残留着旧数据,新旧数据混在一起就变成了"脏数据",而主从复制最核心的语义就是严格保持一致。
这也是为什么我在生产环境里强烈不建议用临时slave去做数据分析或者报表计算。你如果一边让这个replica挂着一个全量同步,一边又往里面写业务数据,一旦触发重新全量同步,它本地的数据会被无情清空。这个坑我见过不止一次,排查起来非常难受。
2.3 全量同步的隐藏成本
全量同步看似只是传一份文件,实际成本很高,有几个隐性开销必须提前意识到。
第一是fork的开销。BGSAVE依赖操作系统的fork机制创建子进程来写RDB,如果实例内存占用非常大,fork本身在极端情况下会造成主线程短暂停顿。Red Hat系服务器上如果开启了transparent huge pages,停顿时间可能被放大,这是很多线上"莫名卡顿"的元凶之一。
第二是网络带宽的占用。一个100GB的实例做全量同步,RDB传输要占用大量带宽,如果master和replica在同一个机房还好说,跨地域复制的场景下,一次全量同步就可能把链路打满,影响正常业务请求。
第三是replica加载期间的不可用窗口。实例越大,这个窗口越长,期间连接上来的读请求会全部超时。所以业务上对replica的可用性有严格要求的团队,通常会把replica的maxmemory控制在合理范围,或者提前规划好主从切换策略,而不是等到全量同步风暴发生了再救火。
3. 增量同步与部分重同步:PSYNC为什么能救场
3.1 老版SYNC协议的笨拙之处
既然说了全量同步成本高,那必然要有一个机制来避免高频触发的全量同步。Redis 2.8之前的做法是:replica断线重连后,无条件做一次全量同步。这在早期实例小、场景单一的时代勉强能忍,但放在现在动辄几十GB的实例上就完全不可接受了。
举个具体场景:master和replica之间网络抖动了一下,连接断开了两秒钟,恢复后replica重新连上master。按照老协议,这一步要重新生成一份RDB全量快照重新传一遍。稍微频繁一点的网络抖动,就会导致主从之间反复打满带宽,master的CPU也被BGSAVE反复消耗,连带着正常的读写请求都受影响。
所以老协议的问题本质上是:它没有任何记忆能力,不知道自己跟master之间的同步进度走到了哪个字节,只能靠"全量重来"解决问题。
3.2 PSYNC记住了什么
Redis 2.8引入了PSYNC协议,核心思路是给复制流增加进度标记,让replica重连时能告诉master"我同步到哪个位置了,你只把缺的部分补给我就行"。
协议层面有三个关键信息:master_replid(以下简称replid)、master_repl_offset(复制偏移量)和repl_backlog(复制积压缓冲区)。
replid可以理解成master的身份标识,每次master启动都会生成一个随机ID。replica在第一次全量同步时把这个ID记下来,重连时带着它去找master,master要判断"你是不是曾经从我这儿同步过数据"。如果replica带着一个完全陌生的replid来,master会直接判定需要全量同步。
复制偏移量是同步进度的尺子。master每向replica传播一个字节的命令数据,偏移量就增加多少;replica每接收执行一个字节,也会记录自己的偏移量。只要两边偏移量一致,说明数据是完全同步的。
repl_backlog是master上维护的一块环形缓冲区,默认大小只有1MB。master会把最近复制给replica的命令数据保存在这个缓冲区里,当replica断线重连时,只需要检查"它要的偏移量所对应的数据还在不在缓冲区里"。如果在,就只把这段缺失的数据补发过去,也就是部分重同步;如果不在,说明断线太久或者数据量太大,缓冲区的数据已经被覆盖了,那就只能老老实实回退到全量同步。
3.3 断线重连的真实流程
模拟一个真实场景你就能完全理解了。
某天下午replica和master断线了,断线持续了30秒。这30秒里master还在处理写请求,比如一共接收了2MB的新命令数据。这些数据既有实时转发给其他在线replica的,也同步写入了自己的repl_backlog。
replica恢复网络后重新连上master,发出PSYNC <之前记录的replid> <断线前的偏移量>。master拿到请求,查出replica的偏移量是100000,而自己现在的偏移量是102000,中间的2000字节数据正是replica缺失的部分。接着master去repl_backlog里查一下:这个偏移量对应的数据还在不在。如果还在,master就回复+CONTINUE,然后把从100000到102000这一段的命令数据发给replica。
这2000字节的补发,走的是普通TCP socket,秒级完成,对业务几乎无感。这就是部分重同步的价值。
反过来,如果replica断线了整整一天,期间master产生了大量新数据,早就把1MB的repl_backlog环形缓冲区覆盖了好几圈。master发现replica要的偏移量对应的数据已经被覆盖了,这种情况只能回复+FULLRESYNC,回到全量同步流程。
3.4 repl_backlog_size到底该配多大
这是面试里我把候选人难住的进阶题,也是生产环境里真正会踩的坑。很多人配主从只改了个replicaof就完事了,repl_backlog默认1MB,从来不动。
怎么算合理值?思路很简单:你的replica业务上允许断线多久,这段时间内master会产生多少写数据量,缓冲区起码要能装下这些数据。
我一般用这个公式估算:
repl_backlog_size = master每秒钟写入字节数 × 允许断线重连的最大秒数 × 2乘以2是安全系数,因为环形缓冲区在覆盖边界上会有额外损耗,而且网络传输本身也需要时间。举个例子:某系统高峰期每秒写入1MB数据,期望容忍replica断线1分钟,那么积压缓冲区至少要配到1MB × 60 × 2 = 120MB。如果你只留默认的1MB,断线超过1秒就触发全量同步,实际上等于没配增量能力。
这里要特别提醒一句:repl_backlog本质上是内存,配得太大了也会白白占用master和replica的内存,所以到底取多少要根据业务写量和网络质量来做权衡,不要无脑拉满。
4. 主从切换、数据一致性窗口与常见深坑
4.1 异步复制下的数据丢失窗口
讲完同步协议,回到最现实的问题:主从架构在故障切换时到底会丢多少数据?
因为复制是异步的,所以存在一个基本事实:master在宕机前的最后几秒甚至几毫秒内收到的写请求,可能还没来得及传播到replica。这时候如果哨兵直接把某个replica提升为新的master,这部分数据就永远丢了。
我在真实业务里见过的最典型场景是:订单系统的主节点突然宕机,哨兵快速完成了故障切换,看起来整个服务只中断了几秒。但事后核对数据时发现,宕机前最后写入的几十个订单在库里怎么也查不到。这就是异步复制丢数据的代价。
对于不能接受数据丢失的业务,Redis本身提供了两个补救手段。
一是WAIT命令。master在处理完一条写命令后,可以显式执行WAIT <numreplicas> <timeout>,等待指定数量的replica确认已经收到这条命令再返回客户端。代价是本来纯异步的操作变成了等待同步确认,写延迟会明显上升,需要业务方自己权衡。
二是min-replicas-to-write和min-replicas-max-lag这两个参数。前者规定了master至少要有多少个在线replica才接受写请求,后者规定了replica的最大允许复制延迟秒数。当主从之间延迟过大、或者可用replica数量不足时,master会直接拒绝写入。这套机制的本质是"牺牲可用性换一致性",防止master变成一个快要和所有副本脱节的孤岛,继续写入大量注定要丢失的数据。
4.2 主节点重启引发雪崩式全量同步
这是我遇到过的最让人头疼的线上问题,必须单拎出来讲。
现象是这样的:某天晚上master因为内存问题重启了,重启速度其实很快,几十秒就恢复了。但恢复之后所有replica几乎同时断连又重连,然后集群里同时出现了多份全量RDB在传输,master的CPU和带宽瞬间打满,整个集群基本处于瘫痪状态。
根因在于replid的变化。Redis启动时会生成全新的replid,而replica重连时带着的是重启前记住的旧replid。master一看,对不上号,只能回退到全量同步。一台master配了三台replica,那就是三份全量RDB几乎同时开始生成和传输。
Redis 4.0之后引入了PSYNC2机制来做优化:master的复制进度和replid会被持久化到RDB或者AOF文件里,重启后如果数据文件还在,能够恢复到重启前的replid,从而让replica只做部分重同步。但前提是数据能被恢复,如果master配置了关掉持久化,那重启后就是空库,replid自然也是全新的,全量同步跑不掉。
所以我的建议很明确:线上master绝不允许关闭持久化。哪怕你有一堆replica兜底,也不要天真地认为replica能帮你把数据救回来。主从复制保证的是"已传播数据的一致性",不是"落盘数据的高可用"。
另外,生产环境里应该尽量让哨兵来执行主从切换,而不是手动重启master。哨兵切换时,replica是主动联系新的master做同步,不会出现"master重启后replica一拥而上"的风暴场景,因为旧的master此刻已经降级为replica,它会自己放下面子去跟新master对齐数据。
4.3 几个我实际踩过的坑
第一个坑是fork阻塞。某项目里Redis实例内存占到20GB,平时运行稳定,但每次配置了定时BGSAVE备份,系统就会出现几秒钟的延迟毛刺。排查了很久,最后发现是内核的transparent huge pages特性导致的fork性能退化。解决方案是关闭THP,把内存页大小固定为4KB。这个坑如果你不做压测很难发现,属于操作系统层面和Redis的经典摩擦点。
第二个坑是replica的maxmemory配置。有人习惯给replica设置比master更小的maxmemory,想着省点内存,结果replica在复制过程中不断触发内存淘汰策略,导致主从数据悄悄不一致。复制机制本身不会校验每条数据的内容,只有主从偏移量对得上,它就认为"已经同步完成了"。应对办法是给replica设置和master一致甚至更大的maxmemory,并开启replica-ignore-maxmemory或者干脆用noeviction演进策略。
第三个坑是replica-serve-stale-data参数。默认情况下replica和master断连超过一定时间后,replica仍然可以对外提供读服务,但此时它读到的是过期数据。对一致性敏感的业务,比如库存查询、账户余额展示,一旦拿到这种过期数据,后果是很严重的。生产上要根据业务容忍度决定是否把这个参数设为no,宁可直接报错,也不要提供错误数据。
5. 生产环境下的配置建议与排查思路
5.1 一份可以直接照抄的配置清单
把上面讨论的所有问题落成一份配置参考,在你搭建主从架构时可以直接对着检查:
| 配置项 | 默认值 | 生产建议 | 原因 |
|---|---|---|---|
| replicaof | 空 | 指向master的IP和端口 | 声明主从关系 |
| repl-backlog-size | 1MB | 按写入速率×断线容忍时间×2 | 避免频繁全量同步 |
| repl-backlog-ttl | 3600秒 | 根据实际断线恢复时间调整 | 超过时间后释放backlog内存 |
| repl-diskless-sync | no | 大实例建议yes | 避免RDB落盘再读取的双重IO |
| min-replicas-to-write | 0 | 按业务一致性要求开启 | 防止master写孤岛 |
| min-replicas-max-lag | 10秒 | 与上面配套设置 | 定义replica可容忍延迟 |
| replica-read-only | yes | 保持yes | 防止replica被误写产生脏数据 |
| replica-priority | 100 | 按节点能力调整 | 影响哨兵选择新master的优先级 |
| replica-serve-stale-data | yes | 一致性敏感业务设no | 断连时避免服务过期数据 |
5.2 排查主从同步故障的命令与指标
遇到主从复制有问题,第一站永远是INFO replication,不要凭感觉猜。重点看这几个字段:master_link_status显示up还是down,master_repl_offset和slave_repl_offset之间的差距就是实时复制延迟,如果两者长期相差越来越大,说明replica消费能力跟不上master的生产速度。
如果主从连接频繁中断,建议去master上执行CLIENT LIST,观察连接replica的那个客户端连接是不是经常处于"SYNC"状态,以及它的omem输出缓冲区是不是一直在涨。输出缓冲区满了,master会直接断开这个replica连接,造成一个"同步到一半又断了"的怪圈。
对于主从延迟问题,我习惯写一个简单的巡检脚本,定时对比master和每个replica的offset,差距超过阈值就告警。这个脚本逻辑很简单,没有太多技术含量,但比出了事再排查要省心太多。
5.3 一个值得养成的日常习惯
最后说一个日常习惯:每次改动主从架构,先在小流量节点上演练,再推全量。我见过太多团队在master上改一个参数,顺手就把线上所有节点一起改了,结果某个replica的backlog配置没跟上,第二天就触发了一轮全量同步风暴。
主从复制原理讲起来无非那几条链路,但真正决定一个团队能不能把这套架构玩稳的,往往是对这些细节的敬畏程度。熟悉复制协议、算清楚backlog大小、管理好故障切换窗口,这套基本功比记多少面试题都值钱。