Redis主从复制的原理是什么?
作者:程序员马丁
在线博客:https://open8gu.com
大话面试,技术同学面试必备的八股文小册,以精彩回答应对深度问题,助力你在面试中拿个offer。
回答话术
当从节点初次连接到主节点,或者掉线重连后进度落后较多时会进行一次全量数据同步。此时,主节点会生成 RDB 快照并传输给从节点,在此期间,主节点接受到的增量命令将会先写入 replication_buffer
缓冲区,等到从节点加载完 RDB 快照的数据后,再将缓冲区的命令传输给从节点,以次完成初次同步。
当从节点掉线重连后,如果进度落后的不多,将会进行增量同步。主节点内部维护了一个环形的固定大小的 repl_backlog_buffer
缓冲区,它用于记录最近传播的命令。其中,主节点和从节点会分别在该缓冲区维护一个 offset ,用于表示自己的写进度和读进度。当从节点掉线重连后,将会检查主节点和从节点 offset 之差是否小于缓冲区大小,如果确实小于,说明从节点同步进度落后不多,则主节点将该缓冲区中的两 offset 之间的增量命令发送给从节点,完成增量同步。
当主从节点完成初次同步后,将会建立长连接进行命令传播。简单的来说,就是每当主节点执行一条命令,它就会写入 replication_buffer
缓冲区,随后再将缓冲区的命令通过节点间的长连接发送给对应的从节点。
问题详解
1. 全量同步
一般来说,当两个节点第一次建立主从关系的时候,一定会触发一次全量同步。整个过程大致分为四步:
- 从节点向主节点发送 psync 请求获取 runID 和 offset;
- 主节点返回自己的 runID 与该从节点的 offset;
- 主节点生成 RDB 文件,并传输给从节点,从节点加载文件后获取全量数据。
- 主节点确认从节点加载完 RDB 文件后,将这期间缓存的增量命令发送给从节点,从节点加载完毕后结束第一次同步。
1.1. 获取 runID 与 offset
当我们对一个 Redis 实例使用 replicaof
让指定从节点与主节点构成主从关系时,从节点将会根据你指定的 host 和端口号请求对应的服务实例,并向主节点发送 psync {runID} {offset}
请求。
其中,runID 是主节点启动时生成的标识 ID,而 offset 则表示当前从节点从主节点复制数据的偏移量,也就是复制进度。最开始的时候,由于是第一次同步,因此从节点并不知道主节点的 runID,因此 runID 为 ?
,由于也还没有复制过数据,因此 offset 为 -1。
当主节点响应请求时,将会返回主节点自己的 runID 和当前从节点的复制进度 offset。并且因为是首次同步,因此主节点会返回 FULLRESYNC
响应,表示从节点需要进行全量同步。
1.2. 加载 RDB 数据
在前文 Redis 的持久化部分,我们提到过,Redis 恢复数据或者进行主从同步的时候是通过 RDB 文件完成的,实际上指的就是全量同步。
在从节点接受了响应以后,主节点将会执行 bgsave 去生成一个 RDB 文件——这个流程与正常生成 RDB 文件一致——并发送给从节点,从节点接受后,由于是全量同步,因此会先清空自己的已有数据,然后再加载 RDB 文件中的数据。
此时,从节点已经同步过来了主节点的大部分数据,不过由于 RDB 实际上只是一个快照,因此在生成 RDB 文件期间主节点的增量数据实际上还没有被从节点获取。
关于 Redis 是如何生成 RDB 文件的,请参见:✅ Redis 的 RDB 是怎么实现的?
1.3. 获取增量数据
在主节点生成 RDB 文件期间,由于 RDB 文件是通过子进程异步生成的,因此在这个过程中主节点仍然还在正常的处理请求,这部分的增量命令将会写入 replication buffer 缓冲区。
当从节点加载完 RDB 中的数据后,将会向主节点发送确认消息,此时主节点会再将缓冲区中的命令发送给从节点,从节点执行完这部分增量命令后,数据即与主节点基本一致,则全量同步完成。
不过,由于主从的更新已有延迟,因此无论如何数据是很难保证完全一致的,不过这就是后面命令的正常传播和增量同步的事情了。
2. 命令传播
当主从之间根据 psync
请求完成第一次全量或增量同步后,就会保持长连接,此后,将会通过长连接进行命令传播。
在这个过程中,主节点将会在每次执行完一个命令后,分别写两个缓冲区:
- replication_buffer:主从复制缓冲区,主节点每拥有一个从节点,就会有一个对应的缓冲区,要传播给从节点的命令会先写入该缓冲区,随后在通过长连接发送给从节点。
- repl_backlog_buffer:最近传播命令缓冲区,一个主节点只有一个,用于记录主节点最近传播出去的命令,主节点和全部从节点都会分别在上面维护一个 offset,用于表示自己已经写入或读取的命令进度。
这里 replication_buffer
实际上就是 IO 操作通常都会有的缓冲区,而 repl_backlog_buffer
则是为了解决从节点掉线重连后的增量同步问题。
3. 增量同步
repl_backlog_buffer
是主节点中一个比较特殊的缓冲区,和每个从节点都有一个 replication_buffer
不同,一个主节点只会有一个 repl_backlog_buffer
。
repl_backlog_buffer
是一个固定大小的“环形”区域,当主节点写入数据时,它会使用 master_offset
记录自己当前已经写到哪个字节,对应的,从节点也会有一个 slave_offset
表示从节点已经读到的哪个字节。当主节点写的数据超过缓冲区大小后,它将会覆盖最早写过的内容。
因此,当从节点要与主节点进行同步时,仅需要在 psync 请求时给出自己的 slave_offset
即可,主节点将计算其与 master_offset
的差值:
- 如果差值大于
repl_backlog_buffer
的大小,说明两者数据已经差了很多,那么需要重新进行全量同步。 - 如果查找小于
repl_backlog_buffer
的大小,说明数据差还在容许范围内,则主节点将返回CONTINUE
响应,让从节点准备进行增量同步,并把repl_backlog_buffer
中的差值部分写入replication_buffer
并发送给从节点,让从节点把同步进度追上来。
简单的来说,如果从节点最后一次读取的命令可以在上面找到,那么说明从节点的数据没有落后太多,因此可以增量同步,否则就需要进行全量同步。
当主从链接断开后,从节点会重新发送 psync 请求向主节点要求同步数据,在 2.8 之前总是会使用全量同步,而在 2.8 及以后的版本,将会使用增量同步。
综上,我们不难意识到,如果你的网络不太稳定,那么最好把 repl_backlog_buffer 调大一些,这样可以尽可能的避免从节点掉线重连后需要频繁的进行全量同步。
4. 主动切换导致数据丢失
当一个 Redis 节点作为从节点使用时,为了保证始终能够正确从主节点同步数据,因此它的 maxmemory
配置将不会生效。
这也就意味,当你的从节点的 maxmemory 小于主节点的 maxmemory 配置时,从节点可能会从主节点同步到超过其 maxmemory 配置的数据量。此时一旦发生主从切换,从节点就会意识到自己的使用内存已经超过最大内存了,因此会立刻开始进行内存淘汰。在这种情况下,节点可能会导致大量的数据丢失,并且在较长一段时间内处于不可用状态。
总而言之,当你使用主从时,记得要让主节点和从节点的配置始终保持一致,避免因为主从切换导致不可预料的后果。