数据库行业在研发面试阶段经常会被问到的一项技术是什么?
答案大概率是 LSM-tree。对于数据库领域的研发同学来讲,它不仅是面试必备的问题,更是各数据库不可或缺的一项技术。不夸张地说,在近十年诞生的数据库都在使用 LSM-tree 数据结构。
今天我们就来聊上一聊,除了介绍 LSM-tree 的原理、优缺点以外,我还会列举它的典型实现,帮大家理解 LSM-tree 受各大数据库青睐的原因。
Vol.1
目前,大部分数据库都在使用基于 LSM-tree 的存储引擎,例如:RocksDB、CockroachDB、TiDB、FoundationDB、Snowflake、Doris、OceanBase、InfluxDB 等。很多朋友都知道,像 MySQL、PostgreSQL、Oracle 这些早期的数据库使用 b-tree 或其变体作为索引。那么,为什么会有那么多数据库都选择使用 LSM-tree 数据结构呢?
这就要从它的优势说起了。LSM-tree 是一个数据结构,在数据库领域主要用作主键索引,由于它 Append-only 的特性,使得在大量写入的场景有明显的优势,尤其是在 SSD 硬件普及的今天,写入优势更大。另外,由于它的分层结构、文件的不可变性,在分布式数据库中有着天然的优势。因为分布式数据库经常会做基于文件的同步、balance 等操作,不可变的文件天然就不用考虑并发、读写冲突等问题。
关于 LSM-tree 的具体理论细节,可以参考https://en.wikipedia.org/wiki/Log-structured_merge-tree。其全称是 Log-structured merge-tree,名字里没有一个词是多余的,核心的设计思路是以日志的形式存储数据,这也充分利用了磁盘的顺序读写性能高的特点,相比之下,B-tree 需要原地更新数据,所以每次写入就涉及到磁盘寻道、树分割、移动等操作,使得写入性能存在瓶颈。
但只有 append-only 还不够,因为数据库不是只有 insert 操作,还有大量的 update、delete 操作,对于 delete 则向 log 中记录一条删除记录,同时查询时只需使用最新的记录就可以满足 delete 和 update 的需求。不难发现,这样的设计非常有利于写入性能,但会占用很多额外的存储空间,即存储空间放大。所以,LSM-tree 还有额外的 compaction 机制,通过 compaction 将重复主键的数据进行清理,同时将数据按照主键进行排序以便后续查询使用,这就是名字中 merge 的来历。
至于 tree 则体现在数据是以多层的形式组织,越上层的 sstable 越小,越下层的 sstable 越大,这有利于控制 sstable 的数量,防止数据合并时的写放大。可以参考下图:
图源:interviewnoodle.com/secret-sauc...
其中数据刚写入是先进入内存的 memtable,按照一定规则 flush 进 L0 层,再按照一定 compaction 规则逐层的将数据 L0 --> L1,L1 --> L2,L2 --> L3 进行合并。LSM 的层数没有固定的值,用户需要根据写入负载、数据规模、查询性能、存储介质来权衡,一般来说数据量越大层数多,对查询性能要求高层数越浅。
看到这里,应该可以回答"为什么新兴的数据库都在使用 LSM-tree"的问题,原因是:
-
需要存储的数据量激增,LSM-tree 在写入优势和数据存储量上有明显优势
-
SSD 等高性能存储介质普及,LSM-tree 能充分利用其性能
-
分布式需求增加,我们不展开"分布式数据库是不是伪需求"这个敏感话题,只从现状来说分布式数据库确实有很多,而且 LSM-tree 对分布式场景更友好
-
此外,基于 LSM-tree 的开源的 RocksDB 是个非常好的存储引擎,直接用就行了
只说优势不说缺点是不客观的,所以还是要直面 LSM-tree 的缺点:
相比于 b-tree, LSM-tree 最大的劣势就是查询性能,LSM-tree 的查询流程需要从上到下依次查询,直到查询到对应的 key 为止,最坏的情况要查询所有层,此时性能会急剧下降。对此,一些常规的优化手段是给 sstable 文件增加索引并且将其放到内存中,以加快 key 查询。
compaction 对性能的影响,compaction 期间会消耗节点的 CPU 和内存,如果资源不足对正常的写入、查询都会有影响。
通过以上的分析,不难看出 LSM-tree 是一个相对较新,对现代硬件、架构友好,但也有自身局限性的索引。接下来,后面我将通过 RocksDB 的例子,更具象化地分析 LSM-tree。
Vol.2
RocksDB 几乎可以说是现代分布式数据库的基石,大部分近十年内诞生的数据库、大数据产品都在使用或使用过RocksDB,它自身的定位是一个嵌入式的 kv 数据库,这样的定位非常适合作为一个存储引擎。
www.cockroachlabs.com/blog/cockro...这篇文章 CockroachDB 详细分析了为什么选择 RocksDB,当然由于一些其他原因,CockrochDB 后来自研了一款存储引擎 PebbleKV。
RocksDB 的历史可以追溯到 LevelDB,是 Google 开源的产品,主要应用在嵌入式设备或浏览器中,并且后期维护演进较慢。于是,Facebook 的 Dhruba Borthakur 于 2012 年 4 月 Fork 了 LevelDB,在其基础之上改进了服务器工作负载的性能,以充分利用多核处理器和快速存储设备的性能,特别适用于 I/O 密集型工作负载。
RocksDB 不只实现了 LSM-tree 数据结构,还在此之上增加了一系列存储引擎的功能,例如 ACID 支持、事务、WAL、丰富的查询接口、快照、TTL、列簇等功能,
在 Facebook 内部,基于 RocksDB 实现了一个 MyRocks 项目,用来替换 MySQL 的 InnoDB 引擎,性能测试的结果显示在写入场景中远超 MySQL,查询性能也相差不大。其实对于一款数据库产品来讲,写入的性能往往更关键,毕竟想要更好的查询性能可以用 OLAP 数据库,而 OLAP 数据库往往对存储引擎的需求没那么大。
Vol.3
之前的分析都在讲 LSM-tree 适合高吞吐、SSD 的场景,但现在数据库技术的发展趋势是存算分离、云原生,这种场景无疑会带来新的挑战。例如,本地文件读写变成了远程调用,多节点之间的并发读写冲突等问题。
对于这些问题,大厂是如何解决的?以 Facebook 为例,近期他们发表了一篇文章总结了他们在存算分离场景的最佳实践。读下来,感觉 Facebook 的基础架构在其他公司不一定适用,经验不一定能直接移植,但他们解决问题的思路是值得借鉴的。
首先,Facebook使用了内部的一套分布式文件系统 Tectonic File System,与 HDFS 类似但是自研的,所以性能、适用场景肯定和 HDFS 有区别。其次,这次改造的目标也很明确,就是节约成本,存算一体架构最大的问题是资源使用率不足,往往存储已经满了但 CPU 还没跑满,而且存算一体需要为每个应用预留存储空间资源浪费,而存算分离可以让资源尽量复用。
存算分离对 RocksDB 来说,有几个挑战,分别是:
-
性能相比使用本地磁盘差
-
增加很多额外开销,本地磁盘获取文件列表、统计信息等几乎没有延迟,而分布式系统不同,都有额外的开销
-
使用本地磁盘时,多个节点之间的数据天然隔离的,而存算分离场景下存储是在同一个目录,需要解决多个 writer 并发修改的问题
-
故障处理更复杂,需要进行大量代码改造
针对上述问题,Facebook 进行了大量优化,包括优化尾部 IO 延迟、metadata 缓存、计算节点缓存、并行 IO、优化 compaction 策略等来提升性能;通过 IO fencing 技术解决存储并发冲突;不使用纠删码而使用多副本降低额外开销;增加大量 tracing、监控等手段发现瓶颈点、异常逐步解决。
经过实验存储引擎的写吞吐大概降低 25%,读延迟大概有 3~5 倍。实话实说,这个结果已经很不错了,毕竟网络再怎么优化也是比本地磁盘多了一层开销。
Facebook 还做过这样的实验:把内部的 ZipplyDB 中的 RocksDB 替换成存算分离版本,空间使用率从原来的 35% 提升到了 75%,故障恢复时间从 51 min 优化到了 49 s,而端到端的写延迟几乎没有影响,P99 的读延迟增加了 50%。这个结果还挺不错的,相对于性能的一点损失,资源使用率提升太明显了,同时故障恢复时间降低了太多,能够大大提升运维的便捷度,意味着运维遇到问题时敢于重启了。
总的来说,Facebook 可以把 RocksDB 改成存算分离版本,并从中拿到很好的结果,说明这条路是可行的。当然,这只是个例,是否具有普适性还有待验证。毕竟 Facebook 的基建优势和人才储备,其他公司大概率不具备类似的条件。
Vol.4
本文从 LSM-tree 的原理一路讲到了它最火热的开源实现 RocksDB,又延伸出一些 RocksDB 在存算分离方面的探索,存储技术一路进化的推动力是底层存储介质的变化,用户对性能、成本越来越高的期待,以及技术人不断的探索。未来,可能会有更适合时代的存储技术出现,并慢慢取代 LSM-tree(就像 b-tree 慢慢地被取代一样),但当下这个时间点,学好 LSM-tree、RocksDB 技术,在数据库行业混口饭吃还是绰绰有余的。