5

脑洞:时光机数据库

 1 year ago
source link: https://www.zenlife.tk/time-machine-db.md
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

脑洞:时光机数据库

2022-08-11

许多年前,周sir 写过一篇这样的博客 <回到过去,找回遗失的珍宝 -="" TiDB 的历史读功能> 。 虽然我们内部一直当一个梗来玩的,但是今天开会讨论用户需求的时候,又开了一个脑洞。="">

游戏那边的用户,其实对回滚到历史数据还是很有需求的。比如出了重大 bug 需要回档之类的。做定期备份之类的,但是只能回滚到备份时间点,而且定期备份对资源的浪费还挺大的。

要是能有一个时光机数据库就好了!可以在历史时间随意穿梭。怎么来设计这个东西呢?其实只要在周sir 的时光机方案上做一点儿改动。

数据库这边,其实核心就是把写操作变成 (raft) log,然后 apply log,得到数据。由于要处理事务,数据是用 MVCC 形式组织的。 只要不做 GC,MVCC 其实保留了所有历史数据的。这也是实现 TiDB 历史读功能的必要条件。

可以把 MVCC 数据想象成这样子的:

key1 -> v3 -> xx
     -> v2 -> xy
     -> v1 -> yz
     -> ...
...
key2 -> v2 -> 42
     -> v0 -> 56

如果我们要读 timestamp v2 的时刻的数据,得到的就是 key1=xy, key2=42。 如果读 timestamp v1 时刻的数据,则是 key1=yz, key2=56。

MVCC 的删除操作是很重的,因为删除是靠标记删除,然后做 compaction 的方式。 假设在 v4 时刻删除 key1,上面会变成:

key1 -> v4 -> DEL
     -> v3 -> xx
     -> v2 -> xy
     -> v1 -> yz
     -> ...
...
key2 -> v2 -> 42
     -> v0 -> 56

会留一个 DEL 的 tombstone 标记,读取的时候遇到它,就知道更早的版本不用看了。

TiDB 的历史读功能,set @@tidb_snapshot 之后,就会读历史版本的数据,而不是当时时间戳的数据。把 "历史读"功能,变成"时光机"功能,有两个问题没有解决。这两个问题分别是:

假设用户想回档到某个历史版本,只需要 set @@tidb_snapshot 就可以了,但是设置了 snapshot 之后,就无法支持写操作了。 用户想要的是,回档到这个时间点之后,可以继续写入; 以及更高级的,写入之后,还可以选择回到另一个时间。

很容易想到的一个处理方式是,set @@tidb_snapshot 之后,把数据全导出去,再在另外一个数据库中恢复出来。但是如果数据库很大的情况下,这个做法不怎么靠谱,比如说数据库有 10T,光是导出去就不怎么现实。

我们设想一下,如果把回到某个历史版本,把数据导出去,再回到当前,把数据导回来,得到的 MVCC 是什么样子的?它会是这样:

key -> v2022-08-11-14:33  -> dOld
    -> v2022-08-11-13:32  -> xx
	-> v2022-08-11-13:16  -> yy
	-> v2022-08-11-10:53  -> dOld

也就是说,实际结果等价于,把历史版本的数据取出来,然后以当时时间戳,形成 kv 再追加到最新的 MVCC 数据上面。 这个操作可以下推,由每一个 tikv,各个 region 去执行。它跟 2pc 执行事务差不多,只不过是一条特殊的 value 而已。 所以我们可以想象成,set @@timestamp = ts 是把整个库设置到一个只读的历史版本的状态,如果实现一条命令 flashback ts,则是把 MVCC 的那个时间的 value,作为新的值写到最新的 MVCC 数据上。

这个时间是可以随意跳的,比如说,在 12:50 的时候,将数据库变回到了 8:00 的状态,那 MVCC 记录就是:

12:50 -> v1
若干历史记录 ...
8:05 -> v2
若干历史记录 ...
8:00 -> v1

这个时候可以写任何的东西,之后变成了

13:15 -> xxx
若干历史记录 ...
12:50 -> v1
若干历史记录 ...
8:05 -> v2
若干历史记录 ...
8:00 -> v1

接下来,还可以回到 8:05 的时候,也可以回到 13:05 的时候。回到 13:05 相当于在 12:50 的时候回档到 8:05,回档之后又往进走了一会儿,现在仍然可以回档到 8:06,又或者是 13:15,或者是任何的时间,只要数据没有被 GC 掉。

所以只要在历史读的基础上,做一个把历史版本追加写到最新版本的功能,就可以实现回档写入。只读约束就解决了。

接下来看另一个问题,性能。为什么需要做 GC,因为不清理数据,会占用很多的空间。更要紧的是,会影响性能。当前 tidb 的 key 编码的布局是这种:

key1 -> v4 -> xx
     -> v2 -> xy
     -> v1 -> yz
     -> ...
...
key2 -> v2 -> 42
     -> v0 -> 56

如果更新很频繁的情况下,单个 key 的版本会特别多。这个时候做 scan 性能就不好了,rocksdb 那边要跳过非常多的 key,扫描的开销会变大。 假设我们把 latest cf 分离出来,扫描 key1 -> key2 就是性能开销常量了。这里有个 RFC 是想解决这种问题的。

如果这两个点都满足了,那事实上就可以实现一个时光机数据库了,只要盘够存非常多的历史数据,想跳到哪个时间点都可以。 当然,这两个点可能不那么好解决,所以先脑洞一下。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK