调研背景
本次调研着重分析对 Rocksdb Secondary Instances 功能的调研, 并非性能测试, 为存算分离 Rocksdb 上云工作提供一些技术铺垫.
资源情况
| 资源 | 版本 |
|---|---|
| Rocksdb | rocksdb-6.2.4/rocksdb-7.1.1 |
| OS | CentOS Linux release 8.5.2111 |
| GCC | gcc (GCC) 4.8.1/gcc (GCC) 8.5.0 |
| MemTotal | 8G |
| CPU | 4 * Intel(R) Xeon(R) CPU E5-2630 v3 @ 2.40GHz |
| Kernel | Linux version 5.16.3-1.el8.elrepo.x86_64 |
| File system | ext4 |
概念速通
WAL
Write Ahead Log 预写日志, 保存当前 rocksdb 的 memtable 中的文件信息,当 immutable memtable 中的数据刷到 L0 之后即之前的会被删除 — 即于 DB/WAL 目录下的 000xx.log.
MANIFEST
Rocksdb 状态变化的事务日志文件. MANIFEST 文件用于在重启的时候, 恢复实例到上一次关闭前的最后一个一致的状态. 其内容主要是 SST 文件的各个版本信息 (当 sst 文件被改动, 即会生成对应的 versionEdit, 并触发 sync 写 manifest 文件), 用于异常断电后恢复 — 即 DB 目录下 MANIFEST-00000x 的文件.
CURRENT
记录当前最新的 manifest 文件编号.
Memtable
常驻于内存中, 用于在写完 WAL 之后, 再接收具体的 key-value 数据. 每个 memtable 大小以及个数都有指定的参数进行控制, write_buffer_size 表示 memtable 的大小, max_write_buffer_number 表示内存中最多可以同时存在多少个 memtable 的个数.
Immutable memtable
当 memtable 被写满之后会生成一个新的 memtable 继续接写请求, active_memtable 就会变成 immutable_memtable, 只读的状态, 并开始 flush 到磁盘的 L0.
SST文件
核心 key-value 的存储文件. DB 目录下的 0000xx.sst 形态.
部署架构
根据 官方文档 解释, 我们能初步得到以下两个结论:
- RocksDB 只读模式有两种实例: 只读实例以及辅助实例. 其中只读实例只能提供静态视图不支持追赶 Primary, 所以只读实例不在我们这次调研范围内;
- 根据 Secondary Instances 的文件共享策略, 我们得到下图的部署架构.
部署 rocksdb 实例数 2 个, 其中一个 rocksdb 实例为 Primary 实例, 负责只写操作以及 compact 操作;一个 rocksdb 实例为 Secondary 实例, 负责只读, 其文件路径为 Primary 实例的文件路径(不包括 log ). 多实例部署在同一个机器上模拟分布式文件系统下的使用场景.
- Primary 实例采用 DB::Open() 方法,并设置 WAL.
- Secondary 实例采用 DB::OpenAsSecondary() 方法.
测试计划
Rocksdb 版本
通过翻阅 commit 得知,TryCatchUpWithPrimary 方法是在 v6.2 以上版本引入的。所以测试版本分为两组分别是 rocksdb-6.2.4/rocksdb-7.1.1.
测试脚本:
基于 v6.2.4 /examples 目录的 multi_processes_example.cc 脚本进行修改,改动包括:
- 修改 Primary 写逻辑,默认不关闭写实例;
- 添加 memtable 可观测逻辑,观测当前 memtable 大小;
具体实施
前提条件:Primary 需要早于 生产,Secondary 关闭,这样测试才有意义。
| 编号 | 操作 |
|---|---|
| 0 | Primary 生产,WAL 关闭,Secondary 追赶,观察 Secondary 最后一次 seekToLast 的 offset 是否和 Primary 最后一次 flush 数据相同,memtable 是否更新; |
| 1 | Primary 生产,WAL 开启,Secondary 追赶,观察 Secondary 最后一次 seekToLast 的 offset 是否和 Primary 最后一次 flush 数据相同,memtable 是否更新; |
| 2 | Primary 重复打开关闭操作,Secondary 追赶,观察 Secondary 最后一次 seekToLast 的 offset 是否和 Primary 相同,memtable 是否更新; |
| 3 | Primary 关闭,WAL 开启,观察 Secondary 最后一次 seekToLast 的 offset 是否和 Primary 相同,memtable 是否更新; |
| 4 | Primary 关闭,WAL 关闭,观察 Secondary 最后一次 seekToLast 的 offset 是否和 Primary 相同,memtable 是否更新; |
| 5 | Primary compact,观察是否 Secondary 最后一次 seekToLast 的 offset 是否和 Primary 相同,memtable 是否更新。 |
测试结果
0 号测试结果
v6.2.4
v7.1.1
其中 Primary 最后停止位置 key -- 203999 和 Secondary catch 到的 key 数量 -- 200999 差 3000 是一次程序 batch 操作的 key 个数。以此证明 seekToLast 的成功。
根据两次对比结果表明 Secondary 已经追赶到了 Primary flush 的地方,可以 seekToLast 到 SST 文件的末尾。
1 号测试结果
v6.2.4
v7.1.1
结果表明 Secondary 仍旧已经追赶到了 Primary flush 的地方,并且可以看到 v7.1.1 里 memtable 已经有了数据(这里为什么 v6.2.4 没有是因为还有实现 wal catch),可以 seekToLast 到 SST 文件的末尾,但是 WAL 文件中数据没有写入到 Secondary。
2 号测试结果
2022/04/15-10:52:58.119474 7fb26c14ca60 [/version_set.cc:5505] Switched to new manifest: /tmp/rocksdb_multi_processes_example/MANIFEST-000013
出现多次以上 LOG ,加大打开频率 ,无论是 v6.2.x 还是 v7.1.1 的 Secondary 其日志开始刷屏,发现 TryCatchUpWithPrimary 方法阻塞,停止 Primary 重启,TryCatchUpWithPrimary 恢复正常。原因是 Secondary 感知 MANIFEST 文件的频繁变化导致 TryCatchUpWithPrimary 阻塞,停止重启操作即可正常。
3 号测试结果
v6.2.4
v7.1.1
Secondary 实例 memtable 开始有数据
4 号测试结果
因为无WAL,所以 Secondary 实例 memtable 无数据 ,但是 MANIFEST 文件依旧可以提供 catch
5 号测试结果
v7.1.1
Last sequence
因为 v6.2.4 没有完成完全的 catch 操作,所以不进行此测试。 v7.1.1 Secondary 实例 是可以感知到 compact 操作的,在 compact 时,通过读取 wal log seq 判断当下 flush 时机,但是 flush 的操作是 active mem --> imm mem --> 淘汰,这么一个操作,所以能看到此时 size 从 1323472 到 2048 的一个变化。
测试结论
Secondary 实例可以共享 Primary 实例的 SST 文件,首先 Secondary 实例在启动阶段要先获取 Primary 的 MANIFEST 文件,然后默认打开 Primary 所有 SST 文件利用文件系统引用的特性保证文件在 compact 时不会被真正删除,再通过 MANIFEST 文件中的元信息能确定列族信息, compact 时机以及 Primary version 中已经 Apply 的的数据,同时 Secondary 通过不断的 TryCatchUpWithPrimary 还能获得 Primary WAL 中的数据用来填充自己的 memtable 。但是目前不支持WAL追踪【实时从节点】!
引申
MANIFEST 详细信息
在 RocksDB 中 MANIFEST 保存了存储引擎的内部的一些状态元数据,简单来说当系统异常重启,或者程序异常被退出之后,RocksDB 需要有一种机制能够恢复到一个一致性的状态, 而这个一致性的状态就是靠 MANIFEST 来保证的.MANIFEST 在 RocksDB 中是一个单独的文件,而这个文件所保存的数据基本是来自于 VersionEdit 这个结构.MANIFEST 包含了两个文件,一个 log 文件一个包含最新 MANIFEST 文件名的文件,Manifest 的 log 文件名是这样 MANIFEST-(seqnumber),这个 seq 会一直增长.只有当超过了指定的大小之后,MANIFEST 会刷新一个新的文件,当新的文件刷新到磁盘(并且文件名更新)之后,老的文件会被删除掉.这里可以认为每一次 MANIFEST 的更新都代表一次 snapshot.在 RocksDB 中任意时间存储引擎的状态都会保存为一个 Version (也就是 SST 的集合),而每次对 Version 的修改都是一个 VersionEdit,而最终这些 VersionEdit 就是 组成 manifest-log 文件的内容.整个MANIFEST涉及到三个数据结构分别是VersionEdit/Version/VersionSet,其中前两个上面已经有介绍,而最后一个VersionSet顾名思义表示一堆Version的集合,其实就是 记录了各个版本的信息用来管理整个Version.