最近研究了一款新的数据库存储引擎 ------ SlateDB。这款存储引擎的最大特点,就是它完全基于云端的对象存储,比如S3、Azure Blob Storage、Google Cloud Storage等等,而不需要依赖本地磁盘存储,却可以提供高性能的读写操作。我之前在"如何将RocksDB这样的存储引擎应用在分布式环境中"这样的问题上花费了很多的时间,也曾经思考过基于对象存储这个方向,因此就对它非常感兴趣,它几乎就是我一直想要的存储引擎了。
接下来,我们就从适用场景、核心优势、数据结构、写操作、读操作等方面来对它进行展开学习。
适用场景
尽管官方声称SlateDB是一款OLTP存储引擎,但如果你的业务场景需要低延迟的读写,并且要求所有的延迟必须严格控制在一个极小的范围,那么SlateDB恐怕不是很适合。它更适合用在这样的场景中:
- 批量读取一组状态
- 对这组状态进行修改
- 批量原子地提交这批状态
也就是类似于"微批",介于低延迟和高吞吐之间。这样的业务有哪些?最常见的:
- 实时流处理
- 基于事件驱动的微服务
- 工作流编排 / Durable Execution
- 基于事件驱动的AI Agent
如果你正在做以上方面的相关业务,并且为状态存储的架构和成本所苦恼,那么SlateDB就非常值得一试。
SlateDB同LevelDB和RocksDB一样,都是采用库的形式嵌入到你的应用当中,只不过,SlateDB是用Rust开发的,但它也支持诸如Go、Python、Java(对JDK的版本要求较高,需要JDK24)等语言。
核心优势
SlateDB的核心优势我觉着就两点:非常简单 以及非常省钱。就是这么的朴实无华。
非常简单
SlateDB的简单性体现在多个方面,尤其是在分布式架构设计上:
无需考虑数据复制和一致性问题。这是SlateDB最大的架构优势。传统的分布式数据库需要在多个节点之间复制数据,这就不可避免地要处理复杂的一致性问题------Raft、Paxos、主从复制、脑裂、网络分区等等。而SlateDB完全依赖对象存储的高可用性和持久性保证,对象存储服务商(如AWS S3)已经在底层处理了数据复制和一致性,你的应用层完全不需要关心这些。这大大降低了系统的复杂度。
本地完全无状态,分区设计极其简单。由于所有数据都在对象存储中,每个SlateDB实例本地不保存任何持久化状态(除了少量缓存)。这带来了巨大的灵活性:
- 一个进程多个实例:SlateDB非常轻量,你可以在一个进程中启动多个SlateDB实例,每个实例对应一个分区。这让"每个分区独享一个SlateDB实例"成为可能,避免了分区之间的资源竞争,也简化了分区管理的逻辑。
- Rebalance无需数据迁移:当你需要重新分配分区时,不需要在节点之间迁移数据,只需要重新分配分区到对象存储路径的映射关系即可。新节点启动后直接从对象存储读取数据,旧节点直接关闭,整个过程可以在秒级完成。
部署和扩展极其简单。在云原生环境中,如果你使用RocksDB,当容器重启或迁移时,你需要考虑如何持久化本地数据,通常需要使用持久化卷(Persistent Volume)。而SlateDB天然就是无状态的,容器可以随意启停和迁移,数据始终在对象存储中。这让水平扩展变得非常容易------启动新实例即可,无需数据迁移。
开发体验友好。SlateDB提供了简洁的API,你只需要指定对象存储的配置(如S3 bucket和路径前缀),就可以像使用本地数据库一样进行读写操作。它支持多种语言绑定,让不同技术栈的团队都能轻松集成。
非常省钱
成本优势是SlateDB最吸引人的特点之一:
存储成本大幅降低 。对象存储的价格远低于块存储(如EBS)。以AWS为例,S3标准存储每GB每月约 <math xmlns="http://www.w3.org/1998/Math/MathML"> 0.023 ,而 E B S g p 3 每 G B 每月约 0.023,而EBS gp3每GB每月约 </math>0.023,而EBSgp3每GB每月约0.08,相差3-4倍。如果你的数据量达到TB级别,这个差异会非常显著。
无需预留磁盘空间。使用本地磁盘时,你通常需要预留足够的空间以应对数据增长,这意味着你会为未使用的空间付费。而对象存储是按实际使用量计费的,用多少付多少。
降低运维成本。不需要监控磁盘使用率、不需要扩容磁盘、不需要配置备份任务、不需要担心磁盘故障------这些都能节省大量的运维时间和人力成本。
计算与存储分离。在传统架构中,你可能需要为了存储而选择更大的实例类型。而SlateDB让你可以选择更小、更便宜的计算实例,因为存储完全在对象存储上。这种计算与存储分离的架构,让资源配置更加灵活和经济。
数据结构
SlateDB采用了LSM Tree(Log-Structured Merge Tree)结构,这与LevelDB和RocksDB类似,但针对对象存储做了特殊优化。主要包含以下几个核心组件:
Memtable(内存表) 。这是一个内存中的可变数据结构,通常使用跳表(Skip List)实现。所有的写操作首先写入Memtable,提供快速的写入性能。当Memtable达到一定大小(如64MB)时,会被冻结并转换为Immutable Memtable,然后刷写到对象存储。
WAL(Write-Ahead Log)与SST文件 。这是SlateDB设计中最有意思的地方:WAL和SST采用了完全相同的文件格式。两者都是排序的键值对集合,都包含数据块、索引块和布隆过滤器,都存储在对象存储中。它们的区别仅在于服务目标不同:
- WAL的目标是写入和恢复:保证数据持久性,在崩溃后能够恢复未刷写的数据。
- SST的目标是读取:优化查询性能,通过分层组织来减少读放大。
这种统一的格式设计带来了很多好处,我们在写操作部分会详细说明。
完全基于对象存储 。需要特别强调的是,无论是WAL还是SST,都完全存储在对象存储中,本地不保留任何持久化数据。这是SlateDB与传统LSM Tree最大的区别。
顺序存储,字典序排序。与RocksDB不同,SlateDB只允许按照字典序(lexicographic order)排序键值对,不支持自定义排序函数。这个限制简化了实现,也更适合对象存储的顺序访问特性。所有的SST文件内部都是严格按照键的字典序排列的。
前缀压缩优化 。在每个SST块中,SlateDB使用前缀压缩来减少存储空间。由于键是排序的,相邻的键往往有相同的前缀(比如user:1001:name和user:1001:email),SlateDB会存储第一个完整的键,后续的键只存储与前一个键不同的后缀部分。这可以显著减少存储空间和网络传输量。
Manifest文件。Manifest记录了数据库的元数据,包括当前有哪些SST文件、它们属于哪个层级、键范围是什么等信息。这是数据库的"目录",用于快速定位数据。
对象存储布局。SlateDB在对象存储中的文件组织非常清晰:
/wal/目录存储WAL文件/sst/目录存储SST文件/manifest/目录存储Manifest文件
这种结构让数据管理变得透明和可追溯。
写操作
SlateDB的写路径与传统LSM Tree有本质区别,必须是批量写入,这是对象存储的高延迟特性决定的。
批量写入是强制要求
与RocksDB可以高效处理单条写入不同,SlateDB要求用户必须控制好批量写入的大小。如果你每次只写入一个键值对,性能会非常糟糕,因为每次都要等待对象存储的往返延迟(通常几十毫秒)。正确的使用方式是积累一批写入(比如几百到几千条),然后一次性提交。这也是为什么SlateDB更适合"微批"场景。
为什么WAL采用SST格式
这是一个精妙的设计决策。传统的WAL是简单的追加日志,而SlateDB的WAL采用与SST相同的格式,原因在于:
- 批量写入优化:当你提交一批写入时,SlateDB会将这批数据排序后写入WAL。由于数据已经排序,WAL本身就是一个有效的SST文件,可以直接用于查询。
- 减少写放大:如果WAL是无序的,恢复时需要重新排序并写入SST,这会产生额外的写入。而有序的WAL可以直接作为Level 0的SST文件,或者与其他文件合并,减少了一次写入。
- 统一的代码路径:读取WAL和读取SST使用相同的代码,简化了实现。
WAL和SST的服务目标的差异。尽管WAL与SST格式相同,但它们的服务目标不同:
- WAL用于写入和恢复:保证数据持久性。即使进程崩溃,重启后也能从WAL恢复数据到Memtable。WAL文件在数据刷写到SST后就可以删除。
- SST用于读取:优化查询性能。SST文件会被组织成多个层级,通过Compaction来减少读放大。
写入流程
1. 批量积累。应用层积累一批写入操作,可能是几百到几千个键值对。
2. 排序并写入WAL。SlateDB将这批数据按照键的字典序排序,然后以SST格式写入对象存储的WAL目录。由于是批量写入,可以充分利用对象存储的带宽,摊销延迟成本。
3. 写入Memtable。WAL写入成功后,数据被插入到内存中的Memtable。此时写操作就可以返回给调用者了。
4. Memtable刷写。当Memtable大小达到阈值时,它会被标记为Immutable,并在后台异步刷写到对象存储,生成一个新的Level 0 SST文件。同时创建一个新的Memtable接收后续的写入。
写入保证。SlateDB提供了持久性保证:一旦批量写操作返回成功,数据就已经持久化到对象存储中的WAL。即使进程崩溃,重启后也能从WAL恢复数据。
读操作
读操作需要在多个层级中查找数据,SlateDB通过多种优化技术来降低延迟:
1. 查询Memtable。读操作首先在当前的Memtable和Immutable Memtable中查找。由于这些都在内存中,查询非常快。如果找到了数据,直接返回,这是最快的路径。
2. 查询SST文件。如果内存中没有找到,需要查询对象存储中的SST文件。SlateDB会按照从新到旧的顺序(Level 0 → Level 1 → ...)查找。为了减少不必要的读取,SlateDB使用了多种优化:
- 布隆过滤器:每个SST文件都有一个布隆过滤器,可以快速判断某个键是否可能存在于该文件中,避免无效的对象存储读取。
- 索引块:文件包含索引,可以快速定位到包含目标键的数据块,而不需要读取整个文件。
- 本地缓存:SlateDB会在本地缓存热点数据块和索引块,减少对象存储的访问次数。
3. 范围查询优化 。对于范围查询(如scan(start_key, end_key)),SlateDB需要合并多个文件的结果。它使用归并排序的方式,从多个层级中读取数据并按顺序返回。
用户需要增加缓存层
需要特别强调的是,仅靠SlateDB提供的这些优化可能还不够。由于对象存储的延迟通常在几十毫秒级别,对于延迟敏感的应用,你需要根据自己的业务特点,在SlateDB之上增加一层缓存(如Redis或本地内存缓存)。比如:
- 缓存热点数据,减少对SlateDB的查询
- 缓存查询结果,避免重复的范围扫描
- 使用预取策略,提前加载可能需要的数据
这种分层缓存的架构,可以让你在享受对象存储低成本的同时,也能满足业务的延迟要求。
Compaction
Compaction是LSM Tree中的关键操作,用于合并和清理SST文件,SlateDB的Compaction完全在对象存储上进行:
为什么需要Compaction
随着写入的进行,Level 0会积累越来越多的SST文件,这会导致读放大(需要查询更多文件)。同时,删除操作和更新操作会产生过期数据,占用存储空间。Compaction通过合并文件来解决这些问题。
支持多种策略,默认分层Compaction
SlateDB在架构设计上支持多种Compaction策略,但目前默认只实现了分层Compaction(Leveled Compaction)。这种策略的特点是:
- Level 0的文件可能有重叠的键范围
- Level 1及以上的文件键范围不重叠
- 当某一层的文件数量或大小超过阈值时,选择部分文件与下一层的重叠文件合并
分层设置的权衡。配置Compaction的层数时需要注意:
- 分层过少:会导致写放大。因为每次Compaction需要合并的数据量更大,相同的数据会被反复写入多次。
- 分层过多:会导致读放大。因为查询时需要检查更多的层级,即使有布隆过滤器,也会增加延迟。
通常需要根据你的读写比例来调整:写多读少的场景可以增加层数,读多写少的场景可以减少层数。
在对象存储上的Compaction
这是SlateDB的创新之处:
- Compaction任务可以在任何地方运行,不需要访问本地磁盘。你可以启动专门的Compaction worker,甚至使用Serverless函数(如AWS Lambda)来执行Compaction。
- Compaction读取对象存储中的旧SST文件,合并后写入新的SST文件,然后更新Manifest。旧文件可以异步删除。
- 这种设计让Compaction的计算资源与主应用解耦,你可以根据需要独立扩展Compaction能力。
避免性能抖动 。传统的LSM Tree(如RocksDB)中,Compaction操作会与正常的读写操作竞争CPU、内存和磁盘IO,导致性能抖动------有时候写入很快,有时候突然变慢。而SlateDB的独立Compaction进程可以完全避免这个问题。你可以将Compaction任务调度到单独的机器或容器上运行,主应用的性能不会受到影响。这对于需要稳定延迟的应用非常重要。
Compaction调度。SlateDB会根据各层级的文件数量和大小,智能地调度Compaction任务。你也可以配置Compaction的并发度和触发阈值,以平衡写入性能和读取性能。
空间回收。Compaction不仅优化了读性能,还回收了被删除和更新数据占用的空间。由于对象存储按实际使用量计费,及时的Compaction可以直接降低存储成本。
总结
SlateDB代表了一种新的存储引擎设计思路:完全拥抱云原生,将对象存储作为一等公民。它牺牲了一些极致的低延迟,换来了极大的简单性、灵活性和成本优势。
如果你正在构建云原生应用,特别是流处理、事件驱动或工作流编排类的系统,SlateDB值得认真考虑。它让你可以专注于业务逻辑,而不是纠结于存储架构和运维问题。
随着对象存储性能的不断提升(如S3 Express One Zone),以及SlateDB自身的持续优化,这种架构的适用场景会越来越广。或许在不久的将来,基于对象存储的数据库引擎会成为云上应用的标准选择。