
RivetKit Actor 的 SQLite 大负载写入基准测试从 1 MiB 到 10 MiB 的性能剖析与瓶颈定位【免费下载链接】actorsRivet Actors are the primitive for stateful workloads. Built for AI agents, collaborative apps, and durable execution.项目地址: https://gitcode.com/GitHub_Trending/riv/actorsRivetKit Actor 通过 KV-backed SQLite VFS 为有状态 Actor 提供原生 SQL 持久化能力。本文以仓库内examples/sqlite-raw示例自带的大负载插入基准为切入口完整呈现 1 MiB / 5 MiB / 10 MiB 三类负载下 Actor 路径与本地原生 SQLite 的实测差距并结合BENCH_RESULTS.md的调试追踪线索与底层源码逐层拆解慢在哪里、为什么慢、证据是什么。读完本文你将掌握该基准的复现方法、结果解读方式以及从 KV 往返次数、页分块粒度、引擎提交限额三个维度定位 SQLite-over-KV 性能瓶颈的完整思路。基准背景RivetKit Actor 的 SQLite-over-KV 架构RivetKit Actor 把 SQLite 当作持久化存储暴露给用户但底层数据并不直接落在本地磁盘文件上而是通过一个KV-backed SQLite VFSsqlite_native::vfs存储到 Actor 的 key-value store 中。examples/sqlite-raw/README.md明确说明The database uses the KV-backed SQLite VFS, which stores data in a key-value store.这意味着每一次 SQLite 的页读写都会被翻译成 KV 的get/put操作并通过引擎通道tunnel在客户端与远端 Actor 之间往返。用户感知到的是一条 SQL底层发生的却是一连串跨网络的 KV 往返——这正是理解后续所有基准数据的前提。在 examples/sqlite-raw/src/index.ts 中Actor 通过db(...)配置项声明数据库并在onMigrate钩子中用原生 SQL 建表payload_bench表即基准专用db: db({ onMigrate: async (db) { await db.execute( CREATE TABLE IF NOT EXISTS payload_bench ( id INTEGER PRIMARY KEY AUTOINCREMENT, label TEXT NOT NULL, payload TEXT NOT NULL, payload_bytes INTEGER NOT NULL, created_at INTEGER NOT NULL ) ); }, })同时actionTimeout: 300_0005 分钟保证大负载插入动作不会被超时中断。基准测试环境与复现方法前置准备示例位于examples/sqlite-raw采用 pnpm workspace 管理依赖pnpm install pnpm devpnpm dev会以tsx --watch启动 src/index.ts拉起本地 Actor registry。基准脚本默认连接引擎端点http://127.0.0.1:6420本地 dev 引擎默认端口。运行命令BENCH_RESULTS.md记录的基准命令在仓库根目录执行pnpm --dir examples/sqlite-raw bench:large-insert通过环境变量可以调节负载规模与端点BENCH_MB1 pnpm --dir examples/sqlite-raw bench:large-insert BENCH_MB5 pnpm --dir examples/sqlite-raw bench:large-insert BENCH_MB10 pnpm --dir examples/sqlite-raw bench:large-insert开启 VFS 层调试日志用于 KV 往返追踪RUST_LOGrivetkit_sqlite_native::vfsdebug BENCH_MB1 pnpm --dir examples/sqlite-raw bench:large-insert环境变量与测量口径依据 examples/sqlite-raw/README.md 和 scripts/bench-large-insert.ts三个环境变量的语义如下环境变量默认值含义BENCH_MB10总负载大小MiB经totalBytes DEFAULT_MB * 1024 * 1024换算BENCH_ROWS1负载拆分的行数payloadBytes totalBytes / rowCountRIVET_ENDPOINThttp://127.0.0.1:6420引擎端点测量指标共四组两组取自 Actor 返回结果两组由脚本本地计时Actor DB InsertActor 侧benchInsertPayload动作内部BEGIN到COMMIT的插入耗时performance.now()计时Actor DB Verify插入完成后执行SELECT COALESCE(SUM(payload_bytes), 0), COUNT(*) ... WHERE label ?的校验耗时用于确认数据真实落库End-to-End Action客户端createClient(...).todoList.getOrCreate([...])拿到 actor 句柄后调用benchInsertPayload动作的完整往返耗时Native SQLite Insert脚本用node:sqlite的DatabaseSync在本地临时目录mkdtempSync(join(tmpdir(), sqlite-raw-bench-))建库开启PRAGMA journal_modeWAL与PRAGMA synchronousNORMAL在单个事务内插入同样字节数的行作为基线见 scripts/bench-large-insert.ts。脚本最后输出两个倍率actor db vs nativeActor 库内耗时 / 原生耗时与end-to-end vs native含网络与动作调度后的端到端耗时 / 原生耗时完整输出逻辑见 scripts/bench-large-insert.ts。实测结果完整数据与解读BENCH_RESULTS.md捕获于2026-04-15负载形态为单行含一个大型TEXT负载对比基线为本地磁盘上的原生 SQLite给出的三组实测数据如下PayloadActor DB InsertActor DB VerifyEnd-to-End ActionNative SQLite InsertActor DB vs NativeEnd-to-End vs Native1 MiB832.2ms0.4ms1137.6ms1.8ms461.11x630.34x5 MiB4199.6ms8973.5ms8186.3ms25.3ms166.19x323.96x10 MiB9438.2ms8973.5ms19244.0ms45.5ms207.34x422.75x注原表 5 MiB 行的 Verify 列记录为 8973.5ms与 10 MiB 行数值相同疑为记录笔误本文按原表照录。读者复现时建议以脚本实时输出为准。解读要点差距随负载非线性放大1 MiB 时 Actor 库内插入 832ms慢 461 倍10 MiB 时已达 9.4s慢 207 倍且端到端达到 19.2s。Verify 阶段的异常10 MiB 下插入后仅做一次SUMCOUNT聚合查询就耗费约 9s说明读取路径同样昂贵而非写入独慢。原生 SQLite 完全不是瓶颈45.5ms 完成 10 MiB 单事务写入印证BENCH_RESULTS.md的结论——Native SQLite is fast enough that the bottleneck is clearly not SQLite itself.端到端 库内10 MiB 下端到端 19.2s 比 Actor 库内 9.4s 多出约 9.8s这部分来自客户端到引擎的通道传输、动作调度与序列化开销。调试追踪线索一次 1 MiB 插入的 KV 账本BENCH_RESULTS.md记录了一次带RUST_LOGrivetkit_sqlite_native::vfsdebug的 1 MiB 追踪运行数据极具诊断价值317次 KV 往返round-trips30次get(...)调用287次put(...)调用577个写入的 keykey 数量大于 put 次数说明单次 put 可能携带多个 key或存在页覆盖写追踪到的 KV 聚合耗时get共63.1msput共856.0ms把这三组数字放在一起瓶颈画像已经清晰一次仅 1 MiB 的插入就要触发 317 次跨通道 KV 往返平均每次往返约 2.9msput侧承担了绝对大头856ms / 63.1ms ≈ 13.6 倍于 get即写放大主导延迟而 287 次 put 只写出 577 个 key暗示单次 KV 写覆盖多个 4 KiB 页。瓶颈定位4 KiB 页分块与 KV 通道的写放大BENCH_RESULTS.md给出的Likely Bottleneck结论原文是The current SQLite-over-KV path is chunking the database into4 KiB pagesand issuing a large number of KV writes and reads through the tunnel for a single large insert. The evidence points much more strongly at the SQLite VFS / KV channel / engine path than at raw SQLite execution.结合仓库源码这条结论可以从三个层面印证1. 页粒度与 KV 写放大SQLite 以固定页默认 4096 字节即 4 KiB为单位读写数据库文件。KV-backed VFS 将每个页作为一个 KV 值存储因此插入 10 MiB 负载至少涉及约 2560 个页的落盘再加上 B-tree 内部页、索引页payload_bench上有idx_payload_bench_label索引的变更脏页总数会进一步放大。每个脏页都要经通道put到远端写放大因子远高于 1。2. 引擎侧的提交限额约束KV-backed SQLite 的每次提交都会落到引擎的 depot 层。在 engine/packages/depot/src/conveyer/constants.rs 中可以看到两个硬性限额pub const MAX_COMMIT_RAW_DIRTY_BYTES: usize MAX_COMMIT_DIRTY_PAGES * crate::conveyer::keys::PAGE_SIZE as usize; pub const MAX_SINGLE_SHOT_COMMIT_DIRTY_PAGES: usize 320; pub const MAX_SINGLE_SHOT_COMMIT_RAW_DIRTY_BYTES: usize MAX_SINGLE_SHOT_COMMIT_DIRTY_PAGES * crate::conveyer::keys::PAGE_SIZE as usize;单次unstaged提交最多携带320 个脏页按 4 KiB 页计算即约 1.3 MiBrivetkit-core的注释也确认了这一点Depot rejects SQLite commits that dirty more thanMAX_COMMIT_RAW_DIRTY_BYTES(320 pages * 4 KiB 1.3 MiB)。于是 10 MiB 负载会被迫拆成多批提交每批都完整经历一次页收集、KV 传输与引擎持久化延迟随负载线性甚至超线性累积。3. 运行时侧的交易预算与分批在 rivetkit-rust/packages/rivetkit-core/src/actor/internal_storage/mod.rs 中运行时为每笔 KV 事务定义了明确的预算这正是为了配合 depot 的提交限额/// Depot rejects SQLite commits that dirty more than MAX_COMMIT_RAW_DIRTY_BYTES /// (320 pages * 4 KiB 1.3 MiB) ... pub(crate) const KV_TX_MAX_PAYLOAD_BYTES: usize 512 * 1024; /// Row cap per transaction paired with the payload budget above. pub(crate) const KV_TX_MAX_ROWS: usize 128;split_kv_tx_chunks同一文件 L50-L75会把超预算的 KV 条目切分为多个 512 KiB / 128 行的块逐块提交——大写入被进一步碎片化。同时用户 KV 值存在 128 KiB 上限、key 存在 2 KiB 上限同文件 L26-L304 KiB 页虽然落在限制内但大量小页值的批量 put 反而放大了每笔 KV 的固定开销。从源码结构可以推断单次大 INSERT 的实际路径是SQL 执行 → 页缓存刷盘 → 按页拆 KV → 按预算分批 → 每批经通道往返 → 引擎 depot 限额校验 → 分段提交链条中任意一环节的固定开销都会被页数放大。性能现状与结论综合BENCH_RESULTS.md的 Notes 与源码证据可以得出以下经过验证的结论瓶颈不在 SQLite 本身原生 SQLite 10 MiB 插入仅需 45.5ms而 Actor 路径端到端 19.2s本机复现值生产环境报告为 26.2s瓶颈在 SQLite VFS / KV 通道 / 引擎路径单次 1 MiB 插入即产生 317 次 KV 往返、287 次 putput 侧累计耗时 856ms页级 KV 写放大是延迟的主要来源读路径同样昂贵10 MiB 负载下插入后的聚合校验查询Verify耗时约 9s说明 KV 页读取的往返成本与大页集扫描不可忽视分批提交放大固定开销引擎单次提交 320 脏页约 1.3 MiB的限额与运行时 512 KiB / 128 行的交易预算共同迫使大负载拆分为多批跨通道提交。复现与后续验证建议若要在当前仓库复现或进一步验证上述结论可以按以下路径操作复现基准确保本地引擎在http://127.0.0.1:6420运行后依次执行BENCH_MB1 / 5 / 10 pnpm --dir examples/sqlite-raw bench:large-insert比对输出与本文结果表观察 KV 往返使用RUST_LOGrivetkit_sqlite_native::vfsdebug运行核对get/put次数与聚合耗时验证 4 KiB 页分块假设验证提交限额阅读 engine/packages/depot/src/conveyer/constants.rs 与 rivetkit-rust/packages/rivetkit-core/src/actor/internal_storage/mod.rs确认当前版本的分批参数是否仍是 320 脏页 / 512 KiB 预算关注后续优化大负载场景下可重点跟踪 VFS 层是否引入更大的页大小、页级写入合并write coalescing或批量 KV 通道能力——这些方向直接针对本文定位的写放大与往返次数两个根因。BENCH_RESULTS.md作为一份问题定位型基准记录价值不在于数字本身而在于它示范了一条可复用的诊断链路先量端到端再分库内/客户端两层切分最后用 VFS 日志量化 KV 账本。任何 SQLite-over-KV 类存储的性能排查都可以照此流程收敛到具体环节。【免费下载链接】actorsRivet Actors are the primitive for stateful workloads. Built for AI agents, collaborative apps, and durable execution.项目地址: https://gitcode.com/GitHub_Trending/riv/actors创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考