☰
Apache Cassandra SSTable 格式 API 全解析:从组件模型到自定义格式实现
2026/9/25 2:39:23 网站建设 项目流程
  • 数据库
  • 分布式数据库
  • 后端

【免费下载链接】cassandra

Mirror of Apache Cassandra

项目地址:https://gitcode.com/gh_mirrors/cassandr/cassandra
点击查看免费下载

本文以仓库内 SSTable_API.md 为骨架,结合SSTableFormat、BigFormat、BtiFormat、DatabaseDescriptor等源码与 cassandra.yaml 配置,系统讲解 Cassandra 的 SSTable 格式抽象、基于 ServiceLoader 的格式发现与配置机制、组件(Component)模型,以及如何从零实现一个自定义 SSTable 格式(Reader / Writer / Scrubber / Verifier)。读完你将掌握sstable.selected_format配置的含义、格式工厂的注册方式,以及扩展新格式所需的全部接口与约定。

1. SSTable 格式是什么

SSTable(Sorted String Table)是 Cassandra 落盘数据的存储单位。在较新版本的 Cassandra 中,SSTable 不再是一套"写死"的文件布局,而是一个可插拔的抽象:SSTable 格式(SSTable format)是SSTableFormat接口的一个实现,它负责为该格式创建 reader、writer、scrubber、verifier 以及其他处理 sstable 的组件(见 SSTable_API.md)。

该设计源自 CEP-17: SSTable format API(对应 CASSANDRA-17056),目标是让不同的存储格式(例如传统的 big table 格式与新的 Trie 索引 BTI 格式)以统一的方式被读写、清理与校验。

一个格式实现必须附带一个实现SSTableFormat.Factory接口的工厂类。工厂负责两件事:

  1. 提供该格式实现唯一的名称;
  2. 提供一个创建格式实例的方法。

从源码看,SSTableFormat接口(SSTableFormat.java)的完整职责包括:返回格式名称(name())、版本对象(getLatestVersion()/getVersion())、writer/reader 工厂、若干预定义的组件集合(allComponents()、primaryComponents()、batchComponents()、uploadComponents()、mutableComponents()、generatedOnLoadComponents())、key cache 值序列化器、scrubber 工厂、格式专属 metrics 提供者,以及删除/清理 sstable 组件的方法。可以说,一个格式实现定义了该格式 sstable 的"全部生命周期行为"。

2. 格式的发现与配置

2.1 通过 Java ServiceLoader 发现格式

SSTable 格式工厂使用Java Service Loader机制被发现。在 DatabaseDescriptor.java 的applySSTableFormats()中可以看到实际加载逻辑:

ServiceLoader<SSTableFormat.Factory> loader = ServiceLoader.load(SSTableFormat.Factory.class, DatabaseDescriptor.class.getClassLoader()); List<SSTableFormat.Factory> factories = Iterables.toList(loader); if (factories.isEmpty()) factories = ImmutableList.of(new BigFormat.BigFormatFactory()); applySSTableFormats(factories, conf.sstable);

这里有两个关键点:

  • 加载到的所有格式都能用于读取现有 sstable:启动时 Cassandra 会把 ServiceLoader 发现的全部工厂实例化,并注册到全局格式表(sstableFormats)中,供读取任意版本/任意格式的 sstable 使用;
  • 若 ServiceLoader 一个工厂都没找到,则回退到BigFormat:代码中显式兜底为new BigFormat.BigFormatFactory(),这与文档中"未指定时假定为BigFormat实现"的描述一致。

随后applySSTableFormats(factories, sstableFormatsConfig)会对每个工厂调用getInstance(options)并验证配置(失败时抛出ConfigurationException),最后通过getAndValidateWriteFormat(...)确定写入格式(selectedSSTableFormat)。

2.2cassandra.yaml中的格式配置

格式相关配置位于cassandra.yaml的sstable键下(完整注释示例见 conf/cassandra.yaml 中的#sstable:/# selected_format: big)。配置结构如下:

sstable: selected_format: 〈name of the default SSTableFormat implementation〉 format: 〈format1 name〉: param1: 〈format specific parameter 1〉 param2: 〈format specific parameter 2〉 # ... 〈format2 name〉: param1: 〈format specific parameter 1〉 param2: 〈format specific parameter 2〉 # ...
  • selected_format:指定默认(写入)格式实现的名称。如果省略,默认是big(即BigFormat)。
  • format:以格式名为键的嵌套映射,每个键下的参数会被原样传给对应格式的工厂方法Factory.getInstance(Map<String, String> options)。所有参数都是可选的,且含义完全由具体实现决定——即不同格式可以定义各自的参数语义。

对应的 Java 配置类在 Config.java:

public static class SSTableConfig { public String selected_format = BigFormat.NAME; public Map<String, Map<String, String>> format = new HashMap<>(); } public final SSTableConfig sstable = new SSTableConfig();

可以看到默认值就是BigFormat.NAME(即字符串"big"),format默认是空 map。

2.3 两种典型配置示例

与空配置等价的默认配置:

sstable: selected_format: big

以bti作为默认写入格式的示例配置:

sstable: selected_format: bti format: big: param1: value1 param2: value2 bti: param1: value1 param2: value2

2.4 格式名称的约束

每个实现必须有一个唯一名称,用于无歧义地标识格式。该名称必须:

  • 由SSTableFormat和SSTableFormat.Factory实现中的name()方法一致地返回;
  • 只包含小写 ASCII 字母。

工厂接口中对此有明确注释:"Format name must not be empty, must be unique and must consist only of lowercase letters"(见 SSTableFormat.java)。当前仓库中的两个内置实现分别使用"big"(BigFormat.java 的NAME常量)和"bti"(BtiFormat.java 的NAME常量),均满足这一约束。

3. SSTable 组件模型

3.1 组件与组件类型

每个 sstable 由一组组件(component)构成——既有必需组件也有可选组件。一个组件构成一个标识符,用来获得与 sstable descriptor 对应的确切文件。组件按**类型(Type)**分组:

  • 单例类型(singleton):例如stats组件,一个 sstable 最多有一个;
  • 非单例类型(non-singleton):例如secondary index组件,一个 sstable 可以有多个。

通用类型集合定义在SSTableFormat.Components中(见 SSTableFormat.java),被认为对所有 sstable 实现通用。它们包括:

单例类型单例组件(文件名)含义(据源码注释)
DATAData.dbsstable 的基础数据,其余组件可基于它重新生成
COMPRESSION_INFOCompressionInfo.db未压缩数据长度、块偏移等压缩元信息
STATSStatistics.dbsstable 内容的统计元数据
FILTERFilter.db行键的序列化布隆过滤器
DIGESTDigest.crc32数据文件的 CRC32 校验和
CRCCRC.db未压缩文件各块的 CRC32
TOCTOC.txt目录表,列出该 sstable 的全部组件

非单例类型包括SECONDARY_INDEX(文件名模式SI_.*.db,每个 sstable 可有多个)和CUSTOM(自定义组件,例如供自定义压缩策略使用)。

3.2 格式专属组件类型

除通用组件外,每种 sstable 格式还可以描述自己的专属组件类型。例如big table 格式额外定义了PRIMARY_INDEX(Index.db,行键索引及在数据文件中的位置指针)和SUMMARY(Summary.db,Index 组件的抽样,用于内存中的快速近似定位)两个单例类型及其单例组件(见 BigFormat.java):

public static class Types extends SSTableFormat.Components.Types { // index of the row keys with pointers to their positions in the data file public static final Component.Type PRIMARY_INDEX = Component.Type.createSingleton("PRIMARY_INDEX", "Index.db", true, BigFormat.class); // holds SSTable Index Summary (sampling of Index component) public static final Component.Type SUMMARY = Component.Type.createSingleton("SUMMARY", "Summary.db", true, BigFormat.class); }

BTI 格式则定义PARTITION_INDEX(Partitions.db)与ROW_INDEX(Rows.db)两个单例类型(见 BtiFormat.java),这是它与 big 格式在索引结构上的核心差异。

3.3 创建自定义类型与类型注册表

自定义类型可以通过以下方法创建:

  • Component.Type.create(name, repr, streamable, formatClass)
  • Component.Type.createSingleton(name, repr, streamable, formatClass)

每个创建出来的类型都会注册进全局类型注册表。类型注册表是分层的(hierarchical):某个 sstable 格式实现可以使用为它自己的格式类定义的类型,也可以使用所有父格式类定义的类型。例如,为BigFormat类定义的类型集合,扩展了为SSTableFormat接口定义的通用类型集合。

3.4 单例组件与非单例组件

单例组件与单例类型一一对应,通过<type>.getSingleton()方法立即获取:

public static class Components extends AbstractSSTableFormat.Components { public final static Component PRIMARY_INDEX = Types.PRIMARY_INDEX.getSingleton(); public final static Component SUMMARY = Types.SUMMARY.getSingleton(); }

非单例组件则需要显式创建,例如:

Component idx1 = Types.SECONDARY_INDEX.createComponent("SI_idx1.db");

每个格式还要在allComponents()、primaryComponents()、batchComponents()、uploadComponents()、mutableComponents()、generatedOnLoadComponents()中返回预定义的组件集合(见 SSTableFormat.java)。这些集合各有用途,例如:

  • allComponents():writer 能产出、reader 能读取的全部组件;
  • primaryComponents():定位/读取所需的最小主组件集(big 为DATA+PRIMARY_INDEX,BTI 为DATA+PARTITION_INDEX);
  • batchComponents():离线压缩(如拆分 sstable)所需组件;
  • uploadComponents():sstableloader 上传时应选取的组件;
  • mutableComponents():sstable 写入后仍可被修改的组件(big 为STATS、SUMMARY;BTI 仅STATS);
  • generatedOnLoadComponents():加载时可自动生成、因此非强制存在的组件(big 为FILTER、SUMMARY;BTI 为FILTER)。

组件集合的实现细节都集中在对应格式类的Components内部类中(ImmutableSet构造),这正好对应文档中"应将这些集合声明为常量且不可变的集合"的约定。

4. 实现一个新格式:初始化与基类选择

文档强烈建议主格式类继承AbstractSSTableFormat(AbstractSSTableFormat.java),因为它包含了一些不应被重新实现的方法——例如:

  • name()返回构造时传入的格式名;
  • equals()/hashCode()基于名称判定(Objects.equals(name, that.name)),保证同名格式全局唯一可比;
  • toString()输出name:options。

4.1 初始化流程

Cassandra 初始化 sstable 格式类有两种方式:

  1. 通过构造函数将格式类作为单例实例化;
  2. 通过访问类中的静态字段instance获取实例。

作为初始化的一部分,Cassandra 会调用setup方法并提供配置参数。紧接着,Cassandra 调用allComponents()方法,以确认该格式定义的所有组件都已初始化且可用。这一点在 DatabaseDescriptor.java 中有直接印证:

sstableFormats.values().forEach(SSTableFormat::allComponents); // make sure to reach all supported components for a type so that we know all of them are registered

4.2 预定义的组件集合

如前所述,格式要定义若干组件集合,并应将这些集合声明为常量、不可变的集合(使用ImmutableSet.of(...)),以保证组件注册的确定性与线程安全。

5. 实现 Reader

5.1 构造方式:simple builder 与 loading builder

SSTable reader(SSTableReader.java)负责从 sstable 读取数据。它由两种 builder 创建:

  • simple builder(SSTableReader.Builder):只做基本校验、存储 reader 构造函数需要访问的值,不执行任何逻辑;
  • loading builder(SSTableReaderLoadingBuilder):执行更复杂的操作——复杂校验、打开资源、加载缓存、索引、过滤器等,内部会创建一个 simple builder 并最终实例化 reader。

两种 builder 均由reader factory(SSTableFormat.SSTableReaderFactory)提供,接口定义了builder(Descriptor)、loadingBuilder(Descriptor, TableMetadataRef, Set<Component>)、readKeyRange(Descriptor, IPartitioner)与getReaderClass()四个方法。

具体SSTableReader实现的构造函数应接受两个参数:

  1. 格式专属的simple builder;
  2. sstable owner(通常是ColumnFamilyStore实例,也可以是null)。

构造函数应当简单——只把 builder 中的值赋给内部字段,不做其他事情。从 SSTableReader.java 的基类构造看,它接收Builder<?, ?>并依次取出statsMetadata、serializationHeader、dataFile、maxDataAge、openReason、first、last等字段赋值——这正是"构造即赋值"约定的体现。

新 reader 实现应包含一个public static 的 simple builder 内部类,继承SSTableReader.Builder泛型 reader builder(或SSTableReaderWithFilter.Builder,见下文"Filter")。

加载入口方面,SSTableReader.open(...)(SSTableReader.java)展示了二者的协作:

public static SSTableReader open(Owner owner, Descriptor descriptor, Set<Component> components, TableMetadataRef metadata, boolean validate, boolean isOffline) { SSTableReaderLoadingBuilder<?, ?> builder = descriptor.getFormat().getReaderFactory().loadingBuilder(descriptor, metadata, components); return builder.build(owner, validate, !isOffline); }

即:从 descriptor 拿到对应格式,经 reader factory 创建 loading builder,再由 loading builder 完成build()。

5.2 通用注意事项

  • 如果 builder 携带了一些可关闭资源给 reader,这些资源应通过setupInstance方法返回;
  • 需要实现一些cloneXXX方法时,务必在传给runWithLock()方法的 lambda 中创建 reader 克隆——runWithLock的注释明确指出这是为了避免与 index summary 重分配竞争(CASSANDRA-15861,见 SSTableReader.java)。

5.3 Unbuilding:unbuildTo方法

实现unbuildTo方法很方便:它接收一个simple builder并初始化它,使该 builder 能产出同一个 reader。方法还接收sharedCopy布尔参数,表示引用可关闭资源的字段是直接拷贝给 builder,还是以(共享)拷贝形式传递。约定的细节还包括:

  • 仅在 builder 中对应字段未设置(为null)时才拷贝资源;
  • 方法第一步应调用super.unbuildTo,使父类管理的字段先被拷贝,实际实现里只需赋值本格式专属的字段。

big table 格式 reader 的实现示例(文档原文):

protected final Builder unbuildTo(Builder builder, boolean sharedCopy) { Builder b = super.unbuildTo(builder, sharedCopy); if (builder.getIndexFile() == null) b.setIndexFile(sharedCopy ? sharedCopyOrNull(ifile) : ifile); if (builder.getIndexSummary() == null) b.setIndexSummary(sharedCopy ? sharedCopyOrNull(indexSummary) : indexSummary); b.setKeyCache(keyCache); return b; }

基类 SSTableReader.java 的unbuildTo同样遵循"资源仅当 builder 中为 null 才覆盖"的规则(if (builder.getDataFile() == null) b.setDataFile(...)),并顺带拷贝statsMetadata、serializationHeader、maxDataAge、openReason、first、last等字段。

5.4 Filter:继承SSTableReaderWithFilter

如果 sstable 包含filter,reader 类应继承抽象类SSTableReaderWithFilter,其 simple builder 则应继承SSTableReaderWithFilter.SSTableReaderWithFilterBuilder。

SSTableReaderWithFilter为扩展实现提供了isPresentInFilter方法,还实现了系统依赖的其它 filter 专属方法。注意:

  • 若 reader 继承SSTableReaderWithFilter,必须把FILTER组件包含进相应的组件集合;
  • reader with filter 实现自带额外的 metrics。

5.5 Index summary:实现IndexSummarySupport

部分格式(如 big table 格式)会使用index summaries。如果 reader 使用 index summaries,应实现IndexSummarySupport接口。

index summaries 的支持同样带来额外 metrics。在 big 格式的读取路径中,index summary 承担"先粗定位再精确查找"的角色:查主键时先经SUMMARY组件(IndexSummary抽样)获得在索引文件中的大致位置,再进PRIMARY_INDEX(RowIndexEntry)精确定位数据文件偏移(见 BigFormat.java 的组件生命周期注释)。

5.6 Key cache:实现KeyCacheSupport

如果格式实现使用行键缓存,应实现KeyCacheSupport接口。具体来说:

  • 存储一个KeyCache实例,并通过getKeyCache()返回;
  • 该接口为系统依赖的若干方法提供了默认实现;
  • 接口自带额外 metrics。

有趣的是,key cache 并非所有格式的必需品:BtiFormat的getKeyCacheValueSerializer()直接抛出AssertionError("BTI sstables do not use key cache")(见 BtiFormat.java),说明 Trie 索引格式的设计意图是让索引足够紧凑高效,从而不需要额外的行键缓存层。

5.7 格式专属 metrics

自定义格式可以在表、keyspace 和全局三个层级提供额外指标,这些指标可通过 JMX 访问。SSTableFormat实现通过getFormatSpecificMetricsProviders方法暴露这些指标,该方法应返回一个实现MetricsProviders接口的单例对象。目前仅支持自定义 gauge,但接口可随时扩展。

每个自定义指标(gauge)都是GaugeProvider抽象类的实现。虽然该类要求实现为每个聚合层级都提供 gauge,但有一个辅助类SimpleGaugeProvider可以用一个提供的归约(reduction)lambda 自动完成。此外还有AbstractMetricsProviders,它是MetricsProviders接口的部分实现,在提供的方法中借助SimpleGaugeProvider。

示例——为支持 index summaries 的 sstable 添加指标(完整示例见 IndexSummaryMetrics.java):

private final GaugeProvider<Long> indexSummaryOffHeapMemoryUsed = newGaugeProvider("IndexSummaryOffHeapMemoryUsed", 0L, r -> r.getIndexSummary().getOffHeapSize(), Long::sum);

big 格式的BigTableSpecificMetricsProviders(BigFormat.java)就是把BloomFilterMetrics、IndexSummaryMetrics、KeyCacheMetrics三者的 gauge 提供者拼接起来;BTI 格式则只汇聚BloomFilterMetrics(BtiFormat.java)。

6. 实现 Writer

6.1 构造方式

SSTable writer(SSTableWriter.java)负责把数据写入 sstable 文件。它由builder(SSTableWriter.Builder)创建,builder 由writer factory(SSTableFormat.SSTableWriterFactory)提供。writer factory 接口要求:

  • builder(Descriptor):返回可创建SSTableWriter实例的 builder;与 loading builder 类似,应在build(...)调用时打开所需资源,不允许调用方通过 setter 直接传入可关闭资源;若构建失败,所有已打开资源都应被释放;
  • estimateSize(SSTableWriter.SSTableSizeParameters):根据参数估算所有 sstable 文件的总大小。

两种内置格式的估算策略不同(可从源码对比看出):big 格式按"两倍分区键大小(索引项 + 数据文件中的键)+ 数据大小"再乘 1.2 估算(BigFormat.java),BTI 格式则用"分区数 × 8 字节(索引项)+ 分区键大小 + 数据大小"再乘 1.2(BtiFormat.java),反映出两种索引结构的空间开销差异。

6.2 SortedTableWriter:通用默认实现

writer 需要实现的方法不多,最值得注意的是append——它负责把给定的分区写入磁盘。不过,有一个通用的默认实现SortedTableWriter已经处理了大量公共工作:

  • 使用默认序列化器写入数据文件;
  • 通用支持分区索引;
  • 通知(notifications);
  • 元数据收集;
  • 构建 filter。

writer 在添加数据时会触发细粒度事件,子类可以覆写这些方法以施加特定行为,例如onPartitionStart、onRow、onStaticRow等。最终它会调用一个抽象方法createRowIndexEntry,由子类实现(不同格式在此处生成各自的索引项,例如 big 的RowIndexEntry与 BTI 的TrieIndexEntry)。

7. 实现 Scrubber 与 Verifier

自定义 sstable 格式还应自带自己的 verifier 与 scrubber,分别实现IVerifier与IScrubber接口。一个通用的部分实现由以下类提供:

  • SortedTableVerifier:基于有序表语义的通用校验器骨架;
  • SortedTableScrubber:基于有序表语义的通用清洗器骨架,同时提供deleteOrphanedComponents静态工具用于清理孤儿组件。

格式类通过getScrubber(ColumnFamilyStore cfs, LifecycleTransaction transaction, OutputHandler outputHandler, IScrubber.Options options)提供 scrubber 实例。big 与 BTI 格式的实现分别返回BigTableScrubber与BtiTableScrubber,且都会先断言事务中 sstable 的元数据与当前 CFS 元数据一致(Preconditions.checkArgument(cfs.metadata().equals(transaction.onlyOne().metadata()), ...)),以保证清洗过程不会破坏 schema 不匹配的数据。

8. 仓库中的两种内置格式

8.1 BigFormat(big)

传统 big table 格式,对应文件 BigFormat.java。其组件与生命周期在类注释中有完整描述:查主键时先经SUMMARY粗定位,再经PRIMARY_INDEX精确定位DATA中的位置;STATS记录最小时间戳等统计以支持 vint 编码 TTL 与 markForDeleteAt;COMPRESSION_INFO保存压缩元数据;DIGEST/CRC提供校验;FILTER是布隆过滤器;TOC列出全部组件。

版本演进(BigVersion源码注释)从 3.0 系列一路到 5.0:

  • ma(3.0.0):交换布隆过滤器哈希顺序、原生存储行;
  • mb/mc:引入 commit log lower bound / intervals;
  • md/me:修正 min/max clustering、加入源节点 hostId;
  • na(4.0-rc1):未压缩块、pending repair session、isTransient、带校验和的元数据文件、新布隆过滤器格式;
  • nb(4.0.0):originating host id;
  • oa(5.0):改进的 min/max、分区级删除存在标记、key range(CASSANDRA-18134)、无符号 deletionTime 防 TTL 溢出、token space coverage。

当前版本由存储兼容模式决定(DatabaseDescriptor.getStorageCompatibilityMode().isBefore(5) ? "nb" : "oa")。

8.2 BtiFormat(bti)

"Big Trie-Indexed" 格式(BtiFormat.java),随 CEP-25: Trie-indexed SSTable format。BTI 当前版本为da(5.0 初始版本),且该格式不依赖 key cache。

9. 总结

SSTable 格式 API 是 Cassandra 存储层可扩展性的关键抽象。要落地一个新格式,核心步骤可以归纳为:

  1. 工厂:实现SSTableFormat.Factory,提供唯一小写名称与getInstance(options),并通过 ServiceLoader 注册;
  2. 格式类:继承AbstractSSTableFormat,实现组件集合、reader/writer 工厂、metrics 提供者、scrubber 工厂等接口方法;
  3. 组件:在Components.Types中声明格式专属的单例/非单例类型,注册进全局分层类型注册表;
  4. Reader:实现 simple builder(继承SSTableReader.Builder或SSTableReaderWithFilterBuilder)与 loading builder,按需接入 filter、index summary、key cache 支持接口;
  5. Writer:复用SortedTableWriter的通用逻辑,实现append与createRowIndexEntry;
  6. Scrubber / Verifier:继承SortedTableScrubber/SortedTableVerifier骨架;
  7. 配置:在cassandra.yaml的sstable.selected_format中选择默认写入格式,并在sstable.format.<name>下提供格式专属参数。

无论是评估现有格式(big/bti)的行为,还是为特定工作负载定制存储布局,SSTable_API.md 与本文梳理的源码路径(SSTableFormat.java、SSTableReader.java、SortedTableWriter.java、DatabaseDescriptor.java)都值得作为第一手参考资料。

  • 数据库
  • 分布式数据库
  • 后端

【免费下载链接】cassandra

Mirror of Apache Cassandra

项目地址:https://gitcode.com/gh_mirrors/cassandr/cassandra
点击查看免费下载

相关推荐

上一篇:Ethereum 模拟测试环境 Ganache CLI 使用教程
下一篇:【免费下载】 WhoDB 安装与配置指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询