作者:京东物流 刘家存

随着数据量的增大,传统关系型数据库越来越不能满足对于海量数据存储的需求。对于分布式关系型数据库,我们了解其底层存储结构是非常重要的。本文将介绍下分布式关系型数据库 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. SQL优化步骤

    当生产数据量急剧增长后,很多SQL语句可能会开始暴露出性能问题.当面对一个有SQL性能问题的数据库时,应该从何处入手进行系统的分析,使得能够尽快定位到问题SQL处并尽快解决问题? 第一步:查看SQL执 ...

  2. Docker | 容器互联互通

    上篇讲到创建自定义网络,我创建了 mynet 网络,并指定了网关和子网地址.在上篇结尾呢,我抛出了一个问题:其它网络下的容器可以直接访问mynet网络下的容器吗?今天就让我们一块看下怎么实现容器互联. ...

  3. Typora设置代码块Mac风格三个圆点

    写作不停,美化不止! mac小圆点效果 原本代码块样式就挺....干净的,光秃秃的,太单调了: 是吧很丑,于是自己发挥改成了这样: 好吧还是太单调,也没好看到哪里去,于是隔了两天又重新改,DuangD ...

  4. 前端开发日常——CSS动画无限轮播

    近来没有什么值得写的东西,空闲的时候帮前端的同学做了些大屏上的展示模块,就放在这里写写吧,手把手"需求->设计-> 实现",受众偏新手向. 为了直观便于理解, 直接把结 ...

  5. 我要手撕mybatis源码

    传统的JDBC编程中的一般操作: 1.注册数据库驱动类,指定数据库的URL地址.数据库用户名.密码等连接信息 2.通过DriverManager打开数据库连接 3.通过数据库连接创建Statement ...

  6. goroutine&waitgroup下载文件

    0.1.索引 https://blog.waterflow.link/articles/1663078266267 当我们下载一个大文件的时候,会因为下载时间太久而超时或者出错.那么我么我们可以利用g ...

  7. KNN算法之集美大学

     在本篇文章中,我即将以在集美大学收集到的一些数据集为基础,使用KNN算法进行一系列的操作 一.KNN算法 首先,什么是KNN算法呢,这得用到老祖宗说的一句话"近朱者赤近墨者黑", ...

  8. HDLBits答案——Circuits

    1 Combinational Logic 1.1 Basic Gates 1.1.1 Exams/m2014 q4h module top_module ( input in, output out ...

  9. C#winfrom调整任意控件宽和高

    在winform项目开发中,偶尔需要用到边框拖拽.度娘也没找到相关的轮子(可能是我不配,没推给我).只能自己造一个 上效果图(鼠标没录制上,问题不大) 上代码 private void Form1_L ...

  10. i春秋Fuzzing

    先查看源码...没东西,抓包 发现也没什么,但是右边有个提示hint: ip,Large internal network(最大内网ip) 可能需要我们伪造代码进行访问,这还不简单,直接在reques ...