尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Redis高可用基石:主从复制核心机制与排障实战

Redis高可用基石:主从复制核心机制与排障实战 要说 Redis 高可用我向来主张先把主从复制这块硬骨头啃下来再去看哨兵和集群。原因很简单不管是哨兵做故障转移还是集群里的分片容灾底层跑的都是同一套主从复制逻辑。你如果只会往配置文件里写一行 replicaof那遇到同步延迟、断线后疯狂全量复制、主库切换后从库数据对不齐这类问题基本就是抓瞎。这篇是 Redis 高可用进阶的第一篇我就把主从复制的核心机制从头拆到尾从复制ID和偏移量这两个地基概念到全量同步、增量续传的完整链路再到日常运维里最值得关注的参数和排障手法。1. 复制的本质和定位——高可用进阶为什么要先啃主从复制1.1 单机 Redis 的三个软肋复制逐一化解先搞清楚一件事主从复制解决的是什么最直观的是单点故障。我早期接手过一套线上 Redis单节点部署某天物理机宕机整个业务晚高峰直接瘫痪。等运维把机器恢复、重新加载 RDB时间根本不是自己说了算好几个 G 的快照加载加上后面追日志用户体验已经被打得稀烂。所以“数据必须有热副本”是主从复制存在的第一理由。其次是读压力。一台 Redis 哪怕纯内存在单线程模型下复杂命令一多CPU 还是会成为瓶颈。把一部分读流量分流到从库是成本最低的横向扩展方式。注意我说的是读写入依然集中在主库。主从复制不是一个分布式分片方案它解决的是可用性和读扩展不是容量扩展。容量要上去得靠集群这个后面再说。第三也是最容易被忽略的一点主从复制是 Redis 所有高可用方案的底座。你去看哨兵它的核心工作就是发现主库故障后把一个数据最新的从库提升为新主库你去看集群每个分片都是一个主节点带若干从节点的复制组。底层的同步机制完全相同。所以说白了搞不懂主从复制的人看哨兵和集群只能看到 API看不到内核。1.2 复制在 Redis 高可用演进路线里的位置Redis 的高可用演进其实是一条很清晰的路线单机 - 主从复制 - 哨兵 - 集群。每一步都不是凭空出现的替代关系而是向上叠加。主从复制提供了“一台机器倒下还有副本能顶上”的基本能力哨兵把“顶上”这个动作自动化不再需要人工去改从库的 replicaof集群则进一步把数据分片和多副本组合起来解决超大容量下的高可用。理解了这条线你就会明白为什么我把主从复制放在第一篇。它不是最炫的技术但它是后面所有方案的地基。地基不稳后面全是空中楼阁。2. 复制的地基复制 ID 与复制偏移量2.1 复制 ID数据历史的身份证很多第一次接触 Redis 内部机制的人都会有一个疑问主从之间用 IP 和端口来标识关系不就行了吗为什么还要搞一个复制 IDreplidIP 端口是网络层面的身份它只能告诉你“我连的是哪台机器”不能告诉你“这台机器上的数据属于哪段历史”。复制 ID 是 40 字节十六进制字符串主库每次启动都会生成一个全新的 replid。从库首次全量同步成功后会把主库的 replid 继承下来。这相当于主从之间确认了“我们来自同一条数据时间线”。一旦主库重启replid 变了旧从库带着旧 replid 来请求同步主库一看身份对不上只能走全量复制。为什么要设计得这么严格因为 Redis 的复制本质是命令流的重放命令执行顺序错乱一点数据就分叉了。复制 ID 就是用来判断“两个节点是否在同一个数据历史上”的准绳。这里还牵出一个关键概念replid2。它是主库保留的“上一个 replid”。比如从库 A 被提升为新主库时它会把自己的旧 replid 保留为 replid2。原来同一批的兄弟从库带着旧 replid 请求同步时新主库能认出它们继续增量走而不是一股脑全量。这就是 PSYNC 2.0 让人印象最深的改进后面断线续传部分我还会细讲。2.2 复制偏移量逐字节对齐的游标复制偏移量offset是一个不断累加的计数器单位是字节。主库每发送一段复制流master_repl_offset 就累加从库每执行完一段复制流slave_repl_offset 也跟着累加。主从各自维护自己的 offset最终它们应该收敛到同一个值。你可以把 offset 想象成两本账本上的页码。主库写完了第 100 页从库只记到第 98 页说明还有两页内容没追上。通过差值我们就能精确判断同步延迟的大小。这也是从库做心跳上报的核心数据从库每秒都会把自己的 offset 通过 REPLCONF ACK 报告给主库主库据此更新 lag 字段。有一点值得说清楚offset 不是命令条数而是字节数。一条 SET 命令可能就几十字节一条批量写入或者一个超大 key 的写入可能一下就推进几千字节。所以用 offset 的差距判断延迟比看命令数更准确。2.3 握手协议PSYNC 的两种裁决从库发起同步时核心命令是 PSYNC格式是PSYNC replid offset如果从库是第一次连接或者不记得主库的 replid 了它会发PSYNC ? -1主库收到后会做两个判断从库请求的 replid 是不是我的或者我的 replid2以及请求的 offset 是否还在我的复制积压缓冲区有效区间内。两个条件都满足返回 CONTINUE进入增量续传任何一个不满足就返回 FULLRESYNC 走进全量同步。这一段是整个复制机制的“入口裁决”也是后续所有流程的分岔口。理解它下面再说全量和增量就顺理成章了。补充一点历史Redis 2.8 之前只有 SYNC 命令断线重连一律全量同步代价极大2.8 引入 PSYNC 支持部分重同步4.0 又做了 PSYNC 2.0专门解决主从切换后兄弟从库被迫全量的痛点。3. 全量复制从零到一完整链路拆解3.1 首次同步的十个关键环节第一次同步最直观也最复杂我把完整链路拆成十个环节从库配置 replicaof 记录主库地址主动发起 TCP 连接连接建立后从库发送 PING 确认主库存活如果主库开启了密码认证从库发送 AUTH 完成身份校验从库发送携带 replid 和 offset 的 PSYNC 命令主库发现无法增量续传返回 FULLRESYNC携带自己的 replid 和最新 offset主库触发 BGSAVEfork 出子进程生成 RDB 快照RDB 生成期间主库继续服务新来的写命令不会进 RDB而是同时写入复制积压缓冲区和当前从库的发送缓冲区主库把 RDB 文件发送给从库从库接收完 RDB 后先清空旧数据再加载 RDB 到内存主库把积压的写命令继续发给从库从库执行完毕后进入 online 状态转为持续增量复制。这十个环节看起来不复杂但每一个都藏着一个“为什么”。我挑几个关键的说。第 6 步为什么用 fork 子进程而不是在主线程直接生成快照因为 Redis 是单线程服务模型如果主线程一边处理客户端请求一边写 RDB期间的停顿会让延迟飙升。fork 子进程的好处是子进程和主进程共享同一份内存快照子进程专注生成 RDB主进程继续处理命令。代价是 fork 瞬间要复制页表而且后续主进程每写入一个内存页都会触发写时复制COW内存占用会临时上涨。第 9 步从库为什么要先清空旧数据这一点很多新手想不通。原因在于复制是全量覆盖为了保证从库和主库的数据历史严格一致旧数据必须让位给主库发来的 RDB。这也是为什么从库在加载 RDB 期间对外服务能力会下降甚至暂时不可用它正在主动重构自己的数据集。3.2 为什么同步期间的写入不会丢有一个非常容易困惑的点RDB 是某个时间点的静态快照那在快照生成之后、从库加载完成之前主库上的新写入怎么办答案是这些写入不会被丢全量同步不是“RDB 一个文件就搞定”的事而是“RDB 积压写命令”的合力。RDB 生成期间主库会同时做两个动作一是把复制流写进全局的 repl_backlog 复制积压缓冲区这是为了将来其他从库断线重连时能做增量续传二是写入当前正在同步的从库自己的输出缓冲区这是为了在这个从库加载完 RDB 之后立刻把缺失的命令补给它。等从库把 RDB 加载完主库就开始吐缓冲区里积压的命令从库逐条执行直到 offset 彻底对齐。这个设计让我想到一个词接力。RDB 负责把过去的全量状态搬过去积压命令负责把搬的过程中新发生的事补上。两者合在一起才能保证最终一致。3.3 主库资源消耗与无盘复制全量同步对主库的资源消耗是实打实的。首先是 fork 子进程带来的内存暴涨内存越大的实例越明显。我给你一个具体感觉一台 8G 内存、写入压力不小的 Redis全量同步期间 COW 可能让实际内存多出 1 到 2G。如果机器内存本就很紧张这个峰值很可能触发 OOM。其次是磁盘BGSAVE 要写 RDB 文件磁盘慢的话同步时间会被拉长。再就是带宽RDB 动辄十几 G网络差一点传输时间非常可观。针对磁盘 IO 这种瓶颈Redis 提供了无盘复制diskless replication核心参数是repl-diskless-sync yes repl-diskless-sync-delay 5开启后主库不再生成 RDB 文件落盘而是由 fork 出的子进程直接把 RDB 数据流式写到从库的 socket。适合磁盘慢、网络带宽充足的环境。但要注意它也有短板没有落盘 RDB 文件等于少了一份可用于备份的快照而且如果从库中途断开整个传输就得推倒重来。所以这个开关要根据实际硬件环境来定别盲目开启。还有一个细节如果多个从库同时发起全量同步主库会等待 repl-diskless-sync-delay 的秒数把多个从库的请求合并成一次快照生成减少 fork 次数和系统开销。4. 增量复制断线续传的核心智慧4.1 复制积压缓冲区一条可以倒带的录像带前面的全量流程里反复提到了 repl_backlog但它真正的主场在这里。复制积压缓冲区是主库在内存里维护的一个环形缓冲区保存最近一段时间内复制流的字节内容。我把它比作录像带它只保留最近一小段新内容不断写入旧内容被覆盖。从库断线重连后主库只要确认从库请求的 offset 还在这盘录像带里就能直接把缺失那段数据发过去不需要从头再来。这里很容易和另一个概念混淆client output buffer。backlog 是全局的属于主库服务于任何一个可能断线重连的从库而 output buffer 是每个从库连接独立的服务于正在同步的这个从库。简单说backlog 面向过去的断线output buffer 面向当下的传输。这两个缓冲区在全量同步期间同时工作但职责完全不同。4.2 断线重连CONTINUE 还是 FULLRESYNC从库断线后重连心里是“有底”的它记得自己已经执行到哪个 offset 了也知道自己继承的是哪条数据历史的 replid。于是它重新连接发 PSYNC把这两样报给主库。主库的判断逻辑我前面已经提过replid 必须匹配从库的 offset 必须大于等于 backlog 的有效起始位置小于等于主库当前的最新 offset。两个条件都满足返回 CONTINUE主库从 backlog 中捞出缺失的字节流发给从库从库继续执行。反之只要有一个不满足就返回 FULLRESYNC回到全量同步。这个判断是性能的分水岭。全量同步要生成 RDB、传输大文件、加载内存耗时动辄分钟级起步而增量续传可能只是发送几十 KB 的命令秒级完成。所以在高可用架构里backlog 的容量设计直接决定了断线重连的代价。这里还有一个 PSYNC 2.0 的典型场景值得展开。假设主库 M 挂着从库 A 和从库 BA 被提升为新的主库。如果 PSYNC 还是老版本B 发现新的主库 A 的 replid 和 M 不一样只能全量同步。但 PSYNC 2.0 下A 把自己从 M 继承的旧 replid 保留为 replid2B 带着旧 replid 来请求A 能认出“我们来自同一条历史线”并且只要 B 的 offset 大于等于 A 被提升那一刻的 second_repl_offset就能继续增量同步。这个机制让主从切换后的恢复成本大幅下降。4.3 积压缓冲区容量怎么估算才算合理repl-backlog-size 的默认值是 1MB这在实际业务里往往偏小。怎么估算合适的大小我的做法是抓两个变量断线窗口和多久时间内允许它全量。公式很简单backlog_size 平均每秒写入字节数 * 期望容忍的断线秒数 * 2为什么乘 2因为 backlog 要保留的是从断线点到重连完成这段窗口的数据重连过程本身也需要时间留出余量更稳妥。举个例子业务高峰每秒写入约 1MB希望容忍 15 分钟内断线重连那就是 1MB * 900 秒 * 2 1800MB。设成 2GB 左右比较稳妥。如果内存紧张至少也要按峰值写入来算别拿平均值糊弄。怎么拿到平均每秒写入字节数可以直接在 INFO stats 里看瞬时写请求量或者用 redis-cli --stat 观察一段时间再结合实际命令的平均大小估算。还有一个更直观的方法看 INFO replication 里 repl_backlog_histlen它表示 backlog 里当前保存的有效数据字节数。如果这个值长期顶着 repl_backlog_size 的上限说明缓冲区一直在被写满你要么调大容量要么就得接受断线后走全量的现实。5. 复制链路上的心跳、状态与参数5.1 心跳机制与超时判定主从复制不是建好连接就万事大吉的它需要一套心跳机制来探测对方是否还活着。主库每隔 repl-ping-replica-period 秒默认 10 秒向从库发送一次 PING从库则每秒向主库发送一次 REPLCONF ACK带上自己的复制偏移量。双方哪一方在 repl-timeout默认 60 秒内没有收到对方的有效数据就判定连接超时。这个超时时间很有讲究。设小了网络一抖动就容易误判从库来回重连甚至触发全量同步设大了主库真的宕机时从库要过很久才感知到拖慢哨兵切换的进度。我一般建议 repl-timeout 和哨兵的 down-after-milliseconds 配套起来看整体超时节奏要协调别让一个环节成为另一个环节的瓶颈。5.2 INFO replication 状态解读排障时最常用的命令就是 INFO replication主库视角的输出长这样# Replication role:master connected_slaves:2 slave0:ip10.0.2.21,port6379,stateonline,offset94217762,lag0 slave1:ip10.0.2.22,port6379,stateonline,offset94217760,lag1 master_replid:7b3f7c5e0d8a4f22aa9e4c7f6d3e2b1a9c8d7e6f master_replid2:0000000000000000000000000000000000000000 master_repl_offset:94217762 repl_backlog_active:1 repl_backlog_size:67108864 repl_backlog_first_byte_offset:88217762 repl_backlog_histlen:5999999几个字段重点看connected_slaves 表示当前有几个从库连上来每个从库的 lag 表示距离上一次收到它 ACK 的秒数稳定在 0 或 1 是健康的持续变大说明有问题master_repl_offset 是主库当前复制的字节位置repl_backlog_first_byte_offset 和 repl_backlog_histlen 则告诉你 backlog 里有效数据的区间断线的从库如果请求的 offset 比 first_byte_offset 还小那就只能全量。从库视角的 INFO replication 我一般看 master_link_status 是不是 up还有 slave_repl_offset 和 master_repl_offset 的差值。这两个值差得越大说明同步堆积越严重。5.3 值得过一遍的配置参数清单我把日常运维中真正用得上的参数整理成了一张表参数默认值作用与建议replicaof无指定主库地址端口有配置文件法和运行期 REPLICAOF 命令两种replica-read-onlyyes从库只读避免误写入导致主从数据分叉没有特殊理由别改动replica-serve-stale-datayes同步中断时是否继续用旧数据服务请求一致性要求高可改 norepl-backlog-size1mb复制积压缓冲大小建议按带宽与断线时间估算后调大repl-timeout60s主从连接超时阈值网络不稳定可适当调大repl-ping-replica-period10s主库向从库发送心跳的间隔min-replicas-to-write0至少几个从库在线且延迟在限制内主库才接受写命令min-replicas-max-lag10s与上面的参数配合从库最大允许延迟masterauth无从库连接主库时使用的密码主库开启 requirepass 后必须配置repl-diskless-syncno是否开启无盘复制磁盘慢网络快的场景可开启这堆参数里容易被忽视的是 min-replicas-to-write 和 min-replicas-max-lag 这对组合。它们的意思是当主库发现存活且延迟达标的从库数量不足时直接拒绝写命令。听起来反直觉但它的价值在于“用可用性换一致性”。一旦所有从库都断了主库继续写入只会积累大量没法被同步的数据未来恢复的代价巨大。对于一些对一致性要求很高的内部系统这个开关能兜底。6. 常见问题与排障实操实录6.1 一觉醒来从库全在疯狂全量同步这个场景我碰到过不止一次。明明业务没有上线新功能从库却莫名其妙地反复全量同步主库 CPU 和带宽被打满从库卡顿。第一件事就是用 REPLICAOF NO ONE 加 REPLICAOF 把从库的同步链路重置先止血。然后再排查根因。常见的元凶有三个。第一主库重启过replid 变了所有从库都认不出旧身份只能全量。第二repl_backlog_size 设置太小从库断线时间稍长请求的 offset 已经被 backlog 覆盖条件不满足走了全量。第三网络不稳定导致从库频繁断线重连每次重连都因为 backlog 不够而全量形成恶性循环。对应解法也很直接别随意重启主库按前面说的公式调大 backlog修复网络稳定性。另外提一句通过 INFO replication 里的 sync_in_progress 和 master_sync_in_progress能快速确认此刻是不是有从库在全量同步。6.2 主从延迟居高不下最典型的症状是读完从库拿到的数据和主库相差很远监控里 master_repl_offset 和 slave_repl_offset 的差值持续走高。排查时我会按优先级一层层来。第一层是网络ping 一下主从之间的延迟和丢包重点看带宽是不是被 RDB 传输或者大 key 写满。第二层是主库本身是不是有慢查询、大 key、大批量写入导致复制流生成的速度超过从库消费的速度。第三层是从库自身CPU 负载高、内存 swap、慢查询都会拖慢命令应用的速度。从库的 info commandstats 可以看哪些命令占了大部分执行时间。这里有个特别容易踩的坑超大 key 的复制放大效应。主库写一个 100MB 的 list 对象复制流里这个对象也会以协议形式被完整传输。从库要接收、解析、再写入内存这个放大效应会直接把延迟拉高。所以 Redis 性能调优里反复强调避免 bigkey在复制场景下也同样成立它不只是查询慢的问题还直接拖累复制链路。6.3 从库“孤儿化”认证与配置的坑有一次排查一个从库一直处于断线重连状态日志里刷着认证失败的报错。查了一圈是主库开启了 requirepass但从库配置里的 masterauth 写错了一个字符。这类认证类的问题在真实环境里出现率极高尤其是主库密码滚动更新时经常有人换了主库密码却忘记同步改所有从库的 masterauth结果从库一个个掉线。另一个常见的坑是运行期动态调整主从关系后没有持久化。REPLICAOF 命令是即时生效的但不会写进 redis.conf一旦进程重启从库就回到原来的配置甚至变成独立的 master。所以我维护环境时有一个习惯任何主从关系的变动都要同步更新配置文件并做好交接文档否则下次上线打包或重启时就会踩坑。另外切换主从时REPLICAOF NO ONE 和新的 REPLICAOF 命令要按顺序执行中间确认一下角色变化别在集群或哨兵环境下自己乱切那是另一个话题了。6.4 主动断网验证复制收敛能力最后分享一个我自己常用的故障演练手法。在业务低峰期用防火墙规则把从库和主库之间的流量掐断几分钟然后放开观察从库重连之后的表现。执行 INFO replication看它是返回了增量续传还是重新全量同步。如果走的是增量说明 backlog 容量够断线时间在容忍范围内。如果走了全量就要回看一下 backlog 是否设置过小或者断线期间的写入量是不是太大。这个演练成本极低但对复制链路的健康度检验非常有效。我还喜欢顺带验证一下哨兵的切换流程手动把主库停掉看哨兵能否按预期提升新主兄弟从库能否快速跟上。高可用方案平时看着再美不演练永远不知道哪里会露馅。最后说点我的个人心得。带 Redis 这么多年我最大的体会是主从复制这套东西网上讲“怎么配”的文章很多讲“为什么这么设计”的文章很少。而真正让你在故障面前不慌的恰恰是那些“为什么”。replid 为什么存在、backlog 为什么这么算、复制 ID 为什么在主库重启后会变化把这些串成一条线之后你再去看哨兵的选主、集群的迁移会发现很多设计思路都是共通的。这个系列既然起了个头后面应该还会继续写哨兵和集群。如果这篇文章里的某一段能帮你少踩一个坑那就值了。题外话建议你下次维护 Redis 环境时把 INFO replication 打开对照着本文的字段解释多看几遍比死记配置文件有用得多。
返回列表