- 数据库
- 分布式数据库
- 后端
【免费下载链接】cassandra
Mirror of Apache 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接口的工厂类。工厂负责两件事:
- 提供该格式实现唯一的名称;
- 提供一个创建格式实例的方法。
从源码看,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: value22.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 实现通用。它们包括:
| 单例类型 | 单例组件(文件名) | 含义(据源码注释) |
|---|---|---|
DATA | Data.db | sstable 的基础数据,其余组件可基于它重新生成 |
COMPRESSION_INFO | CompressionInfo.db | 未压缩数据长度、块偏移等压缩元信息 |
STATS | Statistics.db | sstable 内容的统计元数据 |
FILTER | Filter.db | 行键的序列化布隆过滤器 |
DIGEST | Digest.crc32 | 数据文件的 CRC32 校验和 |
CRC | CRC.db | 未压缩文件各块的 CRC32 |
TOC | TOC.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 格式类有两种方式:
- 通过构造函数将格式类作为单例实例化;
- 通过访问类中的静态字段
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 registered4.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实现的构造函数应接受两个参数:
- 格式专属的simple builder;
- 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 存储层可扩展性的关键抽象。要落地一个新格式,核心步骤可以归纳为:
- 工厂:实现
SSTableFormat.Factory,提供唯一小写名称与getInstance(options),并通过 ServiceLoader 注册; - 格式类:继承
AbstractSSTableFormat,实现组件集合、reader/writer 工厂、metrics 提供者、scrubber 工厂等接口方法; - 组件:在
Components.Types中声明格式专属的单例/非单例类型,注册进全局分层类型注册表; - Reader:实现 simple builder(继承
SSTableReader.Builder或SSTableReaderWithFilterBuilder)与 loading builder,按需接入 filter、index summary、key cache 支持接口; - Writer:复用
SortedTableWriter的通用逻辑,实现append与createRowIndexEntry; - Scrubber / Verifier:继承
SortedTableScrubber/SortedTableVerifier骨架; - 配置:在
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
相关推荐
Cassandra SSTable API 深度指南:可插拔 SSTable 格式体系的实现与配置(CEP-17 / CASSANDRA-17056)
Cassandra SSTable API 深度指南:可插拔 SSTable 格式体系的实现与配置(CEP 17 / CASSANDRA 17056) 本指南围
数据库分布式数据库大数据后端NumPy 数组打印格式完全指南:从 set_printoptions 到自定义格式化
NumPy 数组打印格式完全指南:从 set_printoptions 到自定义格式化 导读 NumPy 数组在 REPL、Jupyter Notebook 或
科学计算数据分析从HuggingFace到自定义格式:LLM模型转换完全指南
从HuggingFace到自定义格式:LLM模型转换完全指南 模型格式转换是大语言模型 LLM 开发与部署中的关键环节,尤其对于需要在不同框架间迁移模型的场景。
人工智能大模型强化学习RLHF分布式训练微调
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考