Pebble 介绍:由 RocksDB 启发,用 Go 编写的 K/V 存储

  • A+
所属分类:安全开发

土拨鼠导读:自 CockroachDB 成立以来,一直使用 RocksDB 作为其键值存储引擎。我们选择 RocksDB 是因为其非常符合我们的需求。RocksDB 经过了我们严格的测试,性能很高,并且具有丰富的功能集。我们是 RocksDB 的忠实拥护者并且当被问及为什么不选择其他存储引擎时,我们经常赞扬 RocksDB。


今天,我们将介绍 Pebble:这是一个受 RocksDB 启发并且与 RocksDB 兼容的键值存储,专注于 CockroachDB 的需求。Pebble 具有更好的性能和稳定性,避免了使用 Cgo 带来的挑战,并使我们能够更好地控制针对 CockroachDB 需求量身定制的未来要增强的功能。在今年秋天即将发布的20.2版本中,Pebble 将取代 RocksDB 作为默认存储引擎。而这是我们编写 Pebble 的原因,以及我们如何更改 CockroachDB 的基础组件的故事。


01

动机

存储引擎是数据库的重要组成部分,它为数据库的性能和稳定性奠定了基础。传统的 SQL 和 NoSQL 数据库通常使用其自己的专有存储引擎来构建。MySQL 使用 InnoDB,Postgres 带有内部 B树,哈希和堆存储系统,Cassandra 带有 LSM树的实现。最近,其中一些数据库添加了 RocksDB 的后端(例如 MyRocks 和Rocksandra)。从长远角度去看,这让人感觉 RocksDB 正在吞噬低级存储的生态系统。但是经过仔细检查后发现,这些现有系统的 RocksDB 后端存在重大问题。
当构建任何复杂的软件时,不可能从头开始构建每个组件。重用现有组件可以缩短上市时间,而且通常是更好的产品,因为领域专家们花了很多时间来制作和调整各个组件的性能和耦合。使用 RocksDB 的这个选择当然是对的,但是随着时间的推移,计算发生了变化。许多不同的系统都使用 RocksDB。这种广泛的用途意味着要进行大量的测试和性能调整,但这也意味着 RocksDB 服务于许多主服务器。我们可以在 RocksDB 的非常庞大的功能集和配置表域中看到此效果。RocksDB 的代码库随着时间的推移而扩展,从 LevelDB 的原始3万行代码发展到目前的 350k+ 行代码状态。当然,只通过代码行来判断是一个不严谨的指标,但是这些数据确实提供了相对复杂性的粗略感觉。
RocksDB 为 CockroachDB 奠定了坚实的基础。不幸的是,随着 CockroachDB 的成熟,我们在 RocksDB 中遇到了严重的问题。例如,RocksDB 在与压缩有关的代码中存在一个错误,该错误导致对特定 sstable 进行无限次的压缩循环,从而使 LSM树 的其他部分无法进行压缩。虽然我们在 RocksDB 中遇到的 bug 的绝对数量是可以接受的,但它们的严重性通常很高,而修复它们的紧迫性通常是 House Is On Fire。这就使 Cockroach Labs 工程师深入研究 RocksDB 代码库,作为可以进行错误调查的一个基础要求。定位超过35万行外来 C++ 代码是可行的(我们已经做到了),但很难说这是一个好的解决方案。CockroachDB 主要是用 Go 编写的,而 Cockroach Labs 的工程师已经在 Go 的开发积累了广泛的专业知识。C++ 的专业知识很少,所以在 Go 和 C++ 之间心里上的障碍是真是存在的。这个障碍会阻止在本机 Go 配置文件工具的使用中进行内部检查 C++ 代码或进行 C++ 堆栈跟踪。为了避免频繁地从 Go 过渡到 C++ 的性能开销,有时我们不得不用 C++ 编写大量的 Go 中已经存在的逻辑。
RocksDB 通常具有很高的性能,但是我们也遇到了重大的性能问题。CockroachDB 是范围删除的早期采用者,但我们也是在早期实现中一些性能缺陷的早期发现者。我们在上游进行了针对范围删除的性能修复程序,并协助设计了v2实施方案。
RocksDB具有全功能,但有时这些功能存在缺陷。有时,我们选择解决 CockroachDB 代码中的这些缺陷,而不是在 RocksDB 中进行修复。但是这些决定不一定是有意识地做出的(有关 Go 和 C++ 之间的心理障碍,请参见上文)。这种解决方法的一个示例是 CockroachDB Compactor。压缩程序用于在 RocksDB 中强制压缩最近通过 DeleteRange 操作删除的部分数据。与不执行任何操作相比,这可以更快地恢复磁盘空间。对Compactor 的需求源于 RocksDB 在其压缩决策中未考虑范围删除操作。从底层细节更进一步,得出的结论是,存储引擎对 CockroachDB 的功能和行为具有至关重要的影响。拥有自研的存储层使 CockroachDB 可以更直接地控制自己的命运。
挑剔的读者可能会指出,以上几点并未得出重新实现 RocksDB 的结论。相反,我们本可以选择提升内部专业知识。我们本可以选择派生 RocksDB,剥离不需要的部分,并根据 CockroachDB 的需求进行增强。我们也对后一种方法进行了认真的考虑,但是最终我们拒绝在 Go 中重新实现 C++ 的逻辑,因为我们认为消除 Go / C ++ 障碍将使长期的更快开发成为可能。
最后的选择是使用另一个存储引擎,例如 Badger 或 BoltDB(如果我们想坚持使用Go)。出于多种原因,未认真考虑此替代方法。这些存储引擎并未提供我们所需的所有功能,因此我们需要对其进行重大增强。如果我们使用另一个存储引擎,运行RocksDB 的 CockroachDB 群集的迁移过程将变得更加复杂,这使得我们可能需要在相当长的时间内支持两个存储引擎。支持多个存储引擎本身就是一项巨大的工作:它显着增加了测试表的域,并且替代存储引擎通常会带来很多额外的问题(例如 MyRocks 不支持 SAVEPOINT)。最后,各种 RocksDB-ism 已渗入 CockroachDB 代码库,例如使用 sstable 格式在节点之间发送数据快照。删除这些 RocksDB-ism 或提供适配器,将是一项巨大的工程工作,或者会带来不可接受的性能开销。

02

构建 Pebble

更换与 RocksDB 一样大的组件是一项艰巨的任务。但是我们确实有几个有利因素:
  • 我们非常了解 CockroachDB 对 RocksDB 的使用。Pebble 的目的不是要完全替代 RocksDB,而仅仅是替代 CockroachDB 使用的 RocksDB 中的功能。据估算,这将更换任务的范围至少减少了50%。目前,Pebble 代码库的拥有略超过 45k 行代码和另外 45k 行测试。这只是 RocksDB 代码大小的一小部分,主要原因是我们没有复制 RocksDB 的所有功能。
  • 我们不是从零开始。LevelDB 的 Go 端口几年前就开始着手编写了,但从未完成。虽说这些代码很少还保存在 Pebble 中,但它确实列出了最初的框架并提供了用于读取和写入低级文件格式的早期代码。
  • 我们可以将 RocksDB 的代码称为实现模板。例如,虽然未正式指定低级 RocksDB 文件格式,但 RocksDB 代码提供了足够多的关于这些格式的文档。虽然重用 RocksDB 文件格式从 Pebble 设计中消除了一定程度的自由度,但这并不是一个繁重的约束。而且,不仅仅是文件格式,我们还可以从 RocksDB 代码的所有部分中获取灵感和想法。
Pebble 的 API 和内部结构类似于 RocksDB。Pebble 是一个 LSM 键值存储,它提供 Set,Merge,Delete和DeleteRange 操作。可以将操作分组为原子批处理。可以通过 Get 单独读取记录,也可以使用 Iterator 以 key顺序遍历记录。轻量级时间点只读快照可提供数据库的稳定视图。在内部,Pebble 中的数据存储在预写日志(WAL)和排序字符串表(sstables)的组合中。最近写入的数据被缓存在一系列 Memtable 中的内存中,这些Memtable 在后台由并发抢占式的跳表实现。Memtables 刷新到磁盘以创建 sstables。稳定器会在后台定期压缩。Pebble 中的压缩机制和启发式方法都与 RocksDB 中存在的机制(至少对于CockroachDB使用的配置)相似。
任何熟悉 RocksDB 内部知识的人都会在 Pebble 代码中看到许多相似之处。同时也有许多差异,但是我们已经将一些比较大的调整记录成了文档。例如,范围删除实现与 RocksDB 中的实现完全不同,后者实现了更多优化,从而可以在迭代过程中跳过已删除键的范围。索引批次的处理完全不同,这使 Pebble 实现可以支持所有变异操作的索引,而 RocksDB 当前不支持(例如 RocksDB 不支持批量范围删除的索引)。这些示例并不是在指责 RocksDB 有问题。我们只是希望 RocksDB 会采纳 Pebble 中的一些好的点,就像我们将继续从 RocksDB 中挑选好的点子一样。

2.1 功能

Pebble 实现了 CockroachDB 使用的 RocksDB 功能的子集。我们并不希望最终将 RocksDB 中的所有功能都包括在内。实际上,事实恰恰相反。我们打算通过是否对 CockroachDB 有用的标准来过滤所有功能添加和性能改进。那么,Pebble 包括哪些功能?
  • 基本操作:Set, Get, Merge, Delete, Single Delete, Range Delete
  • 批处理
    • 索引批处理
    • 只写批处理
  • 基于块的稳定器
    • 表级布隆过滤器
    • 前缀Bloom过滤器
  • 检查站
  • 迭代器
    • 迭代器选项(上下限,表格过滤器)
    • 前缀迭代
    • 反向迭代
  • 基于各种级别的压缩
    • 并发压缩
    • 手动压缩
    • L0内压缩
  • SSTable摄取
  • 快照

RocksDB 已有的功能但是 Pebble 不包括:
  • Backups
  • Column families
  • Delete files in range
  • FIFO compaction style
  • Forward iterator / tailing iterator
  • Hash table format
  • Memtable bloom filter
  • Persistent cache
  • Pin iterator key / value
  • Plain table format
  • SSTable ingest-behind
  • Sub-compactions
  • Transactions
  • Universal compaction style
上面的某些项可能会让你感到惊讶。鉴 于CockroachDB 提供对备份或事务的支持,Pebble 为何不包括对备份或事务的支持?CockroachDB 的备份和事务实现从未使用过 RocksDB 中的备份和事务功能。本地键值存储区上的事务不需要实施分布式事务。相反,CockroachDB 使用批处理(它为一组操作提供原子性)作为构建分布式事务的基础。

2.2 双向兼容

我们很早就决定将 Pebble 定位为与 RocksDB 双向兼容,以实现 Pebble 的初始发行版。更准确地说,Pebble当前与 RocksDB 6.2.1(CockroachDB当前使用的RocksDB版本)双向兼容,用于 CockroachDB 使用的RocksDB 功能的子集。双向兼容性意味着 Pebble 可以读取 RocksDB 生成的 DB,而 RocksDB 也可以读取Pebble 生成的 DB。与 RocksDB 兼容可以实现无缝迁移到 Pebble,只需要使用新的命令行标志 ---storage-engine = pebble 重新启动 Cockroach 节点即可。双向兼容可提高安全性:如果在使用 Pebble 时遇到问题,我们可以切换回使用 RocksDB。双向兼容还提高了测试的严格性,这将在“测试”部分中详细讨论。
请注意,与 RocksDB 的双向兼容可能随后会消失。永远保持这种兼容性与我们在 CockroachDB 服务中增强Pebble 的愿望背道而驰。一直保持与新的 RocksDB 功能的兼容性将是巨大的负担。

03

测试

存储引擎是数据库的组成部分,负责将数据持久地写入磁盘。存储引擎中的错误往往很严重,例如数据损坏和数据不可用。存储引擎的测试需要强大。
Pebble 的测试最好是为分层的。当前的测试层为:
  • Pebble unit tests
  • Randomized tests (a.k.a metamorphic tests)
  • Bidirectional compatibility tests
  • CockroachDB unit tests
  • CockroachDB nightly tests (a.k.a. roachtests)

3.1 单元测试

测试的第一层是大量的 Pebble 单元测试。这些单元测试旨在测试所有正常情况和极端情况。列出所有极端情况是一项艰巨的任务。即使是勤奋的工程师也可能会错过一些极端情况。更麻烦的是,对代码进行小的更改可能会引入新的极端情况。相信在进行任何更改时,我们能够识别出那些新的极端案例是很好的,但我们的经验表明,情况并非如此。

3.2 随机测试

随机测试是解决近几年来遇到的极端情况的解决方案。模糊测试是随机测试的一个示例,通常用于检查解析器和协议解码器。对于 Pebble 来说,我们可以尝试编写随机生成操作的测试,而不必试图明确列举所有极端情况。随之而来的自然问题是:我们如何知道运算结果是否正确?通过模糊测试,我们只需查找程序崩溃。这也是Pebble 随机测试中的第一层检查,我们通过对某些关键内部数据结构进行不变检查来进一步增强检查的准确性。但是仅查找崩溃和不变的违规情况的结果有点令人不满意。我们想知道操作结果是否正确,但为每一个操作的预期结果维护一个单独的模型是一项艰巨的任务。因为由于存在快照(隐式和显式)和范围删除,Pebble 所实现的数据模型不仅仅是键和值的有序映射。而我们对此给出的解决方案是变质测试。我们随机生成一系列操作,然后针对不同的 Pebble 配置多次执行这些操作。比较不同运行的输出,任何差异都令人担忧。我们调整 Pebble 的配置项包括块缓存的大小,内存表的大小以及 sstables 的目标大小。更改这些配置操作将导致在 Pebble 内部执行不同的内部代码路径。例如,更改 sstable 的目标大小会导致在处理范围删除时出现不同的情况。在编写本文时,变质测试的每个实例都针对 19 个预定义配置和 10 个随机生成的配置运行。
实际上,我们已经实现了两种不同版本的变形测试。第一个仅在 Pebble API 上运行,并且仅针对其自身测试 Pebble。您可能在想:为什么不同时对 RocksDB 进行测试?我们也有过同样的想法。不幸的是,与 RocksDB  相比,Pebble API 具有一些细微的区别和概括,这使这一尝试变得更加棘手。相反,我们实现了第二个变质测试,该测试在 CockroachDB 内的 Pebble / RocksDB 集成层上有效。这第二个变形测试不仅验证了 Pebble 和RocksDB 产生的结果相同,而且还验证了 CockroachDB 中特定于 Pebble 和 RocksDB 的胶合代码产生了相同的结果。事实证明,变形测试对于发现现有的错误以及在引入新功能时快速捕获回归方面非常有用。

3.3 崩溃测试

存储引擎的关键属性是将数据持久地写入磁盘。为了为更高层次的构建提供有用的基础,Pebble 和 RocksDB 允许将写操作“同步”到磁盘,并且当操作完成时,调用者可以知道即使进程或机器也存在数据崩溃。尝试崩溃恢复是一个有趣的挑战。在 Pebble 中,我们将崩溃测试与变形测试集成在一起。随机的一系列操作还包括“重新启动”操作。遇到“重新启动”操作时,将丢弃所有已写入操作系统但未“同步”的数据。实现这种丢弃行为相对简单,因为 Pebble 中的所有文件系统操作都是通过文件系统接口执行的。我们只需要添加此接口的新实现即可,该实现可缓存未同步的数据,并在发生“重新启动”时丢弃此缓存的数据。

3.4 双向兼容测试

如前所述,Pebble 的目标是与 RocksDB 双向兼容。为了测试这种兼容性,我们再次扩展了变形测试。更改了“重新启动”操作,以在 Pebble 和 RocksDB 之间随机切换。该测试已经发现了 Pebble 和 RocksDB 之间的一些不兼容性,例如 Pebble 错误地在 sstables 上设置了一个属性,这导致 RocksDB 解释这些 sstables 与Pebble 不同。除了在变形测试中进行兼容性测试外,我们还实现了 CockroachDB 级别的集成测试,该测试模仿了用户为验证双向兼容性而可能做的事情。该测试将启动 CockroachDB 群集,然后随机终止并重新启动群集中的节点,并且切换使用的存储引擎。
在此测试中发现的错误类型从琐碎的差异到最严重的数据损坏类型不等。后者的一个示例是 Bloom 过滤器代码使用的哈希函数之间的细微差别:将带符号的8位整数扩展为32位所产生的值与将带符号的8位整数扩展为32位所导致的值不同。这导致 Pebble 的 Bloom Filter 哈希函数与 RocksDB 的 Bloom Filter Hash 函数对于一部分键(即包含高位字节的键)产生不同的值。该错误的原因本身很有趣。Pebble 的 Bloom Filter 哈希函数是从 go-leveldb 继承的,而 go-leveldb 是从 LevelDB 继承的。LevelDB 哈希函数的最初实现的行为取决于 C字符类型是带符号的还是无符号的(可通过 gcc / clang 的标志来控制)。这种微妙的依赖关系几年前在 LevelDB 和RocksDB 中都得到了修复,但是在翻译成 Go 语言时,这种依赖关系又退回到了某个地方。

3.5 利用CockroachDB测试

Pebble 测试的最后几层利用了现有的 CockroachDB 单元测试和夜间测试。我们添加了一个环境变量(COCKROACH_STORAGE_ENGINE),该变量控制 CockroachDB 单元测试是使用 Pebble 还是 RocksDB。我们还实现了另一个存储引擎,以进行进一步的测试。Tee 存储引擎顾名思义就是这样:它将所有写操作都发送给 Pebble 和 RocksDB。读取操作针对两个基础存储引擎,并进行比较以确保返回相同的结果。
CockroachDB 运行一套夜间集成测试,称为 roachtests。roachtests 在 AWS 或 GCP 上启动集群并执行集群级别的测试。可以使用相同的 COCKROACH_STORAGE_ENGINE 环境变量在 Pebble 上运行这些测试。

4

性能

如果不提高性能,我们也就不会发布新的存储引擎了。如果性能受到重大影响,那么将 Pebble 替换为 RocksDB 是行不通的。RocksDB 的性能很高,我们必须花费大量精力才能达到或超过其性能。存储引擎的性能表域非常大,该职位仅涉及其中的一小部分。性能不仅与原始吞吐量和延迟有关,还与资源消耗(例如 CPU 和内存使用情况)有关。归根结底,我们最关心的是 Pebble vs RocksDB 在 CockroachDB 级别的工作负载上的性能。
YCSB 是用于检查存储引擎性能的标准基准。它运行六个工作负载:工作负载A是50%读取和50%更新的混合。工作量B是95%读取和5%更新的混合。工作负载C是100%读取。工作量D是95%的读取和5%的插入。工作量E是95%扫描和5%插入。工作负载F是50%的读取和50%的读取-修改-写入。Pebble和RocksDB的配置选项类似(重叠的位置相同)。所有工作负载的数据集大小都适合内存,尽管我们还使用不适合缓存的数据集对工作负载进行了测试。

Pebble 介绍:由 RocksDB 启发,用 Go 编写的 K/V 存储

Pebble 在6个标准 YCSB 工作负载上达到或超过了 RocksDB。但是 CockroachDB 的性能存在存储引擎之外的瓶颈,为了更直接地比较存储引擎的性能,我们直接在 Pebble 和 RocksDB 之上实现了一部分 YCSB 工作负载。

Pebble 介绍:由 RocksDB 启发,用 Go 编写的 K/V 存储

请注意,在此仅存储引擎基准测试工具中未实现工作负载F。工作负载C上的较大增量是由于 Pebble 的块和表缓存结构的并发性更好。从 CockroachDB 级别的比较可以看出,当考虑整个系统时,这种更好的并发性的效果变得很弱。

5

结论和未来工作方向

去年五月,CockroachDB 的 20.1 版本引入了 Pebble 作为 RocksDB 的替代存储引擎。我们在介绍时非常谨慎,没有对其进行广泛的宣传,而是要求用户专门选择使用 Pebble。我们首先在 CockroachCloud 集群上测试Pebble,首先是内部测试集群,最近是生产集群。现在,我们对 Pebble 的稳定性和性能充满信心。随着今年秋天 20.2 的发布,Pebble 将成为 CockroachDB 的默认存储引擎。RocksDB 在 20.2 中仍是备用存储引擎,但之后的日子里,我们计划在后续版本中将其完全删除。
20.2 版本还将为 Pebble 带来增强。我们对压缩试探法和机制进行了改进,从而大大加快了存储引擎瓶颈所致的IMPORT 和 RESTORE 工作负载。我们在压缩启发式方法中合并了范围删除功能,使我们摆脱了前面提到的 CockroachDB 中的 Compactor 解决方法。这些只是我们最终理想中的 Pebble 的冰山一角。存储引擎是CockroachDB 性能和稳定性的基础,我们计划继续增强 Pebble,以追求更高的性能和稳定性。

相关链接

Introducing Pebble: A RocksDB Inspired Key-Value Store Written in Go:https://www.cockroachlabs.com/blog/pebble-rocksdb-kv-store/

本文始发于微信公众号(GoCN):Pebble 介绍:由 RocksDB 启发,用 Go 编写的 K/V 存储

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: