作者: 贾世闻 展恩强
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)
OK 胜利
其余 error
send->ping
返回:
ERR invalid password 明码谬误
NOAUTH Authentication required. 没有发送明码
operation not permitted 操作没权限
PONG 明码胜利
作用:检测主从节点之间的网络是否可用。查看主从节点以后是否承受解决命令。
发送从节点端口信息
REPLCONF listening-port <port>
-->OK 胜利
--> 其余 失败
发送从节点 IP
REPLCONF ip-address <IP>
--> OK 胜利
--> 其余 失败
发送 EOF 能力(capability)
REPLCONF capa eof
--> OK 胜利
--> 失败
作用:
是否反对 EOF 格调的 RDB 传输,用于无盘复制,就是可能解析出 RDB 文件的 EOF 流格局。用于无盘复制的形式中。redis4.0 反对两种能力 EOF 和 PSYNC2
redis4.0 之前版本仅反对 EOF 能力
发送 PSYNC2 能力
REPLCONF capa PSYNC2
--> OK 胜利
--> 失败
作用:
通知 master 反对 PSYNC2 命令 , master 会疏忽它不反对的能力. PSYNC2 则示意反对 Redis4.0 最新的 PSYN 复制操作。
发送 PSYNC
PSYNC {replid} {offset}
--> FULLRESYNC {replid} {offset} 残缺同步
--> CONTINUE 局部同步
--> -ERR 主服务器低于 2.8, 不反对 psync, 从服务器须要发送 sync
--> NOMASTERLINK 重试
--> LOADING 重试
--> 超过重试机制阈值宕掉工作
读取 PSYNC 命令状态,判断是局部同步还是残缺同步
PSYNC —> 启动 heartbeat
REPLCONF ACK <replication_offset>
心跳检测
在命令流传阶段,从服务器默认会以每秒一次的频率
发送 REPLCONF ACK 命令对于主从服务器有三个作用:作用:
检测主从服务器的网络连接状态;辅助实现 min-slaves 选项;检测命令失落。REPLCONF GETACK
->REPLCONF ACK <replication_offset>
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 检查点构造:
HASH hset redis-syncer-checkpoint {value}
{value}:
* {ip}:{port}-runid {replid}
* {ip}:{port}-offset {offset}
* pointcheckVersion {version}
在 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 局部
REDIS RDB 文件构造结尾局部示例
—————————-# RDB is a binary format. There are no new lines or spaces in the file.
52 45 44 49 53 # Magic String “REDIS”
30 30 30 37 # 4 digit ASCCII RDB Version Number. In this case, version = “0007” = 7 RDB VERSION 字段
FE 00 # FE = code that indicates database selector. db number = 00
对于 RDB VERSION 查看局部伪代码
def rdbLoad(filename):
rio = rioInitWithFile(filename);
# 设置标记:# a. 服务器状态:rdb_loading = 1
# b. 载入工夫:loading_start_time = now_time
# c. 载入大小:loading_total_bytes = filename.size
startLoading(rio)
# 1. 查看该文件是否为 RDB 文件(即文件结尾前 5 个字符是否为 "REDIS")if !checkRDBHeader(rio):
redislog("error, Wrong signature trying to load DB from file")
return
# 2. 查看以后 RDB 文件版本是否兼容(向下兼容)if !checkRDBVersion(rio):
redislog("error, Can't handle RDB format version")
return
………
//Redis 中对于 RDB_VERSION 查看的代码
rdbver = atoi(buf+5);
if (rdbver < 1 || rdbver > RDB_VERSION) {rdbCheckError("Can't handle RDB format version %d",rdbver);
goto err;
}
RDB 同步过程中的大 Key 拆分
RedisSyncer 在全量同步阶段在遇到 LIST、SET、ZSET、HASH 等构造等时候,当数据大小超过阀值后 RedisSyncer 会通过迭代器的模式将 key 拆分成多个子命令写入指标库。避免局部超大 key 一次性读入内存导致程序产生 oom 并进步同步的速度。而对于不存在大 key 的命令同步工具会通过序列化逆序列化的模式写入指标。
附录一 Redis RDB 协定
redis RDB Dump 文件格式
----------------------------# RDB is a binary format. There are no new lines or spaces in the file.
52 45 44 49 53 # Magic String "REDIS"
30 30 30 37 # 4 digit ASCCII RDB Version Number. In this case, version = "0007" = 7
----------------------------
FE 00 # FE = code that indicates database selector. db number = 00
----------------------------# Key-Value pair starts
FD $unsigned int # FD indicates "expiry time in seconds". After that, expiry time is read as a 4 byte unsigned int
$value-type # 1 byte flag indicating the type of value - set, map, sorted set etc.
$string-encoded-key # The key, encoded as a redis string
$encoded-value # The value. Encoding depends on $value-type
----------------------------
FC $unsigned long # FC indicates "expiry time in ms". After that, expiry time is read as a 8 byte unsigned long
$value-type # 1 byte flag indicating the type of value - set, map, sorted set etc.
$string-encoded-key # The key, encoded as a redis string
$encoded-value # The value. Encoding depends on $value-type
----------------------------
$value-type # This key value pair doesn't have an expiry. $value_type guaranteed != to FD, FC, FE and FF
$string-encoded-key
$encoded-value
----------------------------
FE $length-encoding # Previous db ends, next db starts. Database number read using length encoding.
----------------------------
... # Key value pairs for this database, additonal database
FF ## End of RDB file indicator
8 byte checksum ## CRC 64 checksum of the entire file.
RDB 文件以魔术字符串“REDIS”结尾。52 45 44 49 53 # "REDIS"
RDB 版本号
接下来的 4 个字节存储 rdb 格局的版本号。这 4 个字节被解释为 ascii 字符,而后应用字符串到整数转换转换为整数。00 00 00 03 # Version = 3
Database Selector
一个 Redis 实例能够有多个数据库。单个字节 0xFE 标记数据库选择器的开始。在该字节之后,一个可变长度字段批示数据库编号。请参阅“长度编码”局部以理解如何读取此数据库编号。键值对
在数据库选择器之后,该文件蕴含一系列键值对。za
每个键值对有 4 个局部 -
1. 密钥到期工夫戳。2. 批示值类型的一字节标记
3. 密钥,编码为 Redis 字符串。请参阅“Redis 字符串编码”4. 依据值类型编码的值。参见“Redis 值编码”
附录二 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”)。如:
“+OK\r\n”
1
RESP Errors
“-Error message\r\n”
1
如:
-ERR unknown command ‘foobar’
-WRONGTYPE Operation against a key holding the wrong kind of value
1
2
RESP Integers
Integers 只是一个 CRLF 终止的字符串,代表一个整数,以“:”字节为前缀。例如
“:0\r\n”
“:1000\r\n”
1
2
Bulk Strings
用于示意长度最大为 512 MB 的单个二进制平安字符串。批量字符串按以下形式编码:
“$”字节后跟组成字符串的字节数(前缀长度),以 CRLF 结尾。
理论的字符串数据。
最初的 CRLF。
“foobar”的编码如下:
“$6\r\nfoobar\r\n”
1
当字符串为空
“$0\r\n\r\n”
1
Bulk Strings 还能够用于示意 Null 值的非凡格局来示意值不存在。在这种非凡格局中,长度为 -1,并且没有数据,因而 Null 示意为:
“$-1\r\n”
1
RESP Arrays
格局:
一个’*’ 字符作为第一个字节,而后是数组中元素的数量作为十进制数,而后是 CRLF。
https://segmentfault.com/writ…
Array 的每个元素的附加 RESP 类型。空数组示意为:
“*0\r\n”
1
“foo”和“bar”的数组示意为
“*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n”
1
[“foo”,nil,“bar”](Null elements in Arrays)
*3\r\n$3\r\nfoo\r\n$-1\r\n$3\r\nbar\r\n