一、背景
公司基于业务发展以及战略部署,需要实现在多个数据中心单元化部署,一方面可以实现多数据中心容灾,另外可以提升用户请求访问速度。需要保证多数据中心容灾或者实现用户就近访问的话,需要各个数据中心拥有一致的全量数据,如果真正实现用户就近读写,也就是实现真正的业务异地多活,数据同步是异地多活的基础,这就需要多数据中心间数据能够双向同步。
二、原生 redis 遇到的问题
1、不支持双主同步
原生 redis 并没有提供跨机房的主主同步机制,仅支持主从同步;如果仅利用 redis 的主从数据同步机制,只能将主节点与从节点部署在不同的机房。当主节点所在机房出现故障时,从节点可以升级为主节点,应用可以持续对外提供服务。但这种模式下,若要写数据,则只能通过主节点写,异地机房无法实现就近写入,所以不能做到真正的异地多活,只能做到备份容灾。而且机房故障切换时,需要运维手动介入。
因此,想要实现主主同步机制,需要同步工具模拟从节点方式,将本地机房中数据同步到其他机房,其他机房亦如此。同时,使用同步工具实现跨数据中心数据同步,会遇到以下一些问题。
(1)数据回环
数据回环的意思是,A 机房就近写入的数据,通过同步工具同步到 B 机房后,然后又通过 B 机房同步工具同步回 A 机房了。所以在同步的过程中需要识别本地就近写入的数据还是其他数据中心同步过来的数据,只有本地就近写入的数据需要同步到其他数据中心。
(2)幂等性
同步过程中的命令可能因断点续传等原因导致重复同步了,此时需要保证同一命令多次执行保证幂等。
(3)多写冲突
以双写冲突为例,如下图所示:
DC1 写入 set a 1,同时 DC2 写入 set a 2,当这两条命令通过同步工具同步到对方机房时,导致最终 DC1 中保存的 a 为 2,DC2 中保存的 a 为 1,也就是说两个机房最终数据不一致。
2、断点续传
针对瞬时的断开重连、从节点重启等场景,redis 为了提高该场景下的主从同步效率,在主节点中增加了环形复制缓冲区,主节点往从节点写数据的同时也往复制缓冲区中也写入一份数据,当从节点断开重连时,则只需要通过复制缓冲区把断开期间新增的增量数据发送给从节点即可,避免了全量同步,提升了这些场景下的同步效率。
但是,该内存复制缓冲区一般来说不会太大,生产目前默认设置为 64M,跨数据中心同步场景下,网络环境复杂,断线的频率和时长可能比同机房更频繁和更长;同时,跨数据中心同步数据也是为了机房级故障容灾,所以要求能够支持更长时间的断点续传,无限增大内存复制缓冲区大小显然不是一个好主意。
下面来看看我们支持 redis 跨数据中心同步的优化工作。
三、redis 节点改造
为了支持异地多活场景,我们对原生 redis 代码进行了优化改造,主要包括以下几个方面:
1、对 RESP 协议进行扩展
为了支持更高效的断点续传,以及为了解决数据回环问题,我们在 redis 主节点中对每条需要同步给从节点的命令(大部分为写命令)增加了 id,并且扩展了 RESP 协议,在每条相关命令的头部增加了形如 #{id}rn 形式的协议。
本地业务客户端写入的数据依然遵循原生 RESP 协议,主节点执行完命令后,同步到从节点的写命令在同步前会进行协议扩展,增加头部 id 协议;非本地业务客户端(即来自其他数据中心同步)写入的数据均使用扩展的 RESP 协议。
2、写命令实时写日志
为了支持更长时间的断点续传,容忍长时间的机房级故障,本地业务客户端写入的写命令在进行协议扩展后,会顺序写入日志文件,同时生成对应的索引文件;为了减少日志文件大小,以及提高通过日志文件断点续传的效率,来自其他数据中心同步过来的数据不写入日志文件中。
3、同步流程改造
原生 redis 数据同步分为全量同步和部分同步,并且每个主节点有一个内存环形复制缓冲区;初次同步使用全量同步,断点续传时使用部分同步,即先尝试从主节点环形复制缓冲区中进行同步,同步成功的话则同步完缓冲区中的数据后即可进行增量数据同步,如果不成功,则仍然需要先进行全量同步再增量同步。
由于全量同步需要生成一个子进程,并且在子进程中生成一个 RDB 文件,所以对主节点性能影响比较大,我们应该尽量减少全量同步的次数。
为了减少全量同步的次数,我们对 redis 同步流程进行改造,当部分同步中无法使用环形复制缓冲区完成同步时,增加先尝试使用日志 rlog 进行同步,如果同步成功,则同步完日志中数据后即可进行增量同步,否则需要先进行全量同步。
四、rLog 日志设计
分为索引文件与日志文件,均采用顺序写的方式,提高性能,经测试与原生 redis 开启 aof 持久化性能一致;但是 rlog 会定期删除,原生 redis 为了防止 aof 文件无限膨胀,会定期通过子进程执行 aof 文件重写,这个对主节点性能比较大,所以实质上 rlog 对 redis 的性能相对于 aof 会更小。
索引文件和日志文件文件名均为文件中保存的第一条命令的 id。
索引文件与日志文件均先写内存缓冲区,然后批量写入操作系统缓冲区,并每秒定期刷新操作系统缓冲区真正落入磁盘文件中。相比较于 aof 文件缓冲区,我们对 rlog 缓冲区进行了预分配优化,达到提升性能目的。
1、索引文件格式
索引文件格式如下所示,每条命令对应的索引数据包含三部分:
- pos:该条命令第一个字节在对应的日志文件中相对于该日志文件起始位置的偏移
- len:该条命令的长度
- offset:该条命令第一个字节在主节点复制缓冲区中累积的偏移
2、日志文件拆分
为了防止单个文件无限膨胀,redis 在写文件时会定期对文件进行拆分,拆分依据两个维度,分别是文件大小和时间。
默认拆分阈值分别为,当日志文件大小达到 128M 或者每隔一小时同时并且日志条目数大于 10w 时,写新的日志文件和索引文件。
在每次循环处理中,当内存缓冲区的数据全部写入文件时,判断是否满足日志文件拆分条件,如果满足,加上一个日志文件拆分标志,下一次循环处理中,将内存缓冲区数据写入文件之前,先关闭当前的索引文件和日志,同时新建索引文件和日志文件。
3、日志文件删除
为了防止日志文件数量无限增长并且消耗磁盘存储空间,以及由于未做日志重写、通过过多的文件进行断点续传效率低下、意义不大,所以 redis 定期对日志文件和相应的索引文件进行删除。
默认日志文件最多保留一天,redis 定期删除一天以前的日志文件和索引文件,也就是最多容忍一天时间的机房级故障,否则需要进行机房间数据全量同步。
在断点续传时,如果需要从日志文件中同步数据,在同步开始前会临时禁止日志文件删除逻辑,待同步完成后恢复正常,避免出现在同步的数据被删除的情况。
五、redis 数据同步
1、断点续传
如前所述,为了容忍更长时间的机房级故障,提高跨数据中心容灾能力,提升机房间故障恢复效率,我们对 redis 同步流程进行改造,当部分同步中无法使用环形复制缓冲区完成同步时,增加先尝试使用日志 rlog 进行同步,流程图如下所示:
首先,同步工具连接上主节点后,除了发送认证外,需要先通过 replconf capa 命令告知主节点具备通过 rlog 断点续传的能力。