存储引擎的底层数据结构
2022-02-28 本文已影响0人
睡不醒的大橘
1. 哈希存储引擎
介绍
-
哈希存储引擎
是哈希表的持久化实现,支持增、删、改以及随机读取的操作。但不支持顺序扫描。一般使用哈希存储引擎的存储系统为键值(K-V)类型。 - 对于key-value的插入以及查询,哈希表的复杂度都是O(1)
- 代表性的应用有:Redis,Memcache,以及存储系统Bitcask。
基本实现
- Bitcask数据文件中的数据是采用不断追加(顺序写)的形式写入的,记录包含key、value、主键长度、value长度、时间戳以及crc校验值。
- Bitcask数据存储所在服务器在内存中采用基于哈希表的索引数据结构。该哈希表索引保存了每个key到数据文件中value所在的位置的映射。
- 因此仅需所有的key可以放入内存,value的实际内容则可以超过内存大小。- 每次读取只需一次磁盘寻址,就可以将value从磁盘加载到内存。如果那部分数据文件已经在文件系统的缓存中,则不需要任何的磁盘IO。每个写操作总共需要进行一次顺序的磁盘写入和一次内存操作。
优化和实现细节
-
为避免不断追加最终用尽磁盘空间,可以将存于磁盘的value分解成一定大小的段。当文件达到一定大小时,可以在这些段内进行压缩(丢弃重复的键,仅保留每个键最新的值),同时可以对多个段进行合并,最后将压缩后的value写入最新的段。
-
数据库重新启动后,内存的hash map丢失,可通过重新扫描整个文件重建map file。Bitcask通过将每个段的hash map快照存储在磁盘上,加快hash map索引重建速度。
-
由于写操作是以严格顺序的顺序附加到日志中的,所以常见的实现选择是只有一个写入器线程。数据文件段是附加的,或者是不可变的,所以它们可以被多个线程同时读取。
特点总结
- 适合key数量不多,但每个key的value频繁更新的情况。
- 范围查询效率不高。不适合查找值在一定范围内的场景。
- 不是所有的数据都可以用哈希函数套用的,也不是所有键值数据都能很好处理冲突问题。
2. LSM树存储引擎
介绍
- LSM树存储引擎可用于无法将所有key放入内存的场景。
- 根据key-value对的键顺序进行存储。适用于查找值在一定范围内的场景。
- 代表性的应用有:LevelDB, RocksDB, Hbase, Cassandra 等大多数 NoSQL 数据库。
基本实现
-
LSM 树
由两个或以上的存储结构组成,一个存储结构常驻内存中,称为内存表(memtable)
。另外一个存储结构常驻在硬盘中,称为排序字符串表(SSTable)
。 - 在内存中维护顺序结构相比磁盘更加容易。常用数据结构包括
红黑树
,AVL树
等。当数据写入时,将其添加到内存中的平衡树数据结构。 - 当内存表大于某个阈值(通常为几兆字节)时,将其作为SSTable文件写入磁盘。这可以高效地完成,因为树已经维护了按键排序的键值对。新的SSTable文件成为数据库的最新部分。当SSTable被写入磁盘时,写入可以继续到一个新的内存表实例。
- 为了提供读取请求,首先尝试在内存表中找到关键字,然后在最近的磁盘段中,然后在下一个较旧的段中找到该关键字。
- 有时会在后台运行合并和压缩过程以组合段文件并丢弃覆盖或删除的值。
-
与使用散列索引的日志段相比,在SSTable合并段是简单而高效的,可使用归并排序算法进行合并。当多个段包含相同的键时,我们可以保留最近段的值,并丢弃旧段中的值。
-
由于排序特性,不再需要保存内存中所有键的索引。只需要一个内存中索引来告诉您一些键的偏移量,它可以很稀疏。由于读取请求无论如何都需要扫描所请求范围内的多个键值对,因此可以将这些记录分组到块中,并在将其写入磁盘之前对其进行压缩(下图中的阴影区域所示) 。稀疏内存中索引的每个条目都指向压缩块的开始处。
优化和实现细节
- 为避免数据库重新启动后,内存表数据丢失,可以在磁盘上保存一个单独的日志,每个写入都会立即被附加到磁盘上。该日志不是按排序顺序,因为它的唯一目的是在崩溃后恢复内存表。每当内存表写出到SSTable时,相应的日志都可以被丢弃。
- 有不同的策略来确定SSTables如何被压缩和合并的顺序和时间。常见包括
大小分级
和分层压缩
。在大小分级压缩中,较新的和较小的SSTable被连续合并到较旧和较大的SSTable。在分层压缩中,键的范围被分裂成更小的SSTables,而较旧的数据被移动到单独的“层级”,这样压缩可以逐步进行并节省磁盘空间。 - 当查找数据库中不存在的键时,LSM树算法可能会很慢:先检查内存表,然后将段一直回溯访问到最旧的段文件(可能必须从磁盘多次读取),然后才能确定键不存在。为了优化这种访问,存储引擎通常使用额外的
布隆过滤器
。布隆过滤器只需占用极小的空间,便可给出键 “可能存在” 和 “肯定不存在”的判断。从而可以提前过滤掉很多不必要的查询,节省了大量的磁盘IO。 -
Bloom过滤器
是由一个长度为N的01 数组组成的。首先将数组array每个元素初始设为0,对集合A中的每个元素w,做K次哈希,第i次哈希值对N取模得到一个index(i).即:
index(i) = Hash_i(w)% N
将array数组中的array[index(i)] 置为1.最终变为一个这些元素为1的01数组。
特点总结
- 适合
高吞吐写
的场景。它通过将数据增删改全部转化为顺序写入从而显著提高写的性能。 - 基于 LSM-Tree 分层存储能够做到写的高吞吐,带来的副作用是整个系统必须频繁的进行 compaction ,写入量越大, Compaction 的过程越频繁。而 compaction 是一个 compare & merge 的过程,非常消耗 CPU 和存储 IO ,在高吞吐的写入情形下,大量的compaction操作占用大量系统资源,必然带来整个系统性能断崖式下跌,对应用系统产生巨大影响,当然我们可以禁用自动 Major Compaction ,在每天系统低峰期定期触发合并,来避免这个问题。
- LSM-Tree 的更新、删除全部是通过增加新数据间接实现,那么 Key 值必然存在多版本,而且事务锁的机制与 B-Tree 相比而言会宽松很多,而且键值重复很多,这也就导致了它不适合事务要求非常强的结构化数据处理场景。
3. B树存储引擎
介绍
-
B树
将数据库分解成固定大小的块或页面,传统上大小为4KB(有时会更大),并且一次只能读取或写入一个页面。这种设计更接近于底层硬件,因为磁盘也被安排在固定大小的块中。 -
每个页面都可以使用地址或位置来标识,这允许一个页面引用另一个页面 —— 类似于指针,但在磁盘而不是在内存中。我们可以使用这些页面引用来构建一个页面树
- 一个页面会被指定为B树的根;在索引中查找一个键时,就从这里开始。该页面包含几个键和对子页面的引用。每个子页面负责一段连续范围的键,引用之间的键,指明了引用子页面的键范围。
- 在B树的一个页面中对子页面的引用的数量称为
分支因子
。例如,在上图中,分支因子是 6 。在实践中,分支因子取决于存储页面参考和范围边界所需的空间量,但通常是几百个。
基本实现
- 如果要更新B树中现有键的值,则搜索包含该键的叶页,更改该页中的值,并将该页写回到磁盘(对该页的任何引用保持有效) 。如果你想添加一个新的键,你需要找到其范围包含新键的页面,并将其添加到该页面。如果页面中没有足够的可用空间容纳新键,则将其分成两个半满页面,并更新父页面以解释键范围的新分区
- 该算法确保树保持平衡:具有 n 个键的B树总是具有 O(logn) 的深度。大多数数据库可以放入一个三到四层的B树,所以你不需要遵追踪多页面引用来找到你正在查找的页面。
- B树的基本底层写操作是用新数据覆盖磁盘上的页面。假定覆盖不改变页面的位置;即,当页面被覆盖时,对该页面的所有引用保持完整。这与日志结构索引(如LSM树)形成鲜明对比,后者只附加到文件(并最终删除过时的文件),但从不修改文件。
优化和实现细节
- 为了使数据库对崩溃具有韧性,B树实现通常会带有一个额外的磁盘数据结构:
预写式日志(WAL, write-ahead-log)
(也称为重做日志(redo log)
)。这是一个仅追加的文件,每个B树修改都可以应用到树本身的页面上。当数据库在崩溃后恢复时,这个日志被用来使B树恢复到一致的状态 - 更新页面的一个额外的复杂情况是,如果多个线程要同时访问B树,则需要仔细的并发控制 —— 否则线程可能会看到树处于不一致的状态。这通常通过使用
锁存器(latches)
(轻量级锁)保护树的数据结构来完成。日志结构化的方法在这方面更简单,因为它们在后台进行所有的合并,而不会干扰传入的查询,并且不时地将旧的分段原子交换为新的分段。 - 一些数据库(如LMDB)使用
写时复制
方案,而不是覆盖页面并维护WAL进行崩溃恢复。修改的页面被写入到不同的位置,并且树中的父页面的新版本被创建,指向新的位置。这种方法对于并发控制也很有用。 - 我们可以通过不存储整个键来节省页面空间,但可以缩小它的大小。特别是在树内部的页面上,键只需要提供足够的信息来充当键范围之间的边界。在页面中包含更多的键允许树具有更高的分支因子,因此更少的层次
- 通常,页面可以放置在磁盘上的任何位置;没有什么要求附近的键范围页面附近的磁盘上。如果查询需要按照排序顺序扫描大部分关键字范围,那么每个页面的布局可能会非常不方便,因为每个读取的页面都可能需要磁盘查找。因此,许多B树实现尝试布局树,使得叶子页面按顺序出现在磁盘上。但是,随着树的增长,维持这个顺序是很困难的。相比之下,由于LSM树在合并过程中一次又一次地重写存储的大部分,所以它们更容易使顺序键在磁盘上彼此靠近。
- 额外的指针已添加到树中。例如,每个叶子页面可以在左边和右边具有对其兄弟页面的引用,这允许不跳回父页面就能顺序扫描。
特点总结
- 适合
结构化的数据
的随机读取
方面。其随机检索能力非常强,因为一方面它可以通过平衡树的算法,通过少数几次二分法思想的查找就可以在索引定位数据的位置;另外一方面它也不用担心像哈希存储引擎那样需要考虑冲突的解决办法,需要考虑哈希函数的健壮性问题。对于数据存取的模式来讲, B+Tree 则是将数据拆分为固定大小的 Block 或 Page, 一般是 4KB 大小,和磁盘一个扇区的大小对应, Page 是读写的最小单位,更适合固定大小的结构化数据结构的存取。 - 不适合
高吞吐写
的场景。主要原因就是它的插入、更新、删除的这些操作都是建立在检索的前提之下的,虽然这种操作更适合事务型的业务场景,但是对于事务型不是非常强,但是吞吐量巨大的场景并不适合。