作者:京东物流 刘家存

随着数据量的增大,传统关系型数据库越来越不能满足对于海量数据存储的需求。对于分布式关系型数据库,我们了解其底层存储结构是非常重要的。本文将介绍下分布式关系型数据库 TiDB 所采用的底层存储结构 LSM 树的原理。

1 LSM 树介绍

LSM 树(Log-Structured-Merge-Tree) 日志结构合并树由 Patrick O’Neil 等人在论文《The Log-Structured Merge Tree》(https://www.cs.umb.edu/~poneil/lsmtree.pdf)中提出,它实际上不是一棵树,而是2个或者多个不同层次的树或类似树的结构的集合。

LSM 树的核心特点是利用顺序写来提高写性能,代价就是会稍微降低读性能(读放大),写入量增大(写放大)和占用空间增大(空间放大)。

LSM 树主要被用于 NoSql 数据库中,如 HBase、RocksDB、LevelDB 等,知名的分布式关系型数据库 TiDB 的 kv 存储引擎 TiKV 底层存储就是用的上面所说的 RocksDB,也就是用的 LSM 树。

2 LSM 树算法大概思路

LSM 树由两个或多个树状的结构组成。

这一节我们以两个树状的结构构成的简单的双层 LSM 树举例,来简单说下 LSM 树大概思路,让大家对 LSM 树实现有个整体的认识。

原论文中的图

2.1 数据结构

双层 LSM 树有一个较小的层,该层完全驻留在内存中,作为 C0 树(或 C0 层),以及驻留在磁盘上的较大层,称为 C1 树。

尽管 C1 层驻留在磁盘上,但 C1 中经常引用的节点将保留在内存缓冲区中,因此C1经常引用的节点也可以被视为内存驻留节点。

2.2 写入

写入时,首先将记录行写入顺序日志文件 WAL 中,然后再将此记录行的索引项插入到内存驻留的 C0 树中,然后通过异步任务及时迁移到磁盘上的 C1 树中。

2.3 读取

任何搜索索引项将首先在 C0 中查找,在 C0 中未找到,然后再在 C1 中查找。

如果存在崩溃恢复,还需要读取恢复崩溃前未从磁盘中取出的索引项。

2.4 Compact 过程

将索引条目插入驻留在内存中的 C0 树的操作没有 I/O 成本,然而,与磁盘相比,容纳 C0 组件的内存容量成本较高,这对其大小施加了限制。达到一定大小后,我们就需要将数据迁移到下一层。

我们需要一种有效的方法将记录项迁移到驻留在成本较低的磁盘介质上的 C1 树中。为了实现这一点,当插入达到或接近每一层分配的最大值的阈值大小,将进行一个滚动合并(Compact)过程,用于从 C0 树中删除一些连续的记录项,并将其合并到 C1 中。

Compact 目前有两种策略,size-tiered 策略,leveled策略,我们将在下面的内容里详细介绍这两种策略。

2.5 崩溃恢复

在 C0 树中的项迁移到驻留在磁盘上的C1树之前,存在一定的延迟(延迟),为了保证机器崩溃后C0树中的数据不丢失,在生成每个新的历史记录行时,首先将用于恢复此插入的日志记录写入以常规方式创建的顺序日志文件 WAL 中,然后再写入 C0 中。

3 LSM 树的组成

LSM树有三个重要组成部分,MemTable,Immutable MemTable,SSTable(Sorted String Table),如下图。

这张经典图片来自 Flink PMC 的 Stefan Richter 在Flink Forward 2018演讲的PPT

这几个组成部分分别对应 LSM 树的不同层次,不同层级间数据转移见下图。这节就是介绍 LSM 树抽象的不同层的树状数据结构的某个具体实现方式。

3.1 MemTable

MemTable 是在内存中的数据结构,用于保存最近更新的数据,会按照 Key 有序地组织这些数据。LSM 树对于具体如何组织有序地组织数据并没有明确的数据结构定义,例如你可以任意选择红黑树、跳表等数据结构来保证内存中 key 的有序。

3.2 Immutable MemTable

为了使内存数据持久化到磁盘时不阻塞数据的更新操作,在 MemTable 变为 SSTable 中间加了一个 Immutable MemTable。

当 MemTable 达到一定大小后,会转化成 Immutable MemTable,并加入到 Immutable MemTable 队列尾部,然后会有任务从 Immutable MemTable 队列头部取出 Immutable MemTable 并持久化磁盘里。

3.3 SSTable(Sorted String Table)

有序键值对集合,是 LSM 树组在磁盘中的数据结构。

其文件结构基本思路就是先划分为数据块(类似于 mysql 中的页),然后再为数据块建立索引,索引项放在文件末尾,并用布隆过滤器优化查找。

4 LSM 树的 Compact 策略

当某层数据量大小达到我们预设的阈值后,我们就会通过 Compact 策略将其转化到下一层。

在介绍 Compact 策略前,我们先想想如果让我们自己设计 Compact 策略,对于以下几个问题,我们该如何选择。

  1. 对于某一层的树,我们用单个文件还是多个文件进行实现?
  2. 如果是多个文件,那同一层 SSTable 的 key 范围是有序还是重合?有序方便读,重合方便写。
  3. 每层 SSTable 的大小以及不同层之间文件大小是否相等。
  4. 每层 SSTable 的数量。如果同一层 key 范围是重合的,则数量越多,读的效率越低。

不同的选择会造成不同的读写策略,基于以上 3 个问题,又带来了 3 个概念:

  1. 读放大:读取数据时实际读取的数据量大于真正的数据量。例如在 LSM 树中可能需要在所有层次的树中查看当前 key 是否存在。
  2. 写放大:写入数据时实际写入的数据量大于真正的数据量。例如在 LSM 树中写入时可能触发Compact 操作,导致实际写入的数据量远大于数据的大小。
  3. 空间放大:数据实际占用的磁盘空间比数据的真正大小更多。LSM 树中同一 key 在不同层次里或者同一层次的不同 SSTable 里可能会重复。

不同的策略实际就是围绕这三个概念之间做出权衡和取舍,我们主要介绍两种基本策略:size-tiered 策略和 leveled 策略,这两个策略对于以上 3 个概念做了不同的取舍。

4.1 size-tiered 策略

4.1.1 算法

  1. size-tiered 策略每层 SSTable 的大小相近。
  2. 当每一层 SSTable 的数量达到 N 后,则触发 Compact 操作合并这些 SSTable,并将合并后的结果写入到一个更大的 SStable。
  3. 新的更大的 SStable 将直接放到下一层 SStable 的队尾。所以同一层不同 SStable key 范围重合,查找时要从后向前扫描,且最坏情况下可能会扫描同一层所有 SStable ,这增大了读放大的问题(之所以说增大,是因为 LSM 树不同层之间也有读放大问题)。

4.1.2 总结

由此可以看出 size-tiered 策略几个特点:

  1. 每层 SSTable 的数量相近。
  2. 当层数达到一定数量时,最底层的单个 SSTable 的大小会变得非常大。
  3. 不但不同层之间,哪怕同一层不同 SSTable 之间,key 也可能会出现重复。空间放大比较严重。只有当该层的 SSTable 执行 compact 操作才会消除这些 key 的冗余记录。
  4. 读操作时,需要同时读取同一层所有 SSTable ,读放大严重。

4.2 leveled 策略

4.2.1 算法

  1. leveled 策略和 size-tiered 策略不同的是,它限制 SSTable 文件的大小,每一层不同 SSTable 文件 key 范围不重叠且后面的最小 key 大于前一个文件的最大 key
  2. 当每一层 SSTable 的总大小达到阈值 N 后,则触发 Compact 操作。
  3. 首先会随机选择一个 SSTable 合并到下层,由于下一层 key 是全局有序的,这就要求 leveled 策略 Compact 操作时需要当前 SSTable 和下一层里和当前 SSTable key 存在范围重叠的所有 SSTable 进行合并。最坏情况下可能下一层所有 SSTable 都参与合并,这就增大了写放大问题(之所以说增大,是因为 LSM 树不同层之间 Compact 也有写放大问题)。

4.2.2 总结

由此可以看出 leveled 策略几个特点:

  1. 不会出现非常大的 SSTable 文件。
  2. 每一层不同 SSTable 文件 key 范围不重叠。相对于 size-tiered 策略读放大更小。
  3. Compact 操作时,需要同时和下一层 SSTable 一起合并,写放大严重。

5 LSM 树的插入、修改、删除

从 LSM 树的名字,Log-Structured-Merge-Tree 日志结构合并树中我们大概就能知道 LSM 树的插入、修改、删除的方法了——顺序追加而非修改(对磁盘操作而言)。

  1. LSM 树的插入、修改、删除都是在 L0 层的树里插入、修改、删除一条记录,并记录记录项的时间戳,由于只需要取最新的内容即可,所以不需要操作后面层次的树。
  2. 历史的插入、修改、删除的记录会在每次 Compact 操作时被后面的记录覆盖。

6 LSM 树的查找

  1. 由于后面的操作会覆盖前面的操作,所以查找只需从 L0 层往下查,直到查到某个 key 的记录就可以了,之前的记录不需要再查了。
  2. 对于 size-tiered 策略,同一层 SSTable 需要从后向前遍历,直到找到符合的索引项。
  3. 在查找过程中也会使用其他一些手段进行优化,例如增加缓存、布隆过滤器等。

7 LSM 树和 B+ 树的比较

  1. 不考虑写日志等操作,插入、修改、删除一条记录 B+ 树需要先找到数据位置,可能需要多次磁盘 IO;LSM 树不需要磁盘 IO,单次插入耗时短,所以其写入的最大吞吐量是高于 B+ 树的。
  2. LSM 树后面的 Compact 操作也会操作这条数据几次,总的写入量是大于 B+ 树的,但可以通过将 Compact 操作放到业务低峰时来降低这个劣势的影响。
  3. 查找时, LSM 树需要遍历所有层次的树,查找效率上要低于 B+ 树,但 LSM 树写入时节省的磁盘资源占用,可以一定程度上弥补读效率上的差距。

8 总结

LSM 树特点:顺序写入、Compact 操作、读、写和空间放大。

LSM 树适用场景:对于写操作吞吐量要求很高、读操作吞吐量要就较高的场景,目前主要在 NoSql 数据库中用的比较多。

TiDB 底层存储结构 LSM 树原理介绍的更多相关文章

  1. Redis(一):基本数据类型与底层存储结构

    最近在整理有关redis的相关知识,对于redis的基本数据类型以及其底层的存储结构简要的进行汇总和备注(主要为面试用) Redis对外提供的基本数据类型主要为五类,分别是 STRING:可以存储字符 ...

  2. Druid.io索引过程分析——时间窗,列存储,LSM树,充分利用内存,concise压缩

    Druid底层不保存原始数据,而是借鉴了Apache Lucene.Apache Solr以及ElasticSearch等检索引擎的基本做法,对数据按列建立索引,最终转化为Segment,用于存储.查 ...

  3. 17-看图理解数据结构与算法系列(NoSQL存储-LSM树)

    关于LSM树 LSM树,即日志结构合并树(Log-Structured Merge-Tree).其实它并不属于一个具体的数据结构,它更多是一种数据结构的设计思想.大多NoSQL数据库核心思想都是基于L ...

  4. MongoDB索引存储BTree与LSM树(转载)

    1.为什么 MongoDB 使用B-树,而不是B+树 MongoDB 是一种 nosql,也存储在磁盘上,被设计用在数据模型简单,性能要求高的场合.性能要求高,我们看B-树与B+树的区别: B+树内节 ...

  5. 看图轻松理解数据结构与算法系列(NoSQL存储-LSM树) - 全文

    <看图轻松理解数据结构和算法>,主要使用图片来描述常见的数据结构和算法,轻松阅读并理解掌握.本系列包括各种堆.各种队列.各种列表.各种树.各种图.各种排序等等几十篇的样子. 关于LSM树 ...

  6. Atitit.数据索引 的种类以及原理实现机制 索引常用的存储结构

    Atitit.数据索引 的种类以及原理实现机制 索引常用的存储结构 1. 索引的分类1 1.1. 按照存储结构划分btree,hash,bitmap,fulltext1 1.2. 索引的类型  按查找 ...

  7. Atitit.数据索引 的种类以及原理实现机制 索引常用的存储结构

    Atitit.数据索引 的种类以及原理实现机制 索引常用的存储结构 1. 索引的分类1 1.1. 索引的类型  按查找方式分,两种,分块索引 vs编号索引1 1.2. 按索引与数据的查找顺序可分为 正 ...

  8. 数据的存储结构浅析LSM-Tree和B-tree

    目录 顺序存储与哈希索引 SSTable和LSM tree B-Tree 存储结构的比对 小结 本篇主要讨论的是不同存储结构(主要是LSM-tree和B-tree),它们应对的不同场景,所采用的底层存 ...

  9. B+树vs. LSM树(转)

    原文:<大型网站技术架构:核心原理与案例分析>,作者:李智慧 本书前面提到,由于传统的机械磁盘具有快速顺序读写.慢速随机读写的访问特性,这个特性对磁盘存储结构和算法的选择影响甚大. 为了改 ...

  10. C#创建安全的字典(Dictionary)存储结构

    在上面介绍过栈(Stack)的存储结构,接下来介绍另一种存储结构字典(Dictionary). 字典(Dictionary)里面的每一个元素都是一个键值对(由二个元素组成:键和值) 键必须是唯一的,而 ...

随机推荐

  1. 2022最新版JDK1.8的安装教程、包含jdk1.8的提取码(亲测可用)

    文章目录 1.jdk的安装 1.1.下载(百度网盘jdk1.8提取码永久有效) 1.2.双击提取出来的exe,运行程序.如下图 1.3.进入安装向导 1.4.选择默认(安装所有的组件).同时更改安装路 ...

  2. FlinkSQL之Windowing TVF

    Windowing TVF 在Flink1.13版本之后出现的替代之前的Group window的产物,官网描述其 is more powerful and effective  //TVF 中的tu ...

  3. .NET周报【11月第1期 2022-11-07】

    国内文章 开源·安全·赋能 - .NET Conf China 2022 https://mp.weixin.qq.com/s/_tYpfPeQgyEGsnR4vVLzHg .NET Conf Chi ...

  4. RocketMQ 在物流行业的应用与运维

    本文作者:丁威 - 中通快递资深架构师,<RocketMQ技术内幕>作者,Apache RocketMQ社区首席布道师,公众号「中间件兴趣圈」维护者. 01 物流行业的业务特点 物流行业有 ...

  5. mybatis-plus分页失效原因

    mybatis-plus分页失效解决方法 方法一.在启动类添加如下配置 @SpringBootApplication @MapperScan("com.**.mapper") pu ...

  6. ENS框架下一次控制灯的调试记录

    正常流程 登录小站,点击管理--磁盘,在硬盘下创建分区并挂载 安全下电,拔掉硬盘和TEC,再上电 硬件端 drv_fault_check_init 初始化 并绑定硬件回调 drv_fault_chec ...

  7. 01-Docker实战,搭建NodeJs环境

    目的 实现简单的docker的nodejs容器,使用Dockerfile构建我们的使用nodejs开发的系统 技术栈 Docker Nodejs Express Linux step1 下拉nodej ...

  8. Shell脚本--信号发送与捕捉

    信号(Signal):信号是在软件层次上对中断机制的一种模拟,通过给一个进程发送信号,执行相 应的处理函数. 进程可以通过三种方式来响应一个信号: 忽略信号,即对信号不做任何处理,其中有两个信号不能忽 ...

  9. [排序算法] 树形选择排序 (C++)

    树形选择排序解释 树形选择排序 又称为锦标赛排序,其实理解起来很简单. 数组的 n 个元素就好像在进行锦标赛一样,每一轮小比赛每两个一组决出胜负(比较谁更小). 再将每一轮的胜者每两个一组进行小比赛, ...

  10. toB应用私有化交付发展历程、技术对比和选型

    由于数据隐私和网络安全的考虑,大多数toB场景的客户需要私有化应用交付,也就是需要交付到客户的环境里,这样的客户有政府.金融.军工.公安.大型企业.特色行业等,这些私有化场景限制很多,如何提高私有化应 ...