LSM-tree基本原理及应用

LSM-tree基本原理及应用

LSM-tree是什么

log-structed merge-tree

日志结构的:系统日志是不会出错的,只需要在后面追加。所以日志结构就代指追加型结构。

原理:把磁盘看做一个日志,在日志中存放永久性数据及其索引,每次都添加到日志的末尾。文件传输(存取)大多是顺序的,提高磁盘带宽利用率。

LSM-tree是专门为key-value存储系统设计的,主要业务是查找和插入。

LSM的特点是利用磁盘的顺序写,写入速度比随机写入的B-树更快。

LSM-tree来自哪里

Bigtable是一个分布式的结构化数据存储系统,它被设计用来处理海量数据,其在提供Tablet服务时使用内存中的memtable和GFS中的SSTable来相互配合着来存储数据更新。

其中存储和更新的方法与日志结构的合并树(Log-Structured Merge-Tree,LSM-tree)类似,并以其为基础。

层次结构

LSM-tree是由两个或两个以上存储数据的结构组成的。最简单的LSM-tree由两个部件构成。一个部件常驻内存,称为C0树(或C0),可以为任何方便键值查找的数据结构,另一个部件常驻硬盘之中,称为C1树(或C1),其数据结构与B-tree类似

C1的结构与B-tree相似,但其结点中的条目是满的,结点的大小为一页,树根之下的 所有单页结点合并到地址连续的多页块中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RdPkCDp8-1652670559611)(C:\Users\8208191402\AppData\Roaming\Typora\typora-user-images\image-20220410152618946.png)]

j-1层结点包含连续的指向j层结点(node1,node2,…nodeK)的指针(P1,P2,…,PK)和分割指针的键(S1,S2,…,SK-1)。

J层结点连续存放在多页块的K页中,并且不必按照键的大小排列。

M:多页块分割的标记,表示直到下一个M标记或空结点,二者之间的所有后续结点都存放在同一个多页块中。如果J层的两个结点存放于同一个多页块中,那这两个结点的键值之间的所有结点也存放在这个多页块中。

基本原理

  • 写入:程序向内存写入记录日志,添加到C0层。当C0层达到一定大小,就把C0和C1合并,用新的C1替代磁盘里原来的C1,C1达到一定大小就和C2合并,以此类推
    • 数据的新老版本问题只在合并的时候处理。
    • 写入过程只涉及内存,所以合并磁盘层任务可以后台异步执行,不阻塞写入
  • 查询:从C0向Ck查询,即从最新的向最旧的查询。

一次查询可能涉及多次单点查询,而且优势在于顺序写入磁盘的速度快,所以LSM-tree适用于写密集,查询少的场景。

每一层Ci的结构都是索引树,各个相邻部件Ci-1和Ci的滚动合并是异步的,一个条目会插入到C0中,之后经过不断的异步滚动合并过程,最终合并至CK中。

内部数据结构

MemTable往往是一个跳表(Skip List)组织的有序数据结构。

SSTable一般由一组数据block和一组元数据block组成。元数据block存储了SSTable数据block的描述信息,如索引、BloomFilter、压缩、统计等信息。因为SSTable是不可更改的,且是有序的,索引往往采用二分法数组结构就可以了。

应用场景

LSM-tree基本原理及应用_第1张图片

增删改查

DBLSM-tree文件在内存中有两种:不可修改的immutable table,可修改的,正常用于写入的memtable,

磁盘上:存放有序字节组的表SStable(Sorted String Table),存储数据的key。

SStable 一共有七层(L0 到 L6),大小十倍递增。

SStable一旦写入磁盘就不能修改,(这就是Log-Structured Merge Tree名字中Log-Structured一词的由来)。

当要修改现有数据时,LSM Tree并不直接修改旧数据,而是直接将新数据写入新的SSTable中

删除数据时,LSM Tree也不直接删除旧数据,而是写一个相应数据的删除标记的记录到一个新的SSTable

这样一来,LSM Tree写数据时对磁盘的操作都是顺序块写入操作,而没有随机写操作。

写入流程

1.数据准备写入,写入操作加入到写前日志(WAL)中,要写的数据写入到memtable中。

WAL用来当系统崩溃重启时重放操作,使MemTableImmutable MemTable中未持久化到磁盘中的数据不会丢失。

2.当memtable满了,切换memtable为immutable,开一个新的memtable接收写请求,immutable 块刷入磁盘L0层。

这个过程如果导致L0层大小超过阈值,就会滚动合并。

合并要保证L1-L6的数据的key是有序的,L0因为会直接接受内存传来的数据,所以允许有重叠。

LSM-tree基本原理及应用_第2张图片

L0上a是有重叠的,L1上a和e是最新的。

滚动合并流程:

  • C1中读入未合并的叶子结点,存储于内存的清空块中(磁盘—>内存)
  • 从C0中读取叶子结点,并与清空块中的叶子结点进行合并排序(内存—>内存)
  • 将合并排序的结果写入填充块中,并从C0中删除用于合并排序的旧叶子结点;
  • 不断地重复步骤2和3,当填充块被合并排序的结果填满时,将填充块追加到硬盘的新位置,并从C1中删除用于合并排序的旧叶子结点;当清空块被全部读取完时,再从C1中读入未合并的叶子结点(内存—>磁盘,磁盘—>内存)
  • 当C0和C1的所有叶子节点都被读入内存进行合并,并产生新的叶子结点之后,C0和C1的一次滚动合并就结束了。

滚动合并图示,左侧为磁盘,右侧为内存,二者都读入emptying block进行合并操作,然后write back。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AvFMJOAh-1652670559614)(C:\Users\8208191402\AppData\Roaming\Typora\typora-user-images\image-20220410153830982.png)]

当设置检查点时,不满的memtable的信息会被强制写入硬盘。

当根结点分裂,C1树的深度增加时,所有的多页块缓存都将被写入硬盘。

LSM和B-树的插入效率比较

B-树插入要先执行一次查询,找到要插入的条目位置,如果是随机插入,之前插入所缓存的结点页面可能就无法重复使用了。

定义:

De = 使用B-tree进行随机访问时,在内存缓存中未找到的页数

为了执行一个插入操作,需要进行De次I/O操作以在磁盘的B-tree叶结点中找到插入的位置

插入数据之后,再进行1次I/O操作将新数据写回磁盘。由于相关结点的分裂对开销作用不大,这里我们就不考虑结点的分裂问题了。B-tree的插入代价就是

C O S T B − i n s = C O S T P ( D e + 1 ) COST_{B−ins} = COST_P (De + 1) COSTBins=COSTP(De+1)

LSM-tree的插入操作首先是直接作用在常驻内存的C0上的,然后是批量地从C0将条目滚动合并到C1中。

因此开销主要在滚动合并上。

定义:

滚动合并是以多页块作为单元进行合并的,合并时读取一页所需的I/O代价为COST_n

M=从C0合并到C1的平均条目数

Se=条目的字节大小

Sp=硬盘页的字节大小

S0=C0的叶子层的大小(以兆字节为单位)

S1=C1的叶子层的大小(以兆字节为单位)

一个硬盘页(通常就是一个结点的大小)中所含的条目数为Sp /Se,那么滚动合并时一页中来自于C0的条目约为S0 /(S0 + S1)。那么M就可以估计为:

M = S p S e ∗ S 0 S 0 + S 1 M = \frac {Sp} {Se}*\frac {S0} {S0+S1} M=SeSpS0+S1S0

LSM-tree在插入C0之后,滚动合并时需要读进C1的叶子结点,再写回C1。由于滚动合并是批量插入C0的条目,所以一次插入的代价是滚动合并代价的1/M
C O S T L S M − i n s = 2 ∗ C O S T n / M COST_{LSM−ins} = 2 * COST_n/M COSTLSMins=2COSTn/M
二者比较
C O S T L S M − i n s C O S T B − i n s = 2 M ∗ ( D e + 1 ) ∗ C O S T n C O S T p \frac{COST_{LSM−ins}}{COST_{B−ins}} = \frac {2} {M * (De + 1)}*\frac {COST_n} {COST_p} COSTBinsCOSTLSMins=M(De+1)2COSTpCOSTn
由于C1的多页块在硬盘中是连续存储的,COSTπ一般会比COSTP来得小,De对于磁盘来说是定值,故公式的大小其实取决于M。

当一页中包含的条目数较多,C0的规模与C1差距不大时,LSM-tree在数据插入上是优于B-tree的。

代价,数据温度

B-tree往往限制在硬盘或内存中,而LSM-tree则是跨越硬盘和内存实现数据存取的优化。

B-树的代价:

假设一个使用B-树的程序大小占S(单位MB),并需要每秒随机访问硬盘中的数据H次。

则应用程序访问磁盘的代价就是H∙COSTB-ins,使用硬盘进行存储的代价就是S∙COSTdisk。

以下过程是通过程序运行时间增加,导致H不断增大而产生的代价变化。

LSM-tree基本原理及应用_第3张图片

  • 程序刚启动的时候,没有之前的缓存数据,主要是加载应用程序的数据等初始化动作。这时随机访问硬盘的次数较少,H相当小,这时应用程序的开销主要是硬盘存储数据的开销,即是S∙COSTdisk。
  • 随着程序的不断运行,应用程序内的业务逻辑会需要访问之前存储的数据,随机访问硬盘的次数就会越来越大,此时S∙COSTdisk
  • 随机访问的不断增加会使得访问磁盘的功率逐渐达到最大,但在访问硬盘的代价小于使用内存缓存(即H∙COSTB-insS∙COSTmemory时,将应用程序存储数据的B-tree全部缓存到内存中,可以使得应用程序的开销代价减小,这样应用程序的开销代价就是S∙COSTmemory。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SWiaPbHm-1652670559616)(C:\Users\8208191402\AppData\Roaming\Typora\typora-user-images\image-20220410162238641.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Gl2ftGTi-1652670559617)(C:\Users\8208191402\AppData\Roaming\Typora\typora-user-images\image-20220410162340501.png)]

定义H/S定义为数据的温度,将两个转折点之间的区域称温数据,两端的区域分别称为冷数据和热数据。

冷数据区域的代价由硬盘的存储代价决定,不大能反映数据结构和算法的好坏。但越迟走出冷数据就越能反映对硬盘的利用率。

热数据区域的代价主要集中在内存存储的代价上,同样也不大能反映数据结构和算法的好坏。但越迟进入就越能反映对内存的利用率。

LSM-tree的代价曲线在B-tree之下,可见LSM-tree对硬盘和内存的利用率都比B-tree要高。这主要是因为两点:

  1. 滚动合并时批量写入新插入的数据时,平摊单个条目插入的开销;
  2. C1的各层结点存储在多页块中,合并时顺序读写多页块,减小读取代价。

当从C0向C1合并的日志条目M达到以下条件时,每次从C1读取多个多页块的代价比直接把这些块写入内存进行合并的代价大,B-树效率大于LSM树。

M < K 1 ∗ C O S T n C O S T p MM<K1COSTpCOSTn

因此,我们需要扩大memtable的大小,也就是C0的大小。

一个解决方法就是在日益增大的C1和有空间上限的C0之间加入一个C做为C0和C1的缓冲,也就是磁盘的多层结构。

虽然磁盘相邻层之间的合并是异步的,但是对数据进行并发性访问会有同步/并发访问的问题。

例如当进行精确检索或者加载多页块至内存时,Ci里的一个节点会被读入内存;

当进行范围检索或滚动合并时,Ci里的多页块会被读入内存中。

假设ci层正在合并,所以没有上锁的节点都可以访问,而后台合并因为是异步任务,是不加锁的,因此正在滚动合并的节点也是可以访问的,但这些节点的数据有一部分加载到内存,是不完整的。

访问规则:

  • 当硬盘中的相邻部件进行滚动合并的时候,当前参与合并的结点不能被查找;
  • 当C0和C1进行滚动合并的时候,当前参与合并的C0的结点周边不能被查找和插入;
  • 在Ci-1和Ci与Ci和Ci+1同时进行滚动合并时,Ci-1与Ci滚动合并的游标有时会超过Ci和Ci+1滚动合并的游标。
避免磁盘在存取时的物理冲突

设置以节点为单位的锁。

  • 在进行滚动合并时,正在合并的结点会在写模式下被锁住,直到有来自较大部件的结点被合并才被释放。
  • 在进行查找时,正在被读取的结点会在读模式下被锁住,读取完毕后,锁即被释放。

与C0和C1的滚动合并相比,硬盘内的相邻部件Ci-1和Ci之间的滚动合并多了一个清空块和填充块。

LSM-tree基本原理及应用_第4张图片

Ci-1不会将所有的条目都拿去合并,而是会保留一部分条目(例如新插入的那部分条目)到Ci-1的填充块。

当前正在进行滚动合并的结点被加锁(红圈圈住的点),写保护。三角形上蓝色的点表示游标,绿色的点表示游标未到达的结点,树上折线表示从树根到游标的路径

查询流程

因为写入的数据是有时间戳的,所以查询的时候无法像B+树在一个全局索引表中查找,

  • 从最新的向最旧的查找。

  • 为了提高查找效率,将磁盘的SStable分层,同层数据不重复且有序。每一个SSTable内的数据是有序的(前一个SSTable的最大数据值小于后一个SSTable的最小数据值)。

  • 找到了需查找的数据或相应的删除标记,则直接返回查找结果

先查memtable,再查 immutable memtable,然后查 L0 层的所有文件,一层一层往下查。

效率提高:将搜索限制在前几个的部件的搜索上。

在滚动合并时,如果将最近τ时间内被访问的条目保留下来,而将其它条目用于合并,那么经常被访问的那些数据就会被依次保存在C0,C1,…,CK-1和CK中。

我们可以简单的认为,C0保存的是最近τ时间内被访问的条目,C1保存的是除了C0保存的数据外,最近2τ时间内被访问的条目,C2保存的是除了C0和C1保存的数据外,最近3τ时间内被访问的条目。

LSM-tree基本原理及应用_第5张图片

LSM-tree的优势:其能推迟写回硬盘的时间,进而达到批量地插入数据的目的。

读写放大

读写放大 = 磁盘上实际读写的数据量 / 用户需要的数据量,分子是和磁盘交互的数据量

写放大:因为写入一个很小的数据可能导致多层合并,磁盘读写量远大于需要数据量。

读放大:为了查询一个 1KB 的数据。最坏需要读 L0 层的 8 个文件,再读 L1 到 L6 的每一个文件,一共 14 个文件。

每一个文件内部需要读 16KB 的索引,4KB的布隆过滤器,4KB的数据块(从一个SSTable里查一个key,需要读)。一共 24*14/1=336倍。key-value 越小,读放大越大。

删除流程

为了更高效地利用LSM-tree的插入优势,删除操作被设计为通过插入操作来执行。

  • 当C0所索引的一个条目被删除时,首先在C0上查找该条目所对应的索引是否存在,若不存在,就建立一个索引。然后在该索引键值的位置上设置删除条目(delete node entry)。

  • 删除条目的意义仅在于通知未来所有访问该索引的操作:“此索引键值所索引的条目已经被删除了”。

  • 后续的滚动合并中,凡是在更大的存储层中碰到的与该索引键值相同的条目都将被删除。

    • 后续查找此条目时,如果碰到这种删除条目,就会直接返回未找到。(有的人活着,但已经死了)

条目修改时,依仗LSM-tree的插入优势,可以先插入一个对应的删除条目,待删除条目经滚动合并离开C0后,再在上插入该条目的新值

崩溃恢复

崩溃记录

原因:在内存进行滚动合并操作时,可能会出现操作失败,需要恢复到磁盘数据写入到内存之前的状态。

通过在日志中保存插入数据的记录(被插入数据的行的号码及插入的域和值),当故障出现时,通过检查点恢复数据。

能在当前时间点T0设置检查点的要求:

  • 完成当前所有部件的合并,确保结点上的锁就会被释放;
  • 所有新条目的插入操作以及滚动合并推迟至检查点设置完成之后;
  • 将C0写入硬盘中的一个已知的位置;此后对C0的插入操作可以开始,但是合并操作还要继续等待;
  • 硬盘中的所有部件(C1~CK)在内存中缓存的结点写入硬盘
  • 最后向日志中写入一条特殊的检查点日志:
    • T0时刻最后一个插入的已索引行的日志序列号LSN0(Log Sequence Number)
    • 硬盘中的所有部件层的根索引在硬盘中的地址(磁盘每一层仍然是一个类似于B-树结构)
    • 各个部件的合并游标(各个层部件滚动合并到的位置)
    • 以上两点是为了适应""当正在进行滚动合并,却临时需要设置检查点时"的情况
    • 新多页块动态分配的当前信息。在以后的恢复中,硬盘存储的动态分配算法将使用此信息判别哪些多页块是可用的。
    • LSM-tree基本原理及应用_第6张图片
  • 检查点寿命:
    • 一旦检查点的信息设置完毕,就可以开始执行被推迟的新条目的插入操作了。
    • 由于后续合并操作中向硬盘写入多页块时,会将信息写入硬盘中的新位置,所以检查点的信息不会被消除。
    • 只有当后续检查点使得过期的多页块作废时,检查点的信息才会被废弃。

恢复

  • 在日志中找到一个检查点;
  • 将之前写入硬盘的C0和其它部件在内存中缓存的多页块加载到内存中;(回退到滚动合并之前)
  • 将日志中在LSN0之后的部分读入内存,执行其中索引条目的插入操作;(把内存更新到当前状态,即执行检查点之后的插入操作)
  • 读取检查点日志中硬盘部件(C1~CK)的根的位置和合并游标,继续当前检查点处的滚动合并,覆盖检查点之后的多页块(滚动合并所需的所有的多页块都会从硬盘重新读回内存)
    • 由于所有的多页块的新位置较之设置检查点时的旧位置都发生了改变,这样所有目录结点的指针都需要更新。
    • 这听起来似乎是一大笔性能开销,但这些多页块其实都已加载到内存里了,所以没有I/O开销。
  • 当检查点之后的所有新索引条目都已插入至LSM-tree且被索引后,恢复即完成。

小结

日志结构的合并树(LSM-tree)是一种基于硬盘的数据结构,与B-tree相比,能显著地减少硬盘磁盘臂(查询)的开销,并能在较长的时间提供对文件的高速插入(删除)。然而LSM-tree在某些情况下,特别是在查询需要快速响应时性能不佳。通常LSM-tree适用于索引插入比检索更频繁的应用系统。Bigtable在提供Tablet服务时,使用GFS来存储日志和SSTable,而GFS的设计初衷就是希望通过添加新数据的方式而不是通过重写旧数据的方式来修改文件。而LSM-tree通过滚动合并和多页块的方法推迟和批量进行索引更新,充分利用内存来存储近期或常用数据以降低查找代价,利用硬盘来存储不常用数据以减少存储代价

小结

日志结构的合并树(LSM-tree)是一种基于硬盘的数据结构,与B-tree相比,能显著地减少硬盘磁盘臂(查询)的开销,并能在较长的时间提供对文件的高速插入(删除)。然而LSM-tree在某些情况下,特别是在查询需要快速响应时性能不佳。通常LSM-tree适用于索引插入比检索更频繁的应用系统。Bigtable在提供Tablet服务时,使用GFS来存储日志和SSTable,而GFS的设计初衷就是希望通过添加新数据的方式而不是通过重写旧数据的方式来修改文件。而LSM-tree通过滚动合并和多页块的方法推迟和批量进行索引更新,充分利用内存来存储近期或常用数据以降低查找代价,利用硬盘来存储不常用数据以减少存储代价

你可能感兴趣的:(算法,数据结构)