关于redis: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)

  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

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理