对于Redis来说,其高可用性是由两方面来保证的:
- 一方面是数据尽量少丢失,前一篇总结推文提到的AOF和RDB实现了。
- 另一方面是服务尽量少中断,Redis则是靠增加副本冗余量即同一份数据保存在多个实例上来实现的。
对于多副本模式,Redis和关系型数据库一样,提供了主从库模式来保证数据副本的一致性。主从库之间采用的是读写分离的方式,即读操作可以被主库/从库接收,但是写操作只能先被主库接收执行然后才由主库同步给从库。
那么问题来了,为啥要用读写分离实现?
因为,如果每次的修改请求都发到不同的实例上,要保证数据的一致性,就需要涉及到加锁和协商是否完成修改等操作,这会带来很大的性能开销。而如果所有的修改都只在主库上进行,就无需协调三个实例,只需要同步给从库,进而保持数据一致性。
Redis主从库同步流程详解
假设现在我们有两个实例,分别是主库实例1(172.16.19.3)和实例2(172.16.19.5),我们的目标就是让实例2成为实例1的从库,并进行数据同步。
第一次同步过程
在实战中,只需要实例2的shell中执行以下命令就可以将实例2作为实例1的从库,并从实例1上复制数据。
replicaof 172.16.19.3 6369
虽然就这一个命令真简单,而在这背后,Redis偷偷摸摸地进行了三个阶段的操作,如下图所示:
阶段1:从库向主库发送psync命令,表示进行数据同步。该命令格式如下所示:
# 命令格式:psync runID offset# psync ? -1 # 其中?代表从库并不知道主库的runID,-1表示第一次复制
主库收到psync命令确认后,会向从库发送一个FULLERSYNC的响应命令,并带上主库的runID和目前的复制进度offset。
# 命令格式:FULLERSYNC runID offset# FULLERSYNC代表全量复制,一般用于第一次数据同步
阶段2:主库通过发送RDB文件给从库,从库收到后在本地完成数据加载。
需要注意的点:
(1)主库会首先执行一次bgsave(bgsave不会阻塞主线程)生成RDB文件,然后才发送给从库。
(2)从库会首先清空已有的数据,然后再加载RDB数据,因为在同步之前可能保存了其他数据,不清除的话可能会数据不一致。
(3)主库在同步数据给从库中产生的写操作会用专门的replication buffer记录,然后在第三个阶段同步过去。
阶段3:主库将第二阶段执行过程收到的新的写操作命令,同步给从库。
在实现中,主库会将新收到的写操作放到replication buffer中记录下来,然后将这些操作修改发给从库,从库收到后再重新执行一遍这些操作。
以上三个阶段完成之后,主从库就算完成了一次数据同步。
为什么用RDB不用AOF?
Redis主库在向从库同步数据时使用的RDB文件,那么问题来了:
AOF记录的操作命令更全,相比RDB丢失的数据更少,为什么主库用RDB不用AOF呢?
一来RDB读取速度相对较快,从库可以快速完成RDB的读取,然后再去消费replication buffer的数据完成一次同步。而如果使用AOF,其体积大读取速度慢,且需要更大空间的replication buffer,对于一个主节点多个从节点来说的话,内存的占用就会更大;
二来AOF是Append追加模式,同时读写需要考虑并发安全问题,并且AOF是文本文件,体积较大,浪费网络带宽。
主从级联模式降低主库压力
主从库同步过程中,主库需要完成两个耗时的操作:生成RDB文件 和 传输RDB文件。现实场景中,从库一般都会有多个,如果都要和主库同步的话,会造成主库的性能压力 和 网络压力。
因为主库需要fork子进程来生成RDB文件,这个fork操作是会阻塞主线程处理正常请求的,虽然后续的bgsave过程不会阻塞主线程。
此外,传输RDB文件也会占用主库服务器的网络带宽。
Redis提供了主从级联模式,也就是所谓的“主-从-从”模式。
在主-从-从模式下,新增的从库可以设置从 集群中的某一个从库 中进行数据同步,从而避免每次都从主库进行同步,降低主库的资源消耗,保证系统的稳定性。
比如,在实际中通常会手动选择一个 内存配置较高的 从库 来作为同步源,其他新的从库加入后可以从这个从库中同步,建立主从关系。例如,下面的命令就可以实现新增从库和某一个从库建立主从关系:
replicaof 所选从库ID 6379
具体的示意图如下图所示:
全量同步后的增量同步
主从库通过FULLERSYNC进行全量复制同步 + 主从级联模式分担主库压力 之后,Redis的主从库之间就会一直维护一个长连接来进行增量的命令操作同步,这个过程又被称之为“基于长连接的命令传播”。
为何选择长连接?因为可以避免频繁建立连接的开销。
虽然长连接很方便,但也存在一个风险点:主从库网络断了 或 阻塞了。这个风险可能导致的问题就是:主从库之间数据无法保持一致,客户端可能从读库读到过时的数据。
当然,Redis早就已经为我们想好了解决方案,不过得分为两个版本来看:
Redis 2.8之前,如果出现了网络闪断,Redis主从库间会重新进行一次全量复制。当然,全量复制就意味着有很大的开销。
Redis 2.8之后,如果出现了网络闪断,Redis主从库间会采用增量复制的方式继续同步。可以看到,增量复制肯定比全量复制开销要小得多。
这里的增量复制的核心要点就在于Redis引入了repl_backlog_buffer缓冲区,现在我们就来看看这个repl_backlog_buffer到底是个啥东东。
repl_backlog_buffer是一个环形缓冲区,主库会记录自己写到的位置,从库则会记录自己已经读到的位置。下图展示了repl_backlog_buffer的示意图:
可以看到,正常情况下,主从库的偏移量基本相等。随着主库不断接收新的写操作,它在缓冲区中的写位置会逐步偏离起始位置。对主库来说,对应的偏移量就是 master_repl_offset,只要主库的新写操作越多,这个值也就越大。同理,从库在复制完写操作命令后,它在缓冲区中的读位置也开始逐步偏移起始位置。对从库来说,其对应的已复制偏移量就是 slave_repl_offset。
当网络闪断异常情况下,主库可能会接收到新的写操作命令,因此,可以看出,主库的偏移量master_repl_offset > 从库的偏移量slave_repl_offset。那么,此时就只需要把 master_repl_offset 和 slave_repl_offset 之间的命令操作同步给从库就ok了。
综上所述,增量同步的过程整体如下:
repl_backlog_buffer扩展
在增量复制过程中,需要注意的点:repl_backlog_buffer是一个环形缓冲区,在缓冲区被写满了之后,主库再次写入时就会覆盖掉之前写入的操作。
那么问题来了,如果从库读取的速度很慢,就有可能出现从库读取到了不一致的数据。如何解决?
Redis提供了一个 repl_backlog_size 的参数,它与缓冲区的空间大小紧密相关。在实际中,一般其设置为:repl_backlog_size = 缓冲区空间大小 * 2,这样可以降低由于读库消费速度慢导致的数据不一致。
缓冲空间的计算公式是:
缓冲空间大小 = 主库写入命令速度 * 操作大小 - 主从库间网络传输命令速度 * 操作大小
But,如果并发请求量特别大,两倍的缓冲区空间都不够用,主从库仍然存在不一致的风险,那么此时,可能需要根据Redis所在服务器的性能指标(主要是内存资源)再增加一些 repl_backlog_size 值。
End总结
本文总结了Redis主从库读写分离模式数据同步的总体机制 及 基本流程,了解了全量同步 和 增量同步的过程,涉及了 主从级联模式 和 repl_backlog_buffer缓冲区。
参考资料
极客时间,蒋德钧《Redis核心技术与实战》
黄建宏,《Redis设计与实现》
拉钩教育,刘海丰《架构设计面试精讲》
作者:Edison Zhou