3年前 (2021-07-08)  相关技术 |   抢沙发  747 
文章评分 0 次,平均分 0.0

为什么我们在RocksDB上创建CockroachDB项目?

如果在数据库课程的期末考试中,你问学生是在日志结构的合并树(LSM)上还是在基于BTree的存储引擎上构建数据库,90%的学生可能会回答说,这个决定取决于你的工作量。”lsm用于写重负载,btree用于读重负载”,有责任心的人会写。如果您调查了目前大多数NewSQL(或分布式SQL)数据库,那么大多数数据库都是构建在LSM(即RocksDB)之上的。因此,您可能会得出这样的结论:这是因为现代应用程序已经转向了更重的写负载。你会错的。

RocksDB采用的主要动机与它选择的LSM数据结构无关。在我们的例子中,引人注目的驱动因素是它丰富的特性集,这对于像分布式数据库这样的复杂产品是必需的。在Cockroach实验室,我们使用RocksDB作为存储引擎,并依赖于许多其他存储引擎所不具备的功能,而不管它们的底层数据结构是基于LSM还是基于BTree的。

什么是存储引擎?

存储引擎的任务是将数据写入单个节点上的磁盘。对于许多数据库(可能是单节点数据库本身)来说,这是工程工作的一大部分,因此与数据库构建本身的工程工作紧密相关。

但是,在分布式环境中,分发、复制和事务协调部分增加了工程复杂性,在Cockroach实验室,我们开始寻找一种成熟的存储引擎产品,而不是从头开始构建。

那么存储引擎到底是什么呢?考虑到对任何数据库(原子性、一致性、隔离性和持久性,或ACID)的巨大要求,一个简单的第一个答案是存储引擎的责任是原子性和持久性。这使数据库的更高层得以专注于分布式协调,以获得强大的隔离保证(如序列化),并提供确保数据完整性所需的一致性原语。

不过,除了原子性和耐久性之外,存储引擎还有另一项重要工作:性能。存储引擎的性能特征在很大程度上决定了整个数据库的性能上限。例如:几乎每个存储引擎都有一个写前日志,用于快速使写持久,但稍后会移到主索引数据结构。这是一个非常重要的性能优化,但要正确进行也是很困难的,并且涉及到微妙的性能权衡,因为在保持高并发性的同时将写操作提交到写前日志是很困难的。

存储引擎还必须提供一个定义的隔离模型(例如,您的读取将反映仍在写前日志中的写入,并且将作为某个单点时间快照),同时支持许多并发操作。虽然该隔离模型可能比数据库提供的整体隔离更简单(例如,RocksDB提供快照隔离,而CockroachDB提供额外的簿记以提供序列化功能),但保持它的高性能仍然很复杂。

如果这听起来像是构建数据库的大量工作,那就是!例如,Postgres并没有一个定义好的“存储引擎”——它都是一个单片系统。但是对于分布式数据库,单个节点的存储引擎是更大的分布式系统的一小部分,所以让我们深入理解如何在CockroachDB使用ROCKSDB,注意我们所使用的鲜为人知的特性。

RocksDB底层

RocksDB是一个单节点键值存储引擎。该设计基于日志结构的合并树(LSMs)。RocksDB是早期Google项目LevelDB的一个分支,LevelDB是一个嵌入式键值存储,其灵感来自BigTable使用的低级存储引擎。RocksDB后来成为了一个更加健壮、功能完整的存储引擎,但其基本结构与LevelDB和许多其他基于LSM的存储引擎相同。

在RocksDB中,键和值作为排序字符串存储在名为SSTables的文件中。这些表分为几层。在单个级别中,SSTables是不重叠的:一个SSTable可能包含覆盖范围[a,b]的键,下一个SSTable可能包含范围[b,d]的键,依此类推。键空间在级别之间确实重叠:如果有两个级别,第一个级别可能有两个SSTable(覆盖上述范围),但是第二个级别可能在键空间[a,e]上有一个SSTable。查找关键字aardvark需要查找两个SSTable:level1中的[a,b)SSTable和level2中的[a,e)SSTable。每个SSTable都是内部排序的(如名称中所示),因此在SSTable内的查找需要log(n)时间。SSTables将它们的键存储在块中,并且有一个内部索引,因此即使单个SSTable可能非常大(大小为千兆字节),也只需要将索引和相关块加载到内存中。

这些级别的结构大体上是这样的:每个级别的总大小是其上级别的10倍。新的密钥到达最高层,当该层变得越来越大并达到阈值时,该层的一些sstable会被压缩成更少(但更大)的sstable,比它低一层。何时压缩(以及如何压缩)的精确细节对性能有很大影响;分层压缩是一种压缩策略,但是对于一个穷尽的资源,markcallaghan的博客有更多关于各种压缩算法以及它们之间的权衡的描述。

在磁盘级别之上是内存中的memtable。memtable是一种排序的内存数据结构(虽然RocksDB有几个选项,但我们使用了并发skiplist),这使得读取便宜,但作为未排序的写前日志(WAL)持久化。如果一个节点崩溃,在启动时,持久的WAL被读回,memtable被重建。由于此数据结构随着写入次数的增加而增长,因此需要将其刷新到磁盘。在持续的写吞吐量期间,这种逐出可能是一个关键的瓶颈。为了使memtable刷新便宜,L0是特殊的:它的SSTables允许重叠。然而,这使得L0成为压缩和读取性能的一个关键瓶颈——它通常被大块压缩成L1,L0中的每个表都增加了读取放大率。

因此,如您所见,写操作创建了延迟的写操作放大,以最终压缩的形式,最终将键向下推到层次结构中。突发的写工作负载可以容纳大量的写操作。持续的写操作需要将IO带宽的一部分用于并发执行一些压缩(最初RocksDB项目的一个相当大的动机是使用多核并发来比在LevelDB中更有效地解决这个问题)。

将高级SQL操作转换为K和V操作

CockroachDB是一个分布式SQL数据库。这些SQL操作向下转换为单个逻辑键空间上的键和值操作。这个逻辑密钥空间被分割成密钥的物理“范围”,每个范围在三个(或更多)Cockroach节点上复制。给定这种结构,一个给定的SQL操作将转换为一组键值操作,这些操作分布在多台机器上。因此,在给定的机器上,这些操作需要在底层存储引擎上执行。

这个API相对简单,因为RocksDB还提供了一个键值接口。但“键值接口”到底是什么意思?缺乏标准化隐藏了很多微妙的细节:这个接口不仅仅是对键的put、get和delete操作。RocksDB还支持扫描范围[开始,结束]。还考虑其他重要操作:删除一个范围(开始,结束),并批量摄取一组键和值。让您的存储引擎以高性能的方式执行这些操作是至关重要的,否则一些SQL操作会变得非常慢。让我们讨论一下为什么这些对像CockroachDB这样的分布式数据库如此重要。

快速扫描

CockroachDB的一个令人惊讶的部分是意识到扫描的频率比你想象的要高!许多学术论文使用put/get操作来测试存储引擎性能,例如使用YCSB作为存储引擎基准。然而,在CockroachDB中,put/scan是两个最主要的操作,因为我们作为可序列化的SQL数据库提供了更高级别的保证。考虑多版本并发控制(MVCC)——CockroachDB存储给定密钥的多个值,并存储每个密钥写入的时间戳。事务在特定的时间戳上发生,并且保证读取该时间戳的最新值。因此,看起来可能是数据库级别的GET操作(例如,SELECT*from tablename,其中primarykeycol=123转换为存储该行的键的值的GET)变成了存储引擎级别的扫描(扫描该键的最新值)。因此,每个CockroachDB密钥都用时间戳进行注释,以创建等效的RocksDB密钥。并更新一个键以创建其他RocksDB键。

MVCC的使用会引起额外的并发症。许多键值存储引擎具有快速的GET操作,但扫描操作较慢。考虑任何日志结构的合并树(LSM)实现:一个特定的密钥可以在LSM的任何级别。这意味着每个GET操作都有一个级别数的读取放大系数(RAF)(一个逻辑读取=RAF磁盘读取)。为了减少这种log(n)倍数,存储引擎通常在sst上使用bloom过滤器。Bloom过滤器是一种概率数据结构,它足够小,可以保存在内存中。bloom过滤器用“no”或“maybe”(如果是“maybe”,则必须执行读取才能找到答案,但“no”表示可以跳过读取)来回答问题:“给定的键在此级别中存在吗?”。

不幸的是,bloom过滤器是每个关键。扫描涉及两个端点之间潜在的无限键空间,因此不能使用bloom过滤器排除要扫描的级别。除非在构建bloom过滤器时可以预处理密钥。RocksDB有一个优雅的特性来实现这一点-前缀bloomfilters,它允许bloomfilter构造在一个键的前缀上。这意味着超过该前缀的扫描可以受益于使用bloom过滤器。否则,存储引擎将不得不扫描每个逻辑GET操作的每个级别,这将对性能造成巨大的影响。前缀bloom过滤器,如RocksDB提供的,因此是MVCC数据库(如CockroachDB)的表桩。

RocksDB快照

Cockroach的分布式复制意味着,偶尔需要通过一些数据的拷贝来加快新节点的速度。这需要扫描一大块密钥空间,并通过网络发送到新节点。根据数据的大小和网络的速度,此操作可能需要相当长的时间(从几秒到几十秒)。

如果需要一段时间,Cockroach有两种选择:长时间扫描,或者做整个扫描并分别保存数据,直到传输完成。包括RocksDB在内的大多数存储引擎都提供了这样一种功能,即任何给定的扫描操作都是在数据库的一致“快照”上完成的—扫描开始后完成的写入操作不会反映在扫描操作中。存储级别上的这种隔离保证是数据库在其上构建的有用构建块。它还用于发送这些快照以进行复制,但挑战在于如何在不使用太多资源的情况下提供快照功能。特别是,读取快照会在读取操作期间锁定memtables,这会减慢传入的写入。单纯地固定memtables是很昂贵的,读取快照以释放存储引擎也是一种选择,只有这样,在传输完成之前,才必须将这些键保持在更高级别的内存中。

CockroachDB将密钥空间分为多个范围,每个范围都高达64mb,这一事实加剧了这个问题。这意味着,当出现一个新节点时,很可能会导致创建许多快照并在整个集群中传输,然后再填充到与其他节点的奇偶校验。所有这一切,而集群本身必须继续正常运行。

RocksDB提供了另一种中间立场——显式快照。显式快照不固定memtables。相反,它们是一种占位符,通知存储引擎的其余部分不要执行压缩,因为压缩会跨越快照的时间边界。保持一个显式快照,即使是几个小时,也不会消耗额外的资源(除了防止一些压缩之外,这些压缩可能会使您的存储布局更有效率)。当您准备对数据进行迭代时,您将创建一个隐式快照(在迭代器运行期间可能需要固定memtables),但是这样,您就不必长时间持有昂贵的隐式快照。

更多RocksDB功能

我们在CockroachDB中使用了很多RocksDB特性,完全覆盖所有这些特性将需要更多的页面!下面是我们使用的许多RocksDB功能的简短预览:

SSTable摄入

在备份的恢复过程中,我们希望摄取包含密钥的文件,这些密钥跨越密钥空间的一部分,我们可以保证在开始摄取时密钥空间是空的。使用正常的写路径是浪费的---正常的写路径包括写到LSM的高层,然后压缩,这涉及到大量的写放大。如果我们知道这个键空间是空的,我们可以简单地在一个低级别上预构建SSTables,以保证不与其他sstable重叠。这大大提高了恢复吞吐量。这对于蟑螂数据库来说是一个相当关键的特性,因此将SSTables摄取到RocksDB中是一个关键特性。

自定义密钥比较器

我们加在键上的MVCC时间戳后缀可以这样编码:字典比较(逐字节)等同于逻辑比较。但是这样的编码比不按字典顺序进行比较的编码和解码要慢。RocksDB通过允许为密钥定义一个自定义比较器,再次起到了解救作用。这使得MVCC时间戳的编码能够快速编码和解码,同时还允许保持正确的顺序(时间戳按降序排序,因为我们希望最新版本的密钥先排序,以便扫描在第一次匹配时停止)。因此,键之间高效的自定义比较器是RocksDB所需要的特性。

范围删除逻辑删除

需要有效地删除整个范围的键,否则,删除表或简单地在节点之间移动数据块之类的操作可能会阻塞很长一段时间(非工程师:在计算机中,移动总是作为一个拷贝执行,后跟一个delete)。RocksDB通过编写范围删除逻辑删除来执行删除范围操作。在读取键时,如果覆盖该键的范围逻辑删除标记的读取级别高于该键的具体值,则该键被视为已删除。表中的实际删除是在压缩过程中执行的。

向后迭代

向后迭代使SELECT*FROM TABLE ORDER BY key DESC LIMIT 100这样的查询有效,即使键上的索引是按升序排列的。一些存储引擎不提供向后迭代的能力,这使得这些查询效率低下。值得注意的是,在不改变底层数据布局的情况下,向后迭代总是比向前迭代更昂贵。RocksDB从系统的更高层次抽象出复杂性。值得注意的是,在这方面有机会提高业绩。

索引批次

批处理是原子写入操作(包含set、delete或delete range操作)的单位。基本批处理是只写的。RocksDB还支持“索引批处理”,允许从批处理读取数据。在分布式数据库中,由于CockroachDB等待远程操作提交,有些写操作可能需要更长的时间。但是,同一事务中的其他操作需要能够读取挂起的写操作。为了支持这一点,我们依赖于存储引擎来提供一种机制来批量处理一组可以原子化应用的更新,同时还提供了一种方法来读取那些更新的合并视图,该视图是在整个数据库之上分层的。RocksDB对索引批处理的支持使这变得更加容易。

加密支持

对于即将推出的特性—静态加密—我们依赖RocksDB的模块化支持来加密SSB表。这做了很多繁重的工作来保持数据的加密,这样我们就可以把重点放在密钥管理、轮换和面向用户的支持加密的部分上。

RocksDB今天在CockroachDB上

如今RocksDB已经深深地嵌入到了Cockroach的建筑中。其他不具备上述功能的存储引擎需要我们进行重大的重新设计才能采用它们,即使这样,也可能导致性能下降。一旦考虑到所有这些因素,在原始密钥/值访问速度方面所承诺的性能提高很可能会消失。

也就是说,RocksDB并不全是玫瑰。毕竟,我们只有在为满足我们的需求而在RocksDB工程上付出了相当大的努力之后,才获得了这样的性能!在C++中,我们的代码库的性能关键部分也有缺点,而系统的其余部分则是GO。跨越CGo的障碍是很微妙的,而且每次呼叫都需要70ns的开销。听起来很快(纳秒是十亿分之一秒),但是我们执行了很多RocksDB调用。最大限度地减少CGo交叉口的数量具有可测量的影响!我们现在在Go中构建我们的整批RocksDB操作(例如一组put),然后在单个CGo调用中传输它们以提高效率。我们还必须将值从C分配的内存复制到Go分配的内存中,从而导致性能下降。Go native存储引擎可以为我们提供许多性能优势,并优化我们的代码库,尽管考虑到上述要求,简单地使用现有的Go native存储引擎有点不太可能(我们不知道有哪一个引擎提供了我们所需的所有功能)。考虑到实现所有这些微妙的性能关键特性或围绕最小化CGo开销的工程的选择,到目前为止,我们发现后者是可管理的,但我们会密切关注这种计算何时发生变化。

最后,RocksDB包含了更多我们在CockroachDB中还没有使用的特性,比如列族、FIFO压缩、备份和检查点、持久缓存和事务……也许我们会在即将发布的CockroachDB版本中找到性能原因!

原文地址:https://www.cockroachlabs.com/blog/cockroachdb-on-rocksd/

 

除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/2094.html

关于

发表评论

表情 格式

暂无评论

登录

忘记密码 ?

切换登录

注册