LSM Tree 原理解析
楔子
十多年前,谷歌发表了三篇论文:GFS、MapReduce、BigTable,为业界的大数据领域带来了无数灵感,其中 GFS 和 MapReduce 塑造了 Hadoop,BigTable 塑造了 HBase、Cassandra、LevelDB、RocksDB 等众多 NoSQL。估计有人会有疑问,BigTable 影响了这么多 NoSQL 吗?没错,因为这些 NoSQL 的数据存储方式都是仿照的 BigTable,这也是我们要探讨的主题:LSM Tree。
什么是 LSM Tree
LSM Tree 全名:Log Structured Merge Tree ,是一种分层,有序,面向磁盘的数据结构,其核心思想是充分利用了磁盘的 "顺序写" 要远比 "随机写" 性能高出很多这一特性。所以,LSM Tree 也是一种在机械硬盘时代大放异彩的存储引擎。
围绕着磁盘 "顺序写" 的性能远高于 "随机写" 这一特性,不断进行优化,以此让写性能达到最优。正如我们普通 Log 的写入方式,这种结构的写入,全部都是以 Append 的模式进行追加,不存在删除和修改。修改一条数据,其实是写入一条新数据;当然删除也是同理,删除一条数据本质上也是写入一条新数据(带有删除标记)。而 LSM Tree 在写数据的时候就分为两步:写 Log 文件、修改内存,因此写的过程是非常简单的,在正常情况下只包含这两步。
因此 LSM Tree 被设计用来提供比传统的 B+ 树或者 ISAM 更好的写操作吞吐量,通过消去随机的本地更新和删除操作来达到这个目标。当然啦,同 LSM Tree 一样,只要是基于日志(Log)的存储引擎,大部分都是这么设计的。但是有得就有舍,这种结构虽然大大提升了数据的写入能力,却是以牺牲部分读取性能为代价,因此通常适合于写多读少的场景,比如以下两种:
数据是被整体访问的,大部分数据库的 WAL(write ahead log),也称预写式日志,比如 MySQL 的 Binlog
数据是通过文件的偏移量 offset 访问的,比如 Kafka
想要提高写操作吞吐量,就势必要利用磁盘的 "顺序写",而一旦采用 "顺序写",那么就会降低读取性能。因为 Log 文件里的数据虽然基于时间是有序的,但对于用户而言则是无序的,所以在查找的时候需要遍历,这也是基于日志进行存储的一个代价。但 LSM Tree 的野心不仅于此,虽然它也是基于日志存储的,但它为了支持更复杂和更高效的读取,LSM Tree 除了利用磁盘的 "顺序写" 之外,还将数据划分成了多层级的合并结构(一会说)。正是基于这种结构再加上不同的优化实现,才造就了各种独具特点的 NoSQL 数据库,如 Hbase,Cassandra,Leveldb,RocksDB,MongoDB,TiDB 等。
LSM Tree 的设计思想
存储的核心是读写,针对读写有不同的优化手段,比如预读,缓存,批量,并发,聚合等等。但是 "优化读"和 "优化写" 能采用的手段其实不同,在机械盘时代,机械盘一定是瓶颈,它的随机性能极差,顺序的性能还能将就。
如果要优化 IO 读,有非常多的优化策略,比如使用多级缓存、CPU Cache、内存、SSD 等等,也可以采用丰富多彩的查询组织结构,比如各种平衡树型结构,提高读的效率。
但是对于写,它一定是受限于磁盘的瓶颈,因为写数据这个流程,要求数据落盘才算完。所以,对于写的优化手段非常有限,无论用什么手段,一定绕不过一点:保持顺序,因为只有这样才能压榨出机械盘的性能。因此只能在写保持顺序的基础上,才去考虑加上其他的优化手段,比如批量,聚合等操作。
这正是 LSM Tree 的设计思想,先考虑极致地提升写的性能(因为手段有限),读的性能则靠其他的手段解决(方案就比较多了)。
SSTable 解析
提到 LSM Tree 这种结构,就得提一下 LevelDB 这个存储引擎,我们知道 Bigtable 是谷歌开源的一篇论文,很难接触到具体的源代码实现。如果说 Bigtable 是分布式闭源的一个高性能 KV 系统,那么 LevelDB 就是这个 KV 系统开源的单机版实现。最重要的是 LevelDB 是由 Bigtable 的原作者 Jeff Dean 和 Sanjay Ghemawat 共同完成,可以说高度复刻了 Bigtable 论文中对于其实现的描述。
在 LSM Tree 里面,核心的数据结构就是 SSTable,全称是 Sorted String Table,SSTable 的概念其实也是来自于 Google 的 Bigtable 论文,论文中对 SSTable 的描述如下:
An SSTable provides a persistent, ordered immutable map from keys to values, where both keys and values are arbitrary byte strings. Operations are provided to look up the value associated with a specified key, and to iterate over all key/value pairs in a specified key range. Internally, each SSTable contains a sequence of blocks (typically each block is 64KB in size, but this is configurable). A block index (stored at the end of the SSTable) is used to locate blocks; the index is loaded into memory when the SSTable is opened. A lookup can be performed with a single disk seek: we first find the appropriate block by performing a binary search in the in-memory index, and then reading the appropriate block from disk. Optionally, an SSTable can be completely mapped into memory, which allows us to perform lookups and scans without touching disk.
具体细节我们一会解释,总之我们上面说了,从用户来讲,Log 文件是无序的,查找效率非常低。所以自然而然,LSM 的架构里就需要引入一种新型的有序数据结构,这个就是 SSTable,全称为 sorted string table。
因此持久化的 Log 数据向 SSTable 转变,是 LSM Tree 的一个核心流程,而存储 SSTable 数据的文件叫做 sst 文件。
sst 文件存储的 SSTable 是一种有序的数据结构。
显然在实际存储中,sst 文件会有很多个,因为 log 文件转变成 sst 文件是持续不断发生的,系统中不会只有一个不断变大 sst 文件。如果只有一个大文件,那么查找效率会很低,并且每次重建一个有序的 sst 文件的开销也会很大。所以在 LSM Tree 的实践中,是划分了很多个有序的空间,这里的空间就是 sst 文件。
因此 LSM Tree 的核心数据结构是 SSTable,sst 文件是其在硬盘上的表现形式,那么下面我们来解析一下 SSTable 这种结构。
SSTable是一种拥有持久化,有序且不可变的键值存储结构,它的 key 和 value 都是任意的字节数组,并且了提供了按指定 key 查找和指定范围的 key 区间迭代遍历的功能。SSTable 内部包含了一系列可配置大小的 Block 块,典型的大小是 64 KB,关于这些 Block 块的 index 存储在 SSTable 的尾部,用于帮助快速查找特定的 Block。当一个 SSTable 被打开的时候,index 会被加载到内存,然后根据 key 在内存 index 里面进行一个二分查找,查到该 key 对应的磁盘的 offset 之后,然后去磁盘把响应的块数据读取出来。当然如果内存足够大的话,可以直接把 SSTable 直接通过 MMap 的技术映射到内存中,从而提供更快的查找。
如果觉得文章对您有所帮助,可以请囊中羞涩的作者喝杯柠檬水,万分感谢,愿每一个来到这里的人都生活愉快,幸福美满。
微信赞赏
支付宝赞赏