convex-backend ScenarioRunner 场景压测指南:由 LoadGenerator 驱动的客户端场景编写与扩展实战
2026/9/24 5:40:30 网站建设 项目流程
  • 数据库
  • 后端

【免费下载链接】convex-backend

The open-source reactive database for app developers

项目地址:https://gitcode.com/gh_mirrors/co/convex-backend
点击查看免费下载

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)│ │ │ └──────────────┘

整个压测流水线分为四层:

  1. LoadGenerator(Rust):负责预置(provision)一个 Convex 后端实例,或对接一个已存在的实例;将预定义或自定义的Scenario通过 WebSocket 下发给 ScenarioRunner;收集事件、生成统计报告,并可选择上报到 Datadog 等监控系统。
  2. ScenarioRunner(TypeScript/Node):本文主角。它在127.0.0.1:<load-generator-port>上与 LoadGenerator 建立 WebSocket 连接(index.ts),接收场景消息、按配置的速率或线程数循环执行场景。
  3. Convex Client:每个场景通过ConvexClient(来自convex/browser)向被测后端发起真实的同步请求,最大程度还原真实客户端行为。
  4. 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 的令牌

两个可选参数成对使用:当provisionHostaccessToken同时给出时,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/noderandom-wordslangchaintiktoken等用于构造搜索与向量场景的测试数据。

三、场景执行核心机制

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 分发到对应场景类:

  • RunFunctionRunFunction,携带pathfn_type
  • ObserveInsertObserveInsert,携带search_indexes
  • ManyIntersectionsManyIntersections,携带num_subscriptions
  • HoldSubscriptionsHoldSubscriptions,携带num_subscriptionshold_duration_secsinvalidation_interval_secsnum_invalidations
  • SnapshotExportCloudBackupSearchVectorSearch无额外参数
  • RunHttpActionRunHttpAction,携带pathmethod

每个场景都有类型化的参数定义(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 防挂死保护

两个与健壮性相关的细节值得注意:

  • closeWithTimeoutclient.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_timeoutexport_completed
  • sendLatencyMetric(value, name, path?):发送延迟指标(秒),如mutation_completedmutation_observedqueryvector_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 的主要能力面:

场景文件名验证目标
RunFunctionrun_function.tspath+fn_type调用任意 query/mutation/action,测量延迟与超时
RunHttpActionrun_http_action.ts通过 HTTP action 路由(如basicstreaming)发起请求
ObserveInsertobserve_insert.ts插入一行数据,测量"mutation 完成"与"订阅观察到插入"两个延迟;search_indexes为 true 时使用带搜索索引的表(场景名变为ObserveInsertWithSearch
Searchsearch.ts全文搜索,校验搜索结果与文档一致性(search_document_mismatch
VectorSearchvector_search.ts随机取一条含 1536 维 embedding(对齐 OpenAI text-embeddings)的文档,执行向量搜索并校验命中 id 与分数(>0.99)
ManyIntersectionsmany_intersections.ts大量订阅并发下的写入观察,num_subscriptions控制订阅数
HoldSubscriptionshold_subscriptions.ts长期持有订阅,周期性触发失效(invalidation_interval_secsnum_invalidations),压测订阅失效链路
SnapshotExportsnapshot_export.ts请求并跟踪快照导出任务,指标含request_export_succeededexport_completedexport_in_progressexport_timeout
CloudBackupcloud_backup.ts请求云备份并等待完成

场景背后的 Convex 函数位于 convex/ 目录:insert.tsupdate.tsquery_index.tssearch.tsschedule.tsvectorSearch.tscomponents.ts(组件查询/变更)、openclaurd.ts(低基数数据、含 1536 维向量)等。convex.json默认将prodUrl指向http://127.0.0.1:8000(本地后端)。

六、添加新场景:三步完整指南

README 给出了添加新场景的三步流程,下面结合源码逐层展开。

第一步:命名场景并注册到分发控制流

  1. 在 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自动生成该文件,避免手工编辑被覆盖。
  2. 在 types.ts 的ScenarioSpec中为新场景声明类型化参数(若需要)。
  3. 在 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(含deploymentUrlloadGenWS、可选的provisionerInfo),并把name传给基类。
  • run(client)中可以使用基类提供的全部工具(见第四节),务必对每个可能失败的请求套上executeOrTimeout系列,防止单个请求卡死整个压测循环。
  • 如果场景建立了订阅,用registerCleanUp注册退订,确保异常后不泄漏。
  • 如果需要在 Rust 侧统计新指标(如延迟、计数、错误名),同步在crates/load_generator/src/metrics.rs中注册。

写完场景类后,从第一步的 switch 中调用它即可。

第三步:在 LoadGenerator 中登记场景

ScenarioRunner 只是执行端,LoadGenerator 必须知道新场景才能下发。需要:

  1. 在 Rust 侧crates/load_generatorScenariostruct 中为新场景增加对应字段/变体(README 明确要求"add a new scenario to theScenariostruct")。
  2. 在 crates/load_generator/src/metrics.rs 注册新指标,运行cargo test -p load_generator重新生成 metrics.ts,保证两端指标枚举一致。
  3. 在 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可取querymutationactionrate可替换为benchmark线程数。这是 crates/load_generator/README.md 推荐的"自定义场景"快捷路径。

八、对自托管 Convex 后端做压测

ScenarioRunner 同样可用于压测自托管后端,完整流程如下:

  1. 推送函数(只在测试专用后端上执行,勿在正式实例上操作):把 scenario-runner 的 Convex 函数部署到自托管后端,这会替换该后端的函数:

    cd npm-packages/scenario-runner npx convex deploy --admin-key=<your-admin-key> --url=<your-backend-url>
  2. 运行 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)为例,一次迭代的完整链路是:

  1. ScenarioRunner 在ws://127.0.0.1:<port>/sync收到{"scenario": {"name": "ObserveInsert", "search_indexes": true}, "rate": 5}
  2. runScenario构造ObserveInsert实例,等待平均 200ms 的随机抖动后开始迭代。
  3. 场景先通过waitForQuery订阅queryMessagesWithArgs(带rand过滤条件),再发起insertMessageWithArgsmutation,测出mutation_completed延迟;随后等待订阅结果中出现rand匹配且timestamp >= startTime的记录,测出mutation_observed延迟。
  4. 两个延迟指标经 WebSocket 回传 LoadGenerator,由其聚合成统计报告;任何超时都会触发mutation_send_timeout/mutation_observed_timeout计数指标。
  5. 迭代结束后cleanUp()退订,进入下一次循环。

这套"真实客户端 + 订阅观察 + 延迟/超时双指标 + 端到端清理"的模式,正是 Convex 反应式数据库压测的核心价值:不仅能测出函数执行延迟,还能量化数据变更传播到订阅客户端的端到端延迟

  • 数据库
  • 后端

【免费下载链接】convex-backend

The open-source reactive database for app developers

项目地址:https://gitcode.com/gh_mirrors/co/convex-backend
点击查看免费下载

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

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

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

立即咨询