单机redis不能满足分区容错,当主机发生单点故障的时候,redis服务就无法访问;如果主机的磁盘设备损坏,甚至有丢失数据的风险。可以通过主从结构来避免单点故障的问题,当主机不可用的时候,从机仍然保持着数据的备份。必要时,可以将从机升级为主机来对外提供服务。
主从结构的需要关注两个重要问题分别是:主从数据的一致性和主从机器的存活状态。本文主要关注主从数据的一致性,即主从复制。
redis主从结构的建立是通过在从机服务器执行SLAVEOF命令实现的,该命令让从服务器连接主服务器,并去复制主服务器的数据。下文皆以master和slave分别指代主、从服务器。
1.复制功能的实现 (redis2.8以前)
复制功能包含两个阶段: 同步和命令传播
- 同步的作用:将slave的数据库状态更新至master的当前状态
- 命令传播的作用: slave更新状态更新后,master接收到新的客户端请求导致master状态改变时,重新使slave更新到与master一致的状态。
1.1 同步的过程
- slave 向 master 发送 SYNC 命令
- master收到slave的SYNC命令, 执行BGSAVE操作,在后台生成数据快照RDB文件, 同时使用一个复制积压缓冲区来记录从生成快照开始,执行的来自于客户端的写命令。
- BGSAVE完成,master 将生成的RDB文件发给slave。slave接收到该文件之后载入文件,更新自己的数据库,使数据同步到与 master 在 BGSAVE 开始时相同的状态。
-
master将缓冲区内的所有写命令发送给 slave, slave执行这些命令,更新自己的数据库,让自己更新至当前master所处的状态
1.2 命令传播
同步过程完成后,master收到来自客户端的写请求,执行后会出现主从状态不一致的情况。为了使二者一致,master需要将这些命令发送到slave,slave执行完这些命令,两者重新回到一致的状态。
1.3 旧版本复制的缺点
每当slave连接master的时候,都需要完成同步,同步通过BGSAVE生成master中全量数据的快照,然后将快照发送给slave。这在slave初次连接到master的时候是有必要的,但是重连情况下,真的有必要全量复制吗?
slave与master断开连接期间,如果master执行的写入命令很少,为了同步这少量的数据,做全量同步是不划算的一件事。因为BGSAVE需要在后台耗费大量的CPU、内存和磁盘IO资源,同时将全部数据发送给slave需要占用较大的网络带宽和流量,影响master对客户端请求的响应。
那么能不呢在开销较小的情况下将这些增量数据同步至重连的客户端呢?有,新版本的复制支持部分重同步就是做这件事的。
2.新版本复制功能
新版本复制功能是通过PSYNC实现的,它具有完整重同步和部分重同步两种模式。
- 完整重同步和初次同步SYNC功能相同,slave发送命令给master,由master生成RDB文件,同时在缓冲区内保存写命令,将RDB文件和写命令发送给slave。主要用于处理初次连接时数据同步。
- 部分重同步用于处理重连时连接断开期间master执行写入命令的同步。当然,如果产生数据量过大,导致master内存不下这么多写入命令,那么仍然要通过全量同步。
思考一下为了同步在连接丢失期间内master的写入数据,我们需要那些信息?
- 首先需要一个容器,记录master的写入命令。 对这个容器有以下要求:第一,不能无限大,否则写入命令就能把内存给占满了;第二,既然容量有限,那么应当优先存放最近写入的命令。基于这两个要求,可以使用有限队列,由于FIFO的特性,当队列满了之后,将头部节点给remove掉,然后在尾部添加新的数据。redis由复制积压缓冲区来承担该角色。它是一个默认大小1MB的FIFO队列。
- 需要master和slave在恢复连接时,二者分别执行到的最后最后一条命令在队列中的位置。redis通过复制偏移量(offset)来完成。
- master每次向slave发送N byte的数据时,将自己的offset 加 N
- slave每次收到master发送来的N byte数据时,将自己的offset 加 N
- slave向master请求重连时,master如何能保证该slave之前就是从自己这里同步的数据呢?显然需要一个身份凭证,redis通过运行Id来完成校验。即slave内需要保存master的运行Id,重连时,带上该Id,master校验这是自己的id,然后才能向slave发送增量数据,否则将其视为需要全量同步。
2.1 复制积压缓冲区
redis内复制积压缓冲区结构如图:它包含了每个字节的值和其对应的偏移量。
2.2服务器offset
服务器内offset如下图: master与slave最近一次命令传播之后,双方offset都在10086. 之后,slave A与master断开连接。在连接断开期间,master收到了新的命令, 之后又向slave传播了33字节数据。此时各服务器的offset状态如图。
假设此时slave A请求重连, 那么他需要向master发送重连请求,并带上自己的offset和内部保存的master运行Id。master收到请求后,校验运行id,然后从buffer内查找slave offset + 1是否在buffer内,如果不在,说明这段时间内请求较多,已经将这个数据给挤出了缓冲区,那么此时就不能进行部分同步,因为这样必然要丢失数据。如果在,那么将slave offset + 1 到 master offset的数据发送给slave即可。
2.3 PSYNC命令(partial sync)
PSYNC有两种调用方式:
- 如果服务器执行过 SLAVEOF no one 或没有复制过任何服务器, 那么他向master 发送PSYNC ? -1 命令,请求master进行完整重同步。
- 如果该服务器复制过某个master, 则他向master发送 PSYNC <runid> <offset>命令请求部分重同步, runid为上次连接master的运行ID, offset为slave的当前偏移量。
主服务器收到PSYNC后,有两种正常返回值和异常返回:
-
+FULLRESYNC <runid> <offset> (完整重同步)和 +CONTINUE (部分重同步)。 完整同步的话,slave需要保存runid和offset,在RDB数据载入数据库之后需要将自己的初始化偏移量更新为该offset;部分同步则只需要等待master将自己缺失的部分数据发送过来,然后将offset + N。
如果为-ERR回复,表示master版本低于Redis2.8,无法识别PSYNC请求
同步之后,命令传播就和旧版本如出一辙了,在此不作赘述。
总结
- 主从主要解决的问题: 单点故障容错,当然也可以做读写分离,不过似乎没什么必要,因为可以做集群,分散每台master的读写压力。
-
- 主从结构如何实现数据复制
- 旧版本: SYNC + 命令传播。缺点在于每次重连都需要完整同步,而完整同步需要较大的硬件资源开销。
- 新版本: PSYNC + 命令传播。重连时通常不希望完整同步,只需要同步在连接断开期间master的写入命令即可。此时通过复制缓冲区和主从双方的offset即可判断是否可以从buffer中取出断开期间的全部写入命令,可以的话则只需要将buffer中[slave offset + 1 , master offset]区间内的数据发送到slave即可。