☰
KurrentDB 次级索引(Secondary Indexes)完全指南:Category 与 Event Type 索引的原理、配置与实战
2026/10/12 2:02:10 网站建设 项目流程
  • 数据库
  • 后端
  • 流处理

【免费下载链接】EventStore

KurrentDB is a database that's engineered for modern software applications and event-driven architectures. Its event-native design simplifies data modeling and preserves data integrity while the integrated streaming engine solves distributed messaging challenges and ensures data consistency.

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

KurrentDB 自 v25.1 起引入次级索引(Secondary Indexes),在数据库内部基于 DuckDB 构建按类别(category)与事件类型(event type)组织的索引,用于加速这两类最常见的查询模式,并取代$by_category、$by_event_type等系统投影产生的链接事件流(link stream)。本文以官方文档 secondary.md 为骨架,结合仓库源码(KurrentDB.SecondaryIndexing 项目与 SecondaryIndexingPlugin.cs),完整讲解次级索引的引入动机、存储与命名约定、启用配置、读写与订阅用法、性能收益、已知限制以及备份恢复注意事项,帮助你在升级到 v25.1 后正确启用并发挥这一新特性的价值。

为什么需要次级索引:系统投影的存储与读放大问题

在引入次级索引之前,KurrentDB 依赖两个系统投影来按类别和事件类型组织事件:

  • $by_category:按类别创建链接事件流
  • $by_event_type:按事件类型创建链接事件流

(完整的标准投影清单还包括$stream_by_category、$streams与$by_correlation_id,见 ProjectionsSubsystem.cs。)

这些系统投影的工作方式是:每当有事件写入,投影就把一条"链接事件"(link event)写入对应的链接流(如$ce-类别名、$et-事件类型名)。读取这些链接流时,KurrentDB 必须把每条链接事件解析回原始事件,这就带来了显著的额外开销:

  • 读放大:每次读取都要先读链接事件、再解析回原始事件;
  • 存储放大:当原始事件所在的流被截断或删除时,链接事件依然保留在数据库日志文件中——KurrentDB 无法从非目标流中移除事件,导致旧 chunk 文件里充斥着指向已删除事件的、永远无法解析的链接事件;
  • 索引放大:链接事件同样会被默认索引记录,白白增加默认索引的体积。

文档给出的生产环境统计数据表明:在通过删除无用数据来控容的系统中,数据库体积中最高可有 50% 是由这些系统投影产生的链接事件构成;某些旧 chunk 文件里,指向已删除事件的链接事件甚至占到磁盘空间的 90%。在这样的系统里,从时间起点重放事件会非常低效:先读链接事件,再逐条解析,而其中很多原始事件已不存在。

次级索引与系统投影的关键差异

次级索引在设计上直接规避了上述问题,与系统投影有四点本质区别:

对比维度系统投影(链接事件流)次级索引
存储位置存储在数据库日志(chunk)文件中,同时被默认索引记录存储在独立的 DuckDB 文件中,与日志文件分开,不影响默认索引大小
读取方式需解析链接事件回原始事件直接按索引字段过滤,无需链接解析
事件删除后的处理链接事件残留,无法清理条目可随原始事件删除而移除(v25.1 尚未实现,计划在未来版本提供)
条目体积链接事件占用较大索引条目更紧凑,存储开销更低

从代码层面看,DefaultIndexProcessor.cs 的TryIndex方法在事件提交后同步把事件写入以 DuckDB 为底层的缓冲视图,并分别向默认索引、事件类型索引与类别索引广播SecondaryIndexCommitted消息;整个过程不产生任何链接事件,也不触碰默认索引的 PTable。

一组可量化的对比数据

文档给出了一组来自真实规模数据集的对比(该数据集同时包含$streams与$stream_by_category产生的链接,但这两者每个流只产生一条链接,影响可忽略):

  • 数据库含1.3 亿个事件(每个约 400 字节),分布在100 万个流上;
  • 启用 category / event type 系统投影后,额外产生了2.8 亿条链接事件;
  • 不含链接事件时数据库文件约48 GB,含链接事件后约102 GB;
  • 不含链接事件时默认索引约3.2 GB,含链接事件后约8.7 GB;
  • 链接事件带来的总存储开销约60 GB(约 100% 的增幅);
  • 作为对照,category 与 event type 两个次级索引合计仅约2.2 GB。

索引命名约定与内部结构

索引名称:$idx-前缀体系

次级索引的名称遵循统一的$idx-前缀体系,定义于 SystemNames.cs:

public const string IndexStreamPrefix = "$idx-"; public const string DefaultSecondaryIndex = $"{IndexStreamPrefix}all"; // $idx-all public const string CategorySecondaryIndexPrefix = $"{IndexStreamPrefix}ce-"; // $idx-ce- public const string EventTypeSecondaryIndexPrefix = $"{IndexStreamPrefix}et-"; // $idx-et-

对应的索引名生成逻辑在 CategoryIndex.cs 与 EventTypeIndex.cs 中:

  • 类别索引:CategoryIndex.Name("订单")生成$idx-ce-订单;
  • 事件类型索引:EventTypeIndex.Name("OrderPlaced")生成$idx-et-OrderPlaced。

存储模型:DuckDB 中的idx_all表

次级索引使用嵌入式 DuckDB 存储。核心表结构定义在 1_Schema.sql(以嵌入式资源形式随程序集加载,见 IndexingDbSchema.cs):

create table idx_all ( log_position bigint not null, -- 事件在日志中的位置(用于推进读取进度) commit_position bigint null, -- 提交位置(可空) stream_revision bigint not null, -- 流内事件序号(仅元数据,不作为索引读取进度) created_at bigint not null, expires_at bigint null, -- 预留:未来清理过期条目的字段 stream varchar not null, stream_hash ubigint not null, schema_name varchar not null, -- 事件类型,供 event type 索引查询 category varchar not null, -- 类别,供 category 索引查询 deleted boolean not null, -- 删除标记(v25.1 中恒为 false,见 DefaultIndexProcessor 的 TODO 注释) schema_id varchar null, schema_format varchar not null, record_id blob not null );

读取索引时实际查询的是名为idx_all_snapshot的视图(定义于 DefaultSql.cs),它把已落盘的idx_all行与内存中尚未提交的缓冲行合并,保证读到的数据包含最新写入。类别索引与事件类型索引的查询 SQL 分别在 CategorySql.cs 与 EventTypeSql.cs 中,例如类别正向查询:

select log_position, commit_position, stream_revision from idx_all_snapshot where category = $1 and log_position > $2 order by coalesce(commit_position, log_position) limit $3;

值得注意的是,类别(category)的提取规则实现在 DefaultIndexProcessor.cs:取流名中第一个-之前的部分,若流名中没有-则以整个流名为类别。这正是文档中"类别索引始终按first模式、以-为分隔符"这一限制的源码级体现——它无法像$by_category投影那样配置first/last模式或自定义分隔符。

启用与配置

默认启用与后台构建

次级索引默认启用。升级到 v25.1 后,节点会在后台开始构建次级索引:

  • 构建过程从日志开头扫描(或从上次已索引的位置继续),耗时从几分钟到几小时不等,取决于数据库规模;
  • 构建期间数据库可正常读写,但依赖次级索引的查询在索引完全建成前可能返回不完整结果;
  • 构建期间数据库的 reads 计数会明显升高(可据此判断初始索引是否仍在进行);
  • 源码层面,构建由IHostedService类型的 DefaultIndexBuilder.cs 驱动:它订阅EventCommitted消息,通过 DefaultIndexSubscription.cs 从上次索引位置(或Position.Start,此时日志会记录 "Rebuilding secondary index from scratch")订阅$all,按CommitBatchSize(默认 50_000 条)批量提交,并在追上日志尾部时记录 "Secondary index rebuild complete"。

警告:初始索引期间的大量读取可能影响数据库其他操作性能,建议在维护窗口或低峰期完成升级。

关闭次级索引

如需禁用,在kurrentdb.conf中设置:

SecondaryIndexing: Enabled: false

从源码看,该配置由 SecondaryIndexingPlugin.cs 的IsEnabled读取:SecondaryIndexing:Enabled未设置时默认启用(enabledOption ?? true)。同一插件还支持SecondaryIndexing:Options:CommitBatchSize(默认 50_000)与SecondaryIndexing:Options:DbPath等选项。测试工程中也有对应的启用/禁用夹具(SecondaryIndexingFixture.cs),通过SecondaryIndexingEnabledFixture与SecondaryIndexingDisabledFixture分别验证两种配置路径。

除 YAML 外的其他配置机制(命令行参数、环境变量等),参见配置指南。

使用次级索引进行读取与订阅

索引建好后,即可按类别或事件类型高效查询。所有支持对$all进行带过滤器读取与订阅的客户端 API 都可以同样方式使用次级索引——本质上,次级索引是"过滤器"而不是"流":

  • 索引读取不会返回链接事件,因此不需要(也不使用)resolveLinkTos;
  • 索引条目不提供连续的流内事件序号,读取与订阅进度必须使用原始事件的日志位置(log position)来跟踪;
  • 使用流名前缀过滤器,且只传一个索引名,而不是一组前缀。

代码示例:按事件类型读取

旧方式——读取事件类型系统投影产生的链接流(需解析链接):

var readFromEtStream = client.ReadStreamAsync( Direction.Forwards, "$et-OrderPlaced", StreamPosition.Start, resolveLinkTos: true, maxCount: 1000 );

新方式——使用事件类型次级索引:

var read = client.ReadAllAsync( Direction.Forwards, Position.Start, StreamFilter.Prefix("$idx-et-OrderPlaced"), maxCount: 1000 );

类别索引的用法完全一致,只需把前缀换成$idx-ce-CATEGORYNAME。

集成测试对三种索引的验证

仓库集成测试 ReadTests.cs 系统地验证了默认索引($idx-all)、类别索引与事件类型索引的正向/反向读取(ReadsAllEventsFromCategoryIndex、ReadsAllEventsFromEventTypeIndex),并断言读取未知索引(如$idx-dummy)会抛出ReadResponseException.IndexNotFound(测试ReadFromUnknownIndexFails)。订阅场景则由 SubscriptionTests.cs 覆盖,测试夹具中的ReadUntil/SubscribeUntil辅助方法(见 SecondaryIndexingFixture.cs)演示了如何等待索引追平写入。

存储与性能收益

  • 存储:相比系统投影产生的链接事件,次级索引可将相关存储占用降低最高 50%(前面 1.3 亿事件示例中的实际对比为 60 GB 对 2.2 GB)。
  • 读取性能:消除了链接解析环节,按类别或事件类型过滤的查询可快最高 10 倍——但前提是使用较大的分页大小。文档建议常规读取至少一次读取1000 条记录;订阅场景下,次级索引订阅默认分页大小为2048,而常规订阅默认只有32,这是为订阅场景专门调优过的。

运行时可观测指标

构建与追平进度可以通过指标观测。SecondaryIndexProgressTracker.cs 以indexes.secondary为前缀注册了三个指标:

  • kurrentdb.indexes.secondary.gap:最后一条已索引日志记录与日志尾部之间的字节差距("还差多少没追上");
  • kurrentdb.indexes.secondary.lag:事件写入到被索引之间的时间差(秒);
  • kurrentdb.indexes.secondary.commit.seconds:每次批量提交索引记录所耗时间(直方图)。

运维时可用这些指标判断初始索引进度与追平延迟。

注意事项与已知限制(v25.1)

  • 最终一致性:次级索引是最终一致的,事件写入与出现在索引之间可能有轻微延迟。
  • 构建耗时:后台构建对大型数据库可能持续较长时间(与默认索引体积、chunk 读取量相关)。
  • 仅支持两种索引类型:当前只有类别与事件类型索引,未来版本可能增加更多索引类型。
  • 进度跟踪基于日志位置:索引不提供连续事件序号,读取与订阅需以日志位置推进。
  • 类别索引不可配置:始终为first模式 +-分隔符(对应源码GetStreamCategory的实现),无法像$by_category投影那样配置first/last与自定义分隔符。
  • 删除事件不会立即清除索引条目:可能导致索引条目指向不存在的事件。这只会略微影响读取速度,不会产生错误——服务端会跳过无法解析的索引记录。未来版本将实现删除事件的自动清理。
  • 保留策略不生效:由于上一条,索引读取不会执行MaxAge、MaxCount等保留策略;已删除的事件在 scavenge 之前仍可能被索引读取返回。
  • 权限限制:当前读取与订阅次级索引依赖客户端读取$all的能力,因此仅$admins组成员可用;未来版本将放开非管理员读取$all与次级索引的限制。

备份与恢复:请使用卷快照而非文件复制

次级索引数据保存在嵌入式 DuckDB 中,DuckDB 文件与主数据库文件同目录存放,但并非 append-only,可能在原地被修改。因此:

  • 基于文件系统的备份(如文件复制)可能产生不一致的备份:若备份过程中 DuckDB 文件正被修改,复制到的索引文件可能处于不一致状态;
  • 推荐方式:对 v25.1 及以后版本使用卷快照(volume snapshot),确保包括 DuckDB 文件在内的所有文件处于一致状态;
  • 如果坚持文件复制备份,必须先停止 KurrentDB 节点再执行复制。

总结

次级索引是 KurrentDB v25.1 在索引领域的一次架构级升级:它以嵌入式 DuckDB 为存储底座,把类别与事件类型查询从"读链接流 + 解析链接"转变为"按索引字段直接过滤",同时消除了链接事件带来的存储膨胀(最高可省 50% 相关存储)与读取开销(大分页下最高 10 倍提速)。它默认启用、后台自动构建,你只需遵循本文的索引命名约定($idx-ce-/$idx-et-)在$all读取与订阅中使用流名前缀过滤器即可受益。需要注意的是 v25.1 版本仍存在删除不清理、无保留策略、仅$admins可用等限制,并且备份时应改用卷快照。若要深入研究其实现,可从 KurrentDB.SecondaryIndexing 的Indexes/Category、Indexes/EventType、Indexes/Default三个目录及其集成测试入手。

  • 数据库
  • 后端
  • 流处理

【免费下载链接】EventStore

KurrentDB is a database that's engineered for modern software applications and event-driven architectures. Its event-native design simplifies data modeling and preserves data integrity while the integrated streaming engine solves distributed messaging challenges and ensures data consistency.

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

相关推荐

上一篇:终极指南:3分钟解锁网易云音乐NCM加密格式,实现跨设备自由播放
下一篇:ncmdump:3分钟终极解密指南,解锁网易云音乐跨设备自由播放

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

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

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

立即咨询