Pebble简介
自成立以来,CockroachDB一直依赖RocksDB作为其键值存储引擎。RocksDB的选择非常适合我们。RocksDB经过了严格的测试,性能卓越,并具有丰富的功能集。我们是RocksDB的忠实拥护者,当被问及为什么不选择其他存储引擎时,我们经常赞扬它。
今天,我们将介绍Pebble:一个受RocksDB启发,与RocksDB兼容的键值存储,并且重点关注CockroachDB的需求。Pebble带来了更好的性能和稳定性,避免了Cgo调用带来的挑战,并使我们能够更好地控制针对CockroachDB需求量身定制的未来增强功能。在今年(2020年)秋天即将发布的20.2版本中,Pebble将取代RocksDB作为默认存储引擎。这就是为什么我们要编写Pebble,以及如何更改CockroachDB这样的基础组件的故事。
动机
存储引擎是数据库的重要组成部分,为性能和稳定性奠定了基础。传统的SQL和NoSQL数据库通常使用其自己的专有存储引擎来构建。MySQL使用InnoDB,Postgres使用B树,哈希和堆存储系统,Cassandra使用LSM树实现的底层存储引擎。最近,其中一些数据库添加了RocksDB后端(例如MyRocks和Rocksandra)。从长远看,这让人感觉RocksDB正在吞噬低级存储生态系统。仔细检查后发现,这些现有系统的RocksDB后端存在严重的风险。
当构建任何复杂的软件时,不可能从头开始构建每个组件。重用现有组件可以缩短系统开发推出时间,并且通常这是更好的选择,因为相关领域专家已经花了很多时间来制作和调整各个组件。使用RocksDB的选择当然是对的,但是随着时间的推移,这个逻辑发生了变化。许多不同的系统都使用RocksDB。这种广泛的用途意味着要进行大量的测试和性能调整,但这也意味着RocksDB服务于许多主服务器。我们可以在RocksDB的非常大的功能集和配置表面积中看到此效果。RocksDB的代码库随着时间的推移而扩展,从LevelDB的原始30k行代码发展到目前的350k +行代码状态。代码行是一个不适当的度量,但是这些大小确实提供了一个相对复杂的粗略感觉。
RocksDB为CockroachDB奠定了坚实的基础。不幸的是,随着CockroachDB的成熟,我们在RocksDB中遇到了严重的错误。例如,RocksDB在与压缩相关的代码中存在一个错误导致对特定的sstable进行压缩的无限循环,从而使LSM树的其他部分无法进行压缩。虽然我们在RocksDB中遇到的bug的绝对数量是适中的,但它们的严重性通常很高,而修复它们的紧迫性通常是House Is On Fire。这要求Cockroach Labs工程师深入研究RocksDB代码库,作为错误调查的一部分。检索超过350k行外部C ++代码是可行的(我们已经做到了),但是很难说是一个好时机。CockroachDB主要是Go代码的基础,而Cockroach Labs的工程师已经在Go中开发中积累广泛的专业知识。而C ++的专业知识很少,Go和C ++之间的障碍在心理上是真实的。该屏障可防止对本机Go配置文件工具的使用进行内省C ++或查看C ++堆栈跟踪。
RocksDB通常是高性能的,但是我们也遇到了重大的性能问题。CockroachDB是范围删除的早期采用者,但是我们还是该特性早期实现中一些性能缺陷的早期发现者。我们在上层协助和修复相关的问题v2的实现。
RocksDB具有全功能,但有时这些功能存在缺陷。有时,我们选择解决CockroachDB代码中的这些缺陷,而不是在RocksDB中进行修复。这些决定不一定是有意识地做出的(有关Go和C ++之间的心理障碍,请参见上文)。这种解决方法的一个示例是CockroachDBCompactor。它用于迫使其最近已经通过删除在RocksDB的数据的一部分的Compaction操作。与不执行任何操作相比,这可以更快地恢复磁盘空间。需要CompactorDeleteRangeCompactor源于RocksDB在压缩决策中未考虑范围删除操作。从低级细节退一步,得出的结论是,存储引擎对CockroachDB的功能和行为具有至关重要的影响。拥有存储层使CockroachDB可以更直接地控制其命运。
挑剔的读者可能会指出,以上几点并未得出重新实现RocksDB的结论。相反,我们本可以选择建立内部专业知识。我们本可以选择派生RocksDB,剥离不需要的部分,并根据CockroachDB的需求进行增强。对后一种方法进行了认真的考虑,但是最终我们决定在Go中重新实现,因为我们认为消除Go / C ++障碍将使长期的更快开发成为可能。
最后的选择是使用另一个存储引擎,例如Badger或BoltDB(如果我们想坚持使用Go)。出于多种原因,未认真考虑此替代方法。这些存储引擎并未提供我们所需的所有功能,因此我们需要对其进行重大增强。运行RocksDB的CockroachDB集群的迁移过程将变得更加复杂,这使得我们可能需要在相当长的时间内支持两个存储引擎。支持多个存储引擎本身就是一项巨大的工作:它极大地增加了测试表面积,并且替代存储引擎通常会带来很多警告(例如MyRocks不支持SAVEPOINTs)。最后,各种RocksDB-ism已渗入CockroachDB代码库,例如使用sstable格式在节点之间发送数据快照。删除这些RocksDB-ism或提供适配器,将是一项巨大的工程工作,或者会带来不可接受的性能开销。
构建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操作。可以将操作聚合为原子batch。记录可以通过Get单独读取,也可以使用迭代器读取。轻量级时间点只读快照可提供数据库的稳定视图。在内部,Pebble中的数据存储在预写日志(WAL)和排序字符串表(sstables)的组合中。最近写入的数据被缓存在一系列Memtable中的内存中,这些Memtable在后台由支持并发的Skiplist实现。将内存表刷新到磁盘以创建sstables。稳定器会在后台定期压缩。Pebble中的压缩机制和启发式方法都与RocksDB中的压缩机制和启发式方法相似(至少对于CockroachDB使用的配置而言)。
任何熟悉RocksDB内部知识的人都会在Pebble代码中看到许多相似之处。也有许多差异。我们已经记录了一些较大的文件。例如,范围删除实现与RocksDB中的实现完全不同,后者实现了更多优化,从而可以在迭代过程中跳过已删除键的范围。索引批次的处理完全不同,这使Pebble实现可以支持所有变异操作的索引,而RocksDB当前不支持(例如RocksDB不支持批量删除范围的索引)。这些示例并不构成对RocksDB的批评。我们完全希望RocksDB会采纳Pebble中的一些好主意,就像我们将继续从RocksDB中挑选好主意一样。
功能性
Pebble实现了CockroachDB使用的RocksDB功能的子集。我们不希望最终将RocksDB中的所有功能都包括在内。实际上,事实恰恰相反。我们打算通过是否对CockroachDB有用的标准来过滤所有功能添加和性能改进。对于通用键值存储引擎来说,这是一个苛刻的过滤器,但这并不是Pebble的目标。那么,Pebble包括哪些功能?
基本操作:设置,获取,合并,删除,单个删除,范围删除
分批
索引批次
只写批处理
基于块的稳定器
表级布隆过滤器
前缀Bloom过滤器
检查点
迭代器
迭代器选项(上下限,表格过滤器)
前缀迭代
反向迭代
基于级别的压缩
并发压实
手动压实
L0内压实
SSTable摄取
快照
RocksDB功能Pebble不包括:
后备
列族
删除范围内的文件
FIFO压缩样式
正向迭代器/拖尾迭代器
哈希表格式
记忆布隆过滤器
永久缓存
引脚迭代器键/值
普通表格格式
SSTable摄入
次紧缩
交易次数
通用压实样式
上面的某些物品可能会引起眉毛升高。鉴于CockroachDB提供对备份或事务的支持,Pebble如何不包括对备份或事务的支持?CockroachDB的备份和事务实现从未使用过RocksDB中的备份和事务功能。本地键值存储上的事务不需要实施分布式事务。相反,CockroachDB使用批处理(它为一组操作提供原子性)作为构建分布式事务的基础。
双向兼容性
我们很早就决定将Pebble定位为与RocksDB的双向兼容性,以便最初发布Pebble。更准确地说,Pebble当前与RocksDB 6.2.1(CockroachDB当前使用的RocksDB版本)双向兼容,以作为CockroachDB使用的RocksDB功能的子集。双向兼容性意味着Pebble可以读取RocksDB生成的DB,而RocksDB可以读取Pebble生成的DB。与RocksDB的兼容性可以无缝迁移到Pebble,仅需使用新的命令行标志重新启动Cockroach节点即可:--storage-engine=pebble。双向兼容性可提高安全性:如果在使用Pebble时遇到问题,我们可以切换回使用RocksDB。双向兼容性还提高了测试的严格性,这将在“测试”部分中详细讨论。
请注意,与RocksDB的双向兼容性有时会消失。永远保持这种兼容性与我们在CockroachDB服务中增强Pebble的愿望背道而驰。保持与新的RocksDB功能的兼容性将是巨大的持续负担。
测试中
存储引擎是数据库的组成部分,负责将数据持久地写入磁盘。存储引擎中的错误往往很严重,例如数据损坏和数据不可用。存储引擎的测试需要强大。
Pebble的测试最好描述为分层的。当前的测试层是:
Pebble单元测试
随机测试(又名变态测试)
双向兼容性测试
CockroachDB单元测试
CockroachDB每晚测试(又名roachtests)
单元测试
测试的第一层是大量的Pebble单元测试。这些单元测试旨在测试所有正常情况和极端情况。列出所有极端情况是一项具有挑战性的工作。即使是勤奋的工程师也可能会错过一些极端情况。更麻烦的是,对代码进行小的更改可能会引入新的极端情况。相信我们会在进行任何更改时识别出这些新的极端情况,这很高兴,但是我们的经验却表明并非如此。
随机测试
随机测试是解决近年来出现的极端情况的一种解决方案。模糊测试是随机测试的一个示例,通常用于检查解析器和协议解码器。对于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的胶水代码产生了相同的结果。事实证明,变形测试对于发现现有的错误以及在引入新功能时快速捕获回归非常有用。
碰撞测试
存储引擎的关键属性是将数据持久地写入磁盘。为了为更高层次的构建提供有用的基础,Pebble和RocksDB允许将写操作“同步”到磁盘,并且当操作完成时,调用者可以知道即使进程或机器也存在数据崩溃。测试崩溃恢复是一个有趣的挑战。在Pebble中,我们将崩溃测试与变态测试集成在一起。随机的一系列操作还包括“重启”操作。遇到“重新启动”操作时,所有已写入操作系统但未“同步”的数据都将被丢弃。实现这种丢弃行为相对简单,因为Pebble中的所有文件系统操作都是通过文件系统接口执行的。我们只需要添加此接口的新实现即可,该实现可缓存未同步的数据,并在发生“重新启动”时丢弃此缓存的数据。
双向兼容性测试
如前所述,Pebble的目标是与RocksDB双向兼容。为了测试这种兼容性,再次扩展了变形测试。更改了“重新启动”操作,以便在Pebble和RocksDB之间随机切换。该测试已经发现了Pebble和RocksDB之间的一些不兼容性,例如Pebble错误地在sstables上设置了一个属性,这导致RocksDB不同于Pebble来解释这些sstable。除了在变态测试中进行兼容性测试外,我们还实现了CockroachDB级集成测试,该测试模仿了用户为验证双向兼容性而可能做的事情。该测试将启动CockroachDB群集,然后随机终止并重新启动群集中的节点,从而切换使用的存储引擎。
在此测试中发现的错误类型从琐碎的差异到最严重的数据损坏类型不等。后者的一个示例是Bloom过滤器代码使用的哈希函数有一个非常细微的差异:将带符号的8位整数扩展到32位所得到的值与将无符号的8位整数扩展到32位所得到的值不同。这导致Pebble的Bloom筛选器哈希函数对于键的子集(即,包含具有高位设置的字节的键)产生与RocksDB的Bloom筛选器哈希函数不同的值。该错误的起源本身很有趣。Pebble的Bloom filter哈希函数是从go-leveldb继承的,go-leveldb是从LevelDB继承的。LevelDB哈希函数的原始实现行为取决于C字符类型是带符号的还是无符号的(可通过gcc / clang的标志来控制)。这种微妙的依赖关系几年前在LevelDB和RocksDB中都得到了修复,但是这种依赖关系在向Go的翻译中退回到了某个地方。
利用CockroachDB测试
Pebble测试的最后几层利用了现有的CockroachDB单元测试和夜间测试。我们添加了一个环境变量(),用于控制CockroachDB单元测试是使用Pebble还是RocksDB。我们还实现了另一个存储引擎,以进行进一步的测试。顾名思义,存储引擎的作用是:将所有写入操作都发送给Pebble和RocksDB。读取操作针对两个基础存储引擎,并进行比较以确保返回相同的结果。COCKROACH_STORAGE_ENGINETee
CockroachDB运行一套夜间集成测试,称为roachtests。Cockroach测试在AWS或GCP上启动集群并执行集群级别的测试。使用相同的环境变量来允许在Pebble上运行这些测试。COCKROACH_STORAGE_ENGINE
表现
如果不提高性能,就不会完成新存储引擎的公告。如果性能受到重大影响,则用RocksDB替换Pebble将是不可能的。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在6个标准YCSB工作负载上达到或超过了RocksDB。CockroachDB的性能存在存储引擎之外的瓶颈。为了更直接地比较存储引擎性能,我们直接在Pebble和RocksDB之上实现了一部分YCSB工作负载。

请注意,仅在此存储引擎基准测试工具中未实现工作负载F。工作负载C上的较大增量是由于Pebble的块和表缓存结构中的并发性更好。从CockroachDB级别的比较可以看出,当考虑整个系统时,这种更好的并发的效果将变得静音。
结论与未来工作
去年五月,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,以追求更高的性能和稳定性。