SAI 写路径和读路径
SAI 与底层数据库的存储引擎深度集成。SAI 不会抽象地索引表。相反,SAI 在写入 **Memtable** 和排序字符串表 (**SSTable**) 时对其进行索引,并在读取时解决这些索引之间的差异。每个 Memtable 都是一个特定于特定数据库表的内存数据结构。Memtable 类似于写回缓存。每个 SSTable 都是一个不可变的数据文件,数据库会定期将 Memtable 写入其中。SSTable 按顺序存储在磁盘上,并为每个数据库表维护。
本主题讨论 SAI 读写路径的详细信息,并检查 SAI 索引生命周期。
SAI 写路径
可以在写入任何数据到 CQL 表之前或之后创建 SAI 索引。作为复习,写入 CQL 表的数据将首先写入 Memtable,然后在数据从 Memtable 刷出后写入 SSTable。创建 SAI 索引后,SAI 会收到对当前 Memtable 的所有变动的通知。与任何其他数据一样,SAI 会更新插入和更新的索引,Apache Cassandra 对其进行完全相同的处理。SAI 还支持分区删除、范围墓碑和行删除。如果在查询中执行删除操作,SAI 会在后过滤步骤中处理索引更改。因此,SAI 在索引频繁删除的列时不会产生任何特殊惩罚。
如果插入或更新包含索引列的有效内容,则该内容将添加到 **Memtable 索引** 中,并且更新行的主键将与索引值相关联。SAI 会计算新条目增量堆使用量的估计值。此估计值会计入底层 Memtable 的堆使用量。此功能还意味着,随着在表上索引的列越来越多,Memtable 刷出率会增加,而刷出的 SSTable 的大小会减小。所有活动 Memtable 索引的总写入次数和估计堆使用量会作为指标公开。请参阅 SAI 指标。
Memtable 刷出
当达到刷出阈值时,SAI 会将 Memtable 索引内容直接刷出到磁盘,而不是创建额外的内存表示。这是可能的,因为 Memtable 索引按术语/值排序,然后按主键排序。当刷出发生时,SAI 会为每个索引列写入一个新的 SSTable 索引,因为 SSTable 正在写入。
刷出操作是一个两阶段过程。在第一阶段,行被写入新的 SSTable。对于每一行,都会生成一个行 ID,并创建三个索引组件。这些组件是
-
行 ID 到其对应令牌值的磁盘映射——SAI 支持 Murmur3Partitioner
-
SSTable 分区偏移量
-
主键到其行 ID 的临时映射,它将在后续阶段使用
第一和第二组件文件的内容是压缩数组,其序数对应于行 ID。
在第二阶段,在所有行都写入新的 SSTable 并且共享的 SSTable 级索引组件已完成之后,SAI 开始对每个索引列进行索引工作。具体来说,在第二阶段,SAI 会遍历 Memtable 索引以生成术语及其令牌排序行 ID 的对。此迭代器使用在第一阶段构建的临时映射结构将主键转换为行 ID。然后,术语迭代器(带帖子)将根据每个索引元素是针对字符串列还是数字列传递给单独的写入组件。
在字符串情况下,SAI 索引写入器会遍历每个术语,首先将它的帖子写入磁盘,然后在磁盘上的字节排序字典树中将这些帖子的偏移量记录为新条目(对于术语本身)的有效负载。在数字情况下,SAI 将数字索引的写入分为两个步骤
术语被传递给平衡的 kd 树写入器,该写入器将 kd 树写入磁盘。当树的叶块被写入时,它们的帖子会暂时存储在内存中。然后,这些临时帖子将用于在磁盘上构建帖子,在叶子处以及索引节点的各个级别。
当列索引刷出完成时,一个特殊的空标记文件将被标记以指示成功。此过程在启动和增量重建操作中使用,以区分以下情况
-
SSTable 索引文件对于某个列丢失。
-
没有可索引的数据——例如,当 SSTable 仅包含墓碑时。(墓碑是行中的一个标记,指示某个列已被删除。在压缩过程中,标记的列会被删除。)
然后,SAI 会增加该列刷出的 Memtable 索引数量的计数器,并将每秒刷出的单元格数量添加到直方图中。
何时触发压缩
回想一下,Apache Cassandra 使用压缩来合并 SSTable。压缩会收集每个唯一行的所有版本,并使用来自 SSTable 的每一行列的最新版本(按时间戳)组装一个完整的行。合并过程效率很高,因为行在每个 SSTable 中按分区键排序,并且合并过程不使用随机 I/O。每一行的最新版本被写入一个新的 SSTable。旧版本以及任何准备删除的行都保留在旧的 SSTable 中,并在任何挂起的读取完成时被删除。
对于 SAI,当触发压缩时,每个索引组都会创建一个 SSTable 刷出观察器,以协调从其各自的 SSTable 写入器实例中新压缩的数据中写入所有附加列索引的过程。与 Memtable 刷出(其中索引术语已排序)不同,在压缩期间迭代合并数据时,SAI 会缓冲索引值及其行 ID,这些 ID 按令牌顺序添加。
为了避免诸如耗尽可用堆资源等问题,SAI 使用累积段缓冲区,该缓冲区通过使用专有计算同步地刷出到磁盘。然后,每个段记录一个段行 ID **偏移量**,并且仅存储段行 ID(SSTable 行 ID 减去段行 ID 偏移量)。SAI 将段同步地刷出到同一个文件中,以避免重写所有段的成本,并降低分区限制查询和分页范围查询的成本,因为它减少了搜索空间。
从每个列索引帖子到 SSTable 偏移量再到 SSTable 分区的磁盘布局
实际的段刷出过程与 Memtable 刷出非常相似。但是,缓冲的术语在可以使用其帖子写入其各自的特定类型磁盘结构之前需要进行排序。在给定索引的压缩结束时,一个特殊的空标记文件将被标记以指示成功,并且段数将在 SAI 指标中记录。请参阅 全局索引指标。
当整个压缩任务完成时,SAI 会收到一个 SSTable 列表变更通知,其中包含事务期间添加和删除的 SSTable。SSTable 上下文管理器和索引视图管理器负责原子地用新的 SSTable 索引替换旧的 SSTable 索引。此时,新的 SSTable 索引可用于查询。
SAI 读取路径
本节介绍 SAI 协调器如何处理索引查询以及副本如何执行索引查询。与传统二级索引不同,传统二级索引每个查询最多使用一个列索引,SAI 实现了一个 Query Plan
,它可以使单个查询使用所有可用的列索引。
SAI 读取操作的总体流程如下
索引选择和协调器处理
当遇到查询时,SAI 协调器执行的第一个操作是识别最具选择性的索引,以利用一个或多个索引。最具选择性的索引是指通过返回估计结果行计算的最低值,最积极地缩小过滤空间和最终结果数量的索引。如果存在多个 SAI 索引(即每个 SAI 索引基于不同的列,但查询涉及多个列),则第一个选择的 SAI 索引无关紧要。
一旦为读取操作选择了最佳索引,该索引就会嵌入到读取命令中,该命令进入分布式范围读取装置。分布式范围读取按令牌顺序查询 Apache Cassandra 集群,可能需要一轮或多轮。SAI 协调器根据本地数据和查询限制估计 **并发因子**(每个范围的行数),以确定要联系的范围数量。对于每一轮,并发因子决定将并行查询多少个副本。
在第一轮开始之前,SAI 通过专有计算估计初始并发因子,此处显示为步骤 1。
一旦建立了初始并发因子,范围读取就开始了。
在步骤 2 中,SAI 协调器根据并发因子并行向所需的范围发送请求。在步骤 3 中,SAI 协调器等待来自请求副本的响应。在步骤 4 中,SAI 协调器收集结果并根据返回的行和查询限制重新计算并发因子。
在每一轮结束时,如果未达到限制,则会调整并发因子以考虑已读取结果的形状。如果第一轮没有返回结果,则并发因子会立即增加到剩余令牌范围的最小计算值和并发因子的最大计算值。如果返回结果,则会更新并发因子。SAI 重复步骤 2、3 和 4,直到满足查询限制。
为了避免查询具有失败索引的副本,每个节点通过八卦将自己的本地索引状态传播到对等节点。在协调器处,读取请求将过滤包含请求中使用的不可查询索引的副本。在大多数情况下,第二轮副本查询应该返回所有必要的结果。如果副本周围的结果分布极不平衡,可能需要进行更多轮次。
更深入地了解:副本查询规划和视图获取
一旦副本从 SAI 协调器收到令牌范围读取请求,本地索引查询就开始了。SAI 通过 Query Plan 实现了一个索引搜索器接口,它可以使单个查询访问所有可用的 SAI 列索引。
Query Plan 对通过读取命令传递给它的表达式进行分析。SAI 确定应使用哪些索引来满足给定列上的查询子句。一旦列表达式与索引配对,Query Controller 就会获取每个列索引的活动 SSTable 索引的视图。为了避免压缩删除正在进行的查询使用的索引文件,在读取任何索引文件之前,Query Controller 会尝试获取对与查询的令牌范围相交的索引文件的 SSTable 的引用,并在读取请求完成后释放它们。
此时,将创建一个令牌流,以从每个列索引流式传输匹配项。这些流以及确定如何合并它们的布尔逻辑被包装在一个操作中,该操作将返回给 Query Plan 组件。
SAI 令牌流框架的作用
SAI 查询引擎围绕一个令牌流框架,该框架定义了 SAI 如何异步地迭代、跳过和合并来自单个 SSTable 索引和整个列索引的匹配分区流。SAI 使用令牌来描述 Cassandra 环形令牌中分区匹配的容器。
迭代是最简单的三个操作之一。具体来说,发布的迭代涉及通过块缓存对行 ID 进行顺序磁盘访问,这些行 ID 用于查找环形令牌和分区键偏移信息。
令牌跳过用于在从先前的分页边界继续时或在令牌交集中找到更大的令牌时跳过不匹配的令牌。
匹配流式传输和后过滤示例
考虑一个具有单个列索引(例如 age = 44
)的示例,生成的流是所有 Memtable 索引和所有 SSTable 索引的并集。
-
SAI 按令牌顺序“延迟”地(即一次一个)迭代每个 Memtable 索引,通过其各个令牌范围分区实例。此功能减少了否则会因对环形末尾数据的无用搜索而产生的开销。
-
磁盘索引:SAI 返回所有匹配的 SSTable 索引的并集。在一个 SSTable 索引中,由于压缩期间的内存限制,可能存在多个段。与 Memtable 索引类似,SAI 按令牌排序顺序延迟搜索段。
当查询中存在多个索引表达式(例如 WHERE age=44 AND state='CA'
)并使用 AND
查询运算符连接时,索引表达式的结果将被交集,这将返回与所有索引表达式匹配的分区键。
在索引搜索之后,SAI 公开了一个分区键流。对于每个分区键,SAI 执行一个分区读取操作,该操作返回给定分区中的行。当行被物化时,SAI 使用过滤器树来应用另一轮过滤。SAI 执行此后续过滤步骤以解决以下问题
-
分区粒度:SAI 跟踪分区偏移量。在宽分区模式的情况下,并非分区中的所有行都将与索引表达式匹配。
-
墓碑:SAI 不索引墓碑。索引行可能已被新添加的墓碑遮蔽。
-
非索引表达式:操作可能包括没有索引结构的非索引表达式。
下一步是什么?
请参阅博客文章 更好的 Cassandra 索引,更好的数据模型:介绍存储附加索引。