
ScyllaDB SSTable 2.x 文件格式深度解析数据文件、压缩、摘要与语义解释【免费下载链接】scylladbNoSQL data store using the Seastar framework, compatible with Apache Cassandra and Amazon DynamoDB项目地址: https://gitcode.com/GitHub_Trending/sc/scylladbSSTableSorted Strings Table是 ScyllaDB 与 Apache Cassandra 共用的持久化文件格式以有序、不可变的磁盘文件集合保存数据。本文以 ScyllaDB 官方架构文档中 SSTable 2.x 章节为骨架完整讲解 2.x 数据文件的字节级格式、分块压缩原理、Summary 摘要文件结构并结合 ScyllaDB 源码sstables/目录佐证实现细节帮助你理解 SSTable 如何在磁盘上布局、如何被随机访问以及行、聚类键、静态列、集合、TTL 与墓碑等高级概念如何在其中编码。SSTable 是什么SSTable 是 ScyllaDB 与 Apache Cassandra 使用的持久化文件格式保存为磁盘上一组持久、有序、不可变的文件。不可变意味着 SSTable 一旦生成就永不被修改它由 MemTable 刷新flush创建由压缩compaction删除。ScyllaDB SSTable 的位置由scylla.yaml中的data_file_directories参数指定默认位置为/var/lib/scylla/data。作为对比SSTable 3.x 比 2.x 更高效、占用更少磁盘空间。2.x 章节主要面向理解历史格式与兼容性3.x 章节docs/architecture/sstable/sstable3/index.rst介绍其更紧凑的布局。本节内容源自 docs/architecture/sstable/_common/sstable_what_is.rst。SSTable 版本支持ScyllaDB 对不同 SSTable 版本的支持情况如下表SSTable 版本ScyllaDB 版本3.xmt2026.2 及以上3.xms2025.4 及以上3.xme2022.2 及以上3.xmd2021.1要点说明当前支持的写入格式为me和mt。md格式仅在从使用md的既有集群升级时使用若将sstable_format参数设置为md该参数会被忽略。ms格式已被mt取代仅在从使用ms的既有集群升级时使用若将sstable_format设置为ms实际写入的将是mt文件。重要sstable_format参数指定的是写入时使用的 SSTable 格式。而旧版格式ka、la、mc仍然支持读取这对于从既有备份恢复集群至关重要。上述版本枚举在源码中有直接对应sstables/version.hh中定义了enum class sstable_version_types { ka, la, mc, md, me, ms, mt }与enum class sstable_format_types { big }其中ka、la、mc属于 legacy 版本列表md、me、ms、mt属于当前版本列表从源码结构可以印证旧格式只读、新格式可写的版本策略。ms 格式基于 Trie 的 SSTable 索引ms格式引入了基于trie前缀树的 SSTable 索引。与传统的两级索引Index Summary相比trie 索引可以更紧凑地表示 key 前缀并支持更高效的前缀查询。其详细格式参见 docs/architecture/sstable/sstable3/sstable-ms-index.rst。ScyllaDB 中的 SSTable 格式与 Cassandra 2.1.8 的兼容性ScyllaDB 支持与 Apache Cassandra 2.1.8 相同的 SSTable 格式这意味着你可以直接把 Cassandra 数据目录中的 SSTable 放入 ScyllaDB 数据目录它可以直接工作见 docs/architecture/sstable/sstable2/sstable-format.rst。不过细看会发现ScyllaDB 维护的 SSTable数量更多、单个更小在 ScyllaDB 中每个 CPU 核shard管理自己的一份 SSTable 子集。这种内部 sharding 使每个核可以更高效地独立工作避免了多个核竞争同一份数据带来的复杂性与延迟。SSTable 数据文件格式数据文件包含数据库中存储的实际数据本质上是一长串行row每行记录其 key 与各列column。本节内容源自 docs/architecture/sstable/sstable2/sstable-data-file.rst。数据文件本身无法高效地按 key 查找某一行为此存在SSTable Index File与Summary File此外还有Bloom Filter File用于快速判断某个 key 是否存在于该 SSTable一张 Cassandra 表会增量地写入多个 SSTable。数据文件可按 SSTable 压缩 一节所述进行压缩。压缩层对未压缩数据提供类似普通文件的随机访问因此在讨论格式时可以假定数据文件未压缩。数据文件整体结构数据文件不过是一长串行的序列struct data_file { struct row[]; };代码通常借助索引文件直接跳到目标 key 所在行的位置因此需要一个能高效读取整行的 API同时也可能需要例如 compaction 场景一个能高效连续迭代各行、避免重复读取相同磁盘块的 API。行的结构参考实现SSTableIdentityIterator.java构造函数、DeletionTime.java。每行以一个头部开始头部包含行的key、行是否以及何时被删除的信息随后是一系列cells列名与值或其他类型的atoms见下文。一行中最后一个 atom 是特殊的行结束标记。struct row { be16 key_length; char key[key_length]; struct deletion_time deletion_time; struct atom atoms[]; };注意行的定义不包含自身长度——读取器增量读取行直到遇到行结束 atom。若想先把整行读入内存再解析则可以利用 Index File 推算长度如果是从某个索引项到达该行那么下一个索引项会指向本行结束后的下一个字节。如果通过索引到达某行可能已经确认 key 正确此时可以直接跳过行首的 key 而不必反序列化它。deletion_time结构决定这是否是一个行墓碑row tombstone——即整行是否被删除以及删除时间struct deletion_time { be32 local_deletion_time; be64 marked_for_delete_at; };特殊值LIVE (MAX_BE32, MIN_BE64)即字节序列7F FF FF 80 00 00 00 00 00 00 00是存活未删除行的默认值。marked_for_delete_at是数据应被视为已删除的时间戳通常为自 UNIX 纪元起的微秒数若为MIN_BE64则表示从未标记删除。local_deletion_time是创建该墓碑时的本地服务器时间戳自 UNIX 纪元起的秒仅用于在gc_grace_seconds过去后清除墓碑。Atomscell、行结束标记及其他参考实现OnDiskAtom.java::deserializeFromSSTable()、ColumnSerializer::deserializeColumnBody()、RangeTombstone::deserializeBody()。一行的值是一个atoms列表其中每个 atom 通常是一个 cell列名加值或行结束 atom但也可能是下文介绍的其他类型。任何类型的 atom 都以列名开头——一个带 16 位长度的字节串。如果名字长度为 0即 atom 以两个 null 字节开头这就是行结束 atom因为其他 atom 类型的名字总是非空的。注意列名在每一行中都会重复出现。压缩层消除了大量磁盘空间浪费但解析这种冗长编码的开销仍然存在。struct atom { be16 column_name_length; char column_name[column_name_length]; }如果 atom 名字非空则它不是行结束标记列名之后紧跟一个单字节maskenum mask { DELETION_MASK 0x01, EXPIRATION_MASK 0x02, COUNTER_MASK 0x04, COUNTER_UPDATE_MASK 0x08, RANGE_TOMBSTONE_MASK 0x10, };struct nonempty_atom : atom { char mask; }mask 决定 atom 的类型若mask (RANGE_TOMBSTONE_MASK | COUNTER_MASK | EXPIRATION_MASK) 0则是普通 cell带 64 位时间戳用于判断同一 cell 哪个值最新与一个以 32 位长度前缀序列化的值struct cell_atom : nonempty_atom { be64 timestamp; be32 value_length; char value[value_length]; };注意COUNTER_UPDATE_MASK与DELETION_MASK也可能在 cell_atom 上置位从而修改其含义。若mask RANGE_TOMBSTONE_MASK则是范围墓碑 atomstruct range_tombstone_atom : nonempty_atom { u16 last_column_length; char last_column_name[last_column_length]; struct deletion_time dt; };该 atom 影响的不仅是column_name这一列而是column_name到last_column_name之间的范围范围按底层列名比较器定义。若mask COUNTER_MASK则是计数器 cellstruct counter_cell_atom : nonempty_atom { be64 timestamp_of_last_delete; be64 timestamp; be32 value_length; char value[value_length]; };若mask EXPIRATION_MASK则是带过期时间的 cellstruct expiring_cell_atom : nonempty_atom { be32 ttl; be32 expiration; be64 timestamp; be32 value_length; char value[value_length]; };注意同一 atom 上不能同时置位多个RANGE_TOMBSTONE_MASK、COUNTER_MASK或EXPIRATION_MASK。名字与值的序列化参考实现Composite.java、CompositeType.java。需要记住上文所述的列名和值都以字节串形式存储前面带 16 位或 32 位长度。但在 Cassandra 中名字和值可能有各种类型由 CQL schema 决定这些类型在作为 atom 的一部分写入磁盘前需要先序列化为字节串。这对数据文件中列名的编码产生了显著影响。从 Apache Cassandra 1.2 起除非表以 WITH compact storage 创建列名总是composite复合的即一组组件的序列。复合列名序列化如下struct serialized_composite_name { struct { be16 component_length; char[] component; // length component_length char end_of_component; // 通常为 0可为 -1 (0xff) 或 1 (0x01) } component[]; }end_of_component通常为 0但也可以是 -1 或 1用于表示范围而非具体列详见Composite.java与CompositeType.java的注释。因此出现一个惊人的结果即使是单组件列名也会产生浪费的双重序列化除非表是 WITH compact storage。例如列名 age只有一个组件的复合名先序列化为\0 \3 a g e \0再把这个序列化串作为列名、前面加上其长度 6 写入\0 \6 \0 \3 a g e \0。这意味着读取 SSTable 时必须知道列名是否是复合的——因此 SSTable 读取器必须知道该表是否声明了 WITH compact storage。CQL Row Marker在某些情况下即通过 CQL 创建、且未使用 WITH compact storage 的表每一行会包含一个奇怪的额外 cell称为CQL Row Marker。Cassandra 开发者添加它是为了让行在所有列都被删除后依然存在。了解这个额外 cell 的存在是有价值的因为它的存在可能让不了解的人感到意外。CQL Row Marker 是行中一个普通的 cell但具有空复合名和空值。注意它的列名并非空空名是行结束标记而是一个含单个空字符串组件的复合名。如上所述这样的复合名序列化为\0 \0 \0——前两个 null 字节是空组件的长度末尾还有一个序列化时附加的 null。这三个 null 字节就作为该 cell 的列名。SSTable 压缩数据文件的分块压缩SSTable 压缩允许对数据文件SSTable 中最大的部分其他部分如索引无法压缩进行可选压缩。本节内容源自 docs/architecture/sstable/sstable2/sstable-compression.rst。因为对数据文件的随机访问很重要Cassandra 实现了chunked分块压缩未压缩文件被划分为固定大小的块通常 64 KB每个块单独压缩后写入压缩数据文件块后紧跟该压缩块的 4 字节校验和。由于各压缩块长度不同必须记录它们的偏移量才能定位到包含目标未压缩数据的任意块。这个偏移量列表存储在一个单独的Compression Info File中如下所述。在 ScyllaDB 源码中压缩参数与元数据处理位于 sstables/compress.cc 与 sstables/compress.hhcompression_parameters::CHUNK_LENGTH_KB chunk_length_in_kb对应压缩块大小参数compress.hh头部注释明确说明 crc_check_chance默认 1.0决定读取压缩块时校验校验和的概率compress.cc中还通过cm-options.elements.push_back({{crc_check_chance}, {1.0}})在未显式配置时写入默认值 1.0印证了文档中默认 1.0的说法。Compression Info File压缩信息文件参考实现CompressedRandomAccessReader.java、CompressionMetadata.java、CompressionParameters.java。Compression Info File 仅在数据文件被压缩时存在。它指定了解压器所需的压缩参数如压缩算法与块大小以及压缩文件中各压缩块的偏移量列表struct short_string { be16 length; char value[length]; };struct option { short_string key; short_string value; };struct compression_info { short_string compressor_name; be32 options_count; option options[options_count]; be32 chunk_length; be64 data_length; be32 chunk_count; be64 chunk_offsets[chunk_count]; };compressor_name可以是 Cassandra 支持的三类压缩器字符串之一LZ4Compressor、SnappyCompressor或DeflateCompressor。Cassandra 默认使用LZ4Compressor用户可在三者中任选。在 ScyllaDB 源码 sstables/compressor.cc 中可以看到同样的算法到名字的映射case algorithm::lz4: return LZ4Compressor;、case algorithm::deflate: return DeflateCompressor;、case algorithm::snappy: return SnappyCompressor;此外源码注释还提到为兼容性保留了 Cassandra 的完整类名org.apache.cassandra.io.compress.LZ4Compressor并说明 Cassandra 的 LZ4 压缩器会在块前附加其未压缩长度见compressor.cc中相关注释。options可能包含解压器需要的附加选项但通常没有选项如果存在通常只有一项crc_check_chance其值为浮点字符串默认若未出现为1.0决定读取某个压缩块时校验其校验和的概率。chunk_length是原始未压缩数据被划分的块长度。解压器需要知道这个块大小给定未压缩文件中的目标字节偏移就能确定需要解压哪个块。块长度默认为 65536 字节但可以是任意 2 的幂。data_length是未压缩数据的总长度。chunk_offsets是压缩文件内各压缩块的偏移量列表。要读取未压缩文件中位置p的数据p / chunk_length即未压缩块编号该块对应的压缩版本从chunk_offsets[p / chunk_length]开始。压缩块在下一个块开始位置前 4 个字节处结束因为如前所述每个压缩块后跟 4 字节校验和。压缩数据文件如前所述压缩数据文件是一系列压缩块的序列每个块是固定大小来自 Compression Info File 的chunk_length未压缩块的压缩版本。每个压缩块后紧跟其压缩后数据的 be32大端 4 字节整数Adler32 校验和可用于验证数据未被损坏。SSTable 解释从磁盘字节到 mutation_partitionSSTable 数据文件包含行数据。本节讨论如何在 ScyllaDB 上下文中解释 SSTable Data File 中描述的各种字段并将其转换为 ScyllaDB 的原生数据结构mutation_partition。本节内容源自 docs/architecture/sstable/sstable2/sstable-interpretation.rst。SSTable 行其实是变异mutationSSTable 中的每一行并不一定是一个完整的数据行。准确地说它只是一个mutation——一组被修改添加或删除的列及其新值删除的列对应 tombstone每个修改带一个时间戳用于调和冲突的 mutations。请求所需的完整数据行可能由多个 SSTable 中的数据组合而成。如后文聚类列部分所述从 SSTable 某行读出的内容最恰当的称呼不是row而是partition。因此 ScyllaDB 内部将读自 SSTable 的行表示为class mutation_partition。列名从完整名字到列 ID如 Data File 文档所述SSTable 行mutation partition是一组cells列值每个 cell 前是完整列名。这在 Cassandra 设计支持多列、任意列时是合理的但 ScyllaDB 更面向 CQL 用例——schema 是已知的。因此 ScyllaDB 的行并不存储完整列名而是存储指向 schema 已知列列表的数字 ID。从 SSTable 读到列名形式的 ID 后需要查 schema 把 ID 翻译回名字。复合列名但 SSTable cell 中提到的列名通常不是 schema 中的字段名还需要进一步处理。第一个问题是从 Apache Cassandra 1.2 起且除非使用WITH COMPACT STORAGE列名不是普通字符串而是composite名——一个简短的组件列表其磁盘编码见 Data File 文档为便于说明下文用(part1:part2:...)表示复合名。在没有聚类键时最简cell 名只有一个组件。例如CREATE TABLE harels ( name text, age int, PRIMARY KEY (name) ); INSERT INTO harels (name, age) VALUES (nadav, 40);该表有一行 key 为 nadav行内有一个 cell列名 age 编码为单组件复合串(age)磁盘上为\003 a g e \0。这唯一组件 age 可在表的 schema 中查到并如前所述转换为mutation_partition中的列 ID。CQL Row Marker如 Data File 文档所述Cassandra 总会COMPACT 表及其他少数例外除外在每行中添加一个名为空、值为空的伪 cell以解决唯一列被删除后如何找到该行等问题。可参见UpdateStatement.java中的注释以及 CASSANDRA-4361。例如用tools/bin/sstable2json检查上例的表会看到{key: nadav, cells: [[,,1426688662900463], [age,40,1426688662900463]]}第一个名字为空、值也为空第二个 的 cell 就是 CQL Row Marker。与往常一样sstable2json 显示的空名 实际不是空字符串而是含一个空组件的复合名()磁盘上序列化为\000 \000 \000。ScyllaDB 希望直接忽略这些 CQL Row Marker cell不在内部格式中重复它们只需另辟蹊径让只有 key、没有数据列的空行得以存在从而绕开 CASSANDRA-4361 与UpdateStatement.java注释中提到的问题。聚类键Clustering Keys当表有聚类键时SSTable 中的列名不再只有单一组件USE try1; CREATE TABLE harels2 ( name text, nick text, age int, PRIMARY KEY (name, nick) ) WITH compression {}; INSERT INTO harels2 (name, nick, age) VALUES (nadav, nyh, 40);注意 name 和 nick 共同构成主键但 CQL 语法规定分区键是 name聚类键是 nick。这意味着 name 相同nick 不同的表条目会出现在同一个分区中即同一个 SSTable 行内。在该分区内可以出现不同的 nick各自带自己的 age。用sstable2json查看{key: nadav, cells: [[nyh:,,1427032626839065], [nyh:age,40,1427032626839065]]}也就是说复合列名 (nyh:age) 用于存储 nick 为 nyh 的 age其他 nick 的 age 会用不同的列存储。注意在 (nyh:age) 中nyh 并不是 CQL 列名之一而是聚类列 nick 的值只有最后一个组件 age 才是 CQL schema 中的真实字段名。在 ScyllaDB 术语中这个单一partitionkey 为 namenadav包含多个rows每个 row 有不同的聚类键值nick。每个 row 照常有若干列列名是 CQL schema 中的字段如前所述以列 ID 而非名字保存。因此把 SSTable 行转换为mutation_partition时需要查 schema 找聚类键。若 nick 是聚类键就不应像往常一样寻找名为 (nick) 的 cell相反预期每个cell 名都有 ≥2 个组件第一个组件是 nick 的值第二个组件才是真实列名。在mutation_partition对象中需要插入多个row对象每个 row 对应第一个组件的一个值。那个 key 为(nyh:)第二组件为空、值为空的奇怪 cell就是前述 CQL Row Marker它为每个row分区键与聚类键的组合各出现一次。静态列Static Columns静态列是同一分区内所有 row 共享的特殊列。如上文所见每个分区多 row 的情形发生在存在聚类列时。Datastax 在 Cassandra 2.0.6 引入静态列时使用了如下示例CREATE TABLE bills ( user text, balance int static, expense_id int, amount int, PRIMARY KEY (user, expense_id) );这是一张账单表用户需支付的金额。按 PRIMARY KEY 行分区键是 userexpense_id 是聚类键这意味着每个用户有一个分区SSTable 行每个分区内可有多个费用rows各有不同的聚类键 expense_id 与对应 amount。而 balance 列属于同一用户的所有费用。插入一条 user1 的费用并设置 user1 的 balanceINSERT INTO bills (user, balance) VALUES (user1, 17); INSERT INTO bills (user, expense_id, amount) VALUES (user1, 1, 8);写入 SSTable 的内容如下同样来自sstable2json输出{key: user1, cells: [[:balance,17,1428849747953348], [1:,,1428849747970947], [1:amount,8,1428849747970947]]}(1:amount) 与 (1:) 就是上文见过的形式新的内容是 (:balance)——一个静态列。所以 SSTable 中的静态列通过复合 cell 名的空首组件特殊标记。需要验证每个这样的 cell 确实对应表 schema 中的已知静态列并将所有静态列收集到 ScyllaDBmutation_partition中存储的单独一行_static_row里。文档附注CompositeType.java的注释说明静态列的第一组件实际上并非大小为 0 的空组件而是伪造大小STATIC_MARKER 0xFFFF (65536)需进一步验证。复合聚类键Compound Clustering Key当聚类键是复合的由多个列组成时SSTable 列名会包含两个以上组件。例如USE try1; CREATE TABLE bills3 ( user text, expense_id int, year int, amount int, PRIMARY KEY (user, year, expense_id) ); INSERT INTO bills3 (user, year, expense_id, amount) VALUES (user1, 2015, 1, 8);照例PRIMARY KEY 中第一个列名 user 是分区键另外两个 year 与 expense_id 都是聚类列构成复合聚类键。即每个分区包含若干行每行由 (year, expense_id) 二元组定义并排序。得到的 SSTable 行是{key: user1, cells: [[2015:1:,,1428853746711253], [2015:1:amount,8,1428853746711253]]}注意列名 amount 现在以两个组件为前缀即两个聚类列的值。当然schema 可以有任意数量的聚类列相应地 SSTable 列名中就会出现同样数量的前缀组件。与之前一样除最后一个组件外的所有组件都预期是分区内各 row 的聚类键值只有最后一个组件是需要在 schema 中查找的列名。不过更稳妥的做法是查阅 schema 中聚类列的数量而不是猜组件数减一这既是良好的健全性检查在涉及集合见下时也是必要的。复合分区键Compound Partition Key如果 schema 声明分区键是复合的从 SSTable 读出的行 key 也可以是复合的。例如CREATE TABLE bills2 ( user text, expense_id int, amount int, PRIMARY KEY ((user, expense_id)) ); INSERT INTO bills (user, expense_id, amount) VALUES (user1, 1, 8);注意 PRIMARY KEY 中多了一对括号表示 expense_id 属于分区键而不是聚类键。此时每个 SSTable 行的 key 是二元组 (user, expense_id)——一个含两个组件的复合键。集合Collections集合在 SSTable 中的编码更复杂。考虑一个包含set集合列的表CREATE TABLE col2 ( user text, favorites settext, PRIMARY KEY (user) ); INSERT INTO col2 (user, favorites) VALUES (user1, {raindrops, kittens});得到的 SSTable 行是{key: user1, cells: [[,,1428855312063525], [favorites:_,favorites:!,1428855312063524,t,1428855312], [favorites:6b697474656e73,,1428855312063525], [favorites:7261696e64726f7073,,1428855312063525]]}这里列名同样有两个组件但需要知道这不是聚类键的情形favorites 不是聚类列的值而是集合。需要查 schema 来区分这两种情况本例中没有聚类列所以任何组件都不需要按聚类列处理因此看到两个组件时必然是集合。在 set 中集合的每个元素是一个 cellcell 名的第二组件是序列化后的元素值。例如 kittens 被 sstable2json 以十六进制显示为6b697474656e73——在真实 SSTable 中并不是十六进制文本而是字符串长度 实际字节。对set而言每个 cell 的值是空的其他集合类型不为空见下。上面 sstable2json 输出开头那个奇怪的 cellfavorites:_不是普通 cell——这是 sstable 打印的range tombstone其范围从 favorites: 的开头到 favorites: 的结尾markedForDeleteAt为 1428855312063524localDeletionTime为 1428855312。这个范围墓碑存在的必要性如下因为集合的每个元素是独立 cell当设置整个集合如本例的 INSERT时意图是删除集合中任何旧元素并加入新元素——范围墓碑负责删除所有旧元素。真实 SSTable 中并没有 sstable2json 打印的 _ 或 ! 字符。它实际有的是\00\09favorites\ff与\00\09favorites\01。即这两个列名都只有一个组件但结尾不是通常的结束组件字节\00第一个以\ffSTART结尾第二个以\01END结尾。这意味着范围墓碑横跨第一个列到最后一个列之间的所有内容正是所期望的效果。第二种集合类型map与set类似只是值不为空而是 map 中想要的值。例如CREATE TABLE col4 ( user text, favorites maptext,int, PRIMARY KEY (user) ) WITH compression {}; INSERT INTO col4 (user, favorites) VALUES (user1, {raindrops : 1, kittens : 2});用 sstable2json 查看 SSTable{key: user1, cells: [[,,1428864848550739], [favorites:_,favorites:!,1428864848550738,t,1428864848], [favorites:6b697474656e73,00000002,1428864848550739], [favorites:7261696e64726f7073,00000001,1428864848550739]]}即与 set 的表示完全相同只是 cell 的值分别是想要的 1 和 2。上面值以字符串0000002打印只是 sstable2json 的处理方式——该值在 SSTable 中实际是序列化的 int32 位长度 4后跟整数的 4 个字节。有序的list集合情况类似但为保持期望的元素顺序而不完全相同CREATE TABLE col1 ( user text, favorites listtext, PRIMARY KEY (user) ); INSERT INTO col1 (user, favorites) VALUES (user1, [raindrops, kittens]);得到的 SSTable 行是{key: user1, cells: [[,,1428854738475900], [favorites:_,favorites:!,1428854738475899,t,1428854738], [favorites:c2bcd290e12d11e49cac000000000000,7261696e64726f7073,1428854738475900], [favorites:c2bcd291e12d11e49cac000000000000,6b697474656e73,1428854738475900]]}注意这一次元素raindrops 与 kittens是 cell 的值而不是列名。列名中是一些旨在按所需列表顺序排序的长字符串。这些长十六进制串被 sstable2json 误读了——它们并不是十六进制串而是 16 字节的 UUID。仅仅保持列表顺序Cassandra 本可以使用小整数而不是 UUID。但这些 UUID 有一个额外好处Cassandra 希望对已有列表支持高效的append与prepend操作——无需先读列表即 append/prepend 是快速的纯写变异而非慢速的 read-modify-write。为此 Cassandra 使用带符号的 time-UUID 作为列表排序串——正时间用于 append负时间用于 prepend。这样保证后续 append 总排在更早 append 之后而 append 无需知道列表中已有哪些元素。ScyllaDB 内部在 mutation 中存储集合的类是collection_mutation需要把上述表示转换为该类。文档附注collection_mutation的内部结构隐藏在 opaque 字节数组之后具体构建函数有待明确。带过期时间的 cellTTLSSTable cell 可以有过期时间。这样的 cell 在 mask 字节置位EXPIRATION_MASK除了正常字段外还有两个附加字段 ttl 与 expiration均为 32 位、以秒计。ttl 是 cell 创建时指定的原始存活时间距过期还有多少秒expiration 是 cell 应过期的绝对时间自 UNIX 纪元起的秒数。下面 CQL 示例创建了一个存活 3600 秒的 cellCREATE TABLE ttl ( name text, age int, PRIMARY KEY (name) ); INSERT INTO ttl (name, age) VALUES (nadav, 40) USING TTL 3600;sstable2json 打印的 SSTable 行为{key: nadav, cells: [[,,1430151018675502,e,3600,1430154618], [age,40,1430151018675502,e,3600,1430154618]]}注意每个 cellCQL Row Marker 与真实数据 cell都有 3600 秒的 TTLexpiration 时间为服务器当前时间加 3600。Row Marker 也获得 TTL 未必是好事——相关讨论见 CASSANDRA-5762。Cell 墓碑Cell Tombstone前面已讨论过行墓碑标记整行删除与范围墓碑标记列范围删除。此外还有 cell 墓碑标记单个 cell 的删除。被删除的 cell 在 SSTable 中编码为普通 cell只是 mask 置位DELETION_MASK并且 cell 的值包含序列化的local_deletion_time自纪元起的本地服务器秒数——这大概只用于在gc_grace_seconds过去后清除墓碑。要在 Cassandra 中创建含被删除 cell 的 SSTable先创建带数据的表CREATE TABLE deleted ( name text, age int, PRIMARY KEY (name) ); INSERT INTO deleted (name, age) VALUES (nadav, 40);然后用bin/nodetool flush keyspacename把数据刷成 SSTable再删除刚添加的 cellDELETE age FROM deleted WHERE name nadav;第二个 SSTable 刷出后就会包含一个 cell 墓碑。sstable2json 显示如下{key: nadav, cells: [[age,1430200516,1430200516937621,d]]}注意该 cell 置位了DELETION_MASK打印为 d其 value 是 local-deletion-time 1430200516照常还有时间戳。SSTable Summary 文件格式Summary 文件用于在 Index File 之上建立粗粒度索引让读取器无需加载完整索引即可快速定位可能包含目标 key 的索引区域。本节内容源自 docs/architecture/sstable/sstable2/sstable-summary-file.rst。struct summary_entry { char key[...]; // 变长 be64 position; };struct summary_la { struct header { be32 min_index_interval; be32 size; be64 memory_size; be32 sampling_level; be32 size_at_full_sampling; } header;le32 positions[header.size]; summary_entry entries[header.size]; be32 first_key_size; char first_key[first_key_size]; be32 last_key_size; char last_key[last_key_size]; };其中min_index_interval为索引抽样间隔决定每隔多少索引项取一个进 Summarysize为摘要项数量memory_size为内存中占用大小sampling_level与size_at_full_sampling用于在满采样level 100与当前采样级别之间换算positions与entries分别保存每个摘要项在 Index File 中的位置与 (key, position) 对first_key/last_key标记该 SSTable 的 key 范围用于快速判定目标 key 是否可能落在这个文件内。阅读指引与延伸本文对应的原始文档位于 docs/architecture/sstable/sstable2/index.rst 及同目录下的五个子文档SSTable Compression —— 数据文件分块压缩与 Compression Info FileSSTable Data File —— 行、atom、mask 与序列化细节SSTable format in ScyllaDB —— 与 Cassandra 2.1.8 的兼容性及 per-core shardingSSTable Interpretation —— 从磁盘字节到mutation_partition的语义映射SSTable Summary File —— Summary 文件字节布局如需了解新一代格式可继续阅读 SSTable 3.x 章节数据文件、Statistics、Summary、Index 与基于 trie 的 ms 索引。实现层面ScyllaDB 的 SSTable 读写代码位于 sstables/ 目录版本枚举见 sstables/version.hh压缩算法映射见 sstables/compressor.cc压缩元数据与参数处理见 sstables/compress.cc 与 sstables/compress.hh。理解 2.x 格式不仅是兼容旧数据与备份恢复的基础也为理解 3.x 的压缩优化更紧凑的索引、更少的数据冗余提供了必要铺垫。【免费下载链接】scylladbNoSQL data store using the Seastar framework, compatible with Apache Cassandra and Amazon DynamoDB项目地址: https://gitcode.com/GitHub_Trending/sc/scylladb创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考