[译] 支撑现代存储系统的算法

读优化 B-Tree 和写优化 LSM-Tree 的不一样用途

做者:Alex Petrov

随着应用程序处理的数据量不断增加,扩展存储变得愈发具备挑战性。每一个数据库系统都有本身的方案。为了从这些方案中作出正确的选择,了解它们是相当重要的。html

每一个应用程序在读写负载平衡、一致性、延迟和访问模式方面各不相同。熟悉数据库和底层存储能帮助你进行架构决策、解释系统行为、排除故障以及根据具体状况调优。前端

优化一个系统不可能作到面面俱到。咱们固然但愿有一个数据结构既能保证最佳的读写性能,又不须要任何存储开销,但显然,这是不存在的。mysql

本文深刻讨论了大多数现代数据库中使用的两种存储系统设计 —— 读优化 B-Tree [1] 和写优化 LSM(log-structured merge)-Tree [5] —— 并描述了它们的用例和优缺权衡。android

B-Tree

B-Tree 是一种流行的读优化索引数据结构,是二叉树的泛化。它有许多变种,而且被用于多种数据库(包括 MySQL InnoDB [4]PostgreSQL [7])甚至文件系统(HFS+ [8]、HTrees ext4 [9])。B-Tree 中的 B 表明原始数据结构的做者 Bayer,或是他当时就任的公司 Boeingios

搜索二叉树中,每一个节点都有两个孩子(称为左右孩子)。左子树的节点值小于当前节点值,右子树反之。为了保持树的深度最小,搜索二叉树必须是平衡的:当随机顺序的值被添加到树中时,若是不加调整,终会致使树的倾斜。git

一种平衡二叉树的方法是所谓的旋转:从新排列节点,将较深子树的父节点向下推到其子节点下方,并将该子节点拉上来,将其放在原父节点的位置。图 1 是平衡二叉树中的旋转示例。在左侧添加节点 2 后,二叉树失去平衡。为了使该树平衡,将其以节点 3 为轴旋转(树围绕它旋转)。而后节点 5(旋转前是根节点和节点 3 的父节点)成为其子节点。旋转完成后,左侧子树的深度减小 1,右侧子树的深度增长 1。树的最大深度已经减少。github

支撑现代存储系统的算法

二叉树是最有用的内存数据结构。然而因为平衡(保持全部子树的深度最小)和低出度(每一个节点最多两个子节点),它们在磁盘上水土不服。B-Tree 容许每一个节点存储两个以上的指针,并经过将节点大小与页面大小(例如,4 KB)进行匹配来与块设备协同工做。今天的一些实现将使用更大的节点大小,跨越多个页面。算法

B-Tree 有如下几个性质:sql

• 有序。这容许顺序扫描并简化查找。数据库

• 自平衡。在插入和删除时不须要平衡树:当 B-Tree 节点已满时,它被分红两部分,而且当相邻节点的利用率低于某个阈值时,合并这两个节点。这也意味着各叶子节点与根节点的距离相等,而且在查找过程当中定位的步数是相同的。

• 对数级查找时间复杂度。查找时间是很是重要的,这使得 B-Tree 成为数据库索引的理想选择。

• 易变。插入、更新、删除(包括所以致使的拆分和合并)过程在磁盘上进行。为了使就地更新成为可能,须要必定的空间开销。B-Tree 能够做为汇集索引,实际数据存储在叶子节点上,也能够做为非汇集索引,称为一个堆文件。

本文讨论的 B+Tree [3] 是一种常常用于数据库存储的 B-Tree 现代变种。B+Tree 与原始 B-Tree [1] 的不一样之处在于:(1)它采用额外连接的叶节点存储值;(2)值不能存储在内部节点上。

剖析 B-Tree

咱们先来仔细看看 B-Tree 的结构,如图 2 所示。B-Tree 的节点有几种类型:根节点,内部节点和叶子节点。根节点(顶部)是没有双亲的节点(即,它不是任何节点的子节点)。内部节点(中间)有双亲和孩子节点;他们将根节点和叶子节点链接起来。叶子节点(底部)持有数据而且没有孩子节点。图 2 描绘了分支因子为 4(4 个指针,内部节点中有 3 个键,叶上有 4 个键/值对)的 B-Tree。

支撑现代存储系统的算法

B-Tree 的特性以下:

• 分支因子 —— 指向子节点的指针数(N)。除指针外,根节点和内部节点还持有 N-1 个键。

• 利用率 —— 节点当前持有的指向子节点的指针数量与可用最大值之比。例如,若某树分支因子是 N,且其中某节点当前持有 N/2 个指针,则该节点利用率为 50%。

• 高度 —— B-Tree 的数量级,表示在查找过程当中必须通过多少指针。

树中的每一个非叶节点最多可持有 N 个键(索引条目),这些键将树分为 N+1 个子树,这些子树能够经过相应的指针定位。项 Ki 中的指针 i 指向某子树,该子树中包含全部 Ki-1 <= K目标 < Ki(其中 K 是一组键)的索引项。首尾指针是特殊的,它们指向的子树中全部的项都小于等于最左子节点的 K0 或大于最右子节点的 KN-1。叶子节点同时持有其同级先后节点的指针,造成兄弟节点间的双向链表。全部节点中的键老是有序的。

查找

进行查找时,将从根节点开始搜索,并通过内部节点递归向下到叶子节点层。在每层中,经过指向子节点的指针将搜索范围缩小到某子树(包含搜索目标值的子树)。图 3 展现了 B-Tree 的一次从根到叶的搜索过程,指针在两个键之间,其中一个大于(或等于)搜索目标,另外一个小于搜索目标。进行点查询时,搜索将在定位到叶子节点后完成。进行范围扫描时,遍历所找到的叶子节点的键和值,而后遍历范围内的兄弟叶子节点。

支撑现代存储系统的算法

在复杂度方面,B-Tree 保证查询的时间复杂度为 log(n),由于查找一个节点中的键使用二分查找,如图 4 所示。二进制搜索能够通俗的解释为在字典中查找以某字母开头的单词,字典中全部单词都按字母顺序排序。首先你翻开正好在字典中间的一页。若是要查找的单词字母顺序小于(在前面)当前页,你继续在字典的左半边查找;不然就继续在右半边查找。你继续像这样将剩余的页码范围分为一半,选择一边,直到找到指望的字母。每一步都将搜索范围减半,所以查找的时间复杂度为对数级。 B-Tree 节点上的键是有序的,且使用二分查找算法进行匹配,所以 B-Tree 的搜索复杂度是对数级的。这也说明了保持树的高利用率和统一访问的重要性。

支撑现代存储系统的算法

插入、更新、删除

进行插入时,第一步是定位目标叶子节点。此过程使用前序搜索算法。在定位目标叶子节点后,键和值将被添加至该节点。若是该节点没有足够的可用空间,这种状况称为溢出,则将叶子节点分割成两部分。这是经过分配一个新的叶子节点,将一半元素移动到新节点并将一个指向这个新节点的指针添加到父节点来完成的。若是父节点没有足够的空间,则也会在父节点上进行分割。操做一直持续到根节点为止。当根节点溢出时,其内容在新分配的节点之间被分割,根节点自己被覆盖以免重定位。这也意味着树(及其高度)老是经过分裂根节点而增加。

LSM-Tree

结构化日志合并树是一个不可变的基于磁盘的写优化数据结构。它适用于写入比查询操做更频繁的场景。LSM-Tree 已经得到了更多的关注,由于它能够避免随机插入,更新和删除。

剖析 LSM-Tree

为了容许连续写入,LSM-Tree 在内存中的表(一般使用支持查找的时间复杂度为对数级的数据结构,例如二叉搜索树或跳跃表)中批量写入和更新,当其大小达到阈值时将它写在磁盘上(这个操做称为刷新)。检索数据时须要搜索树全部磁盘中的部分,检查内存中的表,合并它们的内容,而后再返回结果。图 5 展现了 LSM-Tree 的结构:用于写入的基于内存的表。只要内存表体积达到必定程度,内存表就会被写入磁盘。进行读取时,同时读取磁盘和内存表,经过一个合并操做来整合数据。

支撑现代存储系统的算法

有序串行表

由于 SSTable(有序串行表)的简单性(易于写入,搜索和读取)与合并性能(合并期间,扫描源 SSTable,合并结果的写入是顺序的),多数现代的 LSM-Tree 实现(例如 RocksDBApache Cassandra)都选用 SSTable 做为硬盘表。

SSTable 是一种基于硬盘的有序不可变的数据结构。从结构上来看,SSTable 能够分为两部分:数据块和索引块,如图 6 所示。数据块包含以键为顺序写入的惟一键值对。索引块包含映射到数据块指针的键,指针指向实际记录的位置。为了快速搜索,索引通常使用优化的结构实现,例如 B-Tree 或用于点查询的哈希表。SSTable 中的每个值都有一个时间戳与之对应。时间戳记录了插入、更新(这二者通常不作区分)和删除时间。

支撑现代存储系统的算法

SSTable 具备如下优势:

• 经过查询主键索引能够实现快速的点查询(例如,经过键寻找值)。

• 只须要顺序读取数据块上的键值对就能够实现扫描(例如,遍历制定范围内的键值对)。

SSTable 表明一段时间内全部数据库操做的快照,由于 SSTable 是经过对内存表的刷新操做建立的,该表充当此时段内对数据库状态的缓冲区。

查询

检索数据须要搜索硬盘上的全部 SSTable,检查内存表,而且合并它们的内容后返回结果。要搜索的数据能够存储在多个 SSTable 中,所以合并步骤是必须的。

合并步骤也是确保删除和更新正常工做所必需的。在 LSM-Tree 中,经过插入占位符(一般称为墓碑)来指定哪一个键被标记为删除。一样的,更新操做只是提交一个带较晚时间戳的记录。在读取期间,被标记删除的记录被跳过,不会返回给客户端。更新操做与之相似:在具备相同键的两个记录中,只返回具备较晚时间戳的记录。图 7 展现了一次合并操做,用于对在不一样表中存储的同一个键的数据进行整合:如图,Alex 记录中时间戳是 100,随后更新了新的电话,时间戳为 200;John 记录被删除。另外两项没有改变,由于它们没有被覆盖。

支撑现代存储系统的算法

为了减小搜索 SSTable ,防止为了查找某个键而搜索每一个 SSTable,许多存储系统采用一个被称为布隆过滤器 [10] 的数据结构。这是一个几率数据结构,可用于测试某个元素是否属于某集合。它有可能产生错误的确定(即,判断元素是集合的成员,但实际上并非),但不能产生错误的否认(即,若是返回否认结果,则元素必定不是集合的成员)。换句话说,布隆过滤器用于判断键“可能在 SSTable 中”或“绝对不在 SSTable 中”。在搜索过程当中,将会跳过布隆过滤器返回否认结果的 SSTable。

LSM-Tree 的维护

因为 SSTable 是不可变的,所以它们会按顺序写入,而且不存在用于修改的预留空白空间。这就意味着插入、更新或删除操做将须要重写整个文件。全部修改数据库状态的操做都在内存表中“批处理”。随着时间的推移,磁盘表的数量将增长(同一个键的数据位于几个不一样文件,同一记录有多个不一样的版本,被删除的冗余记录),读取操做的开销将变得愈来愈大。

为了下降读取开销,整合被删除记录占用的空间并减小磁盘表的数量,LSM-Tree 须要一个压缩操做,从磁盘读取完整的 SSTable 并合并它们。因为 SSTable 是以键排序的,所以其压缩工做和归并排序相似,是很是高效的操做:从多个源有序序列中读取记录,进行合并后的输出立刻追加到结果文件中,则结果文件也是有序的。归并排序的一个优势是,即便合并内存吃不消的大文件,它依旧能够高效地工做。结果表保留了原始 SSTable 的顺序。

在此过程当中,被合并的 SSTable 被丢弃并替换为其“压缩”后的版本,如图 8 所示。压缩多个 SSTable 并将它们合并为一个。某些数据库系统在逻辑层面上按大小把不一样的表分为不一样级别,分组到相同的“级别”,并在特定级别的表足够多时开始合并操做。压缩后,SSTable 的数量减小,提升查询效率。

支撑现代存储系统的算法

原子性与持久性

为了减小 I/O 操做并使它们顺序执行,不管是 B-Tree 仍是 LSM-Tree 都在实际更新以前,先在内存中进行批量操做。这意味着,在故障状况时,数据完整性、原子性(将一系列操做赋予原子性,将它们视为一个操做,要么所有执行要么全不执行)、持久性(当进程崩溃或电源失效时,能够确保数据已经到达持久性存储设备)得不到保证。

为了解决这个问题,大多数现代存储系统采用 WAL(预写日志)。WAL 的核心思想是,全部数据库状态改变都先持久化进硬盘中的只追加日志中。若是进程在工做中崩溃,将会重映日志以确保没有数据丢失且全部更改都知足原子性。

在 B-Tree 中,使用 WAL 能够理解为仅在写入操做被记录后才将其写入数据文件。一般,B-Tree 存储系统的日志尺寸相对较小:只要将更改应用于持久存储,它们就能够被弃用。WAL 还能够做为运行时操做的备份:任何未应用于数据页的更改均可以根据日志记录重作。

在 LSM-Tree 中,WAL 用于保存处于内存表但还没有彻底刷新到磁盘上的更改。只要内存表被刷新完毕并置换,即可以在新建立的 SSTable 中进行读取操做,则 WAL 中从内存表刷新到硬盘上的那部分更改就能够丢弃了。

总结

B-Tree 和 LSM-Tree 数据结构最大的差别之一是它们优化的目的以及优化的效果。

咱们来对比一下 B-Tree 和 LSM-Tree 之间的特性。总的来讲,B-Tree 具备如下特性:

• 它是可变的,它容许经过一些空间开销和更多的写入路径来进行就地更新,于是它不须要文件重写或多源合并。

• 它是读优化的,这意味着它不须要从多个源数据中读取(也不须要合并),于是简化了读取路径。

• 写操做可能引发级联节点分裂,这使得写操做开销较高。

• 它针对分页环境(块存储)进行了优化,杜绝了字节定位操做。

• 碎片化, 由频繁更新形成的碎片化可能须要额外的维护和块重写。然而对 B-Tree 的维护通常要比 LSM-Tree 要少。

• 并发访问读写隔离,这涉及锁存器与锁链。

LSM-Tree 具备如下特性:

• 它是不可变的。SSTable 一旦被写入硬盘就不会再改变。压缩操做被用于整合占用空间,删除条目,合并在不一样数据文件中的同键数据。做为压缩操做的一部分,在成功合并后,源 SSTable 将被弃用并删除。这种不可变性给咱们带来了另外一个有用的特性,刷新后的表能够被并发访问。

• 它是写优化的,这意味着写入操做将进入缓冲,随后顺序刷新到硬盘上,可能支持基于硬盘的空间局部性。

• 读取操做可能须要访问多个数据源,由于在不一样时间写入的同一个键的数据有可能位于不一样的数据文件中。必须通过合并过程才能将记录返回给客户端。

• 须要维护 / 压缩,由于缓冲中的写入操做被刷新到硬盘上。

评估存储系统

开发存储系统总要面对相似的挑战,考虑相似的因素。决定优化方向会对结果产生很大影响。你能够在写入过程当中花费更多时间来布局结构以实现更高效的读取,为就地更新预留空间,也能够缓冲数据确保顺序写入以提升写入速度。可是,一次完成这一切是不可能的。理想中的存储系统应该具备最低的读取成本,最低的写入成本,而且没有额外开销。但实际上,数据结构只能在多个因素之间权衡。理解这些取舍是重要的。

来自哈佛 DASlab(数据系统实验室)的研究人员总结了数据库系统优化方向的关键三点:读取开销、更新开销和内存开销(或简称为 RUM)。对于数据结构、访问方法、甚至适用于某些工做负载的选择应该了解哪些参数对你的用例最为重要,由于算法是针对特定用例量身定制的。

RUM 假说 [2] 为上述的两种开销设置了上限,同时为第三种设置了下限。例如,B-Tree 以提升写入开销、预留空间(同时也形成了内存开销)为代价进行读优化。LSM-Tree 以读取时必须进行多硬盘表访问的高读取开销换取低写入开销。在处于竞争三角形的三个参数中,一方面的改进可能就意味着另外一方面的让步。图 9 对 RUM 假说进行了说明。

支撑现代存储系统的算法

B-Tree 优化读取性能:索引的布局方式能够最小化遍历树的磁盘访问需求。经过访问一个索引文件就能够定位数据。这是经过持续更新索引文件来实现的,但这也增长了因为节点拆分和合并,重定位以及碎片、不平衡相关的维护形成的额外写入开销。为了平稳更新成本并减小分割次数,B-Tree 在全部级别的节点上都预留有额外的空间。这有助于在节点饱和以前延迟写入开销的增加。简而言之,B-Tree 牺牲更新和内存性能以得到更好的读取性能。

LSM-Tree 优化写入性能。不管是更新仍是删除都须要在磁盘上定位数据(B-Tree 也同样),而且它经过在内存表中缓存全部插入,更新和删除操做来保证顺序写入。这是以较高的维护成本和压缩需求(这是惟一的缓解不断增加的读取开销和减小磁盘表的数量的方式)和更高的读取成本(由于数据必须从多个源读取并合并)为代价的。同时,LSM-Tree 经过不保留任何预留空间来减小内存开销(不一样于 B-Tree 节点,其平均利用率为 70%,包含就地更新所需的开销),由于更高的利用率和最终文件的不变性,LSM-Tree 支持块压缩。简而言之,LSM-Tree 牺牲读取性能,提升维护成原本得到更好的写入性能和更低的内存占用。

有的数据结构可针对每一个指望的属性进行优化。使用自适应数据结构能够以更高维护成本得到更好的读取性能。添加有助于遍历的元数据(如分散层叠)将会影响写入时间并占用更多空间,但能够提升读取性能。使用压缩优化内存使用率(例如,Gorilla 压缩 [6]delta 编码等诸多算法)会增长一些开销,用于在写入时压缩数据并在读取时解压缩数据。有时候,你能够牺牲功能来提升效率。例如,堆文件和散列索引因为文件格式简单,能够保证很好的性能和较小的空间开销,而做为代价,它们不支持除点查询之外的其余功能。你还能够经过使用近似数据结构(如布隆过滤器、HyperLogLogCount-Min sketch 等)来为了空间与效率牺牲精度。

三种可变参数 —— 读取,更新和内存开销 —— 能够帮助你评估数据库并深刻了解最适合的工做负载。它们都很是直观,将存储系统按其分类很容易,猜想它是如何执行的,而后经过大量测试验证你的假设。

固然,评估存储系统时还有一些其余重要因素须要考虑,例如维护开销,易用性,系统要求,频繁增删的适应性,访问模式等。RUM 假说只是帮助发展直观感受并提供初始方向的一条经验法则。了解你的工做部件是构建可扩展后端的第一步。

一些因素可能因实施而异,甚至两个使用相似存储设计原则的数据库可能会有不一样表现。数据库是包含许多可插拔模块的复杂系统,是许多应用程序的重要组成部分。这些信息将帮助你窥探数据库的底层,而且了解底层数据结构和其内部行为之间的差别,从而决定哪一个是最适合你的。

参考文献

1. Comer, D. 1979. The ubiquitous B-tree. Computing Surveys 11(2); 121-137; citeseerx.ist.psu.edu/viewdoc/dow….

2. Data Systems Laboratory at Harvard. The RUM Conjecture; daslab.seas.harvard.edu/rum-conject….

3. Graefe, G. 2011. Modern B-tree techniques. Foundations and Trends in Databases 3(4): 203-402; citeseerx.ist.psu.edu/viewdoc/dow….

4. MySQL 5.7 Reference Manual. The physical structure of an InnoDB index; dev.mysql.com/doc/refman/….

5. O'Neil, P., Cheng, E., Gawlick, D., O'Neil, E. 1996. The log-structured merge-tree. Acta Informatica 33(4): 351-385; citeseerx.ist.psu.edu/viewdoc/dow….

6. Pelkonen, T., Franklin, S., Teller, J., Cavallaro, P., Huang, Q., Meza, J., Veeraraghavan, K. 2015. Gorilla: a fast, scalable, in-memory time series database. Proceedings of the VLDB Endowment 8(12): 1816-1827; www.vldb.org/pvldb/vol8/….

7. Suzuki, H. 2015-2018. The internals of PostreSQL; www.interdb.jp/pg/pgsql01.….

8. Apple HFS Plus Volume Format; developer.apple.com/legacy/libr…

9. Mathur, A., Cao, M., Bhattacharya, S., Dilger, A., Tomas, A., Vivier, L. (2007). The new ext4 filesystem: current status and future plans. Proceedings of the Linux Symposium. Ottawa, Canada: Red Hat.

10. Bloom, B. H. (1970), Space/time trade-offs in hash coding with allowable errors,Communications of the ACM, 13 (7): 422-426

相关文章

五分钟法则:20 年后闪存将如何改写游戏规则
Goetz Graefe, Hewlett-Packard 实验室
旧规则继续发展,而闪存增长了两条新规则。
queue.acm.org/detail.cfm?…

Disambiguating Databases
Rick Richardson
根据你的访问模型构建数据库。
queue.acm.org/detail.cfm?…

你作错了!
Poul-Henning Kamp
你觉得本身已经掌握了服务器性能的艺术了么?再想想。
queue.acm.org/detail.cfm?…

Alex Petrov (coffeenco.de/, @ifesdjeen (GitHub) @ifesdjeen (Twitter)),一位 Apache Cassandra 贡献者、存储系统爱好者。在过去的几年,他一直致力于数据库,为各个公司创建分布式系统和数据处理管道。

本文英文原文 PDF 文件:下载地址

Copyright © 2018 held by owner/author. Publication rights licensed to ACM.


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索