分布式存储系统的基础知识:Memtable + SSTable

497 阅读13分钟

一、Memtable + SSTable 介绍

在分布式存储系统中,基于 Memtable + SSTable 架构的引擎几乎成为了事实上的标准,特别是在需要高效写入和读取的系统中。这种架构的核心思想是利用内存表(Memtable)和不可变的磁盘排序表(SSTable)相结合,以实现高效数据写入和读取以及高效的压缩和合并操作。

以下是一些采用这类架构的知名引擎和数据库系统:

1. Apache HBase

  • 架构: HBase 是一个分布式、列存储的 NoSQL 数据库,运行在 Hadoop 分布式文件系统 (HDFS) 之上。它使用 MemStore(内存表)存储实时写入数据,并周期性地将它们 flush 到磁盘,形成 HFile(对应于 SSTable)。
  • 用途: 主要用于需要实时读写的大规模数据处理场景,尤其是在线数据查询和分析。

2. Apache Cassandra

  • 架构: Cassandra 是一个分布式 NoSQL 数据库,以高可用性和水平扩展性为设计目标。它使用 Memtable 作为暂时存储数据的内存表,并周期性地将这些数据写到磁盘上形成 SSTable。
  • 用途: 广泛应用于社交网络、推荐系统和日志分析等大规模数据处理场景。

3. Facebook RocksDB

  • 架构: RocksDB 是一个高性能的嵌入式键值存储引擎,基于 Log-Structured Merge-Tree(LSM-Tree)的结构。它使用 Memtable(基于跳表或红黑树等结构)暂存数据,并定期将这些数据持久化到 SSTable。
  • 用途: 适用于延迟敏感的应用,比如消息队列、缓存系统和实时分析。

4. Google Bigtable

  • 架构: Bigtable 是 Google 提供的分布式存储系统,为 HBase 的原型。它使用 Memtable 和 SSTable 的设计,提供高扩展性和高效的存储以及检索功能。
  • 用途: 主要用于 Google 内部的数据存储服务,也是 Google Cloud Bigtable 的基础。

5. ScyllaDB

  • 架构: ScyllaDB 是一个高性能的 NoSQL 数据库,与 Cassandra 兼容。它采用 C++实现,以降低延迟和提升吞吐量。ScyllaDB 使用类似于 Cassandra 的 Memtable + SSTable 结构。
  • 用途: 适用于对性能要求极高的应用,如物联网、时间序列数据和监控系统。

6. LevelDB

  • 架构: LevelDB 由 Google 开发,是一个嵌入式的键值存储库。它使用 Memtable(基于跳表)和 SSTable 存储数据,具有高效的读写性能。
  • 用途: 通常嵌入到需要高效本地存储的应用中,如浏览器缓存、数据库子系统。

7. etc.d

  • 架构: etcd 是一个分布式键值存储,用于协调分布式系统。尽管主要关注一致性和高可用性,但也使用了类似于 Memtable 和 SSTable 的机制来管理内部数据。
  • 用途: 用于分布式系统的配置管理和服务发现,非常适合容器编排系统(例如 Kubernetes)。

8. TiKV

  • 架构: TiKV 是一个分布式的键值存储系统,作为 TiDB 分布式 SQL 数据库的存储层。它使用 RocksDB 作为底层存储引擎,结合了 Memtable 和 SSTable 的架构。
  • 用途: 主要用于需要高可用性和水平扩展的大规模分布式数据库。

小结

Memtable + SSTable 架构的核心优势在于其高效的写入和读取性能以及出色的数据压缩和合并能力。它通过将写入操作首先存储在内存(Memtable)中,然后批量地刷新到磁盘(SSTable),实现了高效的写入与读取。上述系统和引擎广泛应用于各种高性能、分布式数据处理场景,彰显了这种架构的威力和灵活性。

二、一般设计原则

2.1 Memtable 的一般设计

Memtable 是分布式存储系统中的一个关键组件,其主要作用是暂时保存写入的数据,提供快速写入和读取能力。下面是 Memtable 通常的设计要素和策略:

2.1.1 数据结构

Memtable 的核心通常是高效的内存数据结构,用于快速插入、查找和遍历。常用的数据结构包括:

  • 跳表 (Skip List) : 能够在对数时间复杂度内进行插入、删除和查找操作,广泛应用于多个 NoSQL 系统如 LevelDB、RocksDB 等。
  • 红黑树 (Red-Black Tree) : 自平衡二叉搜索树,保证在插入、删除和查找操作上的对数时间复杂度。Cassandra、HBase 等系统使用红黑树作为 Memtable 的数据结构。
  • 哈希表 (Hash Table) : 适用于键值对查找较多的场景,支持高效的查找和写入,但不适用于范围查询。

2.1.2 写入策略

  • Write-Ahead Logging (WAL) : 在将数据写入 Memtable 之前,首先写入持久化的写前日志 (WAL),以防止数据丢失。

    1. 数据写入请求到达。
    2. 将数据记录到 WAL 中并持久化。
    3. 数据写入 Memtable 中。
    4. 返回成功响应给客户端。
  • 批量写入: 在处理高吞吐量写入时,将多条写入操作批量处理,以提高效率并减少系统开销。

2.1.3 读取策略

  • 读缓存 (Read Cache) : Memtable 通常作为系统的一层缓存,在读取时首先检查 Memtable 是否包含目标数据。如果找到,则直接返回,否则到持久化存储(如 SSTable)中查找。

2.1.4 刷新策略 (Flusing)

Memtable 是内存结构,当数据达到一定大小或内存使用量到达阈值时,需要将 Memtable 数据刷新 (flush) 到磁盘以形成持久化的 SSTable。常见的刷新策略包括:

  • 基于大小: 当 Memtable 达到预设大小时触发刷新。
  • 基于时间: 定期检查并刷新 Memtable,防止数据长期滞留在内存中。
  • 基于内存使用: 当系统整体内存使用达到阈值时,优先刷新 Memtable 以释放内存。

2.1.5 合并策略 (Compaction)

持久化之后的 SSTable 可能会存在碎片化或重复数据,因此需要进行合并(compaction)。Memtable 设计中也需考虑该过程:

  • Minor Compaction: 将多个较小的 SSTable 合并为较大的 SSTable,减少文件数量。
  • Major Compaction: 全量合并,去除重复数据并进行压缩,以提高查询效率和存储利用率。

2.1.6 资源管理

  • 内存管理: 为避免 Memtable 占用过多内存,系统通常预设多个 Memtable,以便一个在刷新、合并时,其他仍能处理写入请求。
  • 并发控制: 使用锁或原子操作保障多个写入操作并发安全,避免数据不一致。

2.1.7 容错与恢复

当系统崩溃或宕机时,通过以下机制保证数据不丢失:

  • WAL 重放: 在系统重启时,通过重放 WAL 日志恢复 Memtable 数据。
  • 副本机制: 在分布式系统中,写入操作会同时复制到其他节点,保证任一节点宕机时,数据仍然可以从副本节点恢复。

2.1.8 查询优化

  • 索引结构: 在 Memtable 中可以引入索引(如二级索引)以加速复杂查询。
  • 布隆过滤器 (Bloom Filter) : 在持久化存储中使用布隆过滤器快速判断键是否存在,从而优化查询性能。

2.1.9 示例实现

以 RocksDB 为例,Memtable 的具体设计如下:

  • 跳表: 选择跳表作为 Memtable 的基础数据结构,提供高效插入和范围查询能力。
  • WAL 机制: 写入操作首先持久化到 WAL 文件,再写入 Memtable。
  • 定期刷盘: 通过配置参数设置 Memtable 的大小和刷新阈值,确保数据及时持久化。

2.1.10 小结

Memtable 是任何高性能分布式存储系统中不可或缺的组件,其设计要素涵盖数据结构选择、写入和读取策略、刷新与合并机制、资源管理及错误恢复策略,通过这些策略与机制的综合应用,Memtable 提供了高效的数据写入和读取能力,同时保证系统的可靠性和一致性。

2.2 SSTable 的一般设计

SSTable(Sorted String Table)是一种不可变的、持久化的文件格式,用于存储键值对数据。它广泛应用于许多现代分布式存储系统(如 Bigtable、Cassandra、LevelDB、RocksDB 等),提供高效的数据存储和读取。以下是 SSTable 的一般设计要素和策略:

2.2.1 数据模型

  • 键值存储: 每个 SSTable 文件保存一组有序的键值对。键和值都是二进制数据,且键值对按键的顺序排序。
  • 不可变性: 一旦写入磁盘,SSTable 就不会再被修改。这种设计简化了并发控制和数据一致性管理。

2.2.2 文件格式

2.2.2.1 元数据

SSTable 包含一些元数据,用于描述文件的基本属性和内容。元数据通常包括:

  • 元数据头(Header): 用于存储文件标识、版本等基本信息。
  • 索引区 (Index Block) : 保存键和对应数据位置的索引,便于快速定位。
  • 过滤区 (Filter Block) : 采用布隆过滤器快速判断键是否存在,以减少无效查找。
  • 元数据信息 (Footer) : 包含文件长度、校验和等信息。

2.2.2.2 数据区结构

  • 数据块 (Data Block) : 存储实际的键值对数据。一个 SSTable 文件由多个数据块组成,每个数据块包含一定数量的有序键值对。
  • 索引块 (Index Block) : 数据块的索引,用于快速定位数据块中的键。

数据块和索引块的结构通常如下:

+------------+------------+------+-------------+
| Data Block | Index Block| ...  | Footer      |
+------------+------------+------+-------------+
|                                 |----------------→ 校验和
|                                 |----------------→ 索引位置
|--------------------------------→ 序列化数据

2.2.3 数据写入

SSTable 通过批量写入操作来提高写入效率:

  1. 数据排序: 将 Memtable 中的数据按键排序。
  2. 分块存储: 将排序后的数据分成多个数据块,并生成索引块。
  3. 写入磁盘: 按序写入数据块、索引块、过滤块等到磁盘。

2.2.4 数据读取

读取时,SSTable 提供高效的数据定位和检索:

  1. 布隆过滤器检查: 首先检查布隆过滤器,快速判断目标键是否可能存在,减少无效查找。
  2. 索引查找: 如果布隆过滤器判断键可能存在,则在索引块中查找目标键对应的数据块位置。
  3. 数据块读取: 加载相应的数据块,并在数据块中查找目标键对应的值。

2.2.5 合并(Compaction)

SSTable 是不可变的,随着时间推移,系统中会产生大量的 SSTable 文件,需要定期合并(compaction):

  • Minor Compaction: 将多个小 SSTable 文件合并为一个较大的 SSTable 文件,减少文件数量。
  • Major Compaction: 对所有 SSTable 文件进行全量合并,移除重复数据和无效数据。

2.2.6 删除标记 (Tombstones)

在 SSTable 中,删除操作通常不会立即物理删除数据,而是标记为删除(tombstone),后续的 compaction 过程中会真正移除这些被标记的数据。

2.2.7 压缩与去重

  • 数据压缩: 使用压缩算法(如Snappy、LZ4等)对数据块进行压缩,减少存储空间。
  • 去重: 合并过程中会移除重复的键值对,进一步优化存储利用率。

2.2.8 容错和数据校验

SSTable 包含数据校验和(如CRC32)以确保数据完整性和可靠性。在读取和写入时进行校验,发现数据损坏时采取相应的恢复或通知机制。

2.2.9 示例实现

以 RocksDB 为例,SSTable 的具体设计如下:

  • 数据块: RocksDB 默认使用 4KB 或 8KB 的数据块,每个数据块包含若干个有序键值对。
  • 索引块: 每个数据块对应一个索引条目,索引条目存储在索引块中,用于快速定位。
  • 过滤块: 使用布隆过滤器,减少不必要的I/O操作。
  • 压缩: 默认使用高级压缩算法如 Snappy 或 Zlib,对数据进行压缩。

2.2.10 小结

SSTable 是一种高效、不可变的文件格式,设计用于在分布式存储系统中提供快速的数据存储和读取。其设计要素包括排序的键值数据模型、分层的数据结构、布隆过滤器、数据校验和合并机制等。结合上述要素,SSTable 在提供高性能读写的同时,保证了数据一致性和持久性。

三、故障容错保障

如何保障节点宕机时内存表数据不丢失? 在分布式存储系统中,保障内存数据(Memtable)在节点宕机时不丢失通常通过如下几种方法实现:

3.1 Write-Ahead Logging (WAL)

Write-Ahead Logging 是保障数据不丢失的核心机制。其基本思想是,在将数据写入 Memtable 之前,首先将数据记录到持久存储的日志文件中。这些日志文件通常是顺序写入的,效率很高。

  • 具体步骤

    1. 数据写入请求到达。
    2. 将数据写入WAL日志文件。
    3. 一旦数据成功写入WAL,返回确认给客户端。
    4. 将数据写入Memtable。
    5. 当 Memtable 满或达到一定条件时,将 Memtable 数据刷写到磁盘,形成 SSTable。
  • 故障恢复: 当节点宕机并重启时,可以通过读取WAL日志重新构建Memtable,确保数据不失。

3.2 多副本数据存储

大多数分布式存储系统采用数据副本的方式保障数据的高可用性和可靠性。通过在多个节点上存储数据副本,即使某一个节点宕机,数据仍然可以从其他节点恢复。

  • 数据写入过程

    1. 数据写入请求到达任一节点。
    2. 数据分别写入WAL和Memtable。
    3. 写操作完成时,数据会异步复制到其他副本节点。
    4. 副本节点也在各自的WAL和Memtable中记录数据。
  • 故障恢复: 如果主节点宕机,可以从副本节点中恢复数据,保障数据不丢失。同时,节点重启后通过WAL日志恢复Memtable数据。

3.3 数据快照 (Snapshots)

一些系统定期生成内存数据 (Memtable) 的快照并将快照写入持久存储,以减少重启时通过WAL日志重放恢复数据的时间。

  • 快照机制

    1. 周期性地将当前的Memtable数据生成快照。
    2. 将快照写入磁盘。
    3. 删除写入快照之前的WAL日志,减小日志大小。
  • 故障恢复: 宕机后,通过读取最近的快照,并通过WAL日志重放快照生成之后的变化数据。

3.4 一致性协议

分布式系统通常使用一致性协议(如Paxos, Raft)以保障在网络分区和节点失败情况下数据的一致性和可用性。通过这些协议,可以保证数据在多个节点之间一致。

  • 一致性协议 主要用于:

    • 数据复制与同步。
    • 数据状态变更的共识。
  • 故障恢复: 在节点恢复后,通过一致性协议重新同步数据,确保所有节点数据一致。

3.5 具体实现示例

以Apache Cassandra为例,该数据库系统融合了上述机制以保障数据安全:

  1. Write-Ahead Logging: 数据写入先记录到Commit Log(类似于WAL),确保即使内存数据丢失也能从日志恢复。
  2. 多副本存储: 数据写入到Memtable后,会异步复制到其他节点的Memtable,以提供高可用性。
  3. 数据快照: 定期生成SSTable快照,确保可以快速恢复。
  4. 一致性协议: 使用Gossip协议和一致性哈希环来确保数据一致性和高可用。

3.6 小结

通过整合多种机制,如Write-Ahead Logging (WAL)、多副本数据存储、数据快照、以及一致性协议等,基于Memtable + SSTable 架构的存储系统能够保障在节点宕机时内存数据不丢失。这些机制在提供高性能和高可用性的同时,保障了数据的一致性和持久性。