数据库从0到0.1 (一): LSM-Tree VS B-Tree

数据库最基本两个功能:数据的存储和数据的查询。 当咱们写入数据时,数据库能够存储数据;当咱们须要访问数据时,数据库能够给咱们想要的数据。 数据库会经过特定的数据模型和数据结构存储数据,并支持经过特定的查询语言访问数据。本文将从最简单的数据库开始,讨论数据库如何存储数据,如何查询数据。本文将讨论两种存储引擎:log-structured 存储引擎和以B+树为表明的page-oriented存储引擎。html

1 最简单的数据库

#!/bin/bash

#key,value对追加写入文件的最后一行
db_set () {    
    echo "$1,$2" >> database
}

#查找指定key的最后一行的最新的value
db_get () {    
    grep "^$1," database | sed -e "s/^$1,//" | tail -n 1
}

上面两个Shell函数实现了最简单的Key-Value数据库。 调用db_set能够写入数据,调用db_get能够查询数据,数据的物理存储格式是逗号分隔的普通文本文件。mysql

bash-3.2$ db_set 1 kks
bash-3.2$ db_get 1
kks
bash-3.2$ db_set 2 kangkaisen
bash-3.2$ db_get 2
kangkaisen
bash-3.2$ db_set 1 KKS
bash-3.2$ db_get 1
KKS
bash-3.2$ cat database
1,kks
2,kangkaisen
1,KKS

其中db_set函数拥有很好的写入性能,由于是追加写;可是db_get函数的性能十分糟糕,其时间复杂度是O(n),咱们每次必须全表Scan。git

2 Index

为了可以快速找到特定Key对应的Value, 咱们须要引入一个数据结构:Index。 所谓Index,就是咱们在数据库中增长额外的元数据,而后Index像路标同样能够快速知道咱们须要访问数据的位置和偏移量。 Index相似汉语字典中的索引和通常书籍中的目录。若是咱们须要按照不一样的方式访问相同的数据,咱们有可能须要多种不一样的索引,好比按照Key查询和按照Value查询,咱们会分别须要针对Key的索引和针对Value的索引。github

Index是基于原始数据衍生的附加的数据结构,增长索引必然意味着下降数据写入速度,增大存储空间,因此Index是以数据写入时的处理成本和存储的空间成原本换取查询的加速。这也是数据库设计的一个trade-off,不一样索引的查询加速比,写入时的处理成本,存储的空间成本每每是不一样的,因此在设计数据库时选择何种索引是一个很重要的点。算法

3 Hash Index

下面就让咱们用Index加速以前最简单的Key-Value DB。以前咱们db_get方法查询特定Key必须全表Scan的缘由,是由于咱们不知道特定Key在文件中的Offest,假如咱们知道了每一个Key的Offest,咱们就能够直接Seek到Key对应的Offest,直接读取Key对应的Value。而Key到Offest的映射咱们天然会想起到咱们熟悉的数据结构HashMap,咱们能够在内存中维护一个HashMap,HashMap的Key就是Key-Value DB的每一条记录的Key,HashMap的Value就是每一条记录在文件中的Offest。sql

屏幕快照 2018-05-01 下午5.56.01.png-298kB

有了HashMap后,咱们每次写数据后就必需要更新HashMap,查询数据时先从HashMap获取特定Key的Offest,再直接Seek到文件对应Offest的位置,读取数据。 事实上Bitcask(Riak的默认存储引擎)就是这样作的。数据库

不过显然Hash Index有两个缺陷:bash

  1. 内存的大小必须能够放下Hash Table
  2. Range Scan的效率十分低下

4 Segment

目前为止,咱们都是把数据写到一个文件中,这显然是不合理的。 一个常见的作法就是将文件按照大小拆为为Segment,每一个Segment是不可变的。 Segment的概念很常见,好比Kylin和Druid中都有Segment的概念,指必定大小或者必定时间内不可变的文件。数据结构

第1部分咱们知道,咱们同一个Key的Value的更新只是追加写入,并无删除旧的Value。 当咱们有了多个Segment后,咱们天然就能够按期在后台执行Compaction操做,将同一个Key的旧Value删除,更进一步,若是咱们数据库支持delete的话,咱们能够在一开始只进行标记,并不实际删除,等到Compaction的时候,咱们再进行实际删除。 总之一句话,基于log-structured的存储引擎,咱们能够经过后台的Compaction来实现update和delete,Compaction时依然能够进行数据的写入和查询。架构

屏幕快照 2018-05-01 下午6.10.49.png-239.1kB

至此,每一个Segment文件都在内存中有了对应的Hash table。 咱们查询时为了找到特定Key对应的Value,咱们依次查询每一个Segment文件便可,查询每一个Segment文件的过程和以前同样。

这种Append-only Log-structured的存储引擎的优势:

  1. 顺序写的效率远高于随机写
  2. 并发控制和故障恢复十分简单,由于Segment文件是不可变的,且是Append-only的,

为何再也不对Segment文件作索引呢?

这样咱们就不须要顺序遍历每一个Segment文件了,有了索引咱们就只须要访问包含特定Key的Segment文件。

5 SSTables and LSM-Trees

如今对Segment文件的格式作个简单的改变:咱们要求全部的 key-value对必须按照Key排序。 这种格式咱们称之为Sorted String Table, 简称为SSTable。 咱们也要求在每一个已经Merged的Segment文件中1个Key只会出现一次,Compaction过程保证了这一点。

SSTable相比Log Segments + Hash Indexes 有如下几个明显的优点:

  1. Segment的Merge会更加简单和高效,即便合并的全部文件比内存还大。 由于每一个Segment是有序的,Sort Merge的成本比较低。
  2. 为了查找特定Key,咱们再也不须要在内存中维护一个很大的Hash Map。由于全部的key-value对是按照Key排序的,因此咱们能够维护一个Segment文件的稀疏索引,索引的Key是每一个Segment文件的Start Key,Value就是每一个Segment文件的位置。 其次,在Segment内部,因为Segment有序,咱们再也不须要针对每一个key-value对都构建索引,咱们能够针对Block(几百或者几千行数据)粒度作稀疏索引,Block内存则进行二次查询。
  3. 因为咱们的读取的最小粒度是Block,咱们也能够基于Blcok粒度作压缩,减少磁盘空间和IO。
  4. SSTable不只能够较好的支持Point Query,也能够很好的支持Range Scan。

屏幕快照 2018-05-01 下午6.26.22.png-292.6kB

那么咱们如何保证Segment文件有序呢? 由于数据写入通常都要通过内存,在内存中咱们能够利用Red-black tree 或者AVL tree保证有序。

至此,咱们基于SSTable的存储引擎能够这样Run起来:

  1. 当一条数据写入时,咱们将其插入到基于内存的平衡树中(Red-black tree)。 内存中的树咱们称之为Memtable
  2. 当Memtable的大小超过必定阈值时,咱们将Memtable Flush到磁盘,转为SSTable
  3. 当咱们查询时,须要同时查询内存中的Memtable和磁盘中的SSTable。
  4. 周期性的在后台进行异步的Merge和Compaction操做。
  5. 为了防止Memtable在Flush到磁盘前机器故障致使数据丢失,咱们能够在磁盘上维护一个只追加写的log文件,称之为Write-Ahead-Log,当集群故障后能够从log中恢复出Memtable。 因此咱们在每次写入Memtable,须要先写入WAL。当Memtable flush到磁盘后,对应的WAL文件就能够删除。

至此,LSM-Tree(Log-Structured Merge-Tree)的3个组件:SSTable,Memtable,Write-Ahead-Log终于全了。 从开始最简单的Key-Value 数据库 讲到如今,我相信你已经理解了LSM-Tree的核心思想。

LSM-Tree 已经被普遍使用,好比LevelDB,RocksDB,Cassandra,HBase等,其中的SSTable也是被普遍借鉴,好比ClickHouse,Palo等。

6 磁盘简介

磁盘结构

如图,一个磁盘由多个盘片组成。

磁盘结构

如图,1个盘片由一个个的同心圆组成,一个同心圆就是一个磁道,每一个磁道由多个扇区组成,每一个磁道的扇区数量是一个常量,每一个扇区的大小通常是4KB,扇区是磁盘基本的物理单元

一次磁盘IO的耗时主要由三部分组成:寻道时间 + 旋转延迟 + 数据传输时间

  1. 寻道时间: 将读写磁头移动至正确的磁道上所须要的时间。 目前磁盘的平均寻道时间通常在3-15ms。
  2. 旋转延迟: 盘片旋转将请求数据所在的扇区移动到读写磁盘下方所须要的时间。旋转延迟取决于磁盘转速,转速为15000rpm的磁盘其平均旋转延迟为2ms。
  3. 数据传输时间:传输实际数据所须要的时间,它取决于数据传输率,其值等于数据大小除以数据传输率。目前IDE/ATA能达到133MB/s,SATA II可达到300MB/s的接口数据传输率,数据传输时间一般远小于前两部分消耗时间

提升磁盘读写速度方法就是尽可能减少寻道时间和旋转延迟,而减小寻道时间和旋转延迟的方法就是减小磁盘的随机IO,这就是为何磁盘顺序读写的性能远高于随机读写的缘由。

7 B-Trees

前面咱们从零开始了解了LSM-Tree的核心原理,可是在数据库领域使用最普遍的索引结构是B-tree及其变种。

其实以前咱们为最简单的数据库增长索引的时候,若是咱们同时但愿提升查询性能,支持原地更新和删除,支持Point query和Scan query, 保持高效的插入性能,咱们就会比较天然的想到二叉查找树, 平衡二叉查找树,红黑树,B-Tree 及其最多见的变种B+Tree等树结构, 若是再考虑到面向磁盘,以及更好地支持Scan query,咱们就会选择B+Tree。B+Tree具备较低的深度,这样就减小了磁盘 Seek操做的次数。

相似LSM-Tree,B-Tree也能够提供高效地Point query和Scan query。 可是二者的设计哲学是彻底不一样的:LSM-Tree是将数据拆分为几百M大小的Segments,并是顺序写入;B-Tree则是面向磁盘,将数据拆分为固定大小的Block或Page, 通常是4KB大小,和磁盘一个扇区的大小对应,Page是读写的最小单位。

屏幕快照 2018-05-01 下午6.43.19.png-310kB

在数据的更新和删除方面,B-Tree能够作到原地更新和删除,但因为LSM-Tree只能追加写,因此只能在Segment Compaction的时候进行真正地更新和删除。

你们能够经过B+Tree 可视化理解B+Tree的插入,查找,更新和删除过程。

关于B+Tree更详细的原理能够参考此文MySQL索引背后的数据结构及算法原理

8 B-Tree VS LSM-Tree

通常而言, LSM-tree的写更加高效(追加顺序写),B-tree的读更加高效(LSM-tree须要访问几个不一样的数据结构)。

LSM-Tree的优势:

  1. 高吞吐的写
  2. 能够高效的压缩,更节省磁盘(B-Tree通常会为Page的分裂预留一些空间)

LSM-Tree的缺点:

  1. Compaction会影响正常数据的读写。 阿里为了优化这个问题,X-DB的Compaction使用了FPGA来进行。
  2. 数据量越大,Compaction须要的磁盘带宽就越多。
  3. B-Tree中一个Key只会出如今一个Page,可是LSM-tree中一个key可能出如今多个Segment,因此B-Tree实现事务更加简单。

9 参考资料

[1] InnoDB事务及索引原理 

https://bit.ly/2JinKeo

[2] MySQL B+树索引和哈希索引的区别  

http://blogread.cn/it/article/7630?f=wb_blogread

[3] 十问 TiDB :关于架构设计的一些思考 

https://mp.weixin.qq.com/s/m2_Mf0-x_KpPHbnOawyy2A

[4] 黄东旭:TiDB 数据库的四大应用场景分析 

https://mp.weixin.qq.com/s/t8SA4tlfTjJ77CRynbRm-Q

[5] SnappyData 原理和架构: Streaming Processing,OLTP,OLAP 

https://blog.bcmeng.com/post/snappydata.html

[6] 架构选型之痛,如何构造 HTAP 数据库来收敛技术栈?

https://mp.weixin.qq.com/s/ivJCEXmstbVAdSVgIfv54Q

[7] 畅想TiDB应用场景和HTAP演进之路

https://blog.bcmeng.com/post/tidb-application-htap.html

[8] 数据库从0到0.1 (二): OLTP VS OLAP VS HTAP

https://blog.bcmeng.com/post/oltp-olap-htap.html

[9] CS_Offer/DataStructure/README.md

https://github.com/xuelangZF/CS_Offer/blob/master/DataStructure/README.md

[10] SkipList的那点事儿

https://sylvanassun.github.io/2017/12/31/2017-12-31-skip_list/

[11] key / value 数据库的选型

https://www.keakon.net/2018/07/13/key%20/%20value%20%E6%95%B0%E6%8D%AE%E5%BA%93%E7%9A%84%E9%80%89%E5%9E%8B

相关文章
相关标签/搜索