Redis主从复制与哨兵架构详解:从原理到故障演练
Redis 学到现在我最大的感受是单机再好也扛不住意外。前面几篇我整理过数据结构、内存模型、持久化手段这一篇终于到了高可用层面最关键的一块——主从复制和哨兵。简单说主从复制负责把数据从一台 Redis 复制到另外几台哨兵负责在主库挂了之后自动把从库抬上位。这套组合是 Redis 生产环境高可用架构的基石也是很多人从会用 Redis跨到会维护 Redis的分水岭。下面这些内容是我从动手搭建、配置调优和故障演练里一条条记下来的希望能帮正在学这块的人少走弯路。1. 单机 Redis 再快也有扛不住的事1.1 一台 Redis 的三重风险Redis 性能确实好单实例读写在很多场景下都能顶住但它本质上还是一个进程跑在一台机器上。只要机器出了问题Redis 再快也白搭。我总结下来单机 Redis 主要面临三类风险每一类都不罕见。机器层面的故障断电、内存接触不良、磁盘损坏、系统崩溃导致重启。这些不是会不会发生的问题而是哪一天发生的问题。人为层面的误操作FLUSHALL、FLUSHDB、DEL一个大 key、执行了配置错误导致进程退出。很多线上事故不是攻击者干的就是自己手滑。网络层面的分化机器本身没挂但网络分区把主库隔离了。客户端连不到主库业务照样受影响而且这类问题排查起来最花时间。所以只要业务对 Redis 的可用性有一点要求单机部署就属于裸奔状态。1.2 主从复制解决什么又解决不了什么主从复制的核心价值可以拆成三点数据热备份主库的数据实时追加式同步到从库主库机器彻底报废时从库上仍然有一份完整数据不用从 RDB/AOF 冷恢复。读能力扩展主库负责写从库分担读Redis 的读写分离模式让整体吞吐量大幅提升。高可用的底座只有先把数据复制到多节点后面才能谈自动故障转移也就是哨兵干的事。但有一点必须提前强调Redis 主从复制是异步复制。主库执行完写命令后不是等从库确认再返回客户端而是直接返回成功复制过程在后台异步完成。这意味着它天生无法保证数据零丢失。如果主库在某个写命令还没同步给从库时就宕机了这条最新的写入就丢了。理解了这一点再看后面的脑裂和数据丢失问题就容易多了。2. 主从复制的底层链路从一次 psync 聊起2.1 第一次建立关系时主从之间发生了什么很多人配置完主从就完事了不太关心数据到底怎么过去的。实际上第一次建立复制关系时走的是一条完整的全量同步链路。我把每一步拆出来看。假设现在我有一台主库和一台从库我在从库上执行redis-cli -p 6380 REPLICAOF 127.0.0.1 6379从库会做三件事记录主库地址、建立 TCP 连接、发送PSYNC命令。PSYNC是复制的握手协议语法长这样PSYNC replid offset如果是初次复制从库不知道主库的 replid也不知道自己该从哪个偏移量开始所以会带上?和-1PSYNC ? -1主库收到后如果判断无法做部分同步就会回复FULLRESYNC同时把自己的replid和当前复制偏移量发给从库然后进入全量同步流程主库执行BGSAVE生成 RDB 快照。与此同时主库把新收到的写命令写入复制积压缓冲区和命令传播缓冲区防止快照生成期间的数据丢失。快照生成完成后通过 socket 发给从库。从库清空旧数据加载 RDB 文件。需要注意的是Redis 为了避免磁盘 I/O 成为瓶颈可以改用磁盘无盘化复制repl-diskless-sync yesRDB 在内存中直接生成并通过网络流式传输副本不落盘。这个参数在从库较多时能明显降低主库磁盘压力但要求主从之间网络带宽充足。从库加载完 RDB 后主库还会把复制缓冲区里的增量命令继续发过来确保数据追平。之后双方进入命令传播阶段主库每次执行写命令都会把命令分发给从库执行。整个全量同步的安全点在于BGSAVE生成快照的过程不阻塞主库服务但仍会消耗 CPU 和内存如果数据量特别大建议在业务低峰期做首次复制。2.2 断线重连背后的增量同步机制全量同步只发生在首次建立关系、或者从库落后实在太多的情况下。日常运行中从库和主库之间网络抖动导致短暂断线重新连接时再走一次全量同步就太浪费了。Redis 解决这个问题的机制叫部分重同步。主库内存里维护了一块固定大小的环形缓冲区叫复制积压缓冲区repl_backlog。主库每执行一条写命令都会把命令写入这个缓冲区同时自己的master_repl_offset也往后推进。从库这边同步到哪条命令也会记录自己的slave_repl_offset。断线重连时从库发送PSYNC 主库replid 从库记录的offset主库接过请求后会检查主库 replid 是否匹配不匹配就只能全量同步。从库 offset 之后的数据是否还在本地的复制积压缓冲区中。如果 offset 之后的数据仍在 backlog 覆盖范围内主库就回复CONTINUE把从缺口开始的那一段命令重新发给从库。如果 offset 已经超出了缓冲区范围说明落后的太多主库才回复FULLRESYNC走全量同步。这个机制的一个直接结论就是backlog 缓冲区的大小要按实际断线容忍时间配置。比如生产环境平均每秒写 2000 次每次命令平均 200 字节每秒就是 400KB 的数据量。默认的repl-backlog-size是 1MB算下来只能容忍约 2.5 秒的断线。如果你的网络抖动可能持续 1 分钟这个值至少得配 24MB。缓冲区配大一点代价是内存占用增加配小了断线稍久就会退化成全量同步消耗更大。2.3 主从复制中容易被忽略的三个细节第一主从复制并不是主库主动推数据。从库启动后先打招呼主库才把数据交给它之后每次命令传播也是从库在 TCP 连接上等待。理解这个模型故障排查时就明白为什么网络出问题后从库会不断重连而不是主库单方面断开。第二从库默认只读配置项是replica-read-only yes。如果强行在从库上执行写命令Redis 会直接报错。这样设计既保证一致性也防止开发者把从库当成写入口导致数据错乱。第三过期键的处理是主库说了算。主库遇到 key 过期时会显式向从库发送一条 DEL 命令。从库不会自己判断 key 是否过期并删除只等主库指令。这样可以避免主从各自判断时出现数据不一致。不过从库仍然会在读取时判断逻辑过期返回空值给客户端内存里的清理还是靠主库指令。3. 建立主从关系配置实操与关键参数3.1 三种建立方式怎么选建立主从关系有三种方式方式本身并不复杂但背后的适用场景很有讲究。方式操作特点适用场景命令行REPLICAOF host port即时生效重启后失效临时切换、演练、手动故障转移配置文件在 redis.conf 写replicaof host port持久生效需重启实例正式部署、长期架构启动参数redis-server --replicaof host port持久生效依赖启动脚本容器化、进程管理工具拉起我在实际部署时偏爱配置文件为主、命令行临时调整的组合。平时保持主从关系写在配置里需要做故障演练或主从切换时才用命令行动态操作。有一点要特别注意执行REPLICAOF no one可以让从库解除复制关系、变为主库但这条命令同样不会写入配置文件。如果机器重启它会重新按配置去连原主库。生产环境做切换时一定要把配置文件一起改掉否则机器一重启架构又变回原样事故就埋下了。3.2 五个值得提前调好的配置项repl-backlog-size复制积压缓冲区大小。刚才已经算过需要按每秒写入量和断线容忍时间估算一般 16MB 起步。repl-timeout复制超时时间默认 60 秒。主从之间如果超过这个时间没有心跳从库会判定主库不可用尝试重新连接。网络环境较差时可以适当调大比如 90 秒。repl-diskless-sync是否启用无盘化全量同步。多从库首次全量复制时建议开启减少磁盘 I/O但需要确保主从之间的带宽充足。replica-read-only从库只读开关生产环境必须保持 yes。repl-diskless-sync-delay无盘化同步开始前的等待时间默认 5 秒。这个设计很有意思主库会等一等看这段时间内有没有新的从库来请求复制如果有可以用一次 RDB 覆盖多个从库避免每个从库重复生成快照。3.3 如何在命令行验证复制状态配置完成后在从库执行redis-cli -p 6380 INFO replication关注这几项role:slave master_host:127.0.0.1 master_port:6379 master_link_status:up master_last_io_seconds_ago:0 master_repl_offset:10086 slave_repl_offset:10086 master_replid:e5a3f8...如果master_link_status是up说明连接正常。master_repl_offset和slave_repl_offset数值一致时主从数据追平。实际环境中这两个偏移量会存在短暂差异只要差值不大、且不断收敛就是正常的。我还建议把主库侧的connected_slaves也盯上。主库用INFO replication可以看到当前挂着几个从库以及各自的复制偏移量。这个视角才是运维真正需要的。4. 主库宕机后哨兵到底在忙什么4.1 没有哨兵时手动切换到底有多累先想想看如果只有主从复制、没有哨兵主库宕机后运维要干的活确认某个从库的数据恢复程度看偏移量是否追平。在从库上执行REPLICAOF no one把它升为主库。修改所有业务客户端的 Redis 地址指向新的主库。把原主库降级为从库重新接入新主库。这一套流程听下来不难但每一步都要人工判断而且业务中断时间取决于人的反应速度和操作熟练度。大半夜被叫起来处理故障能保持清醒做完四步已经很不错了。哨兵存在的意义就是把这件事自动化。4.2 主观下线和客观下线哨兵判断主库挂了的两层逻辑哨兵判断主库是否宕机不是一口咬定而是分两步。第一步叫主观下线SDOWN。每个哨兵进程每隔一秒会向所有节点发送 PING 命令如果主库在down-after-milliseconds配置时间内都没有有效回复当前这个哨兵就会认为主库主观下线。注意这只是单个哨兵的看法。由于网络波动、GC 停顿等原因单个哨兵可能误判所以需要引入第二步。第二步叫客观下线ODOWN。当一个哨兵判断主库主观下线后它会向其他哨兵发起询问我认为主库挂了你们怎么看 其他哨兵根据自己的探测结果给出回应。当赞成票数达到配置的quorum阈值时这个哨兵就确认主库客观下线可以进入故障转移流程。这里有一个关键点quorum不是哨兵集群的所有节点数只是确认下线所需的最低票数。假如我有 3 个哨兵quorum配成 2那么至少两个哨兵认定主库下线流程才会启动。哨兵节点数量建议奇数个这是为了后续选主投票时能形成多数派避免平票。4.3 故障转移的完整决策链确认客观下线后接下来发生的事情顺序很有意思哨兵节点之间选出一个领导者由它负责执行故障转移。这个选举机制会让每个哨兵自荐或者推荐别人拿到半数以上票的哨兵成为领导者。领导者从所有从库中选出一个新的主库。选择规则按优先级依次是slave-priority配置值越小越优先优先级相同时复制偏移量最大的从库优先数据最完整再相同按 runid 字典序小的优先。新主库执行REPLICAOF no one从库身份变为主库。其他从库执行REPLICAOF 新主库IP 新主库端口重新指向新主库。原主库恢复可用后哨兵会把它降级为从库也指向新主库。整个过程从故障到切换完成正常情况几秒到十几秒。业务客户端需要能感知哨兵切换通常用哨兵的发布订阅消息或访问哨兵来发现新的主库地址。5. 哨兵最小化部署与故障演练全过程5.1 一套最小但完整的高可用架构我建议学习阶段就搭一主两从三哨兵不要偷懒只搭一个哨兵。单个哨兵自己都可能成为单点故障转移时也无法形成多数派判断。部署规划可以这样节点端口作用主库6379读写入口从库 A6380冷备 读从库 B6381冷备 读哨兵 126379监控和故障转移哨兵 226380监控和故障转移哨兵 326381监控和故障转移实验环境下可以跑在同一台机器上只要端口区分就行。生产环境至少需要三台机器哨兵分布在不同的物理节点上否则机房整体断电会让哨兵集体失效。5.2 哨兵配置文件逐项说明哨兵的配置文件是sentinel.conf核心项如下# 监控的主库名、地址、端口、判定客观下线所需票数 sentinel monitor mymaster 127.0.0.1 6379 2 # 主库无响应多久后判定主观下线默认30000毫秒 sentinel down-after-milliseconds mymaster 5000 # 故障转移超时时间默认180000毫秒 sentinel failover-timeout mymaster 180000 # 故障转移时最多可以同时向几个从库发起同步 sentinel parallel-syncs mymaster 1逐项解读sentinel monitor里的最后一个数字就是quorum我这里是 2意味着至少两个哨兵认定主库下线才切换。down-after-milliseconds设成 5000即 5 秒无响应就报主观下线。生产环境要根据网络质量调太短容易误判太长切换太慢。parallel-syncs建议设 1意思是新主库上位后同一时间只允许一个从库全量同步。如果同时让多个从库一起同步新主库可能因为瞬间负载过高被拖垮。5.3 手动杀掉主库观察哨兵的完整动作搭建完成后我强烈建议做一次故障演练不然哨兵配置到底有没有生效心里没底。我当时的演练过程如下。先把所有节点进程拉起确认哨兵已经发现了主库和从库redis-cli -p 26379 sentinel masters redis-cli -p 26379 sentinel slaves mymaster然后模拟主库故障直接杀掉主库进程redis-cli -p 6379 SHUTDOWN NOSAVE接着盯着哨兵日志看变化。典型的日志顺序是sdown master mymaster 127.0.0.1 6379 odown master mymaster 127.0.0.1 6379 #quorum 2/2 try-failover master mymaster 127.0.0.1 6379 failover-state-select-slave master mymaster 127.0.0.1 6379 selected-slave slave 127.0.0.1:6380 promoted-slave slave 127.0.0.1:6380 switch-master mymaster 127.0.0.1 6379 127.0.0.1 6380看到switch-master就说明切换已经完成。此时再用哨兵查询主库地址返回的应该是 6380 端口。同时原先的从库 B 也会自动指向 6380整个集群恢复可用。演练完之后我把原 6379 重启观察哨兵日志会发现它被自动接入为从库指向 6380。这一步很关键因为它验证了原主库恢复后不会抢占主库位置而是老老实实做从库。6. 复制延迟、脑裂与哨兵系统的边界6.1 复制延迟从哪里来怎么盯主从复制是异步的所以延迟必然存在。延迟的来源主要有三个网络往返主从跨机房部署时RTT 本身就有几十毫秒。大 key 与慢命令主库在生成 RDB 或执行大集合操作时可能阻塞复制链路。从库自身负载从库如果承担了大量读请求CPU 跑满时处理复制命令也会变慢。监控方式就是在主库执行INFO replication观察master_repl_offset和每个从库的slave_repl_offset二者差值就是复制积压量。更省事的方式是利用 Redis 的监控指标工具如命令行--stat或各类可视化监控定期采集这个差值并告警。实际运营中延迟长期超过 1 秒就需要排查了尤其是读写分离场景下延迟会导致读到过期数据。6.2 脑裂场景与最后一道防线 min-replicas脑裂是主从复制模式下最经典的数据丢得神不知鬼不觉的场景。场景是这样的主库和哨兵之间的网络发生了分区哨兵联系不上主库于是把从库 A 提升为新主库。但原主库并没有宕机它还活着而且客户端仍然能通过原地址连接到它继续写入数据。等网络恢复哨兵发现原主库还活着强制它变成从库去同步新主库的数据。此时原主库在分区期间写入的新数据就全被覆盖了因为这些数据根本没同步给任何节点。这个问题的防线在 Redis 配置里叫min-replicas-to-write和min-replicas-max-lag。含义是主库至少要有 N 个从库连接且数据复制延迟不能超过 M 秒否则拒绝处理写请求。min-replicas-to-write 1 min-replicas-max-lag 10意思是如果连一个从库的复制延迟都超过 10 秒那主库就不再接受写入。在脑裂场景里原主库被分区隔离从库全部连不上它复制延迟迅速超过阈值写入被拒绝从而避免了分区期间的数据产生。当然代价是这段时间业务写入直接失败属于牺牲可用性保一致性的选择。实际配置时怎么取舍取决于业务特性但min-replicas-to-write至少设成 1 是常规建议。6.3 哨兵模式解决不了的问题哨兵不等于数据零丢失。主库宕机时还没有同步给从库的那部分数据切换后依然会丢。如果业务对数据完整性要求极高需要在客户端层面做补偿或者接受这个误差否则就得考虑其他方案。哨兵模式下主库仍是单点写入口。从库虽然可以扩展读但写入请求最终还是要打到主库。如果写并发很大、单机内存放不下哨兵解决不了这种容量问题需要走 Redis Cluster 的分片方案。哨兵集群不是配置完就一劳永逸。哨兵之间的网络同样可能分区quorum和节点数量需要结合实际情况认真设计。我在实际搭建时发现哨兵日志的输出信息量大但分散最好在前期就把日志接入统一的监控平台否则真出事时只能靠人肉翻日志。最后再分享一个我自己的小习惯每次搭完主从和哨兵我都会在从库上执行几遍INFO replication和SENTINEL masters确认所有节点视角一致后再交付。高可用这套东西风险都不在配置本身而在配置和现实状态对不上。多演练、多观察、多记录真正出事时才能心里有数。