LSM - 一切皆追加(Append only)!
顺序 与 随机
在磁盘中由于所有的读写操作都需要磁盘机械结构到达对应的区块。
可以想象成黑胶唱片,听歌时唱头会顺序读取唱片上的歌。
如果歌曲是随机存放的话,听歌的时候唱头就得这里转一下,那里转一下,听歌就一顿一顿的了。
注:黑色圆圈:唱头落点;黑色箭头:唱头划过时在放歌;红色箭头:唱头在找这一句歌词唱完之后的下一句时划过的部分
顺序:数据在磁盘上线性连续存储,比如三块数据连续使用三块顺序的块存储。
随机:数据在磁盘上非线性存储,比如三个数据采用了不连续的三个块存储。
LSM 树
LSM 树其实并不能算得上是一个树,或者说它看起来实在是不像一棵树。
LSM 树全名日志结构合并树(Log-Structured Merge Tree),是一种用于存储和管理数据的树状数据结构,常用于写频繁数据库中。
它的结构是这样子的:
其中绿色是写操作,红色是读操作
存储结构
LSM 树下包含多个数据结构,多个数据结构拼凑成了 LSM 的存储结构
Active Mem Table
数据存储时会对其进行哈希运算,取哈希值为 key 进行存储
以跳表或红黑树进行存储,保证数据的绝对有序
Immutable Mem Table
当 Active Mem Table 达到一定规模时转变为 Immutable Mem Table 数据结构,与 Active Mem Table 的核心区别在于,前者由后者达到一定大小后转变,且 Immutable Mem Table 不允许进行修改,只能进行查找。
同时 Immutable Mem Table 也是在 LSM 树种将数据刷入磁盘的基本单位
Block Cache
块缓存,会对 SSTable 进行缓存,加快读写速度
WAL
LSM 树 中最重要的结构之一,由于Immutable Mem Table 和 Active Mem Table 都被放置在内存中,但内存并非持久化存储,所以当发生突发情况时会导致数据来不及持久化就全部消失,WAL 采用顺序存储,当有数据被写入时会对该数据进行日志记录,以防止数据在突发情况下的丢失
SSTable
LSM 树中持久化存储的结构,会被分为不同的层级(Level 0、Level 1、Level 2 等)其中 L0 是唯一可能存在重叠数据的层级,
例如:1 ~ 100、 50 ~ 200
每一层的 SSTable 都有数量限制,当到达限制时会触发合并操作,合并操作后会将当前层的 SSTable 放置到下一层
Compaction
合并操作,随着写入的进行,磁盘上会产生多个 SS Table,为了减少数据冗余和提高读取效率,会定期进行合并操作,将多个 SS Table 合并成一个,并移除过期和重复的数据。
LSM 树的操作
LSM 树通过牺牲了部分读性能来增加写的性能
读
多个 SSTable 中由于是区域存储的(一个 SSTable 中存储 key 从 100~200 的数据)那么找到存放对应数据的 key 就很快了,再加上单个 SSTable 中存有整个 key 对应的布隆过滤器,读取数据时只需要查找对应的 key 就知道在这个 SSTable 中是否存在这个数据了
那为什么还说牺牲了部分读性能来增加写的性能呢?
原因是因为,当数据读取时,应该先去访问 Block Cache ,如果缓存未命中就要去访问未成块的数据 Active Mem Table,再去看看已经成块但是没有存储进入磁盘的 Immutable Mem Table,如果连 Immutable Mem Table 中都没有找到对应的数据,则需要去磁盘中的 SSTable 集群了,其中 L0 是存在重叠的,即使不存在重叠,布隆过滤器也会“骗人”,比如存在这个 key 但不是你要找的那个数据。相较于如此漫长的寻找数据之旅, B+ 树就可以通过地址一路绿灯找到对应的数据。
(布隆过滤器可能会产生‘误判’(False Positive),即它认为 key 存在于该 SSTable 中,但实际上读取后发现并没有。但它能绝对精准地告诉你 key 不存在,从而帮我们跳过大量无关的 SSTable,节省 I/O)
所以 LSM 树的读性能确实感人。
写
但是写性能是 LSM 树最强悍的地方,由于是顺序块写入,大大提升了写性能
当有新数据写入时,首先会将数据写入到内存中的 MemTable(内存表)。写入之前会记录操作在此磁盘WAL日志文件中, MemTable 通常是基于某种数据结构实现,如跳表或哈希表,以便快速地进行插入和查找操作。在 MemTable 中,数据按照键值对的形式存储,新写入的数据会被添加到 MemTable 的合适位置。
当 MemTable 达到内存阈值后,它会被转换为 Immutable MemTable(不可变内存表),这意味着该 MemTable 不再接受新的写入操作,而是准备被刷写到磁盘。
在将 Immutable MemTable 写入磁盘时,实际上等同于将大量的随机IO转化为批量的顺序 IO 也就提高了写入的效率。
Immutable MemTable 中的数据会被刷写到磁盘上,形成一个 SSTable。SSTable 是 LSM 树在磁盘上存储数据的基本单元写入后就不可修改,其中的数据是按照键排序。同时 SSTable 会采用分层设计
修改
LSM 树的修改是采用追加写的方式修改数据的,而不是像 B+ 树那样原地修改。
由于 LSM 树的数据结构中会包含“版本号”,在读取时就会读取最新的版本号,从而无需将旧数据删除
由于在 SSTable 合并时,只会保留最新的版本号对应的数据,所以旧数据不会持续占用空间
删除
LSM树的删除操作并不是直接删除数据,而是通过一种叫墓碑标志的特殊数据来标识数据的删除。
删除操作分为:待删除数据在内存中、待删除数据在磁盘中 和 该数据根本不存在 三种情况。
待删除数据在内存中:当执行删除操作时,并不会直接将数据从MemTable中移除,而是为该数据添加一个“墓碑标记”。这个标记就像是一个特殊的标签,用于告诉系统该数据已经被标记为删除。 例如,若要删除键为“key1”的数据,会在MemTable中找到对应的键值对,并为其设置一个删除标记,而不是立即释放存储该键值对的内存空间。这样做的好处是可以避免频繁的内存分配和释放操作,提高删除操作的效率。 当MemTable达到一定的大小或满足其他条件时,会将其刷写到磁盘上的SSTable中,带有“墓碑标记”的数据也会被写入SSTable。
待删除数据在磁盘中:对于已经存储在磁盘 SSTable 中的数据,同样是通过“墓碑标记”来进行删除操作。当需要删除某个数据时,不会在相应的SSTable中为该数据添加“墓碑标记”。而是在内存中的 MemTable 插入一个墓碑标志。后续在进行 SSTable 的合并或压缩操作时,带有“墓碑标记”的数据不会被复制到新的SSTable中,从而实现了数据的逻辑删除。但在合并或压缩操作之前,带有“墓碑标记”的数据仍然会占用磁盘空间。
该数据根本不存在:当执行删除操作时,如果发现要删除的数据在LSM树中根本不存在,那么这个删除操作实际上是一个“空操作”,不会执行。
技术支持
在 HBase、Cassandra、RocksDB 此类大数据场景中,写入占比能到 80%,此时使用 LSM 树再加上其他技术:如热搜索,LRU 算法等一定程度上提高读取效率
Kafka 吞吐量这么高?不仅仅在于零拷贝的文件转发,更是因为它把消息队列看作一个无限延长的顺序文件,利用了 Page Cache 和顺序 I/O。核心原理与 LSM 树类似,都是最大化利用磁盘的顺序写性能。Kafka 将消息队列抽象为简单的追加日志(Log),完全规避了随机 I/O。
番外 - 随机写和顺序写的性能对比
随机写:
顺序写:
顺序写比随机写性能高了近 30 倍?
好吧,这是因为在顺序写入的时候使用了“块”,众所周知对于 内容和磁盘而言,他们比较喜欢块
无块顺序写:
不过顺序写的性能确实可观,比随机写高了近两倍