博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

b树,LSM, 磁盘

Posted on 2021-08-23 18:15  bw_0927  阅读(248)  评论(0)    收藏  举报

https://blog.csdn.net/AndersCloud/article/details/7181085?spm=1001.2014.3001.5502

 

为什么在磁盘中要使用b+树来进行文件存储呢?

原因还是因为树的高度低的缘故。

磁盘本身是一个顺序读写快,随机读写慢的系统,那么如果想高效的从磁盘中找到数据,势必需要满足一个最重要的条件:减少寻道次数。

 

我们以平衡树为例进行对比,就会发现问题所在了

AVL树的子叶节点中,左子树一定小于等于当前节点,而当前节点的右子树则一定大于当前节点。只有这样,才能够维持全局有序,才能够进行查询。

这也就决定了只有取得某一个子叶节点后,才能够根据这个节点知道他的子树的具体的值情况。

 

这点非常之重要,因为二叉平衡树,只有两个子叶节点,所以如果想找到某个数据,他必须重复更多次“拿到一个节点的两个子节点,判断大小,再从其中一个子节点取出他的两个子节点,判断大小。”这一过程。

 

这个过程重复的次数,就是树的高度。那么既然每个子树只有两个节点,那么N个数据的树的高度也就很容易可以算出了。

 

平衡二叉树这种结构的好处是,没有空间浪费,不会存在空余的空间,但坏处是需要取出多个节点,且无法预测下一个节点的位置

 

这种取出的操作,在内存内进行的时候,速度很快,但如果到磁盘,那么就意味着大量随机寻道。基本磁盘就被查死了

 

而b树,因为其构建过程中引入了有序数组,从而有效的降低了树的高度,一次取出一个连续的数组,这个操作在磁盘上比取出与数组相同数量的离散数据,要便宜的多。因此磁盘上基本都是b树结构

 

不过,b树结构也不是完美的,与二叉树相比,他会耗费更多的空间。在最恶劣的情况下,要有几乎是元数据两倍的格子才能装得下整个数据集(当树的所有节点都进行了分裂后)。

 

https://www.cnblogs.com/my_life/articles/3709028.html

B树,概括来说是一个节点可以拥有多于2个子节点的多查找树

B树相对于平衡二叉树的不同是,每个节点包含的关键字增多了,且有序,减少数据查找的次数和复杂度;

非叶子节点,也是存储节点,存了关键字。

 

 

其实toku DB的网站上有个非常不错的对b树问题的说明,我在这里就再次侵权一下,将他们的图作为说明b树问题的图谱吧,因为真的非常清晰。

http://tokutek.com/downloads/mysqluc-2010-fractal-trees.pdf

 

PS:B+树就是在B树基础上加两个规定  1.非叶子结点只存指针,叶子结点存数据  2.所有叶子结点从左到右用双链表串起来

 

如果是一个运行时间很长的b树,那么几乎所有的请求,都是随机io。因为磁盘块本身已经不再连续,很难保证可以顺序读取。

以上就是b树在磁盘结构中最大的问题了

 

那么如何能够解决这个问题呢?

目前主流的思路有以下几种

1.      放弃部分读性能,使用更加面向顺序写的树的结构来提升写性能。

这个类别里面,从数据结构来说,就我所知并比较流行的是两类,

一类是COLA(Cache-Oblivious Look ahead Array)(代表应用自然是tokuDB)。

一类是LSM tree(Log-structured merge Tree)或SSTABLE

(代表的数据集是cassandra,hbase,bdb java editon,levelDB etc.).

2.      使用ssd,让寻道成为往事。

 

我们在这个系列里,主要还是讲LSM tree吧,因为这个东西几乎要一桶浆糊了。几乎所有的nosql都在使用,然后利用这个宣称自己比mysql的innodb快多少多少倍。。我对此表示比较无语。

因为nosql本身似乎应该是以省去解析和事务锁的方式来提升效能。怎么最后却改了底层数据结构,然后宣称这是nosql比mysql快的原因呢?

毕竟Mysql又不是不能挂接LSM tree的引擎。。。

 

好吧,牢骚我不多说,毕竟还是要感谢nosql运动,让数据库团队都重新审视了一下数据库这个产品的本身。

 

那么下面,我们就来介绍一下LSM Tree的核心思想吧。

 

首先来分析一下为什么b+树会慢

从原理来说,b+树在查询过程中应该是不会慢的,但如果数据插入比较无序的时候,比如先插入5 然后10000然后3然后800 这样跨度很大的数据的时候,就需要先“找到这个数据应该被插入的位置”,然后插入数据。

这个查找到位置的过程,如果非常离散,那么就意味着每次查找的时候,他的子叶节点都不在内存中,这时候就必须使用磁盘寻道时间来进行查找了。更新基本与插入是相同的

 

 

 

 

那么,LSM Tree采取了什么样的方式来优化这个问题呢?

简单来说,就是放弃磁盘读性能来换取写的顺序性

乍一看,似乎会认为读应该是大部分系统最应该保证的特性,所以用读换写似乎不是个好买卖。但别急,听我分析之。

1.      内存的速度远超磁盘,1000倍以上。而读取的性能提升,主要还是依靠内存命中率而非磁盘读的次数

2.      写入不占用磁盘的io,读取就能获取更长时间的磁盘io使用权,从而也可以提升读取效率。

因此,虽然SSTable降低了了读的性能,但如果数据的读取命中率有保障的前提下,因为读取能够获得更多的磁盘io机会,因此读取性能基本没有降低,甚至还会有提升。

而写入的性能则会获得较大幅度的提升,基本上是5~10倍左右。

 

下面来看一下细节

其实从本质来说,k-v存储要解决的问题就是这么一个:尽可能快得写入,以及尽可能快的读取。

让我从写入最快的极端开始说起,阐述一下k-v存储的核心之一—树这个组件吧。

 

我们假设要写入一个1000个节点的key是随机数的数据。

 

对磁盘来说,最快的写入方式一定是顺序的将每一次写入都直接写入到磁盘中即可。

但这样带来的问题是,我没办法查询,因为每次查询一个值都需要遍历整个数据才能找到,这个读性能就太悲剧了。。

 

那么如果我想获取磁盘读性能最高,应该怎么做呢?把数据全部排序就行了,b树就是这样的结构

 

那么,b树的写太烂了,我需要提升写,可以放弃部分磁盘读性能,怎么办呢?

 

简单,那就弄很多个小的有序结构,比如每m个数据,在内存里排序一次,下面100个数据,再排序一次……这样依次做下去,我就可以获得N/m个有序的小的有序结构

 

查询的时候因为不知道这个数据到底是在哪里,所以就从最新的一个小的有序结构里做二分查找,找得到就返回,找不到就继续找下一个小有序结构,一直到找到为止。

 

很容易可以看出,这样的模式,读取的时间复杂度是(N/m)*log2N 。读取效率是会下降的。

这就是最本来意义上的LSM tree的思路

那么这样做,性能还是比较慢的,于是需要再做些事情来提升,怎么做才好呢?

 

于是引入了以下的几个东西来改进它

1.      Bloom filter : 就是个带随即概率的bitmap,可以快速的告诉你,某一个小的有序结构里有没有指定的那个数据的。于是我就可以不用二分查找,而只需简单的计算几次就能知道数据是否在某个小集合里啦。效率得到了提升,但付出的是空间代价。

2.      小树合并为大树: 也就是大家经常看到的compact的过程,因为小树他性能有问题,所以要有个进程不断地将小树合并到大树上,这样大部分的老数据查询也可以直接使用log2N的方式找到,不需要再进行(N/m)*log2n的查询了

 

这就是LSMTree的核心思路和优化方式。

 

不过,LSMTree也有个隐含的条件,就是他实现数据库的insert语义时性能不会很高

原因是,insert的含义是: 事务中,先查找该插入的数据,如果存在,则抛出异常,如果不存在则写入。这个“查找”的过程,会拖慢整个写入。

 

 

这样,我们就又介绍了一种k-v写入的模型啦。在下一次,我们将再去看看另外一种使用了类似思路,但方法完全不同的b树优化方式 COLA树系。敬请期待 ~

 

-------------------------------

COLA树

-------------------------------

 

终于来到了COLA树系,这套东西目前来看呢,确实不如LSM火,不过作为可选方案,也是个值得了解的尝试,不过这块因为只有一组MIT的人搞了个东西出来,所以其实真正的方案也语焉不详的。

 

从性能来说,tokuDB的写入性能很高,但更新似乎不是很给力,查询较好,占用较少的内存。

 

http://www.mysqlperformanceblog.com/2009/04/28/detailed-review-of-tokutek-storage-engine/

这里有一些性能上的指标和分析性文字。确实看起来很心动,不过这东西只适合磁盘结构,到了SSD似乎就挂了。原因不详,因为没有实际的看过他们的代码,所以一切都是推测,如果有问题,请告知我。

 

 

先说原理,上ppt http://tokutek.com/presentations/bender-Scalperf-9-09.pdf,简单来说,就是一帮MIT的小子们,分析了一下为什么磁盘写性能这么慢,读的性能也这么慢,

然后一拍脑袋,说:“哎呀,我知道了,对于两级的存储(比如磁盘对应内存,或内存对于缓存,有两个属性是会对整个查询和写入造成影响的,

一个是容量空间小但速度更快的存储的size,另外一个则是一次传输的block的size.而我们要做的事情,就是尽可能让每次的操作传输尽可能少的数据块。

 

传输的越少,那么查询的性能就越好。

 

 

进而,有人提出了更多种的解决方案。

•B-tree [Bayer, McCreight 72]

• cache-oblivious B-tree [Bender, Demaine, Farach-Colton 00]

• buffer tree [Arge 95]

• buffered-repositorytree[Buchsbaum,Goldwasser,Venkatasubramanian,Westbrook 00]

• Bε

 tree[Brodal, Fagerberg 03]

• log-structured merge tree [O'Neil, Cheng, Gawlick, O'Neil 96]

• string B-tree [Ferragina, Grossi 99]

 

这些结构都是用于解决这样一个问题,在磁盘上能够创建动态的有序查询结构。

 

在今天,主要想介绍的就是COLA,所谓cache-oblivious 就是说,他不需要知道具体的内存大小和一个块的大小,或者说,无论内存多大,块有多大,都可以使用同一套逻辑进行处理,这无疑是具有优势的,因为内存大小虽然可以知道,但内存是随时可能被临时的占用去做其他事情的,这时候,CO就非常有用了。

 

其他我就不多说了,看一下细节吧~再说这个我自己都快绕进去了。

 

众所周知的,磁盘需要的是顺序写入,下一个问题就是,怎么能够保证数据的顺序写

我们假定有这样一个空的数据集合

 

 

 

可以认为树的高度是log2N。

每行要么就是空的,要么就是满的,每行数据都是排序后的数据

 

如果再写一个值的时候,会写在第一行,比如写了3。

再写一个值11的时候,因为第一行已经写满了,所以将3取出来,和11做排序,尝试写第二行。又因为第二行也满了,所以将第二行的5和10也取出,对3,11,5,10 进行排序。写入第四行

 

 

这就是COLA的写入过程。

可以很清楚的看出,COLA的核心其实和LSM类似,每次“将数据从上一层取出,与外部数据进行归并排序后写入新的array”的这个操作,对sas磁盘非常友好。因此,写入性能就会有非常大的提升。

 

并且因为数据结构简单,没有维持太多额外的指针,所以相对的比较节省空间。

 

这样查询会需要针对每个array都进行一次二分查找

性能似乎还不是很高,所以,他们想到了下面这种方式,把它的命名为fractal tree,分形树。

用更简单的方法来说的话呢,就是在merge的时候,上层持有下层数据的一个额外的指针。

来协助进行二分查找。

 

 

这样,利用空间换时间,他的查询速度就又回到了log2N这个级别了。

 

到此,又一个有序结构被我囫囵吞枣了。

 

 


https://blog.csdn.net/AndersCloud/article/details/7182165

LevelDb性能非常突出,官方网站报道其随机写性能达到40万条记录每秒,而随机读性能达到6万条记录每秒

 


 

 

什么是顺序写

每个 fd 都关联一个 当前的 offset,每次写入 offset 都会更新。

顺序写入就是所有的写都是从当前的 offset开始写
随机写就是 offset 会从[0~max]里随机取值写,这些随机的 offset 最后都可能会导致磁头的移动。

那么应用层面什么样的数据写入方式能保证磁盘层面是顺序写呢,简单来说就是,

  1. update-in-place 原地更新
  2. append-only btree/copy on write tree 顺序文件末尾追加

LSM - tree

关系数据库键值数据库存储引擎对比

传统的关系数据库存储采用 B+ 树,数据被按照特定方式放置,能大幅度提升性能,但写性能下降。

而 no-sql 一般 将整个磁盘就看做是一个日志,在日志中存放永久性数据及其索引,每次都添加到日志末尾。

将数据添加到文件,因为完全是顺序写,所以写操作性能优秀。但从日志文件读一些数据将比写操作消耗更多的时间,需要倒序扫描直到找到所需内容。相当于牺牲了部分读性能换来了写性能的提升。

顺序:含义是写操作是顺序的,而不是写的内容是顺序的

 

深入理解 Log-Structured Merge-Tree(LSM-tree)

对比上面,其实就是 将一个大的查找结构(B+ tree),变换为 顺序地写到一堆相似的有序文件(sstable/HBASE) 中。

每个文件包含了短时间段内的一些改动。

因为文件有序,后续查找也会很快。

所以这种方式 文件不可修改,永远不会更新,新操作只会写到新文件中通过周期性的合并来减少文件的个数。

总结一下就是:1. 让操作顺序化,不断追加而不是修改(写性能高);延迟更新,批量写入硬盘。

简单对比读写:写操作被分批处理,只写到顺序块上;读操作有可能访问大量的文件(散乱的读)。

写操作步骤:

  1. 发出写操作
  2. 内存缓存(memtable)中使用树结构来保持key有序
  3. WAL写磁盘(防丢/恢复)
  4. 达到一定规模刷到磁盘上一个新文件里
  5. 越多的数据到存储系统中,就会有越多的不可修改的顺序sstable文件被创建(他们代表了小的、按时间顺序的修改)
  6. 系统周期性发起 compaction,合并文件并删除重复冗余,减少文件个数(因为sstable 是有序结构,可以才用归并排序的思想,所以合并非常高效)

读操作步骤:

  1. 发出读操作
  2. 先检查内存数据(memtable)
  3. 没有这个key
  4. 逆序一个个检查 sstable 直到找到

对比读写操作:
因为需要遍历所有sstable,当数量过多性能就会下降。一方面系统周期性合并sstable、用 cache 等技术;另一方面使用布隆过滤器来避免大量的读文件操作。

 


https://www.cnblogs.com/niceshot/p/14321372.html

四、提高写吞吐量的思路#

既然顺序写比起随机写速度更快。那得想办法将数据顺序写。

4.1 一种方式是数据来后,直接顺序落盘#

这拥有很高的写速度。但是当我们想要查寻一个数据的时候,由于存储下的数据本身是无序的(写的值本身无法控制顺序),无法使用任何算法进行优化,只能挨个查询,读取速度是很慢的。

4.2 另一种方式,是保证落盘的数据是顺序写入的同时,还保证这些数据是有序的#

而请求写入的数据本身是无序且不可预测的,如何保证落盘的数据是有序的呢?这就需要利用内存访问速度比硬盘快的原理。

将写入的请求,先在内存中缓存起来,按一定的有序结构组织,达到一定量后,再写入硬盘,从而使得硬盘顺序写入了有序的数据。

提高数据的写入速度同时,方便了后续基于有序数据的查找(有序的数据结构,可以通过二分查找等算法进行进行快速查询,具体查找算法,得看是哪种有序结构)

 

 

5.4 什么是SSTable#

SSTable 全称Sorted String Table。实际上就是被写入数据的有序存储文件,所以叫sorted.

 

 

 

SSTable文件有DataBlock,IndexBlock,BitSet(不同的实现,有可能没有)

  • DataBlock 一个SSTable包含多个DataBlock数据块,数据按KeyValue的形式有序组织。
  • IndexBlock 记录每个数据块中最大的那个Key的Offset
  • BitSet 使用Bloom Filter来将一个Key映射到BitSet中

数据的有序组织、IndexBlock、BitSet。这些数据结构,都是为了提高数据读取时的速度。那数据是如何进行读取的呢?

由于SSTable是顺序创建,所以最新的SSTable中包含了最新的值。再查找SSTable时,依次查找最新的SSTable。 

 

每一个SSTable的查询流程如下

 

布隆表达式的原理是以极小的数据容量,去存储大量数据存在的可能性。

所以如果通过BitSet的布隆表达式查询该Key存在时,只是一个理论存在可能,接下来要通过IndexBlock真正进行查询。

而如果布隆表达式在BitSet中没有找到,那就是真的没有,可以快速跳过,进入下一个SSTable查找。

布隆表达式的运用,能够大大提高查找效率。

 

5.6 如何进行数据的删除和更新#

为了保证数据的顺序写,所有SSTable都不会因为删除和更新而在原数据所在位置进行更改。

在更新时,仅仅插入一个最新的值去写到新的SSTable中。

在删除时,依然是插入一个基于该Key的删除标记,写入最新的SSTable中。

由于查找某个Key是基于时间新鲜度,反向依次查找SSTable,所以读取某个Key始终读的是最新的值。

 

5.7 SSTable的合并#

随着日积月累,SSTable的文件数会增多,导致查找时性能下降。同时由于数据的更新或删除。让老的SSTable中数据的有效性降低,太多的过期数据占用SSTable,同样会降低查询效率。

所以一般数据库引擎,定期都会有一个SSTable的合并操作。移除过时数据,将多个小SSTable合并成大的SSTable。

 

5.8 最近读取的SSTable IndexBlock缓存#

在大内存的条件下,部分数据库还会将最近读取的SSTable 索引,缓存至内存。这进一步加速了查找的过程。