解读现代存储系统背后的经典算法

做者|Alex Petrov
译者|盖磊
编辑 | Debra
AI 前线导读:本文详细剖析了两种被大多数现代数据库使用的存储系统设计方法,即针对读操做优化的 B 树,以及针对写操做优化的 LSM 树,并介绍了两种方法的一些用例和权衡考虑。

更多干货内容请关注微信公众号“AI 前线”,(ID:ai-front)

文章最早发表在 ACM Queue 期刊第 16 卷第 2 期,可经过 ACM 数字图书馆查阅(https://portal.acm.org/citation.cfm?id=3220266)。引用该文章:“Alex Petrov. 2018. Algorithms Behind Modern Storage Systems. Queue 16, 2, pages 30 (April 2018), 21 pages. DOI: https://doi.org/10.1145/3212477.3220266.”。html

应用处理的数据量在持续增加。数据的增加,对扩展存储能力提出了挑战。就此问题,每种数据库管理系统都有其自身的权衡考虑。对于数据管理者而言,理解这些权衡因素很是关键,这有助于从多种方式中作出正确的选择。mysql

从读 / 写工做负载平衡、一致性需求、延迟和访问模式等方面看,应用是各异的。若是咱们能对数据库和存储内部设施架构决策了然于胸,那么将有助于咱们理解系统行为模式的缘由所在,一旦在问题时能解决问题,并能根据工做负载调优数据库。算法

一个系统不可能在全部方面上都是最优的。确保无存储开销、提供最优读写性能的数据结构只存在于理想状况下,在实践中固然是不可能存在的。sql

本文详细剖析了两种被大多数现代数据库使用的存储系统设计方法,即针对读优化的 B 树1和针对写优化的 LSM(日志结构合并,log-structured merge)树 5,并分别给出了两种方法的一些用例和权衡考虑。数据库

B 树

B 树是一种广为使用的读优化索引数据结构,是二叉树的一种泛化。它具备多种变体,并已用于多种数据库(包括 MySQL InnoDB4 和 PostgreSQL 7)和文件系统(例如,HFS+八、ext4 中的 HTrees 9)。B 树中的“B”表示“Bayer”,指的是数据结构的最初创立者 Rudolf Bayer,也能够说是 Bayer 彼时供职的波音公司(Boeing)。后端

二叉树中,每一个节点有两个子节点(分别称为左子节点和右子节点)。保存在左子树和右子树中的键(Key),其值分别小于和大于当前节点的键。为维持树的深度最小,二叉树必须是平衡的。在添加随机顺序的键到树中时,最终很天然会致使树的一边比另外一边更深。缓存

一种二叉树重平衡(rebalance)的法是称为“旋转”(rotation)方法。旋转方法实现节点的从新排列,它将更深子树的父节点下推到其子节点之下,并上移子节点为有效地置于父节点的原位置。图 1 给出了一个旋转方法的例子,实现了一个二叉树的平衡。左图的二叉树在添加了节点“2”以后,是不平衡的。为了平衡二叉树,咱们以节点“3”为轴心旋转树,而后以节点“5”为轴线。节点“5”是原先的根节点,也是节点“3”的父节点,旋转后成为节点“3”的子节点。在完成旋转后获得右图的树,其中左子树深度下降了 1,右子树的深度增长了 1,而树的最大深度下降了。性能优化


图 1 例子:使用旋转方法平衡二叉树服务器

二叉树是一种十分有用的内存数据结构。因为平衡(即须要保持全部子树的深度最小)和低扇出(每一个节点最多具备两个指针)特性,二叉树在磁盘上的性能并很差。B 树容许每一个节点存储两个以上的指针,并可将节点大小调整为适合页面的大小(例如,4KB),所以可在块设备上良好工做。当前,有一些实现中使用了更大规模的节点,甚至横跨多个页面。微信

B 数据有以下属性:

  • 排序:排序支持顺序扫描,简化了查找。

  • 自平衡:插入和删除操做无需重平衡树。一个 B 树节点在占满后,将分割(split)为两个节点。若是两个近邻节点的利用率(occupancy)降至某个阈值如下时,那么节点会合并(merge)。这意味着,各个叶子节点与根节点间是等距的,在查找时可使用一样的步数定位。

  • 查找操做有对数时间复杂度保证。这一点使 B 树成为数据库索引的很好选择,由于在数据库中,查找时间是很是关键的。

  • 支持可变数据结构。插入、更新和删除(以及随后的节点分割和合并)是在磁盘上执行的,实现就地(in-depth)更新须要必定的空间开销。B 树能够组织为聚束索引,将实际数据存储在叶子节点上,也可使用非聚束索引,将数据存储为堆文件。

本文还将介绍 B+ 树 3。B+ 树是 B 树的一种变体,经常使用于数据库存储。与原始 B 树相比,B+ 树的不一样之处在于:1. B+ 树的叶子节点存储值并造成一个额外的连接层。2.B+ 树的内部节点并不存储值。

B 树剖析

下面咱们仔细查看 B 树的构建模块,如图 2 所示。B 树具备多种节点类型,包括根节点、内部节点和叶子节点。根节点(顶端)是没有父节点的节点(即它不是任何其它节点的子节点)。内部节点(中间)具备父节点和子节点,它们链接了根节点和叶子节点。叶子节点(底端)保存数据,它没有子节点。图 2 显示的 B 树的分支因子(branching factor)为 4,即具备四个指针,内部节点有三个键值,叶子节点有四个键值对。


图 2 例子:B 树

标识一个 B 树,可以使用以下指标:

  • 分支因子:即指向子节点的指针数(N)。考虑存在指针,根节点和内部节点最大可保存 N-1 个键值。

  • 利用率:最大可用指针数中,当前有多少指向子项的指针在用。例如,若是树的分支因子是 N,节点当前保持了 N/2 个指针,那么利用率就是 50%。

  • 高度:B 树的层数,指明了在查找中需遍历的指针个数。

树中每一个非叶子节点最多保持 N 个键(索引项),将树分割为 N+1 个子树,这些子树可用相应的指针定位。在条目 Ki 中的指针 i 指向的子树中,全部索引项是 Ki-1 <= Ksearched < Ki(其中 k 是一组键)。第一个和最后一个指针是特例,最左子节点指向的子树中,全部的条目小于或等于 K0;最右子节点指向的子树中,全部的条目大于 KN-1。叶子节点中包含的指针,可指向同一层中前一个或后一个节点,造成近邻节点的双向连接列表。全部节点中,键老是排序的。

查找

在执行查找时,搜索将从根节点开始,沿内部节点递归下行至叶子层级。在每一层级,经过追随子节点指针,搜索空间可缩减到子树范围(该子树包括搜索值)。图 3 显示的是 B 树中的一次查找,即一次沿着两个键间的指针由根到叶子的遍历,一个指针大于或等于搜索项,另外一个指针小于搜索项。执行一个点查询(Point Query)时,搜索在定位到叶子节点后结束。在范围搜索中,会遍历所找到叶子节点的键和值,而后是近邻的叶子节点,直到到达范围的终点。


图 3 单次由根到叶子的遍历

从复杂性上看,B 树保证了 log(n) 复杂度的查找,由于如何从节点中找到键中使用了二分查找法,如图 4 所示。二分查找法易于解释,当从字典中搜索具备某个首字母的单词时,全部单词是按字母顺序排列的。首先选择从确切的中间位置打开字典。若是搜索字母在字母序上要“小于”(先出现)打开的字母,那么继续在左半部份字典中搜索。不然,在词典右半部份中搜索。而后继续缩减剩余页面范围,经过减半并选择搜索方向,直到找到所需的字母。每步将搜索空间减半,使查找呈对数时间复杂度。B 树中的搜索具备对数时间复杂度,由于节点层级键是排序的,并在查找匹配总使用了二分查找。这也是为何在整个树中保持高利用率和一致性是很是重要的。


图 4 B 树的二分查找

插入、更新和删除

执行插入时,第一步是定位目标叶子节点。在此可以使用上面介绍的搜索算法。定位目标节点后,键和值将添加到该节点中。若是叶子节点的空间不够用,这种状况称为“溢出”(Overflow),叶子节点必须分割为两个。分割的实现是经过分配一个新叶子,将原叶子节点中的半数元素移动到新的叶子节点,并在父节点中分配一个指向新叶子节点的指针。若是父节点中也没有空余的空间,那么就在父节点层级执行分割操做。操做将持续直至到达根节点。若是根节点溢出,节点内容在新分配节点间分割。而后根节点自身将被覆盖,以免从新分配。这也意味着,树(及树的高度)的高度老是在分割根节点时增加。

LSM 树

日志结构合并(LSM)树是一种写优化的数据结构,它是不可变的、驻留于磁盘的,适用于写操做比查找和检索记录更为频繁的系统。因为 LSM 树消除了随机插入、更新和删除,所以它获得了更多的关注。

LSM 树剖析

为支持顺序写,LSM 树在一个驻留内存表(一般使用支持对数时间复杂度查找的数据结构实现,例如二分查找树或跳表)中批量写入和更新,直至内存表规模达到一个设定的阈值,这时再写入到磁盘,该操做称为“刷新”(flush)。检索数据须要搜索树驻留磁盘的全部部分,检查驻留内存表,并在返回结果前合并内容。图 5 显示了一个 LSM 树的结构,其中的驻留内存表用于写入。一旦内存表达到了必定规模大,其中经排序的内容就要就写入到磁盘。读取时须要访问驻留磁盘和驻留内存表,并须要一个合并过程去整合数据。


图 5 LSM 树的结构

排序字符串表(SSTable)

现代多种系统中,例如 RocksDB 和 Apache Cassandra,将 LSM 树的驻留磁盘表实现为一种 SSTable(排序字符串表)。SSTable 具备简单性(易于写入、搜索和读取)及合并属性(在合并期间,源 SSTable 扫描和合并结果写是顺序操做)。

SSTable 是一种不可变的、驻留磁盘的排序数据结构。如图 6 所示,SSTable 在结构上可分为两个部分,即数据块和索引块。数据块是由顺序写入的惟一键值对组成,键值对按键排序。索引块中的键包含映射到数据块指针,指针指向实际记录的位置。索引一般实现为针对快速搜索优化的格式,例如 B 树,或是对于点查询使用哈希表。SSTable 中的每一个值项具备一个与之相关联的时间戳。时间戳指定了插入和更新的写入时间(一般不作区分),以及删除的移除时间。


图 6 SSTable 的结构

SSTable 具备一些很好的特性:

  • 点查询(即根据键找到一个值)可经过查找主索引快速完成。

  • 扫描(即在指定键范围内迭代全部键值对)能够高效完成,仅经过在数据块内顺序读取键值对。

SSTable 给出了一段时间内全部数据库操做的快照。由于 SSTable 是由驻留内存表的刷新操做建立的,该表做为此时期内对数据库状态操做的一个缓冲区。

查找

检索数据时,须要搜索磁盘上全部的 SSTable,检查驻留内存表,并在返回结果前合并其中的内容。读操做须要合并过程,由于所搜索的数据可能存在于多个 SSTable 中。

为确保实现删除和更新,也必需要合并步骤。删除时,会在 LSM 树中插入一个占位符,一般称为“墓碑”(tombstone)。墓碑用于标记被删除的键。相似地,更新时也仅是增长一个具备更迟时间戳的记录。在读取期间,将跳过被标记为删除的记录,不返回给客户。更新中也采起相似的作法,对于两个具备同一键的记录,只返回时间戳更晚的记录。图 7 显示了合并是如何整合存储在独立表中具备同一键的数据。如图所示,Alex 的记录写入的时间戳为 100,更新了电话后记录的时间戳为 200。John 的记录是被删除的。其它两个条目保持原状,由于它们并未作标记。


图 7 例子:合并步骤

为减小需搜索的 SSTable 数量,避免由于搜索某个键而检查每一个 SSTable,一些存储系统使用了一种称为布隆滤波器 10 的数据结构。布隆滤波器是一种几率数据结构,可用于检测一个元素是否属于一个集合的成员。它会产生误报匹配(即指出元素是集合的成员,可是事实上并非),可是不会产生漏报(即若是返回结果是不匹配,那么该元素必定不是集合的成员)。换句话说,布隆滤波器可用于告知一个键是否“可能位于 SSTable 中”,或是“绝对不在 SSTable 中”。若是一个 SSTable 被布隆滤波器返回为不匹配,那么将在查询中跳过。

维护 LSM 树

鉴于 SSTable 是不可变的、是顺序写的,而且并未保留就地更改的空间。这意味着,插入、更新和删除操做须要重写整个文件。全部修改数据库状态的操做,是在内存驻留表中“批量处理”的。随时间的推动,驻留磁盘表的数量将会增加(对应同一键的数据可能会位于多个文件、同一记录的多个版本,或标记为删除的冗余记录中),读取将继续变得代价更为昂贵。

为下降读取的代价、整合被标记的记录空间并下降驻留磁盘表的数量,LSM 树须要一个紧缩(compaction)过程。紧缩过程从磁盘读取整个 SSTable,并合并它们。由于 SSTable 是按键排序的,紧缩过程的工做方式相似于归并排序,因此该操做也是很是高效。记录从多个数据源顺序读取,合并的输出能够即刻顺序地附加到结果文件中。归并排序的一个优势是工做高效,即使是对于归并没有法放入内存中的大型文件。生成的表将保持原始 SSTable 的排序。

在紧缩过程当中,合并后的 SSTable 将被抛弃,并被更“紧缩”的表替代,如图 8 所示。紧缩操做输入为多个 SSTable,输出为合并后的一个表。一些数据库系统在逻辑上将同一规模的表分组为同一“层级”,并在某个层级中的表数量足够多时,开始合并过程。紧缩减小了必需要处理的 SSTable 数量,使查询更加高效。


图 8 紧缩过程

原子性和持久性

为实现 I/O 操做数量减小和顺序化,B 树和 LSM 树均在更新实际发生前作内存中的批处理。这意味着,一旦发生失败,不能保证数据的完整性,并且也不能确保原子性(指一组更改的应用是原子化的,如同单个操做同样,或者所有应用,或者全不该用)和持久性(确保在进程崩溃或掉电时,数据处于一致性存储中)。

为解决这个问题,不少现代存储系统使用了 WAL 技术(预写式日志,Write-Ahead Logging)。WAL 的主要理念是全部数据库状态修改首先持久保持在位于磁盘上的只添加日志中。一旦操做过程当中发生进程崩溃,就会重执行日志,以确保没有数据丢失,实现全部更改的原子化。

B 树中,使用 WAL 可理解为更改只有作日志后,才写到数据文件中。一般状况下,B 树存储系统的日志规模相对较小。一旦更改应用到持久存储,就会被丢弃。WAL 做为一种对未日志化(in-flight)操做的备份机制,即应用到数据页面的任何更改均可以从日志记录重作。

LSM 树中,WAL 用于持久化那些操做了内存表可是并未彻底刷新到磁盘的更改。一旦内存表彻底刷新并切换,读取操做能够在新建立的 SSTable 上完成,就能够丢弃保持了刷新内存表数据的 WAL 段。

总结

B 树和 LSM 树结构上的最大差异之一,在于优化的目的,以及优化的意义。

下面对 B 树和 LSM 树作一个对比。总而言之,B 树具备以下属性:

  • B 树是可变的,这支持经过引入一些空间开销,以及更为关联的写路径,实现就地更新。B 树并不须要彻底的文件重写或多源合并。

  • B 树是读优化的。即 B 树不须要从多个源读取(所以也不须要此后的合并操做),这简化了读路径。

  • 写可能会触发节点的级联分割,这会使一些写操做更昂贵。

  • B 树是针对分页(块存储)环境优化的,其中不存在字节地址。They are optimized for paged environments (block storage), where byte addressing is not possible.

  • 虽然也须要重写,可是一般状况下 B 树存储要比 LSM 树存储须要更少的维护。

  • 并发访问须要读 / 写隔离,其中一系列的锁和闩(latch)。

LSM 树具备以下特性:

  • LSM 树是不可写的。SSTable 是一次性写入磁盘的,永不更新。紧缩操做经过从多个数据文件移除条目,并合并具备相同键的数据,实现空间的整合。在紧缩过程当中,已合并的 SSTable 将被丢弃,并在成功合并后移除。不可写提供的另外一个有用特性,就是刷新后的表可并发访问。

  • LSM 是写优化的。这意味着写入操做将被缓存,并顺序地刷新到磁盘中,潜在地支持磁盘上的空间本地性。

  • 读操做可能须要从多个数据源访问数据。由于不一样时间写入的具备相同键的数据,可能会落在不一样的数据文件中。记录在返回给客户前,必须通过合并过程。

  • LSM 树须要作维护和紧缩,由于缓存的写入操做将被刷新到磁盘。

存储系统的评估

在存储系统的开发中,老是须要考虑一些挑战和因素。优化目标对存储系统选择有着切实的影响。若是能够在写操做上花费更多时间,那么就能够部署针对更高效读操做的结构,预留额外的空间用于就地更新。这有利于实现更快的写操做,并支持将数据缓存在内存中,以确保顺序的写操做。可是,全部这些是不可能一次性达成的。咱们理想中的存储系统具备最低的读代价、最低的写代价,并无其它开销。但在实践中,数据结构需折衷考虑多个因素。理解这些折衷考虑是很是重要的。

哈佛大学 DASlab(数据系统实验室)的研究人员总结了数据库系统优化的三个关键参数:读开销、更新开销和内存开销,统称为“RUM”。对于特定的用例,理解这些参数中哪一个是最重要的,将对数据结构、访问方法,甚至是特定工做负载的适用性产生影响,由于算法须要根据特定的用例作出调整。

“RUM 假说”(http://daslab.seas.harvard.edu/rum-conjecture/)2 指出,若是对 RUM 中的两项设置上限,那么也会对第三项设置下限。例如,B 树是读优化的,代价是写开销,以及预留了额外的空间(于是致使了内存开销)。LSM 树空间开销更少,代价是在读操做期间必须访问多个驻留磁盘表,从而引入了读开销。这三个参数造成了一个彻底三角形,改进其中的一项,意味着对其它项的折衷考虑。图 9 展现了 RUM 假说。


图 9 RUM 假说

B 树是针对读性能优化的。索引的布局方式使得遍历树所需的磁盘访问次数最小化。定位数据时,只需访问单个索引文件。这是经过保持索引文件可写而实现的。可写增长了写入放大(Write Amplification)问题,该问题由节点分割、合并、重定位和碎片化 / 不平衡相关维护等致使。为缓解更新的代价,并下降分割的次数,B 树在各个层级的节点中预留了额外的空闲空间。这有助于推迟发生写入放大问题,直至节点空间满。简而言之,B 树是在更新和内存开销间作了权衡,目的是实现更好的读性能。

LSM 树针对写性能优化。不管更新或是删除,都须要定位数据在磁盘上的位置(B 树也须要)。LSM 树经过在内存驻留表缓存全部插入、更新和删除操做以保证顺序写。这样作的代价是更高的维护代价、须要紧缩操做(紧缩操做只是一种缓解不断增加的读代价、减小驻留磁盘表数量的方式),以及更昂贵的读(由于数据必须从多个源读取并合并)。同时,LSM 树不保持任何空闲空间,这消除了一些内存开销(不一样于 B 树节点平均利用率为 70%,就地更新须要必定的开销)。因为 LSM 树最终文件是不写的,为实现更好的使用率,须要支持块压缩。简而言之,LSM 树是在读性能和维护更好写性能和低内存开销间的权衡。

对于每种所需的特性,都会存在针对此特性优化的数据结构。若是使用相适应的数据结构以支持更好的读性能,那么代价是更高的维护代价。添加元数据有利于遍历(例如分散层叠(fractional cascading)),这将影响写的时间,并占用空间,可是能够改进读的时间。使用压缩技术 (例如,Gorilla 压缩 六、delta encoding 等算法) 可优化内存效率,将对写时数据打包和读时数据解包添加一些开销。有时,咱们能够权衡功能和效率。例如,堆文件和哈希索引因为文件格式的简单性,能够给出很好的性能保证,以及更小的空间开销,但代价是只能支持执行点查询。咱们还也能够经过使用近似数据结构,例如布隆滤波器、HyperLogLog、Count-Min sketch 等,权衡空间精度和效率。

读、更新和内存这三种可调整的开销,有助于咱们评估数据库,并更深刻理解数据库适合何种工做负载。三者很是直观,很容易将存储系统排序在一个桶中,给出执行状况的猜想,进而经过深刻的测试去验证这一假设。

固然,评估存储系统时还存在其它一些重要的因素,例如维护代价、操做简单性、系统需求、对频繁更新和删除的适用性、访问模式等。RUM 假说仅是有助于给出直观感受,并对最初方向提供经验法则。理解咱们本身的工做负载,这是迈向构建可扩展的后端系统的第一步。

在不一样的实现中,一些因素可能会发生变化。即使是使用相似存储设计原则的两个数据库间,最终的表现也可能会彻底不一样。数据库是一个复杂的系统,其中有不少变更因素。数据库也是不少应用中重要且不可分割的部分。性能上的权衡有助于咱们一窥数据库的底层机制。了解底层数据结构及内部原理间的差别,有助于咱们从中作出最优的选择。

参考文献

Comer, D. 1979. The ubiquitous B-tree. Computing Surveys 11(2); 121-137;

http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.96.6637.

哈佛大学 DASlab 实验室. The RUM Conjecture;

http://daslab.seas.harvard.edu/rum-conjecture/.

Graefe, G. 2011. Modern B-tree techniques. Foundations and Trends in Databases 3(4): 203-402;

http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.219.7269&rep=rep1&type=pdf.

MySQL 5.7 参考手册. InnoDB 索引的物理结构 ;

https://dev.mysql.com/doc/refman/5.7/en/innodb-physical-structure.html.

O'Neil, P., Cheng, E., Gawlick, D., O'Neil, E. 1996. The log-structured merge-tree. Acta Informatica 33(4): 351-385;

http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.44.2782.

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;

http://www.vldb.org/pvldb/vol8/p1816-teller.pdf.

Suzuki, H. 2015-2018. The internals of PostgreSQL;

http://www.interdb.jp/pg/pgsql01.html.

Apple HFS Plus Volume 格式 ;

https://developer.apple.com/legacy/library/technotes/tn/tn1150.html#BTrees

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.

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

相关文章

HP 实验室 Goetz Graefe 的文章“五分钟规则(https://queue.acm.org/detail.cfm?id=1413264):20 年后闪存将如何改变规则”。旧规则将持续演进,同时闪存给出了两个新规则。

https://queue.acm.org/detail.cfm?id=1413264

Rick Richardson,“数据库消岐”(https://queue.acm.org/detail.cfm?id=2696453)。使用针对用户访问模式构建的数据库。

https://queue.acm.org/detail.cfm?id=2696453

Poul-Henning Kamp,“这样作并不正确”(https://queue.acm.org/detail.cfm?id=1814327)。你是否定为本身已经掌握了如何处理服务器性能问题?再考虑一下。

https://queue.acm.org/detail.cfm?id=1814327

做者简介

Alex Petrov(http://coffeenco.de/,@ifesdjeen (GitHub), @ifesdjeen (Twitter))是 Apache Cassandra 项目的提交者,也是存储系统技术爱好者。他在多家企业从事数据库、构建分布式系统和数据处理流水线方面的工做。

查看英文原文:

https://queue.acm.org/detail.cfm?id=3220266

相关文章
相关标签/搜索