ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Redis主从复制全解析:全量同步、增量同步与PSYNC协议实践

Redis主从复制全解析:全量同步、增量同步与PSYNC协议实践 1. 先想明白主从模式到底是在解决什么问题有一回我和同事排查一个线上告警从节点断了几十秒重连之后主节点的 CPU 和带宽突然一起飙升日志里反复出现全量数据同步的记录。当时第一反应是网络出了问题但查来查去根子其实出在复制积压缓冲区太小断线期间的增量数据已经保不住了所以被迫退化成一次完整的全量数据同步。那次之后我越来越觉得Redis 主从模式不难搭但要把全量数据同步和增量数据同步这两套机制吃透才谈得上在故障面前不手忙脚乱。主从模式是 Redis 高可用的地基。你可以先把它理解成一个“一个老师带多个学生”的结构主节点负责接收写请求从节点实时跟着主节点的数据走平时主要承担读请求。这样做最直接的好处有三个读写分离把读流量分散到多个从节点上主节点压力变小。数据冗余主节点挂了从节点手里还有一份完整数据能顶上。高可用基础哨兵模式、集群模式底层都依赖主从复制这套数据同步机制。搭建主从的方式也很简单无非两种。一种是配置文件中写好replicaof指令另一种是在命令行动态执行# 在从节点上执行把当前实例变成 master 的从节点 REPLICAOF 10.0.0.11 6379注意命令是REPLICAOF不是老写法SLAVEOF。新版本里虽然SLAVEOF还能用但官方已经不推荐了主从这两个词在 Redis 的运维语义里正逐步被替换掉。很多初学者会有个疑问主从复制到底是推数据还是拉数据答案是“拉”。从节点主动找主节点要数据主节点被动响应。这个设计很关键因为如果是主节点主动推送主节点还得维护一张从节点清单谁掉线了、谁新加入管理起来非常麻烦。改成从节点主动拉取之后主节点只需要在收到请求时干活整个链路的控制权就交给了从节点。另一个容易误解的点是主从模式并不保证强一致性。Redis 的复制默认是异步的主节点执行完写命令后就返回客户端了不会等着所有从节点都确认写完。所以从节点上的数据一定比主节点“慢一点点”这个滞后是常态。理解了这一点你再去看全量同步和增量同步的设计就会有更清晰的主线一切努力都是为了缩小这个滞后窗口同时让断线重连后的追平成本尽可能低。2. 全量同步第一次握手是怎么把整份数据搬过去的2.1 初次建立复制的入口PSYNC ? -1不管是从零搭建一主一从还是从节点数据已经被污染需要重建第一次建立复制时从节点对主节点说的一句话永远是PSYNC ? -1翻译成人话就是“我对自己要复制的这段数据历史一无所知从哪个偏移量开始追我也不知道干脆把我需要的所有数据都发给我吧。”主节点收到这个请求之后如果同意全量同步会回复FULLRESYNC replid offsetreplid是主节点当前复制流的身份标识你可以把它理解成这部“数据连续剧”的剧目编号offset是主节点当前已经写到的字节位置相当于播放进度。拿到这两个信息之后从节点才正式开始全量同步。这里有个值得拆解的细节为什么第一次同步必须用全量而不是把主节点上的每条写命令都重新发给从节点执行一遍原因很朴素——主节点可能已经跑了几个月写操作记录太多了如果从头开始逐条“补作业”网络开销和耗时都不可控。而 RDB 快照把当前所有数据压缩成一个文件一次性发过去虽然过程重但终点明确。2.2 全量同步的完整时序BGSAVE、积压缓冲区、清空装载全量同步不是主节点一个BGSAVE然后把文件丢给从节点就完事儿的中间藏着好几个容易忽略的环节。我把完整流程拆开来看从节点发送PSYNC ? -1主节点决定走全量同步返回FULLRESYNC。主节点执行BGSAVE生成 RDB 快照文件。在BGSAVE执行期间主节点依然对外服务所有新的写命令会被同时记录到复制积压缓冲区replication backlog里。RDB 文件生成完毕主节点把文件发给从节点。从节点收到 RDB 之前会先把自己的旧数据清空然后加载 RDB 文件。RDB 发送结束后主节点再把积压缓冲区里这段时间积攒的写命令补发给从节点。从节点执行完这些补发的命令主从数据追平进入后续的增量同步阶段。注意第 5 步“清空旧数据”不是在拿到 RDB 之前就做的。从节点会先把旧数据留着等 RDB 文件完整接收且校验通过后再清空再加载这样即使 RDB 传坏了至少从节点还能保留原来的数据不至于两边都变成空壳。这里就是全量同步最容易出性能问题的环节了。主节点执行BGSAVE需要 fork 出一个子进程来生成快照。fork 本身很快但如果实例里堆积了大量大 key子进程在生成 RDB 时会触发内存页的复制Copy-On-Write内存和 CPU 都会有一波明显消耗。我在生产环境里见过上百 GB 的实例做全量同步主节点的瞬时 CPU 直接飙到多核打满耗时是以分钟计的。所以 Redis 提供了一些调节阀比如# 开启无盘复制主节点不生成 RDB 文件落盘通过 socket 直接发给从节点 config set repl-diskless-sync yes # 等待多个从节点一起请求延迟几秒后只生成一次 RDB服务所有从节点 config set repl-diskless-sync-delay 5无盘复制适合主节点磁盘 IO 比较吃紧、但网卡带宽还宽裕的场景。如果从节点很多我会建议开启repl-diskless-sync-delay让多个从节点集合到一起共享一次 RDB 生成结果避免每个从节点各 fork 一次。2.3 从节点装载 RDB 时是阻塞的别忽视从节点加载 RDB 文件的过程是阻塞的。也就是说加载期间从节点没法对外提供读服务INFO replication里会看到master_link_status:down同时在日志里出现SYNC with master in progress。这个状态持续多久取决于 RDB 文件的大小和机器的磁盘速度。如果你的业务对从节点的读可用性要求比较高可以关注一下 Redis 7 引入的repl-diskless-load配置# 可选on-empty-db / swapdb降低装载 RDB 期间的阻塞影响 config set repl-diskless-load on-empty-db这个配置的大体思路是从节点先把新 RDB 数据加载到临时内存再切换视图让旧数据尽可能多服务一会儿。不过它也有自己的副作用比如临时内存消耗会翻倍。所以要不要开还得看你的内存余量不能照单全收。再补一个容易踩的坑如果从节点配置了 AOF 持久化那么从节点在完成 RDB 装载之后还会基于新数据集做一次 AOF 重写。换句话说磁盘上会先出现短暂的“旧 AOF 和文件新内容并存”的状态如果你监控的是磁盘空间这段时间要做好心理准备别把磁盘告警阈值卡得太死。3. 增量同步复制偏移量、积压缓冲区与命令传播3.1 复制的“里程表”offset 是怎么记录进度的全量同步结束之后主从之间就进入增量数据同步阶段也叫命令传播阶段。这时候机制变得轻量很多主节点每执行一条写命令除了写自己的内存数据还会把这条命令追加到复制积压缓冲区同时把自身的master_repl_offset往后推进。从节点每收到一条命令并执行完也会把自己这边的slave_repl_offset往前推进。也就是说主节点和从节点各自维护着一个“进度条”。虽然它们都是朝着同一个方向前进但异步复制决定了这两个进度条不可能完全同步一定存在一个差值这个差值就是我前面提到的数据滞后量。你可以把复制流想象成一条两端不断增长的长河offset就是河上的里程标。主节点在上游从节点在下游河水每流一米两边的里程标都会更新。这里的“河水”就是命令字节流每个字节都有自己唯一的偏移量。命令传播阶段其实就两件事主节点往“河”里注入新的命令流并记录master_repl_offset。从节点从“河”里消费命令流执行后上报自己的slave_repl_offset。从节点会每秒上报一次自己的offset通过REPLCONF ACK offset命令告诉主节点“我追到哪儿了”。主节点收到之后不但能算出滞后量还能判断这个从节点是否还活着——如果超过timeout时间没收到 ACK主节点就会认为这个从节点已经断开了。3.2 断线重连后主节点怎么决定从哪接着传从节点断线之后重连时发送的就不再是PSYNC ? -1了而是PSYNC replid offset意思变成“我带着上次的身份和进度来了你看看能不能从断点接着给我传。”主节点收到这个请求后会做两个判断第一这个replid是不是我当前的身份第二这个offset是否还落在我的复制积压缓冲区里。如果两个条件都满足主节点返回CONTINUE然后从offset对应的位置开始把后面的命令流继续发给从节点。这个过程就是增量数据同步也就是官方文档里说的“部分重同步”partial resynchronization。代价非常小网络开销只等于断线期间积攒的那部分命令。如果条件不满足主节点返回FULLRESYNC整个流程退回全量同步。具体来说以下两种情况最常见从节点发送的replid和主节点当前replid对不上。从节点发送的offset太旧了已经不在积压缓冲区里主节点找不到那个位置的数据了。3.3 backlog 大小是增量同步的命门对象积压缓冲区的官方配置名是repl-backlog-size默认只有 1MB。1MB 听着不小但放在写流量大的主节点上可能不到一分钟就被环形缓冲区覆盖掉了。为什么说是环形缓冲区因为它的存储区域是固定大小循环使用的。新命令不断写入最老的命令就被顶掉。主节点只会保留最近 N 字节的命令流。举个例子复制积压缓冲区当前范围是 offset 10000 到 11024。从节点重连后上报 offset 10500落在这个区间内可以增量同步。如果从节点上报 offset 9900说明它需要的命令已经有一部分被顶掉了只能全量同步。这个例子能直观看出backlog 越大能容忍的断线时间越长增量同步的成功率越高。生产环境里怎么定这个大小我一般会先做个粗估算repl-backlog-size 主节点每秒写入量(bytes) x 预期最长断线时间(秒)比如主节点平均每秒产生 2MB 写命令我希望从节点断线 120 秒后重连还能走增量同步那就得给到 256MB 以上。注意这里算的是写命令的字节量不是业务写入的数据量。一条更新 100 字节字段的命令在复制流里可能也要占几十到上百字节所以用粗略值乘个 1.5 系数更稳妥。设置方式config set repl-backlog-size 268435456如果实例已经运行一段时间了Redis 会直接从当前offset开始填充新的环形区间不需要重启。生效后可以通过INFO replication里的repl_backlog_active和repl_backlog_histlen确认。我踩过的一个真实教训是某业务从节点数量有七八个平时增量同步都正常但只要某个从节点所在的网络偶尔抖动超过一两分钟重连后就必然是FULLRESYNC。后来把repl-backlog-size从默认 1MB 调到 128MB复制风暴立刻消失。查日志的时候看到的关键字一般是Trying a partial resynchronization (request ...) Full resync requested前面的说明在尝试增量后面说明失败了、退化成全量。如果你在日志里看到这两种状态交替出现第一反应就应该是去看 backlog 大小而不是去怀疑网络。4. PSYNC 协议演进断点续传能力是怎样一步步变强的4.1 SYNC 时代的无奈每次重连都是一次全量现在大家接触 Redis 基本都是 4.0 以上版本PSYNC 已经是默认行为。但如果你翻过老代码或者看过更早的运维文档会发现最初的复制协议只有一个命令SYNC。SYNC的逻辑非常简单粗暴从节点一发SYNC主节点就BGSAVE生成 RDB 发过去。在这个协议下从节点只要断开重连不管断了几秒、缺了多少数据主节点都得重新生成一份完整快照再全量发一遍。那个时期的 Redis从节点数量稍微多一点只要赶上一次网络抖动就可能产生连锁反应每个从节点都触发一次全量同步每个全量同步都会让主节点 fork 并发送 RDB主节点 CPU、内存、带宽同时被打满。可以说SYNC协议是把“简单”写在了脸上代价全留给了运维。4.2 PSYNC 与 replid第一代断点续传是怎么实现的后来 Redis 引入了PSYNC核心变化就是给复制流增加了两个“坐标”replid和offset。从节点可以在断线重连时带上自己上次记住的坐标主节点只要发现自己手里还留着这段数据就能从断点继续传不必每次都全量。这背后的实现就是我前面讲的复制积压缓冲区。可以说PSYNC协议的价值和 backlog 是绑定的没有 backlog 就没有断点续传。平时用INFO replication能看到主节点上这么一行master_replid:8e0d1f3c...这就是当前复制流的身份 ID。只要主节点维护的这个 ID 不变从节点带着旧的replid和offset回来就有机会继续增量同步。4.3 PSYNC2主从切换后不再强制全量但第一代 PSYNC 有一个明显的硬伤一旦发生主从切换原本的从节点被提升为新主节点它自己的replid是全新的原来那批从节点带着旧主节点的replid再连过来两边身份对不上统统退化为全量同步。设想一个场景A 是主节点B 和 C 是 A 的从节点。某天 A 宕机哨兵把 B 提升为新主节点。C 节点还在正常运行它手里的数据就差 A 挂掉那一刻没来得及同步的一小段而已。理论上B 和 C 都从 A 复制了一份几乎相同的数据C 连到 B 时完全有资格做增量同步。但旧协议不认账C 会被迫从头开始全量拉一次 B 的数据浪费大量带宽。第二代 PSYNC常被称为 PSYNC2解决的就是这个问题。它引入了一个replid2的概念当一个从节点被提升为主节点时它会保留自己作为从节点时期的主节点replid把它存到replid2里。C 节点再连过来时B 会检查你的replid虽然跟我不一样但和我保存的replid2匹配而且你的offset还在我的 backlog 范围内那就允许增量同步。这样一来主从切换后的大部分从节点都能继续走增量数据同步只有极少情况下才需要全量。这在大型集群里的收益非常明显少了全量同步等于少了 N 次 RDB 文件传输主节点的 fork 压力和网络压力都大大缓解。可以用一张小表把三代协议的区别说清楚能力SYNCPSYNCPSYNC2初次全量同步支持支持支持断线重连续传不支持支持支持主从切换后续传不支持不支持支持需要 backlog不需要需要需要5. 主从复制链路中的坑与排查经验5.1 反反复复的全量同步先别怪网络如果你的从节点日志里一直出现“先尝试部分重同步、紧接着又变成全量同步”的状态第一排查项一定是 backlog 容量。环型缓冲区的特性决定了它只认“最近发过多少字节的命令”不认“你断线了多长时间”。哪怕只断了几秒只要这几秒内主节点的写命令字节量超过了 backlog 的上限重连照样得全量。排查步骤也不复杂# 主节点上查看 backlog 实际使用情况 redis-cli info replication | grep -E backlog|offset重点看几个字段master_repl_offset主节点当前写入进度。slave_repl_offset从节点当前进度。repl_backlog_histlenbacklog 里目前保存的有效命令流长度。如果你的repl_backlog_histlen已经等于repl-backlog-size说明环形缓冲已经被写满里面的内容时刻在滚动淘汰。这种状态下断线重连增量同步的成功率非常低。解决办法也很直接把 backlog 调大同时结合业务写流量估算出合理值。调整完不是结束还应该盯两轮第一轮观察断线重连的日志确认出现的是CONTINUE而不是FULLRESYNC第二轮观察主节点 CPU确认没有因为频繁全量同步而持续走高。5.2 过期 Key 不会在从节点上主动删除这是个非常经典的主从一致性细节。在主从复制中从节点默认不会主动删除过期的 key。听起来是不是反直觉如果从节点自己能判断 key 过期直接删掉不就行了为什么还要等主节点通知原因在于两个实例的“时间感”不完全一致如果在同一秒内主节点认为 key 过期、从节点认为还没过期两边删除时机不一致就会导致数据分叉。所以 Redis 的策略是过期删除的决策权只在主节点。主节点的处理方式有两类惰性删除客户端请求某个 key 时发现已经过期直接删除并返回空。定期删除后台周期扫描一批 key删除其中过期的。不管是哪种方式删除主节点都会把删除行为转成一条DEL命令通过复制流转发给从节点。从节点收到这条DEL后才会真正把本地这个 key 删掉。这里有个副作用从节点在收到主节点的删除命令之前如果被读到了那个逻辑上已经过期的 key会发生什么按我的实测经验Redis 从节点会直接把这个 key 当不存在处理返回空结果但底层数据仍然占着内存。也就是说从节点会“看不到”这个 key但内存迟迟不释放。如果你发现从节点的内存占用长时间高于预期而大 key 扫描也没扫出什么东西可以怀疑是过期 key 堆积。解决办法并不复杂检查主从复制是否正常确认没有积压很久的延迟因为从节点必须追平主节点、消化掉积压的 DEL 命令才会把这些过期 key 清空。另外要注意从节点一定要开启只读模式config set replica-read-only yes如果有人在从节点上手动写入或修改数据主从数据立刻就会分叉而且很难追回来。这类问题往往比 backlog 问题更隐蔽因为业务表现只是一些偶发的数据“对不上”。5.3 复制风暴与从节点级联控制所谓复制风暴是指短时间内大量从节点同时触发全量同步导致主节点资源被冲垮。典型诱因有两种一个是主节点重启或切换导致所有从节点同时掉线再重连另一个是从节点全网段重启全部在同一时刻重连主节点。应对思路有几个我按优先级说调大 backlog让尽量多的断线重连走增量同步。这是最便宜、最基础的措施。错峰重连靠运维工具控制从节点分批重启不要一把梭。用链式复制即主-从-从结构。中间的从节点既当从节点又当下一级的主节点减轻顶层主节点的压力。开启无盘复制并设置 delay多个从节点可以共享一次 RDB 生成结果。我自己的倾向是从节点数量超过 5 个的架构链式复制是值得认真考虑的尤其是跨机房场景。跨机房带宽本来就紧张如果每台从节点都直接从主节点拉全量 RDB那点带宽瞬间就会被吃光。中间建一层“中继从节点”让它在某个机房内再向下一层复制能显著降低跨机房的复制流量。需要注意链式复制会让最下端从节点的数据滞后时间变大因为命令要经过两次转发。这属于典型的时间换带宽是不是划算得结合业务对读取延迟的要求来权衡。5.4 平时怎么判断主从状态是健康的我养成了一个习惯每次接手一套 Redis 环境第一件事先把INFO replication的输出完整看一遍。重点关注这么几项role是主还是从别搞反了。master_link_status:up复制链路是否正常。状态不是 up 就得马上看日志。master_sync_in_progress:0是否正在全量同步。长时间为 1 说明有大 RDB 在传。slave_repl_offset和主节点的master_repl_offset两者差值是不是在合理范围内长期偏大就说明从节点追不上。master_last_io_seconds_ago最后一次和主节点通信是在多久之前超过timeout值就要警惕掉线。这里还有一个经验之谈从节点追不上主节点不一定是网络慢也可能是主节点发给它的命令本身就重。比如一条命令操作大集合的几百个元素复制流里这条命令的体积很大从节点执行时又会触发内存分配和哈希表扩容耗时远超主节点。这种情况下光是调大 backlog 没用还得优化业务侧的写命令模式比如把大集合的批量写拆开。最后分享一条我个人反复验证过的经验当复制链路出现诡异问题时不要急着改配置。先把redis-cli info replication的快照保存一份再把主从两端的日志级别临时调到 debug复现一次问题把MASTER - REPLICA sync相关日志完整收集起来。八成以上的复制问题从这几行日志里就能看出真正的走向剩下的两成才轮到网络抓包和内核参数排查。这套笨办法帮我在好几次故障里省下了大量半夜时间也建议你下次遇到类似问题时先试试。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表