
ScyllaDB SSTables 3.0 Summary 文件格式采样索引的磁盘布局与源码实现解析【免费下载链接】scylladbNoSQL data store using the Seastar framework, compatible with Apache Cassandra and Amazon DynamoDB项目地址: https://gitcode.com/GitHub_Trending/sc/scylladb导读本文基于 ScyllaDB 仓库中的官方架构文档系统讲解 SSTables 3.0 中 Summary摘要文件的作用、磁盘布局与字段语义。Summary 文件是 SSTable 索引查询的第一级粗粒度过滤层它常驻内存、体积紧凑帮助读取路径跳过绝大多数索引页直接定位到可能包含目标键的索引区间。读完本文你将掌握 Summary 文件的完整二进制布局、采样级别sampling level的数学含义以及 ScyllaDB 源码中 Summary 的生成、封存与查询调用链。Summary 文件在整个 SSTable 读取路径中的角色在 SSTable 3.0 中一次按键查找需要经过三层数据结构与 SSTables 3.0 Index File Format 描述一致Summary 文件对索引文件中键的采样每个条目指向索引文件中一段连续的索引项通常称为index page即索引页Index 文件按序排列的键列表记录每个分区键在数据文件中的位置以及大分区对应的 promoted indexData 文件实际的数据内容。Summary 文件的每一个条目都指向一段索引序列index page目标键就落在这个页中。借助 Summary读取器只需读取并搜索索引文件的一小部分而无需扫描整个索引文件。这也是 ScyllaDB 定位键三次 IOindex-summary → index → data设计中的第一步。从源码调用链可以印证这一分层关系在 index_reader.hh 的advance_to_page()中读取器先通过summary.entries[summary_idx].position拿到索引页的起始偏移再根据downsampling::get_effective_index_interval_after_index()计算出该 summary 条目实际覆盖的分区数从而确定要读取的索引页边界并加载到缓存中。与 SSTable 2.0 格式的差异Summary 文件格式在 3.0 版本中相比旧版本详见 SSTable 2.0 format 及 SSTable 2.0 Summary File改动很小唯一显著的变化是3.0 的 Summary 文件不再存储关于 segment boundaries段边界的信息。这一变更源自 CASSANDRA-8630原因在于 segment 边界信息已经可以方便地从其他数据源获取无需再占用 Summary 文件的空间。除此之外数据格式保持一致。值得注意的是与数据文件、索引文件不同Summary 文件不使用变长整数variable-length integers编码所有整数字段均为固定宽度的大端序或专门说明的小端序整数这使得它非常适合被整体读入内存后直接按结构体访问。Summary 文件整体布局官方文档给出的顶层结构如下sstables-3-summary.rststruct summary { struct summary_header header; struct summary_entries_block summary_entries; struct serialized_key first; struct serialized_key last; };文件自始至终依次排列四部分部分内容summary_header文件元信息最小索引间隔、条目数、条目区大小、采样级别等summary_entries_blockoffsets 数组 entries 数组真正的采样键与索引位置serialized_key first索引/数据文件中的第一个键serialized_key last索引/数据文件中的最后一个键在 ScyllaDB 源码中这一结构体对应 types.hh 中的summary_ka当前仅支持 ka 及更高版本因此summary summary_ka它额外提供了memory_footprint()方法来计算 Summary 载入内存后的总占用包括 entries、positions、首尾键以及内部_summary_data缓冲区的字节数。Summary Header 详解文件的第一个结构是summary_header布局如下struct summary_header { be32 min_index_interval; be32 entries_count; be64 summary_entries_size; be32 sampling_level; be32 size_at_full_sampling; };各字段的语义对应 types.hh 中summary_ka::header的注释min_index_intervalbe32相邻两个索引摘要条目之间平均分区数的下界。该值越小在满采样full sampling级别下会有更多分区拥有摘要条目。在 ScyllaDB 中它取自 schema 的min_index_interval见下文索引间隔与采样小节ScyllaDB 默认值为 128源码注释位于schema.hh相关默认配置。entries_countbe32summary_entries结构中offsets与entries数组各自的元素个数。对应源码seal_summary()中的s.header.size s.entries.size()见 sstables.cc。summary_entries_sizebe64summary_entries结构的完整字节大小即 offsets 数组与 entries 数组的字节总和。它被用于计算最后一个 summary_entry 的长度。sampling_levelbe32取值在 1 到BASE_SAMPLING_LEVEL等于128之间的整数表示原始索引摘要条目数((1 / indexInterval) * numKeys)中保留了多少。因此当前 Summary 实际包含的条目数为(samplingLevel / BASE_SAMPLING_LEVEL) * ((1 / indexInterval) * numKeys)。size_at_full_samplingbe32如果采样级别等于min_index_interval即满采样时Summary应当拥有的条目数。源码中对应 sstables.cc 的s.header.size_at_full_sampling sstable::get_size_at_full_sampling(state.partition_count, s.header.min_index_interval)。Summary Entries 区summary_entries_block由两个并列的数组组成struct summary_entries_block { uint32 offsets[header.entries_count]; struct summary_entry entries[header.entries_count]; };offsets 数组保存entries数组中每个条目相对于summary_entries_block起点的偏移。由于第一个条目紧跟在 offsets 数组之后因此恒有offsets[0] sizeof(uint32) * header.entries_count。关键字节序细节虽然 SSTable 文件中的整数通常以大端序big-endian写入但offsets 数组按 native order本机字节序写入。ScyllaDB 中总是以小端序little-endian写入这是为了同时保证两种互操作性能够读取 Cassandra 在常见小端机器上写出的 Summary 文件能够让 ScyllaDB 在罕见的大端机器上写出的 Summary 文件被其他实现读取。这一选择在源码中也有印证summary_ka的注释明确指出positions对应 offsets 数组are laid out inMEMORYorder, not BE见 types.hh。entries 数组每个summary_entry的结构为struct summary_entry { byte key[]; // variable-length. be64 position; };key被采样的分区键变长不存储长度positionbe64该键在索引文件中的索引条目位置。注意summary_entry本身不保存键的长度键长可以通过 offsets 数组推导相邻两个 offsets 之差即为前一个条目的长度最后一个条目的长度则可用它的 offset 与header.summary_entries_size计算summary_entries_size - offsets[entries_count-1]。源码中对应的运行时结构是 types.hh 的summary_entry类它额外携带raw_tokentoken 的 int64 表示并提供get_key()、get_token()、get_decorated_key()等便捷方法用于将磁盘上的键还原为排序/比较所需的 decorated key。首键与尾键serialized_keySummary 结构体最后两个字段是serialized_keystruct serialized_key { be32 size; byte key[size]; };它们分别保存索引文件/数据文件中的第一个键与最后一个键。其用途是让读取器在不触碰索引文件与数据文件的情况下就能快速判断某个键是否落在该 SSTable 的键范围内范围检查。在封存 Summary 时源码 seal_summary() 将first_key与last_key写入这两个字段若 SSTable 中只有一个分区没有真正的 last_key则last_key直接复制first_key的值。这也说明文档中空表无法封存 summary的约束seal_summary()中对first_key缺失的情况会抛出断言parse_assert(bool(first_key), {}, attempted to seal summary of empty sstable)。采样级别与索引间隔内存大小的权衡Summary 文件的设计目标是完全读入内存并长期驻留因此它必须保持合理的小。这带来一个内在权衡每个 summary 条目覆盖的索引条目数即 index page 的基数cardinality越小初始查找的精度越高但 Summary 文件体积越大基数越大Summary 越小但定位到目标键需要读取并搜索的索引页越长。调节这一平衡的旋钮就是sampling level采样级别它决定了一个 summary 条目应覆盖多少个索引条目。官方文档中的公式为当前 Summary 条目数 (samplingLevel / BASE_SAMPLING_LEVEL) * ((1 / indexInterval) * numKeys)其中BASE_SAMPLING_LEVEL 128在源码 downsampling.hh 中定义为常量注释明确说明它必须是 2 的幂以保证良好的采样模式sampling pattern且不能随意更改——更改需要重建所有满采样级别的索引摘要。下采样downsampling机制ScyllaDB 的downsampling类downsampling.hh实现了索引摘要的动态缩放get_sampling_pattern(sampling_level)返回下采样轮次的起始索引列表采用递归的奇偶拆分算法确保在后续轮次中起点均匀铺开避免采样点聚集get_original_indexes(sampling_level)将当前摘要条目的索引映射回下采样前的原始索引例如返回[7, 15]表示当前第 0 个条目原本位于第 7 位get_effective_index_interval_after_index(index, sampling_level, min_index_interval)计算第index个摘要条目之后到下一个条目之间实际覆盖的分区数。当sampling_level BASE_SAMPLING_LEVEL时该值等于min_index_interval当摘要被下采样后相邻原始索引的间距会按min_index_interval的倍数放大。索引读取器正是利用这个有效索引间隔来精确计算每个索引页的大小见 index_reader.hhuint64_t quantity downsampling::get_effective_index_interval_after_index( summary_idx, summary.header.sampling_level, summary.header.min_index_interval); uint64_t end summary.entries[summary_idx 1].position; // quantity 与相邻条目位置共同决定本索引页的字节范围从源码看 Summary 的生成与封存ScyllaDB 写路径上Summary 经历三个阶段实现集中在 sstables.cc1. 初始化prepare_summary在写入前根据预估分区数初始化 header。源码 prepare_summary() 将min_index_interval设为 schema 配置值、sampling_level初始化为BASE_SAMPLING_LEVEL满采样并估算满采样时最大可能的条目数expected_partition_count / min_index_interval向上取整若超出uint32_t上限则抛出 malformed sstable 异常。2. 增量追加maybe_add_summary_entry每写入一个分区时调用一次决定是否为它生成摘要条目。源码 maybe_add_summary_entry() 的判定条件为if (s.entries.empty() || data_offset - state.last_data_offset_in_summary state.summary_byte_cost * entry_size || state.partitions_since_last_summary_entry state.max_partitions_per_page) { // 追加 summary_entry }即满足以下任一条件就新增一个摘要条目摘要为空第一条距上一条目的数据偏移超过summary_byte_cost × entry_size即按字节成本触发保证摘要与数据的比例符合配置距上一条目已积累的分区数达到max_partitions_per_page防止单页过大。summary_byte_cost由配置sstable_summary_ratio换算而来sstables.ccsummary_ratio ? (1 / summary_ratio) : default_summary_byte_cost目标是在 SSTable 封存时摘要与数据的体积比达到 1 : cost。3. 封存seal_summary写入全部数据后将 header 的size、size_at_full_sampling填实写入 first/last key并回填 offsets 数组——offsets 在封存时逐条计算累加每个条目的键长与 position 大小对应文档中offsets[0] 即 offsets 数组自身大小的语义seal_summary()。相关配置项与运维含义Summary 相关行为受 ScyllaDB 配置控制定义于 config.hh配置项说明index_summary_capacity_in_mb全节点索引摘要可占用的总内存上限MB。摘要常驻内存此值约束了其整体规模index_summary_resize_interval_in_minutes索引摘要管理器检查/调整各表摘要采样级别的周期分钟sstable_summary_ratio摘要与数据体积的目标比例写入时通过summary_byte_cost影响摘要条目的生成密度sstable_summary_max_partitions_per_page单个索引页最多覆盖的分区数上限防止因数据分布不均导致索引页过大结合前文公式可以看出运维含义当表的min_index_interval较小或分区极多时满采样的摘要会偏大index_summary_capacity_in_mb一旦吃紧摘要管理器会通过下采样降低sampling_level以释放内存此时单页覆盖的分区数增加、查找精度略微下降并在resize_interval周期内重新评估。这是官方文档内存大小 vs 索引页基数权衡在运行时的具体落地。小结SSTables 3.0 的 Summary 文件是 ScyllaDB 读取路径中以内存换 IO的关键设计它以紧凑的固定宽度整数布局summary_header offsets 数组 entries 数组 first/last key描述对索引的采样通过sampling_level1128BASE_SAMPLING_LEVEL 128在摘要体积与索引页基数之间取平衡并借助downsampling类实现运行时的动态上/下采样。相比 2.0 格式3.0 仅移除了 segment boundaries 信息CASSANDRA-8630其余字节布局与字段语义保持一致——这正是 ScyllaDB 能够直接加载 Cassandra 3.0 SSTable通过nodetool refresh导入的格式兼容性基础。若需进一步深入可继续阅读同目录下的 SSTables 3.0 Index File Format 与 SSTables 3.0 Data File Format它们与本文共同构成完整的 3.0 文件格式文档族。【免费下载链接】scylladbNoSQL data store using the Seastar framework, compatible with Apache Cassandra and Amazon DynamoDB项目地址: https://gitcode.com/GitHub_Trending/sc/scylladb创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考