如何基于Redis Replication设计并实现Redis-replicator?

2017-12-28 10:04:36来源:http://www.infoq.com/cn/articles/Redis-Replication-Redis-rep作者:InfoQ人点击

分享

本文整理自 Redis 技术交流群的线上分享活动,有对 Redis 技术感兴趣的朋友可以加群主微信 gnuhpc 加入。


文章主要内容围绕 Redis-replicator 的设计与实现,提纲如下:


Redis-replicator 的设计动机
Redis-replicator 的设计与实现
Redis Replication 的协议简析
设计可插拔式 API 以及开发中的取舍
总结
Q&A

首先,有两个材料可能需要大家提前预习一下,以便更轻松地了解此次分享的内容。


https://redis.io/topics/protocol
https://github.com/leonchen83/redis-replicator/wiki/RDB-dump-data-format
1. Redis-replicator 的设计动机

在之前的开发中,经常有如下需求:


Redis 数据的跨机房同步
异构数据的迁移,比如 Redis 到 MySQL、MQ
1.1 Redis 数据的跨机房同步

Redis 跨机房同步,传统的方式通常采取双写的方式,这样会生产一种非常难以维护的用户代码。稍微好一点的做法是提炼出一个中间层。但也难以保证同时双写成功,因此又需要做复杂的异常处理,这同时也增加了程序的响应时间。


除了双写的方式,还有一种方式是利用 Redis 自身的 Replication 协议,让一台机器成为另一台机器的 slave,用这种方式来同步数据。


这种方式的问题是,双机房中必须有一个是 master,一个是 slave。在切换的过程中,需要作 slave 提升等处理,变相增加了运维难度。而且一般在集群环境中,用户常常期望两个机房各一个独立集群,而不是两个机房组成一个混合集群(这样出问题切换方便些),并且保持两个独立集群之间数据是同步的。


如下图所示:





1.2 异构数据的迁移

上面是属于同构数据迁移,再来说说异构数据迁移。现实需求中,有可能会有异构迁移的情况,比如 Redis 每日数据量很大,需要把一些数据以文件或者数据库存储的方式落盘(MySQL、MQ、SSDB 等),每日异地备份等等,如果还是采用双写等方式处理的话,又会有代码扩张、维护困难等上述提过的问题。


如下图所示:





1.3 如何用 Redis-replicator 来实现需求

在以上的需求中,催生了我开发 Redis-replicator 的动机。 这个工具完整实现了 Redis Replication 协议,并把 RDB 以及 AOF 解析成一个一个的事件供用户消费,并且支持 Redis4.0 的新特性以及新命令。


如果用 Redis-replicator 来实现上述需求的话,可以不干扰用户态的代码,单独用这个工具实现中间件来进行异构,同构数据同步备份等任务。


如下图所示:





2. Redis-replicator 的设计与实现
2.1 Redis-replicator 的架构图

那么讲完了动机,我们可以探寻一下 Redis-replicator 的实现。Redis-replicator 的架构如下所示 :





2.2 Redis-replicator 的样例代码

通用的代码如下:


Replicator replicator = new RedisReplicator("redis://127.0.0.1:6379");
replicator.addRdbListener(new RdbListener.Adaptor() {
// 解析 RDB 事件
@Override
public void handle(Replicator replicator, KeyValuePair<?> kv) {
System.out.println(kv);
}
});
replicator.addCommandListener(new CommandListener() {
// 解析 AOF 实时命令
@Override
public void handle(Replicator replicator, Command command) {
System.out.println(command);
}
});
replicator.open();


这里稍微对代码做一下解释,首先是 Redis 的 URI 表示redis://127.0.0.1:6379
,这种表示通过 socket 进行在线的实时数据同步,不但支持在线实时同步,而且 Redis-replicator 也可以进行离线的 RDB 以及 AOF 文件的解析,相应的 URI 修改为redis:///path/to/dump.rdb
或redis:///path/to/appendonly.aof
,其余的代码保持不变。


RdbListener 表示监听 RDB 事件,CommandListener 表示监听 AOF 事件。所以我们可以仅仅更改 URI 来做到远程同步和文件解析之间的自由切换。


2.3 Redis-replicator 的源码目录结构及源码导读

在对架构和样例代码有一定了解之后,我们来了解一下源码的目录结构和一些关键的 class。


源码结构如下图所示:






上图中 cmd 包和 AOF 事件相关,比如在同步完 RDB 数据之后 master 写入了一条这样的命令set foo bar
,就会产生一条 Command 并触发 CommandListener。(重点类有 Command、CommandParser、CommandListener、ReplyParser)


event 包包含了 RDB 事件与 AOF 事件的基类 Event,以及包含两个自定义事件 PreFullSyncEvent 和 PostFullSyncEvent,这两个自定义事件标记了全量数据同步的开始和结束(增量同步不触发这两个标记事件)。


io、net、util 包与 Redis-replicator 的网络传输以及内部用数据结构相关,不多做介绍。


rdb 包和 RDB 事件相关,会把 RDB 的数据流解析成一个一个 KeyValuePair 并触发 RdbListener。同时这个包也包含了 Module 解析和自定义 RDB 解析器相关的类。(重点的类有 KeyValuePair、Module、ModuleParser、RdbVisitor、RdbParser)


还有根目录下的一些重点类:ReplicatorListener 包含用户所有可以注册的监听器,Configuration 包含一切可配置的参数,Replicator 是实现 Replication 协议的重要接口。


3. Redis Replication 的协议简析

讲到这里,就再仔细说一下 Redis Replication 协议,很多同学以为这个协议很复杂,实现起来很困难。但实际上如果仔细了解这个协议的话,即使用 Java 这种略臃肿的语言,在 3000 行内也可以实现一个完整的同步协议(Redis-replicator 第一版 5000 行代码)。我鼓励大家也去用不同语言来实现 Redis 的同步协议,以丰富 Redis 的工具链。



具体的协议格式是一个非严格(这里的非严格是指 AOF 的格式有可能不是标准格式,因为有可能在两个 AOF 命令之间插入/n
)的 AOF 格式,第一个 AOF 是同步命令的回复,第二个 AOF 命令很特殊,是一个 RESP Bulk String,其内包含了 RDB 格式。


其余的 AOF 就是 master 的实时命令。了解 AOF 格式的话请参照 https://redis.io/topics/protocol,关于增量同步还是全量同步返回的格式也有不同,如下图所示:





3.1 第一个 AOF


第一个 AOF 是同步命令的回复,在同步之前我们要发送同步命令,比如 2.8 版本之前我们要发送SYNC
, 2.8 之后我们要发送PSYNC repl-id repl-offset
开启 PSYNC 同步,repl-id 占 40 字节,不知道 repl-id 的情况下发送?
, repl-offset 表示同步的 offset,不知道 offset 的情况下发送-1
,回复的话有可能是如下形式:+FULLRESYNC repl-id offset/r/n
或者+CONTINUE/r/n
或者 Redis-4.0 引入的 PSYNC2 回复+CONTINUE repl-id/r/n


3.2 第二个 AOF


上面我们说第二个 AOF 是一个 RESP Bulk String,那么其符合$payload/r/nRDB
(注意结尾没有/r/n
) 这样的形式,payload 表示要传输的 rdb 大小,内容的话就是一个完整的 RDB 文件。


关于 RDB 文件的格式,我做了一个 RDB data format wiki 供大家详细了解,在此不做赘述。



https://github.com/leonchen83/redis-replicator/wiki/RDB-dump-data-format



稍微需要注意的是,如果 redis-server 开启了repl-diskless-sync = yes
那么这个格式会稍有变化。



在https://redis.io/topics/protocol
文档中 RESP Bulk String 还有一种没有提到的格式用在同步协议中,$EOF:<40 bytes delimiter>/r/nRDB<40 bytes delimiter>
,此时的 payload 变成EOF:<40 bytes delimiter>
所以在实现同步协议的时候需要注意。



第二点需要注意的是如果 master 产生的 RDB 特别巨大的时候,在同步 RDB 之前会发送连续的/n
以此来维持与 slave 的连接。所以同步的数据流有可能是这样的:



+FULLRESYNC8de1787ba490483314a4d30f1c628bc5025eb761 2443808505/r/n/n/n/n/n/n/n$payload/r/nRDB<其他 AOF 命令>


3.3 其他的 AOF


参照https://redis.io/topics/protocol
进行解析。


3.4 心跳
在 RDB 传输完之后如果 master 端没有其他要同步的命令时,master 会定时发送PING
命令给 replicator。
在 RDB 传输完之后 replicator 要记录传输的字节数当作 offset,定时给 master 发送REPLCONF ACK offset

4. 设计可插拔式 API 以及开发中的取舍
4.1 设计可插拔式 API

我们从第二节的代码中可以用很简单的方式与 Redis master 实现同步,这小节我们主要讲 Redis-replicator 的扩展性,从以下几个方面来详细说明:


当 Redis-server 版本升级到比如 4.2,有 STREAM 相关的新命令时如何扩展
当处理比如超过本机内存的大 KV 如何扩展
当加载 Redis-4.0 新特性 Module(比如 rejson)时如何扩展

先讨论第一点,当升级 Redis-server 有新的命令而 Redis-replicator 不支持时,可以使用命令扩展。


写一个命令解析器并注册进 Redis-replicator 中即可 handle 新的命令。一个详细的例子在 CommandExtensionExample,



https://github.com/leonchen83/redis-replicator/blob/master/examples/com/moilioncircle/examples/extension/CommandExtensionExample.java


再讨论第二点,由于 Redis-replicator 默认是把 KV 完全读到内存再交由用户处理的,当处理比如超过本机内存的大 KV 时,会引发 OOM。一个比较好的方法是以迭代的方式来处理大 KV。


在 Redis-replicator 中,可以注册自己的 RDB 解析器来应对这种情况,一个好消息是此工具已经内置了处理大 KV 的 RDB 解析器 ValueIterableRdbVisitor ,



https://github.com/leonchen83/redis-replicator/blob/master/src/main/java/com/moilioncircle/redis/replicator/rdb/iterable/ValueIterableRdbVisitor.java


与此相关的例子在 HugeKVSocketExample ,



https://github.com/leonchen83/redis-replicator/blob/master/examples/com/moilioncircle/examples/huge/HugeKVSocketExample.java


再讨论第三点,加载自定义 Module 时,可以实现自定义的 Module parser 并注册到 Redis-replicator 中,实现 Module 扩展,一个相关的例子在 ModuleExtensionExample,



https://github.com/leonchen83/redis-replicator/blob/master/examples/com/moilioncircle/examples/extension/ModuleExtensionExample.java


总结设计可插拔式 API 的重点是要求平等对待内建 (built-in)API 和外部 API。Redis-replicator 只提供了一个同步协议的大框架,其内的命令解析、RDB 解析、Module 解析都是可插拔的,这样可以提供最大的灵活性给用户。


4.2 开发中的取舍
4.2.1 无绪


最近我读完一本书很有启发,书名叫《软件框架设计的艺术》,书中提到了一个叫无绪
的概念,大意是当你依赖一个库,可以不用深入了解这个库的内部实现,就可直接根据 API 上手使用,并做出相对可靠的应用程序。


对这个概念我深以为然,但是这本书是我写完 Redis-replicator 之后才读到的,有一些不一致为了兼容性已经不可更改(有兴趣的朋友可以找一找代码存在的问题),但总体上根据 Redis-replicator 提供的文档以及 example 和对 issue 的快速回应以及修改可以让依赖此库风险可控。


4.2.2 兼容


同样还是《软件框架设计的艺术》这本书,提到了一个兼容性问题。书中有一句话:API 就如同恒星,一旦出现,便与我们永恒共存
。大意就是一个 API 在被用户发现并使用了之后,就尽量不要做不兼容的修改,做出不兼容修改用户升级时会产生运行时错误等等问题,降低用户对一个库的好感度。我举一个在 Redis-replicator 中存在的例子。


用户实现自己的 RDB 解析器时需要继承 RdbVisitor 这个类,



https://github.com/leonchen83/redis-replicator/blob/master/src/main/java/com/moilioncircle/redis/replicator/rdb/RdbVisitor.java


这个类如果被设计成接口, Redis 每增加一个存储结构,这个接口就要增加一个方法,即使用户没用到这么高版本的 Redis 也要对实现类进行修改。设计成抽象类的话,每次升级 Redis-replicator,不会对用户代码造成影响,仅仅在同时升级了 Redis-server 的时候才会出现异常。


4.2.3 依赖

开发基础库上选择依赖一定要更加谨慎。因为 Java 的 jar hell 等原因,在一个稍微复杂的系统中,出现循环依赖,以及依赖同一个包的不同版本这种情况会经常发生。比如在一个工程中经常有多个版本的 slf4j-api、netty。在不实际运行的话很难发现问题。


第二点就是在设计公共库涉及写日志时,最好不要依赖具体的 log 实现,要尽量依赖 log 的 API(commons-logging、slf4j-api 等)。一个不好的例子是:


<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.11</version>
</dependency>

这个包经常用在 ZooKeeper 客户端中比如 curator-client,然而这个包依赖了一个很低版本的 log4j 实现库,导致实际应该依赖 log 的 API 变成依赖于 log 的实现库,如果用户选择的是 logback 这种实现库来写日志的话,会有一些冲突,需要各种桥接来做 work around。在 Redis-replicator 中,唯一依赖的 jar 包是 commons-logging,尽最大程度保证用户与自己的工程依赖的兼容性。


5. 总结

限于篇幅和重点,并没有展现 Redis-replicator 的全部功能,比如此工具还可以做 RDB 及 AOF 文件的拆分与合并,RDB 格式转 Redis 的 dump 格式 (和 dump 命令得到的格式一致),以及 RDB 与 AOF 文件的备份和 Redis-4.0 混合格式的支持等。欢迎关注并 star Redis-replicator。


6. Q&A
Q: 这个后续的开发计划是什么?


A: 后续计划是支持redis-4.2,以及增加failover能力和jdk9支持。


Q: Redis-replicator性能怎么样?


A: 性能比唯品汇C语言实现的Redis迁移工具略慢,纯解析大概能达到80%左右的性能。但是因为这个工具的速度也取决于消费事件的速度,如果消费慢的话,会阻塞Redis-server或者Redis-server给主动断开连接。


Q: 我提问的问题可能跨过此中间件的本身了,我更关注于场景。对于跨机房的场景,平均数据延迟有多大,在多大数据量的情况下如何保证延迟降到最低?在出现网络抖动的情况如何避免数据的丢失?


A: 跨机房平均延迟的话有很多相关性,不仅仅和Redis-replicaor相关,还和网络速度等等因素相关。这个工具能尽量做到解析不会影响跨机房同步。再来说出现抖动的情况, 在Redis-server端参数配置合理的情况下,如果出现网络抖动,那么Redis-replicator会尽量采取部分同步来进行重试,如果在Redis-server的backlog之外的话,会全量同步重连。


Q: 可以基于AOF文件合并生成RDB文件吗?对内存的占用是否会造成OOM问题?


A: AOF文件转RDB这个工具还做不到,但反过来自己扩展一些代码可以做到RDB转AOF。在分享中特意有这个OOM的实践,因为有用户成功用这个工具同步8GB单KV,单实例30GB的Redis,就是因为可以自定义RDB解析器,把KV转成迭代的方式减小占用内存。


Q: 消费慢,有统计过过达到多大的并发量?以及当消费慢对redis性能影响有数据统计吗?


A: 消费慢的行为和Redis slave的行为一致,比如某台Redis slave消费慢,有可能产生无限重连的情况,这里redis-replicator和slave的行为是保持一致的,可能需要调整一些参数比如repl-backlog-size、repl-backlog-ttl、repl-ping-slave-periods。


Q: 有因为各种原因中断后进行retry的功能吗?


A: 有断线重联,而且是尽量以避免全量同步的方式重联。并且有标记event来监测到是否是全量同步。


作者介绍


陈宝仪,从事十多年一线软件开发。现任耐克斯托普高级软件工程师,攻城狮朋友圈发起者之一。关注 Redis、Java 后端开发。



感谢雨多田光对本文的审校。


最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台