Redis 双主实现

jopen 9年前
 

redis双主设计

背景

目前redis仅支持主从复制模式,可以支持在线备份、读写分离等功能,实际应用中通常通过sentinel服务做主从切换的管理,这增加了管理的复杂度和维护成本,基于此360基础架构组联合DBA从redis内部实现了双主功能。

主从复制介绍

redis支持树形的主从异步复制,并具有非阻塞、部分同步等特性,下面简单介绍下其实现原理以及目前redis主从复制模式在故障发生时的数据丢失情况。

实现原理

数据同步

slave开始连接到master时需要同步master已有的数据,具体过程如下图所示,主要有以下几个步骤:

  1. slave向master发送PSYNC命令,将缓存的master的runid和reploff发送给master。

  2. master根据slave发送的reploff判断需要开始同步的数据是否在当前 缓冲区(积压空间)中,如果在的话完成和slave的连接建立并向slave发送CONTINUE回复标志,开始发送积压空间中的数据;否则,向 slave发送FULLSYNC标志并开启子进程dump当前时刻的快照数据到本地rdb文件。

  3. slave接收psync命令的返回值,并判断是CONTINUE还是FULLSYNC,如果是CONTINUE则完成连接过程准备接收master数据;否则,触发接收master dump的rdb文件事件,等待master发送rdb文件。

  4. master将快照数据dump到本地文件后,向slave发送rdb文件

  5. slave接收完master发送的rdb文件后,清空本地库并重新加载接收的rdb文件,由于这时的数据变化较大,如果开启了aof选项则还需进行aof rewrite操作

  6. 开始传播master积压空间中的新数据。

Redis 双主实现

数据传播

master实例会维护其slave实例列表,当有更改操作发生时,其会通过连接建立时创建的socket向所有slave实例发送操作命令进行 数据传播,同时为了防止故障恢复slave重新连接master时每次都进行全量同步,master实例会内部维护一个缓冲区(积压空间)来缓存部分 slave命令,master数据传播的具体过程为:

  1. 写入本地库

  2. 写入积压空间

  3. 触发写入slave实例事件,进行异步传播;如果此刻正在进行数据同步操作则将命令写入缓冲区,待同步操作完成后再触发向slave实例写入事件。

数据丢失

redis主从模式并不能保证数据的100%完整,在网络故障、主库宕机等情况下提升slave为master时可能会丢失部分数据,如下图所 示,假如在某个时刻master的数据还未完全传播给slave时,master宕机等情况发生并将slave提升为master,这时原来master 未传播的一部分数据将丢失。

Redis 双主实现

双主复制设计

前提假设

由于时间、精力等因素,目前我们在进行双主设计时结合如下实际项目需求进行了一些设计折衷,后续我们会继续完善相关设计。

  • 上层保证某个时间点只有一个master在写

  • 故障时允许丢失少量数据

总体设计

为避免额外维护成本,双主模块完全在redis内部实现,双主两个实例各自创建一个socket进行彼此通信;加入双主复制后不会影响原有的主从 复制模式,但如下图所示,主从复制实例可以通过我们新增的doublemasterof命令转化为双主复制,双主复制实例也可以通过原有的slaveof 命令转换为主从复制实例。

Redis 双主实现

同步策略

redis双主实例网络故障恢复或重启等情况下会进行重新连接以同步彼此数据,主从模式下同步策略很简单,只需要从库同步主库数据即可,而双主模 式下我们必须根据一定的策略来选出一个实例作为数据同步的对象,我们考虑到两种具体同步策略:基于数据量和基于时间戳的同步策略。

数据量同步策略

基于数据量的同步策略可以理解为数据量少的实例去同步数据量多的实例,这种同步策略在故障发生时数据已经全部传播到另外一个实例的情况下,故障恢 复后可以保证数据完整性,但如下图所示,假如原来操作在双主A上执行,某一时刻双主A上的操作还未同步到双主B上发生了网络故障,上层会切到B上继续写 入,当写入了图中红色所示大小的数据后网络故障恢复,这时进行数据同步时就有可能丢失A或B上的部分数据。

Redis 双主实现

时间戳同步策略

对于基于时间戳的同步策略,我们会在redis内部维护双主实例的最近更新时间戳,故障恢复进行数据同步时时间戳较旧的实例会同步时间戳较新的实 例;和基于数据量同步策略一样当故障发生时如果数据已经全部传播到另外实例则故障恢复后可以保证数据完整性,否则,如下图所示故障恢复后将丢失双主A实例 的部分数据。

Redis 双主实现

我们的选择

根据业务需求,我们需要保证最新写入的数据不会丢失,所以具体实现上我们选择了基于时间戳的同步策略。

同步实现

我们在redis原有的全同步,部分同步的基础上增加了ignore resync策略以实现双主同步,具体实现如下图所示,有以下几个步骤:

  1. 双主实例A链接到另外一个实例B时会通过发送2mastersync命令,该命令会将A实例最后的更新时间戳发送给B

  2. B实例判断A的最后更新时间戳是否比自己新,如果比自己新则直接将A标示为 ONLINE并向A发送IGNORE runid reploff信息,A接收到IGNORE信息后记录下runid reploff,即可完成与B的同步;否则,则按照主从同步方式进行全同步或部分同步。

Redis 双主实现

数据传播

对一个双主实例的更改操作,redis内部会通过双主实例建立连接时创建的socket异步传播给另一个双主实例,这里要解决的问题是要避免数据 再次传播回来,具体实现上我们通过双主实例的runid进行判断,每个双主实例内部会维护其另外一个实例的runid信息,当有更改命令要执行时,我们会 通过runid来判断该命令是否是其双主实例传播过来的,如果是将不再回传。

复制偏移量维护

redis主从实现机制上,当通信模块接收到主库的更改命令时会直接在从库上增加其复制偏移量来记录数据同步的位置,而对于双主实例我们知道为避免数据循环传播,双主实例A传播给双主实例B的命令不会回传过来,那么该如何维护其复制偏移量呢?设计上我们考虑了两种策略:

策略一

如下图所示,双主A向双主B传播一条数据后,B会回复A一个ACK length确认,A接收到确认信息后将自己的复制偏移量增加length。

Redis 双主实现

策略二

如下图所示,双主A再向B传播数据之前自己主动增加复制偏移量,双主B不会向双主A回复确认信息

Redis 双主实现

策略一对比策略二进一步保证了数据的完整性,但同时带来了一定的网络开销,两种策略都不能完全保证复制偏移量再网路故障下的正确性(策略一在ACK丢失的情况下无法保证复制偏移量正确),结合目前的需求为了不影响性能我们选择了策略二。

小结

结合目前项目的需求我们在redis内部实现了双主功能,但是也有一些需要改进的地方,欢迎大家提出意见,后面我们会不断完善,敬请关注!