RedisSyncer 同步引擎的设计与实现
RedisSyncer 是一款通过 replication 协议模拟 slave 来获取源 Redis 节点数据并写入目标 Redis 从而实现数据同步的 Redis 同步中间件。 该项目主要包括以下子项目:
redis 同步服务引擎 redissyncer-server
redissycner 客户端 redissyncer-cli
redis 数据校验工具 redissycner-compare
基于 docker-compse 的一体化部署方案 redissyncer
本文主要介绍 reidssyncer 引擎(既 redissyncer-server)的设计与实现,以及引擎运行的机制。
同步流程
原生 redis master slave 模式主要分为两个阶段,第一个阶段同步 rdb 镜像,也就是全量同步部分;全量同步完成后进入命令传播模式,每个执行成功的数据变更操作会同步给 slave 节点。redissyncer 的模拟了这一机制并将两部分拆解,既可以执行完整同步任务也可以单独执行全量或增量同步。
建立 socket
发送 auth user password (6.0 新增 user)
send->ping
发送从节点端口信息
发送从节点 IP
发送 EOF 能力(capability)
发送 PSYNC2 能力
发送 PSYNC
PSYNC ---> 启动 heartbeat
rdb 镜像同步完成后进入命令传播,master 会不断将变化数据推送给 slave。为了保证 RedisSyncer 内部有断点续传、数据补偿、断线重连等机制来保证数据同步过程中稳定性和可用性,具体的机制如下。
断点续传机制
RedisSyncer 的断点续传机制是基于 Redis 的 replid 和 offset 来实现的,RedisSyncer 有两个版本的断点续传机制 v1 和 v2。
v1 版本:
v1 版本数据写入到目的端 redis 后,将 offset 持久化到本地,这样下次重启就从上次的 offset 拉取。但是由于该方案写目的端的操作和 offset 持久化不是一个原子的操作。如果中间发生中断会导致数据的不一致。 例如,先写入数据到目的端成功,后持久化 offset 还没成功就发生了宕机、重启等情况,那么再次断点续传拉取上一次的 offset 数据最后就不一致了。
v2 版本:
在 v2 版本策略中 RedisSyncer 会将每一个 pipeline 批次中不存在事务的的命令通过 multi 和 exec 进行包装,并在事务尾部插入 offset 检查点。 当断点续传时需要从目标 Redis 的所以 db 库中查找 checkpoint 并找到所对应源节点当最大 offset,再根据该 offset 进行断点续传。目前 v2 版本只支持目标为单机 Redis 的情况。 在 v2 版本中
v2 命令事务封装结构
v2 checkpoint 检查点结构:
在 Redis 的事务机制中虽然不支持回滚,并且如果事务中间命令执行出错后但是事务还是被执行完成,但是除特殊情况外能够保证一致性。 在 v2 的机制中,为了防止'写放大'会在目标 redis 的每一个逻辑库中写入一个 checkpoint,因此在执行断点续传操作的时候,同步工具会先扫描目标各个逻辑库中的 checkpoint 并选出里面最大 offset 的 checkpoint 作为断点续传的参数。
数据补偿机制
在数据同步过程中,存在由于网络稳定性或其他因素导致 key 写入失败的情况,为此 redissyncer 实现了一套补偿机制来保证源端与目的端数据的一致性。 数据补偿的前提是命令写入的幂等性,因此在 RedisSyncer 中会先将 INCR、INCRBY、INCRBYFLOAT、APPEND、DECR、DECRBY 等部分非幂等命令转换成幂等命令后再写入目标端 Redis。 RedisSyncer 在目标为单机 Redis 或者 Proxy 的时候是通过 pipeline 机制将数据写入到目标 Redis 中的,每一个批次的 pipeline 的提交会返回一个结果列表, 同步工具会验证 pipeline 中结果的正确性,如果部分命令写入失败,同步工具对该批次与该 key 相关的命令进行重试。 如果重试超过指定的阀值,将会宕掉任务。对于存在大 key 的 list 等非幂等结构,将不会进行数据补偿,强制结束任务待人工处理。
断线重连机制
由于网络抖动等原因可能会导致同步工具源端与目标端连接在同步过程中断开,因此需要断线重试机制来保证在任务同步的过程中如果出现异常断开的问题。断线重连机制存在于与源 Redis 节点和 RedisSyncer、RedisSyncer 与目标 Redis 节点的连接之间,两者分别有各自的处理机制。
源端重连机制
源 Redis 与 RedisSyncer 的断线重连机制是通过记录的 offset 来实现的,当因网络异常等原因断开了连接时,RedisSyncer 会重新尝试与源 Redis 节点建立连接,并通过当前任务记录的 runid、offset 等信息去拉取断开之前的增量数据,连接重新建立成功后 RedisSyncer 的同步任务将会无感知继续同步。当断线重连超过指定重试阀值或者因为 offset 刷过导致没有办法续传数据时,RedisSyncer 会宕掉当前当同步任务,等待人工干预。
目标端重连机制
RedisSyncer 与目标 Redis 之间的断线重连机制是通过缓存上一批次的 pipeline 的命令来实现的,当连接断开异常时 RedisSyncer 进行重重连回放上一批次写入失败的命令。当回放失败或者超过连续重试次数 RedisSyncer 会宕掉当前当同步任务,等待人工干预。
命令的链式处理
RedisSyncer 中采用链式策略处理同步数据,任何一个策略返回失败,该 key 都将不会被同步。链式策略流程如图所示
每一个 key 在 RedisSyncer 都会经过一个策略链进行处理,只要有一个策略未通过则这个 key 将不会同步到目标 Redis,比如 key 过期时间的计算策略如果计算出全量阶段 key 已过期,则将会自动抛弃该 key。
策略链中的策略包括
任务管理
任务启动流程
任务停止及清理流程
任务主动停止时,RedisSyncer 会先停止源 Redis 端的数据写入然后进入数据保护状态,确保可能还处在 RedisSyncer 中未写入目标的少部分数据能够完整的写入目标端,并且正确的记录写入的最后一条数据的 offset 并持久化,保证断点续传时 RedisSyncer 能够提供正确的 offset。
任务状态
任务异常处理原则
在 RedisSycner 任务中如果遇到可能会导致数据不一致的错误,RedisSyncer 都会宕掉任务,等待人工干预。
rdb 跨版本同步实现
rdb 文件存在向前兼容问题,即高版本的 rdb 文件无法导入低 rdb 版本的 Redis
跨版本迁移实现机制
对于可能存在大 key 的结构比如:SET,ZSET,LIST,HASH 等结构:
对于其他命令如:String 等结构: 为保证其命令幂等性,命令解析器会根据目标 REDIS 节点的 RDB 版本进行序列化(实现 DUMP),传输模块会使用 REPLACE 反序列化到目标节点。(其中在 redis3.0 以下版本 REPLACE 命令不支持[REPLACE])
对于对数据成员没有顺序性要求的命令如:SET,ZSET,HASH 命令解析器将其解析成一个或多个 sadd,zadd,hmset 等命令进行处理
对于对数据成员有顺序性要求的命令如:List 等命令,若被命令解析器判断为大 key 并将其拆分为多个子命令,此时必须保证按顺序发送至目标 REDIS 节点
REDIS 跨版本间存在的问题: 由于 REDIS 是向下兼容(低版本无法兼容高版本 RDB),在其 RDB 文件协议中存在一个 vesion 版本号标识,REDIS 在 RDB 导入或者全量同步执行 rdbLoad 时会先检测 RDB VERSION 是否符合向下兼容,如果不符合则会抛出 Can’t handle RDB format version 错误。
syncer 跨版本实现机制 对于全量同步 RDB 数据部分 syncer 将其分命令为两类进行处理
RDB 文件协议中关于 RDB VERSION 部分
关于 RDB VERSION 检查部分伪代码
RDB 同步过程中的大 Key 拆分
RedisSyncer 在全量同步阶段在遇到 LIST、SET、ZSET、HASH 等结构等时候,当数据大小超过阀值后 RedisSyncer 会通过迭代器的形式将 key 拆分成多个子命令写入目标库。防止部分超大 key 一次性读入内存导致程序产生 oom 并提高同步的速度。而对于不存在大 key 的命令同步工具会通过序列化逆序列化的形式写入目标。
附录一 Redis RDB 协议
redis RDB Dump 文件格式
附录二 Redis RESP 协议
Redis RESP 协议
RESP 协议是在 Redis 1.2 中引入的,但它成为了 Redis 2.0 中与 Redis 服务器通信的标准方式。是在 Redis 客户端中实现的协议。 RESP 实际上是一种序列化协议,它支持以下数据类型:简单字符串、错误、整数、批量字符串和数组。
RESP 在 Redis 中用作请求-响应协议的方式如下:
客户端将命令作为批量字符串的 RESP 数组发送到 Redis 服务器。
服务器根据命令实现以其中一种 RESP 类型进行回复。
在 RESP 中,某些数据的类型取决于第一个字节:
对于简单字符串,回复的第一个字节是“+”
对于错误,回复的第一个字节是“-”
对于整数,回复的第一个字节是“:”
对于批量字符串,回复的第一个字节是“$”
对于数组,回复的第一个字节是“ *”
RESP 能够使用稍后指定的批量字符串或数组的特殊变体来表示 Null 值。在 RESP 中,协议的不同部分总是以“\r\n”(CRLF)终止。
RESP Simple Strings
'+' 字符开头,后跟不能包含 CR 或 LF 字符(不允许换行)的字符串,以 CRLF 结尾(即“\r\n”)。如:
RESP Errors
如:
RESP Integers
Integers 只是一个 CRLF 终止的字符串,代表一个整数,以“:”字节为前缀。 例如
Bulk Strings
用于表示长度最大为 512 MB 的单个二进制安全字符串。批量字符串按以下方式编码:
“$”字节后跟组成字符串的字节数(前缀长度),以 CRLF 结尾。
实际的字符串数据。
最后的 CRLF。
“foobar”的编码如下:
当字符串为空
Bulk Strings 还可以用于表示 Null 值的特殊格式来表示值不存在。在这种特殊格式中,长度为 -1,并且没有数据,因此 Null 表示为:
RESP Arrays
格式:
一个'*'字符作为第一个字节,然后是数组中元素的数量作为十进制数,然后是 CRLF。
Array 的每个元素的附加 RESP 类型。 空数组表示为:
“foo”和“bar”的数组表示为
["foo",nil,"bar"] (Null elements in Arrays)
作者: 贾世闻 展恩强
版权声明: 本文为 InfoQ 作者【京东科技开发者】的原创文章。
原文链接:【http://xie.infoq.cn/article/d9327ae1f94c55e9586b6887d】。文章转载请联系作者。
评论