SpacetimeDB 索引扫描性能基准:perf-test模块与index_scan_gate门控详解
【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB
导读
本文以 SpacetimeDB 仓库中 modules/perf-test/README.md 为线索,深入讲解一个专用于索引(index scan)工作负载压测的 Rust 模块perf-test:它构造一张包含 120 万行的Location表,通过四个 reducer 分别验证单列主键索引、单列 B-tree 索引、多列复合索引的点查与范围扫描性能。同时结合crates/index-scan-gate的基准门(benchmark gate)源码,说明该模块如何被自动调用、如何以中位数耗时作为通过判据,帮助读者掌握 SpacetimeDB 索引性能基准从模块编写、数据加载到门控判定的完整链路。
perf-test模块的定位与作用
perf-test是一个面向 SpacetimeDB 的 Rust 基准测试模块,其 README 中明确了两个核心定位:
- 内置多种
index scan工作负载:覆盖单列索引、多列复合索引在不同扫描规模下的查询场景; - 由
index_scan_gate基准门调用:它不是独立演示用的 demo,而是被crates/index-scan-gate这个可执行程序驱动,用于"确保系统按预期工作"(README 原文:Called by theindex_scan_gatebenchmark to ensure the system is working as expected)。
换句话说,perf-test扮演的是被测负载的提供方:模块内表结构、数据量与各 reducer 的设计,决定了每次基准测试实际测量什么;而index_scan_gate扮演的是调度与判定方,负责编译加载模块、执行 reducer、采样耗时并依据阈值放行或报错。
从 modules/perf-test/Cargo.toml 可以看到模块的工程属性:
[package] name = "perf-test-module" version = "0.1.0" edition.workspace = true license-file = "LICENSE" [lib] crate-type = ["cdylib"] [dependencies] spacetimedb = { path = "../../crates/bindings" }关键点有两个:crate-type = ["cdylib"]表明它被编译成动态库形式的模块(SpacetimeDB 模块的标准产物形态),且仅依赖crates/bindings提供的模块端 SDK。
被测表结构:Location与索引设计
perf-test的核心被测数据表是Location,定义在 modules/perf-test/src/lib.rs:
#[spacetimedb::table(accessor = location, index(accessor = coordinates, btree(columns = [x, z, dimension])))] #[derive(Debug, PartialEq, Eq)] pub struct Location { #[primary_key] pub id: u64, #[index(btree)] pub chunk: u64, #[index(btree)] pub x: i32, pub z: i32, pub dimension: u32, }这张表共设计了三类索引,恰好对应后文四种扫描工作负载:
| 索引 | 类型 | 字段 | 用途 |
|---|---|---|---|
| 主键索引 | 隐式唯一索引 | id | 单行点查(primary key lookup) |
| 单列索引 | B-tree | chunk | 单列等值过滤 + 范围扫描 |
| 单列索引 | B-tree | x | 复合索引的前缀列 |
| 复合索引 | B-tree | x, z, dimension | 多列等值点查 / 前缀扫描 |
其中复合索引通过#[spacetimedb::table(...)]宏的index(accessor = coordinates, btree(columns = [x, z, dimension]))声明,accessor = coordinates为它在 SDK 侧生成的访问器命名,后文 reducer 中出现的location().coordinates()即源于此。而x上单独的#[index(btree)]与复合索引共存的写法,也体现了 SpacetimeDB 表宏支持"普通列索引 + 多列复合索引"组合声明的能力。
数据装载:1.2M 行的确定性构造
在测量索引扫描性能之前,必须先让表拥有足够的数据量。load_location_tablereducer(lib.rs)负责一次性灌入1000 个 chunk × 每 chunk 1200 行 = 120 万行:
const NUM_CHUNKS: u64 = 1000; const ROWS_PER_CHUNK: u64 = 1200; #[spacetimedb::reducer] pub fn load_location_table(ctx: &ReducerContext) { for chunk in 0u64..NUM_CHUNKS { for i in 0u64..ROWS_PER_CHUNK { let id = chunk * 1200 + i; let x = 0i32; let z = chunk as i32; let dimension = id as u32; ctx.db.location().insert(Location { id, chunk, x, z, dimension }); } } }数据构造是完全确定性的,几个设计细节值得注意:
id为全局连续编号:chunk * 1200 + i,因此id与chunk之间存在固定映射关系;x恒为 0,z等于 chunk 编号,dimension等于id;- 同一
chunk内的 1200 行共享相同的chunk与(x, z)值,仅id、dimension不同。
这种布局使得后续四个测试 reducer 能精确预测查询返回的行数:例如按chunk过滤必然命中 1200 行,按(0, z, dimension)三元组过滤必然命中恰好 1 行。可预测的返回规模是基准正确性的前提——断言失败即代表索引行为异常,基准门会因此报错。
此外,模块在文件顶部通过const ID: u64 = 989_987;与const CHUNK: u64 = ID / ROWS_PER_CHUNK;预计算了一个贯穿四个测试的"靶点"数据(对应 chunk 824),保证所有扫描都落在同一批数据上,测试之间可比。
四个索引扫描工作负载详解
四个测试 reducer 按"单列 / 多列"与"点查 / 批量扫描"两个维度覆盖四种组合,且每个 reducer 都在执行后用assert_eq!校验返回结果的行数与字段值。
1. 主键单行点查:test_index_scan_on_id
#[spacetimedb::reducer] /// Probing a single column index for a single row should be fast! pub fn test_index_scan_on_id(ctx: &ReducerContext) { let span = LogStopwatch::new("Index scan on {id}"); let location = ctx.db.location().id().find(ID).unwrap(); span.end(); assert_eq!(ID, location.id); }通过主键索引的访问器location().id().find(ID)做单行等值点查。这是索引的最快路径——按唯一主键定位恰好一行。断言ID == location.id验证取回的行确实是目标行,而非任意行。
2. 单列索引批量扫描:test_index_scan_on_chunk
#[spacetimedb::reducer] /// Scanning a single column index for `ROWS_PER_CHUNK` rows should also be fast! pub fn test_index_scan_on_chunk(ctx: &ReducerContext) { let span = LogStopwatch::new("Index scan on {chunk}"); let n = ctx.db.location().chunk().filter(&CHUNK).count(); span.end(); assert_eq!(n as u64, ROWS_PER_CHUNK); }通过单列 B-tree 索引chunk()做等值过滤,再用.count()统计命中行数。由于每个 chunk 恰好 1200 行,断言命中数等于ROWS_PER_CHUNK——这验证了索引扫描返回的集合是完整的,而非部分或重复。
3. 复合索引精确点查:test_index_scan_on_x_z_dimension
#[spacetimedb::reducer] /// Probing a multi-column index for a single row should be fast! pub fn test_index_scan_on_x_z_dimension(ctx: &ReducerContext) { let z = CHUNK as i32; let dimension = ID as u32; let span = LogStopwatch::new("Index scan on {x, z, dimension}"); let n = ctx.db.location().coordinates().filter((0, z, dimension)).count(); span.end(); assert_eq!(n, 1); }使用复合索引访问器coordinates(),以完整三元组(0, z, dimension)过滤。由于(x, z)相同但dimension不同的行恰好只有一行,断言命中数为 1,对应复合索引的完整键点查。
4. 复合索引前缀扫描:test_index_scan_on_x_z
#[spacetimedb::reducer] /// Probing a multi-column index for `ROWS_PER_CHUNK` rows should also be fast! pub fn test_index_scan_on_x_z(ctx: &ReducerContext) { let z = CHUNK as i32; let span = LogStopwatch::new("Index scan on {x, z}"); let n = ctx.db.location().coordinates().filter((0, z)).count(); span.end(); assert_eq!(n as u64, ROWS_PER_CHUNK); }同样是复合索引coordinates(),但只提供前两个前缀列(0, z)。B-tree 复合索引支持最左前缀匹配,命中该 chunk 下所有 1200 行。这验证了 SpacetimeDB 复合索引的前缀扫描能力,也是"索引必须能用前缀键查询"这一数据库通用语义在模块层的落实。
计时机制:LogStopwatch的底层实现
四个测试 reducer 都用LogStopwatch包裹被测查询,其实现位于 crates/bindings/src/log_stopwatch.rs:
pub struct LogStopwatch { stopwatch_id: u32, } impl LogStopwatch { pub fn new(name: &str) -> Self { let name = name.as_bytes(); let id = unsafe { spacetimedb_bindings_sys::raw::console_timer_start(name.as_ptr(), name.len()) }; Self { stopwatch_id: id } } pub fn end(self) { // just drop self } } impl std::ops::Drop for LogStopwatch { fn drop(&mut self) { unsafe { spacetimedb_bindings_sys::raw::console_timer_end(self.stopwatch_id); } } }实现机制值得注意:new调用console_timer_start向宿主机申请一个计时器并获得 id;end本身是空操作,真正的计时结束发生在Drop——当span变量离开作用域时自动调用console_timer_end上报耗时。这种RAII 式计时保证了即使函数提前return或panic,计时也不会泄漏。计时器名称(如"Index scan on {id}")会进入宿主机日志,便于在控制台输出中定位每个工作负载的耗时。
基准门index_scan_gate:如何运行与判定
运行方式
README 给出的一行命令即整个基准的入口:
cargo bench -p spacetimedb-bench --bench index_scan_gate-p spacetimedb-bench指向基准 cratecrates/bench(其 Cargo.toml 中name = "spacetimedb-bench",并声明了多个[[bench]]目标);--bench index_scan_gate指定只运行索引扫描基准门。
需要说明的是:仓库内crates/bench/benches目录下是callgrind.rs、delete_table.rs、generic.rs、index.rs、special.rs、subscription.rs等 bench 目标,而index_scan_gate的门控程序主体位于独立 cratecrates/index-scan-gate(其包名为spacetimedb-index-scan-gate)。整个门控流程由 crates/index-scan-gate/src/main.rs 承载。
门控流程与判定阈值
index_scan_gate的执行逻辑(main.rs)分为五步:
- 编译模块:
CompiledModule::compile("perf-test", CompilationMode::Release)以 Release 模式编译perf-test模块(基准测试必须用优化构建,否则测不到真实性能); - 加载模块:通过
start_runtime()启动内存运行时,并以IN_MEMORY_CONFIG(内存存储配置)加载模块,基准不涉及持久化; - 装载数据:调用
load_location_tablereducer,灌入 120 万行; - 采样与统计:对
REDUCERS列表中的四个 reducer,每个先做5 次预热(WARMUP_RUNS)再采集31 次有效样本(MEASURED_RUNS),取样本中位数作为该工作负载的代表耗时; - 阈值判定:若任一 reducer 的中位数耗时 ≥
MEDIAN_THRESHOLD(Duration::from_micros(100),即100 微秒),则整体失败并输出失败明细;全部低于阈值则输出通过信息。
四个被测 reducer 在门控程序中以数组形式列出(main.rs):
const REDUCERS: &[&str] = &[ "test_index_scan_on_id", "test_index_scan_on_chunk", "test_index_scan_on_x_z_dimension", "test_index_scan_on_x_z", ];判定结果示例
门控程序运行后会按 reducer 对齐打印中位数耗时,并给出最终结论。失败时输出形如:
test_index_scan_on_id median=... ... index scan benchmark failed; median threshold is 100µs; failures: ...全部通过时输出:
index scan benchmark passed; all medians are below 100µs选用中位数而非平均值作为统计量,是为了抵抗调度抖动等偶发噪声对结果的干扰;而WARMUP_RUNS + MEASURED_RUNS的两段式采样(5 + 31 次)则确保索引与缓存状态稳定后再进入测量。另外,在非 MSVC 目标上,程序还通过tikv_jemallocator将全局分配器切换为 jemalloc(main.rs),以接近生产环境的内存分配行为。
实战指引:如何查看、验证与扩展
- 查看表结构与负载:直接阅读 modules/perf-test/src/lib.rs 即可了解全部表定义、数据规模与四个工作负载的断言逻辑;
- 运行完整基准门:在仓库根目录执行
cargo bench -p spacetimedb-bench --bench index_scan_gate,程序会自动完成模块编译、数据装载、采样与阈值判定; - 观察单次 reducer 耗时:四个 reducer 内部通过
LogStopwatch向宿主日志输出Index scan on {id}、Index scan on {chunk}等计时信息,可在运行时日志中检索这些标记; - 关注判定结果:以 100 微秒中位数阈值为界,高于阈值意味着索引扫描性能出现明显回退,CI 或本地开发中应视为回归信号。
需要说明的适用前提:该基准门以 Release 构建 + 内存配置(IN_MEMORY_CONFIG)运行,其 100µs 阈值是针对此环境的回归门控设计,不直接等同于对外承诺的端到端延迟指标;若在调试构建或持久化配置下运行,数值会明显不同,不宜直接套用同一阈值。文章所述全部细节均基于当前仓库代码,若仓库后续调整表结构、采样次数或阈值常量,请以对应源码为准。
小结
perf-test模块与index_scan_gate基准门共同构成了一套自洽的索引性能回归测试体系:模块层用 120 万行确定性数据与四个 reducer 定义负载,门控层用"5 次预热 + 31 次采样 + 中位数 + 100µs 阈值"定义判定标准。阅读本文后,你可以从 modules/perf-test/README.md 出发,对照 lib.rs 理解每个工作负载的语义,再对照 crates/index-scan-gate/src/main.rs 掌握整条基准链路的调度与判读方式,从而在需要时自行运行、分析甚至扩展 SpacetimeDB 的索引扫描基准。
【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考