redis单元化

背景

单元化:指异地多活,比如两个相离很远的大区域,如北京与深圳两个副本,是两份相同的数据,业务上层保证指对自己本地的数据做写操作,可以读其他地数据。

单元化主要有两个优势

  • 异地多活
  • 读写本地单元时,时延低。按常规的主从复制做的话,读写一个异地的idc时延会很高。

本文不详细讨论单元化的优势、理念,主要讲解一种redis单元化的实现思路。

方案总体如下

  1. 通过一个同步工具将两个redis主节点构成一条单元化链接,该同步工具可以单向或双向的同步两者的写操作。
  2. 将对端单元传过来的数据加上单元信息(如单元归属),本单元不允许对其他单元的key做任何的写操作(比如淘汰、过期本单元均不操作)。

同步工具行为

下文称单元1的redis-server为r1,同理单元2的为r2。以r1为源,r2为目标的情况下讲解。

理念

首先redis引入单元化同步的进度结构,相当于在常规的repl_offset增加了一个单元化的unit_repl_offset。

同步工具在配置文件中配置好两个单元的同步节点,规定好一条同步链路,比如源节点和目标节点的信息。

同步工具有一个设计理念: 一条命令永远不会经过同步工具两次以防止命令回环。
其通过以下方法实现

  1. 每一个真实命令通过其前,会先追加一条自定义的unit_xx命令,该命令被规定为写命令
  2. 因此回环时,会先收到UNIT_SYNC_BINLOG命令,那么意味着后续跟着的真实命令是回环命令,可以直接丢弃不予通过。

这里说一个重要思想,针对同步缓存区:

  • 每个节点记录好自身的单元化的repl_id和offset(新数据结构),主要用于重连时让同步工具能获取到,该节点单元化的同步位点
  • 对于一个单一节点而言,单元化写操作的binlog和一般的命令产生的binlog是共用缓冲区的,同样共用repl_id和offset(redis原生结构)。所以当收到一个psync repl_id offset命令时(无论是否为单元化),实际上都是将backup_log中的该offset之后的数据发给对端。

开始同步

  1. 同步工具向r2发起连接,通过自定义命令UNIT_SYNC_NEW_SESSION,参数为源单元名与同步链路名,以及此次session的id字符串。
  2. 可以从r2收包拿到其单元化的repl_id和offset,后续用于向r1发起同步用
  3. 建立与r1的连接,发起unit_psync命令附带刚获取的repl_idoffset。这里可以将其理解为psync2命令。
  4. r1收到该同步请求时,判断backup_log是否还存在该offset(和redis常规情况一致),其走增量返回CONTINUE还是全量返回FULLRESYNC,以全量举例,r1顺带返回了new_repl_idnew_offset
  5. 同步工具开始获取和解析rdb。这里详细阐述一下

每一个rdb的key-value对,会把他做成一个multi exec命令,一共附带3条命令

  • UNIT_SYNC_BINLOG,附带session_id、binlog_seq(恒递增)、repl_id、offset、源单元名和同步链路名。注意在全量同步解析rdb时,repl_id和offset可以置为初始值,因为不关心。
  • UNIT_SYNC_PRE_RESTORE + key,告知对端即将RESTORE
  • RESTORE + 命令参数REPLACEABSTTL

同步工具会将所有拿到的binlog都存在本地文件里,即内存中不会放无限递增的binlog请求,到了一定程度便持久化到本地文件中。

  1. 最终在rdb所有命令解析为binlog后增加一条UNIT_SYNC_BINLOG,附带真正的repl_idrepl_offset,用于告知对端其同步位点。

同步稳态

稳态即同步工具和r1已经开始稳定的增量同步,同步工具收到包后将其持久化到本地文件里,并定期回复REPLCONF ACK offset

同步工具持续的从本地文件中拉取multi命令包装的binlog发送给r2,相当于自己就是一个业务写入方。通过r2的回复,来裁剪本地文件的binlog数据。

跟R1的稳态交互为:

  1. 同步工具持续解析从r1收命令,如果是unit命令,只更新offset,不持久化到文件。否则包装成MULTI命令附带unit命令,持久化到文件中。
  2. 每一秒回复r1一次REPLCONF ACK offset

跟R2的稳态交互为:

  1. 持续的从文件中取出之前持久化的cmds,如果db文件里没有,则检查binlog_buf中有没有,将这些发送给r2,将已经发送的未收到回复的请求f附带seq记录在队列binlog_info_q里。
  2. 持续的从r2收回复包,每收到一个命令的回复,删除binlog_info_q这里面的这条记录,推进队列的总进度。
  3. 每一段时间根据binlog_info_q记录的进度,删除db文件中的对方已经收到的命令。

redis-server行为

自定义命令与处理

UNIT_SYNC_NEW_SESSION

1
2
3
4
5
6
7
8
9
10
cmd = "KUNIT_SYNC_NEW_SESSION",
/*
req:
KUNIT_SYNC_NEW_SESSION <UNIT_NAME> <RL_NAME> <SID>
rsp:
[
<REPL_ID>
<OFFSET>
]
*/

这个命令主要是同步工具启动初始化时,获取r2的过往同步进度的。比如tcp重连。通过这个命令可以拿到当前同步链路的repl_id和offset。

UNIT_SYNC_BINLOG

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cmd = "KUNIT_SYNC_BINLOG",
/*
req:
MULTI
KUNIT_SYNC_BINLOG <SID> <BINLOG_SEQ_STR> <REPL_ID> <OFFSET_OF_CMD_END> <UNIT_NAME>
<CMD_ARGS>...
...
EXEC
rsp:
+OK
+QUEUED
+QUEUED
...
<rsp of `EXEC`>
*/

通过这个命令更新r2的session中的offset。并设置好接下来的命令的key的单元归属信息,以及该命令表明为单元化同步的命令。

UNIT_SYNC_PRE_RESTORE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cmd = "KUNIT_SYNC_PRE_RESTORE",
/*
req:
MULTI
<KUNIT_SYNC_BINLOG CMD>
KUNIT_SYNC_PRE_RESTORE <KEY>
<RESTORE_CMD>
EXEC
rsp:
+OK
+QUEUED
+QUEUED
+QUEUED
<rsp of `EXEC`>
*/

这条命令用于在restore key之前做校验,必须处于上面binlog命令的事务中。比如一次全量同步时,r1发过来的key的单元归属信息会指定为r1,但实际上有可能是r2的数据。

那么r2可以通过这个命令检查key的单元归属信息拦截掉相应的写请求。

实际写命令处理

对于一般的写命令,我们将单元归属信息都置为unit_id=0,表明该key属于本单元。

而单元化的写命令,都是在MULTI中,在EXEC执行前,清空当前所有的预置的单元归属信息。然后依次执行,在执行到UNIT_SYNC_BINLOG时,设置到接下来的key的单元归属信息。

之后在dbAdd时,把这个object-val的单元归属信息加上即可。