虽然Redis单机可以承载十万量级的并发请求,但是依然具有处理能力的上限,考虑正常情况下,对于数据的读取请求要远大对数据的写入请求。基于这个前提,Redis设计了一个复制的机制,应用这个复制机制,其他的Redis服务器可以拥有一个不断更新的数据副本,这样这些拥有数据副本的Redis服务器可以分担用户的读取请求,如此一来可以进一步提升Redis的并发能力,本篇文章将对Redis主从复制机制的相关功能与实现细节进项介绍。
复制功能概述
首先,彼此连接的Master实例与Slave实例是互为客户端的关系,在前面我们介绍客户端相关内容时,我们介绍了client.flags
这个字段以掩码的形式记录了当前客户端的状态。而Master实例与Slave实例从各自的角度看对方,都是一个client
对象,而在src/server.h之中也为Master与Slave实例定义了对应的状态掩码:
#define CLIENT_SLAVE (1 << 0)#define CLIENT_MASTER (1 << 1)
Redis本身也支持一个Matser实例连接多个Slave实例的模式,这样可以进一步提升系统的并发能力。Redis甚至可以在配置文件redis.conf之中通过配置项min-reolicas-to-write
来指定Master实例所需要的Slave实例的最小数量。一旦Matser实例对应的Slave数量不足,便可以认为这个Master处于一种异常的状态,Master实例可以拒绝在这个状态下执行写命令。
然而如果一个Master实例连接了过多的Slave实例的话,Master与Slave之间的数据传输对于Master实例本身来说也会造成一定的网络负载,因此Redis还支持一种层次化的主从模式,也就是说一个Slave实例也可以拥有它自己的Slave,我们可以称之为Sub-Slave实例。基于这种层次化的结构,顶层的Master实例只需要与若干少数个Slave实例进行数据传输,再由这些与顶层Master直连的Slave将数据发送给更为下层的Slave实例,以降低数据传输对Master实例性能所带来的影响。
简单来说Redis的复制功能主要可以划分两个阶段:
数据同步:Slave节点通过某种方式获取Master节点的数据副本,并将其加载到内存键空间之中,使该Slave节点成为Master节点的镜像节点。
命令转发:Master节点在执行命令时,将该命令转发给自己的Slave节点,以便在运行之中保证Master实例与Slave实例之间数据的一致性。
数据同步
Redis可以通过两种方式开启数据同步:
通过配置文件,在redis.conf配置文件之中,配置
replicaof <masterip> <masterport>
这条配置项,可以设置对应Master节点的地址;如果Master实例需要密码认证,可以通过masterauth <master-password>
配置项来设置连接Master所需要认证密码。执行REPLICAOF命令,命令的格式为
REPLICAOF host port
,通过这条命令,可以使执行这条命令的Redis成为给定地址host
以及port
的另一个Redis实例的Slave节点,不过这条命令有一个缺陷,如果你对应的Master节点需要密码验证,而在redis.conf配置文件之中没有提前设定Master节点的认证密码,那么需要在执行REPLICAOF命令之前,先通过CONFIGSET命令来设置masterauth
的认证密码,否则将无法建立主从实例之间的数据同步。
无论上面哪种建立主从复制的方式,数据同步都不是立即开始的,这两种方式仅是为将要充当Slave的服务器设置了Master的地址,真正开启数据同步则是在设置了Master地址信息之后,Slave通过一系列内部命令来实现的:
通过设置的Master地址信息,建立Slave与Master之间的Socket连接。
Slave向Master发送PING命令以检验二者连接是否正常。
如果Master需要密码认证,则发送AUTH命令验证密码。
Slave会向Master发送一系列的REPLCONF命令,将Slave的信息通知给Master。
Slave向Master发送PSYNC命令,以正式的开启数据同步。
数据同步相当于将Master上键空间之中的全部数据发送给Slave实例,如果看过前面对于RDB持久化的介绍,我们可以发现这其中有很多相似之处,而实际上Master实例与Slave实例之间的数据同步也是通过RDB的机制来实现的。在Redis之中,复制机制中待同步数据的准备与传输方式有两种:
Redis使用正常的方式,启动一个后台子进程来生成RDB文件到磁盘上,等待磁盘上的RDB文件生成结束,在通过网络连接将这个RDB文件发送给Slave节点。
如果Master实例所在的服务器上的磁盘性能很低的话,那么先向磁盘中写入数据再从磁盘进行读取与网络传输的话,会导致数据同步的效率很低。对于这种情况,Redis设计了一种无盘的数据同步方式,也就是不通过磁盘文件,而是在生成RDB文件的过程之中,直接将数据写入与Slave实例所建立的连接的套接字文件描述符上,Slave实例直接通过网络来接收Master发来的同步数据,这样可以大大提升针对单一Slave实例同步的速度。而这得益于Linux操作系统中一切皆文件的设计理念以及Redis自身实现的
rio
通用IO对象数据结构对于IO操作的封装。
虽然采用无盘方式进行数据同步的策略,会提升单一Slave实例的同步效率,免去了磁盘IO所带来的延时,但是这种方式也有一个潜在的问题,便是在多个Slave需要数据同步的时候,无法复用RDB文件。当Master实例正在为一个Slave实例进行无盘的数据同步,如果另外一个Slave实例也来请求数据同步的话,Master不得不进行重复两次RDB的生成过程。那么采用有磁盘的方式来进行数据同步的话,如果在生成RDB文件的过程中,又有一个Slave实例期望与Master实例之间进行数据同步的话,那么我们可以在RDB文件生成结束之后,将这个文件发送给多个等待数据同步的Slave实例,免去了重复生成RDB文件所带来的系统负载。
上面所介绍的数据同步方式,无论是有盘的还是无盘的,其本质都是对Master实例上的数据进行一次全量的备份,Slave实例将网络连接上接收到的同步数据写入一个本地文件之中,当传输接收之后通过rdbLoad
接口使用这个全量的数据备份还原Master上的数据。通过后面我们将要介绍的命令转发,Master与Slave之间可以保持数据上的一致性。但是如果在日常的运行之中,Master与Slave之间的网络连接出现问题,被转发的命令无法传递到Slave实例上,这将导致二者数据的不一致,为了解决这个问题,当Slave与Master直接重新建立连接时,会重新执行一次数据同步以继续维持两者间数据的一致性。早期的Redis也确实使用这种方式在重连时进行全量的数据同步的,但是这其中也存在一个问题,
如果两者之间断线的时间很短呢?是否有必要为了极小的数据差异进行一次全量的数据同步呢?。
在新版本的Redis之中,作者针对这个问题做出了一个优化,也就是引入了增量数据同步的机制,应用这个机制,Redis可以在Master实例与Slave实例数据差异较小的情况下,仅对增量的数据差异进行同步,而无须再进行全量的数据同步,可以大大提升同步的效率。
所谓增量数据同步,其设计思路是:
在Master一侧,
维护一个唯一复制ID
replid
,用于标识当前的Redis实例;维护一个复制偏移
master_repl_offset
,用于记录当前Master实例累积转发命令的计数,每转发一条命令,便会将该命令的长度信息累加到master_repl_offset
这个偏移量上;维护一个有限长度的,先进先出的积压缓冲区
repl_backlog
,当一条命令被转发给Slave实例的时候,也会被写入积压缓冲区之中,同时如果积压缓冲区已满,会将最早被写入的数据清空,以便能够继续写入新的命令。而在Slave一层,代表Master的客户端之中也存储了Master实例同步过来的复制偏移量。
正常请求下,Master实例与Slave实例之中,各自的复制偏移量应该是相同的。一旦某个Slave实例掉线,那么将导致Slave上存储的复制偏移量落后于Master实例中的复制偏移量。如果这部分差异的数据恰好落在Master实例的积压缓冲区里的话,那么在重新连接建立之后,Master只需要将积压缓冲区里对应的数据发送给Slave实例就可以,而无需重新执行RDB的生成过程。只要在第一次建立连接或者重连时两者数据差异过大的时候,才会执行RDB的生成过程。
命令转发
参考前面介绍的AOF持久化策略,可以很好地理解命令转发这一概念。在Master实例与Slave实例完成数据同步之后,Master会在运行过程中将执行的命令发送给Slave对应的client
客户端对象,同时也会将这个命令写入积压缓冲区,以便后续执行增量数据同步的流程。而在Slave端,Master发送来的命令则会被Master对应的client
对象接收,并完成命令的解析与命令的执行,将Master端执行的命令在Slave一层重新执行一次,已完成数据的同步。而对应层次化结构的主从模式,Slave实例在接收到Master发送的命令之后,还会将这个命令转发给自己的Slave实例,也就是Sub-Slave实例,帮助Sub-Slave完成数据的同步。
同步复制
当用户在Master上执行一条写命令时,他可能需要确保这条命令已经被转发到Slave实例之后才可以进行后续的操作。原则上在主从复制机制之中,命令的转发是一个异步的过程,Master默认自己转发给Slave的命令均已经被Slave接收并处理。但是基于前面描述的这个场景,需要Redis能够实现一种对于转发命令的确认机制,因此Redis实现了这个同步复制的功能,来确保Master实例上执行的命令已经被Slave实例所执行。
因此Redis提供了一个WAIT命令,这个命令的格式为:
WAIT <numreplicas> <timeout>
这个命令会阻塞调用该命令的用户,直到该用户所有的写命令已经被至少<numreplicas>
个Slave实例接收并处理,或者等待<timeout>
时间之后,返回超时。
而这个机制它的底层实现则是Master实例与Slave实例之间确认机制:
在Slave实例一端,会以每秒1次的频率向Master发送确认消息
ack
,通报这个Slave当前确认接收的命令数据偏移量。在Master一侧,当有用户执行WAIT命令时,则会向所有在线的Slave广播请求确认消息
getack
,强制要求Slave将自己当前确认的命令数据偏移量通报给Master。
这样一来,Master便可以了解到各个Slave接收命令数据的详细信息。这里又引入了另外一个概念,便是健康的Slave,当某一个Slave实例出现问题,或者与Master之间的网络链接异常导致长时间没有向Master发送确认消息,这样的Slave便称为不健康的Slave;反之在Redis限定的阈值时间内, 能够及时向Matser通报确认消息的Slave便被成为健康的Slave。




