- 数据库
- 后端
【免费下载链接】convex-backend
The open-source reactive database for app developers
ScenarioRunner 是 convex-backend 仓库中与 LoadGenerator 配套的压测组件:LoadGenerator 负责统筹调度与统计,ScenarioRunner 则以真实 Convex 客户端(WebSocket 同步)的身份向被测后端发起 query / mutation / action / HTTP action 请求,并回传指标与错误。本文以 scenario-runner/README.md 为主线,结合其源码与 LoadGenerator 文档,完整讲解运行机制、内置场景以及"三步添加新场景"的扩展方法,读完即可在本地压测 Convex 后端,并编写自己的压测场景。
一、ScenarioRunner 在压测体系中的定位
ScenarioRunner 本身不直接发起压测调度,它被设计为 LoadGenerator 的执行端。在 crates/load_generator/README.md 中给出了完整的组件关系:
┌──────────┐ │ Stats │ │ Report │ ┌────────────────┐ ┌───────────────────┐ ┌───────────────┐ └──────────┘ │ │ │ │ queries, │ │ ▲ │ │ Scenarios │ │ mutations │ │ └─────────┬───│ LoadGenerator │──────────▶│ ScenarioRunner │──────────▶│ Backend │ ┌──────────────┐ │ │ │◀──────────│ │◀──────────│ │ │ │ │ │ │ Events │ │ │ │ │ Metrics │ │ └────────────────┘ └───────────────────┘ └───────────────┘ │ Collector │◀─┘ │(e.g. Datadog)│ │ │ └──────────────┘整个压测流水线分为四层:
- LoadGenerator(Rust):负责预置(provision)一个 Convex 后端实例,或对接一个已存在的实例;将预定义或自定义的
Scenario通过 WebSocket 下发给 ScenarioRunner;收集事件、生成统计报告,并可选择上报到 Datadog 等监控系统。 - ScenarioRunner(TypeScript/Node):本文主角。它在
127.0.0.1:<load-generator-port>上与 LoadGenerator 建立 WebSocket 连接(index.ts),接收场景消息、按配置的速率或线程数循环执行场景。 - Convex Client:每个场景通过
ConvexClient(来自convex/browser)向被测后端发起真实的同步请求,最大程度还原真实客户端行为。 - Backend:被测的 Convex 后端,可以是 LoadGenerator 预置的实例,也可以是自托管实例。
二、启动方式:先起 LoadGenerator,再被拉起
ScenarioRunner 的 README 明确指出:不要直接手动启动 ScenarioRunner,正确方式是运行 LoadGenerator,由它来预置后端并用给定参数拉起 ScenarioRunner。
2.1 LoadGenerator 帮助信息
在仓库根目录执行:
cargo run -p load_generator --bin load-generator -- --help需要追踪日志时,在运行命令前追加RUST_LOG=info。仓库中预置的压测工作负载可通过 Justfile 中的命令自动运行。
2.2 ScenarioRunner 的 CLI 接口
虽然通常由 LoadGenerator 拉起,但 ScenarioRunner 本身是一个 commander CLI(见 index.ts),其参数如下:
| 参数 | 必填 | 说明 |
|---|---|---|
--deployment-url <url> | 是 | 被测部署的 URL,场景将对其发起请求 |
--admin-key <admin_key> | 是 | 访问部署的管理员密钥(SnapshotExport 等场景会用到) |
--scenarios <json> | 是 | JSON 格式的场景列表,{"scenarios": [...]} |
--load-generator-port <port> | 是 | 连接 LoadGenerator 的端口,WebSocket 地址为ws://127.0.0.1:<port>/sync |
--provision-host <host> | 否 | big brain(预置服务)的地址,用于查询部署所属的 team/project |
--access-token <token> | 否 | 访问 big brain 的令牌 |
两个可选参数成对使用:当provisionHost与accessToken同时给出时,ScenarioRunner 会先请求部署的/instance_name,再向${provisionHost}/api/deployment/${deploymentName}/team_and_project查询deploymentId,构造出ProvisionerInfo供场景使用(index.ts)。
2.3 构建与运行
# 在 npm-packages/scenario-runner 目录下 npm run build # 先 tsc 编译,再用 esbuild 打包为 dist/scenario-runner.js(node 平台,含 sourcemap) npm run start # node dist/scenario-runner.js构建脚本见 package.json。依赖方面,场景运行依赖convex(工作区包)、ws、@sentry/node;random-words、langchain、tiktoken等用于构造搜索与向量场景的测试数据。
三、场景执行核心机制
3.1 场景消息与分发
LoadGenerator 下发的每条消息是ScenarioMessage(types.ts):
export type ScenarioMessage = { scenario: ScenarioSpec; rate: number | null; // 每秒请求数;null 表示 benchmark 模式 threads?: number; // benchmark 模式下的并发线程数 };runScenario(index.ts)根据scenarioSpec.name用 switch 分发到对应场景类:
RunFunction→RunFunction,携带path与fn_typeObserveInsert→ObserveInsert,携带search_indexesManyIntersections→ManyIntersections,携带num_subscriptionsHoldSubscriptions→HoldSubscriptions,携带num_subscriptions、hold_duration_secs、invalidation_interval_secs、num_invalidationsSnapshotExport、CloudBackup、Search、VectorSearch无额外参数RunHttpAction→RunHttpAction,携带path与method
每个场景都有类型化的参数定义(ScenarioSpec,见 types.ts),default 分支使用satisfies never做穷尽检查,确保新增场景名称后编译器强制要求补全分发逻辑。
3.2 两种负载模式:rate 模式与 benchmark 模式
rate字段决定执行方式(index.ts):
- rate 模式:
rate为具体数值(每秒请求数)。每次执行前随机等待0 ~ 2 * (1000 / rate)毫秒,模拟带抖动的泊松式到达分布。rate === 0则直接跳过该场景。 - benchmark 模式:
rate === null。此时使用threads(默认 1)创建对应数量的并发"线程",每个线程持有一个独立ConvexClient,无限循环执行场景直到进程被终止,用于打满后端吞吐。
{ "name": "RunFunction", "path": "query_index:queryMessagesWithSearch", "fn_type": "query", "benchmark": 80 }上面是 benchmark_query.json 中benchmark: 80的含义——80 个并发线程持续查询。而 prod.json 则展示了混合 rate 工作负载:查询 10 rps、写入 2 rps、ObserveInsert 5 rps、VectorSearch 5 rps、SnapshotExport 0.0005 rps 等。
3.3 防挂死保护
两个与健壮性相关的细节值得注意:
closeWithTimeout:client.close()只有在 WebSocketonclose事件触发后才 resolve。当连接处于半开/卡死状态(如后端以 1011 InternalServerError 关闭,或 WS 升级失败返回非 101 状态码),该事件可能永不触发,导致场景循环无限挂起、静默停止吞吐。因此用Promise.race加上CLOSE_TIMEOUT(10 秒)兜底(index.ts)。- 超时常量(types.ts):action 与 HTTP action 超时 5 秒;query 与 mutation 超时 2 秒(因为 UDF 超时是 1 秒,需要留出网络与后端开销);导出场景最多 2 小时(对应 500MB 数据库上限)。
四、Scenario 基类:每个场景的公共能力
所有场景类都继承抽象基类Scenario并实现IScenario接口(scenario.ts)。基类封装了指标上报、错误上报、超时执行与资源清理四大能力:
4.1 指标与错误上报
场景通过构造函数注入的loadGenWS(连接 LoadGenerator 的 WebSocket)回传事件,LoadGenerator 端据此生成统计报告:
sendCountMetric(value, name, path?):发送计数指标,如mutation_send_timeout、export_completed。sendLatencyMetric(value, name, path?):发送延迟指标(秒),如mutation_completed、mutation_observed、query、vector_search。sendError(err, name)/sendDefaultError(err):上报错误事件(含消息、错误名、场景名),同时console.error并接入 Sentry(tracesSampleRate: 0.1)。
所有事件经JSON.stringify后通过 WebSocket 发送(scenario.ts)。指标名称使用联合类型ScenarioLatencyMetric/ScenarioCountMetric约束,拼写错误在编译期即被拦截。
4.2 超时执行工具
executeOrTimeout(promise, timeoutDuration, timeoutMetricName, path?) executeOrTimeoutWithLatency(promise, timeoutDuration, timeoutMetricName, latencyMetricName, t0, path?)后者在 promise 完成时记录nowSeconds() - t0的延迟;若超时则发送对应的超时计数指标。RunFunction用它分别测量 query / mutation / action 三种函数类型的延迟与超时(run_function.ts)。
4.3 waitForQuery 订阅等待
waitForQuery(client, query, args, isReady)订阅一个公开 query,返回首个满足isReady(result)的结果的 Promise;订阅会一直保持到场景结束,并自动注册退订清理。ObserveInsert正是用它在插入 mutation 发出后,等待订阅查询观察到这条新数据(observe_insert.ts),从而测得"mutation 完成"与"mutation 被订阅查询观察到"两个关键延迟。
4.4 清理机制
registerCleanUp(fn)注册清理回调(如退订),cleanUp()在每次场景运行结束后统一执行。清理是保证执行的——即使场景超时或出错也会运行(scenario.ts),避免订阅泄漏拖垮后续迭代。
五、内置场景盘点
scenarios/目录下已有 9 个场景实现,覆盖 Convex 的主要能力面:
| 场景 | 文件名 | 验证目标 |
|---|---|---|
| RunFunction | run_function.ts | 按path+fn_type调用任意 query/mutation/action,测量延迟与超时 |
| RunHttpAction | run_http_action.ts | 通过 HTTP action 路由(如basic、streaming)发起请求 |
| ObserveInsert | observe_insert.ts | 插入一行数据,测量"mutation 完成"与"订阅观察到插入"两个延迟;search_indexes为 true 时使用带搜索索引的表(场景名变为ObserveInsertWithSearch) |
| Search | search.ts | 全文搜索,校验搜索结果与文档一致性(search_document_mismatch) |
| VectorSearch | vector_search.ts | 随机取一条含 1536 维 embedding(对齐 OpenAI text-embeddings)的文档,执行向量搜索并校验命中 id 与分数(>0.99) |
| ManyIntersections | many_intersections.ts | 大量订阅并发下的写入观察,num_subscriptions控制订阅数 |
| HoldSubscriptions | hold_subscriptions.ts | 长期持有订阅,周期性触发失效(invalidation_interval_secs、num_invalidations),压测订阅失效链路 |
| SnapshotExport | snapshot_export.ts | 请求并跟踪快照导出任务,指标含request_export_succeeded、export_completed、export_in_progress、export_timeout |
| CloudBackup | cloud_backup.ts | 请求云备份并等待完成 |
场景背后的 Convex 函数位于 convex/ 目录:insert.ts、update.ts、query_index.ts、search.ts、schedule.ts、vectorSearch.ts、components.ts(组件查询/变更)、openclaurd.ts(低基数数据、含 1536 维向量)等。convex.json默认将prodUrl指向http://127.0.0.1:8000(本地后端)。
六、添加新场景:三步完整指南
README 给出了添加新场景的三步流程,下面结合源码逐层展开。
第一步:命名场景并注册到分发控制流
- 在 metrics.ts 的
ScenarioName联合类型中加入新场景名。注意该文件的头部注释:This file is automatically generated by cargo test -p load_generator——正确做法是先在 Rust 侧(crates/load_generator/src/metrics.rs)添加指标,再通过cargo test -p load_generator自动生成该文件,避免手工编辑被覆盖。 - 在 types.ts 的
ScenarioSpec中为新场景声明类型化参数(若需要)。 - 在 index.ts 的
runScenarioswitch 中添加 case,构造对应场景实例并把ScenarioSpec中的参数传入。default 分支的satisfies never会强制编译器在漏加 case 时报错。
第二步:编写场景类
在 scenarios/ 目录新建文件,实现IScenario接口并继承Scenario基类:
import { ConvexClient } from "convex/browser"; import { Config, IScenario, Scenario } from "../scenario"; import { ScenarioError } from "../metrics"; export class MyScenario extends Scenario implements IScenario { constructor(config: Config, /* 你的参数 */) { super("MyScenario", config); // 保存参数 } async run(client: ConvexClient) { // 1. 用 client.query / client.mutation / client.action 发起请求 // 2. 用 this.executeOrTimeoutWithLatency(...) 包住请求以测延迟与超时 // 3. 用 this.waitForQuery(...) 订阅等待结果 // 4. 用 this.sendCountMetric / this.sendLatencyMetric 上报指标 } defaultErrorName(): ScenarioError { return "mutation"; // 或你自定义的错误名 } }要点:
- 构造函数第一个参数必须是
Config(含deploymentUrl、loadGenWS、可选的provisionerInfo),并把name传给基类。 run(client)中可以使用基类提供的全部工具(见第四节),务必对每个可能失败的请求套上executeOrTimeout系列,防止单个请求卡死整个压测循环。- 如果场景建立了订阅,用
registerCleanUp注册退订,确保异常后不泄漏。 - 如果需要在 Rust 侧统计新指标(如延迟、计数、错误名),同步在
crates/load_generator/src/metrics.rs中注册。
写完场景类后,从第一步的 switch 中调用它即可。
第三步:在 LoadGenerator 中登记场景
ScenarioRunner 只是执行端,LoadGenerator 必须知道新场景才能下发。需要:
- 在 Rust 侧
crates/load_generator的Scenariostruct 中为新场景增加对应字段/变体(README 明确要求"add a new scenario to theScenariostruct")。 - 在 crates/load_generator/src/metrics.rs 注册新指标,运行
cargo test -p load_generator重新生成 metrics.ts,保证两端指标枚举一致。 - 在 workload JSON 中引用新场景,例如 prod.json 中的模式:
{"name": "MyScenario", "rate": 5},或 benchmark 模式{"name": "MyScenario", "benchmark": 10}。
七、编写自定义 Convex 函数配合压测
除新建场景类外,更轻量的扩展方式是只新增 Convex 函数并复用内置的RunFunction场景。把无参函数(或接受固定参数的函数)放入 convex/ 文件夹,然后:
{ "name": "your_new_workload", "scenarios": [ { "name": "RunFunction", "path": "<your-new-module>:<your-function-name>", "fn_type": "mutation", "rate": 5 } ] }path格式为<模块名>:<函数名>,fn_type可取query、mutation或action,rate可替换为benchmark线程数。这是 crates/load_generator/README.md 推荐的"自定义场景"快捷路径。
八、对自托管 Convex 后端做压测
ScenarioRunner 同样可用于压测自托管后端,完整流程如下:
推送函数(只在测试专用后端上执行,勿在正式实例上操作):把 scenario-runner 的 Convex 函数部署到自托管后端,这会替换该后端的函数:
cd npm-packages/scenario-runner npx convex deploy --admin-key=<your-admin-key> --url=<your-backend-url>运行 LoadGenerator 指向现有实例(复用 workloads/ 下的示例或自定义 workload):
cd ../../crates/load_generator just self-hosted crates/load_generator/workloads/<your-workload>.json --existing-instance-url <your-backend-url> --existing-instance-admin-key <your-admin-key>
--existing-instance-url与--existing-instance-admin-key让 LoadGenerator 跳过预置流程、直接使用现有实例,其余场景下发与指标收集逻辑与预置模式完全一致。
九、端到端压测一次完整的请求流
以 prod.json 中ObserveInsert(rate 5)为例,一次迭代的完整链路是:
- ScenarioRunner 在
ws://127.0.0.1:<port>/sync收到{"scenario": {"name": "ObserveInsert", "search_indexes": true}, "rate": 5}。 runScenario构造ObserveInsert实例,等待平均 200ms 的随机抖动后开始迭代。- 场景先通过
waitForQuery订阅queryMessagesWithArgs(带rand过滤条件),再发起insertMessageWithArgsmutation,测出mutation_completed延迟;随后等待订阅结果中出现rand匹配且timestamp >= startTime的记录,测出mutation_observed延迟。 - 两个延迟指标经 WebSocket 回传 LoadGenerator,由其聚合成统计报告;任何超时都会触发
mutation_send_timeout/mutation_observed_timeout计数指标。 - 迭代结束后
cleanUp()退订,进入下一次循环。
这套"真实客户端 + 订阅观察 + 延迟/超时双指标 + 端到端清理"的模式,正是 Convex 反应式数据库压测的核心价值:不仅能测出函数执行延迟,还能量化数据变更传播到订阅客户端的端到端延迟。
- 数据库
- 后端
【免费下载链接】convex-backend
The open-source reactive database for app developers
相关推荐
Convex LoadGenerator 压测工具指南:从负载场景编排到延迟统计报告
Convex LoadGenerator 压测工具指南:从负载场景编排到延迟统计报告 LoadGenerator 是 convex backend 仓库内置的负
数据库后端Convex Backend 压测指南:使用 LoadGenerator 对自托管 Convex 实例进行基准测试
Convex Backend 压测指南:使用 LoadGenerator 对自托管 Convex 实例进行基准测试 导读 本文基于 self hosted/ad
数据库后端LifeOS Evals 实战:编写多轮 Agent 场景的 CreateScenario 工作流与 ScenarioRunner 执行机制
LifeOS Evals 实战:编写多轮 Agent 场景的 CreateScenario 工作流与 ScenarioRunner 执行机制 LifeOS 的
AI 技能人工智能AI 应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考