news 2026/9/13 17:53:56

Turso 事务延迟基准测试指南:从零复现 SQLite 与 Turso 的写入延迟对比实验

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Turso 事务延迟基准测试指南:从零复现 SQLite 与 Turso 的写入延迟对比实验

Turso 事务延迟基准测试指南:从零复现 SQLite 与 Turso 的写入延迟对比实验

【免费下载链接】tursoA SQL database in Rust: SQLite-compatible, now also speaking Postgres (experimental). The LLVM of databases.项目地址: https://gitcode.com/GitHub_Trending/tu/turso

本文是 Turso 仓库中事务延迟基准测试(transaction latency benchmark)的完整技术指南。该基准位于 perf/latency 目录,用于测量一个小型写事务从"应该开始执行"到"提交完成"在 SQLite 与 Turso 中的耗时分布,并以 eCDF(经验累积分布函数)图形对比两个引擎在 1、8、16、32 个并发连接下的尾延迟表现。读完本文,你将掌握该基准的完整设计思想(开环负载、无协调遗漏、分阶段采样)、全部环境变量与命令行参数、底层引擎调用链(含 io_uring、MVCC、被动检查点等关键机制),并能直接复现实验、读懂输出文件与绘图结果。

基准测试要回答什么问题

数据库性能对比中最常见的陷阱是"协调遗漏"(coordinated omission):如果测试程序在数据库变慢时停止向其发送新请求,那么数据库"忙于阻塞"的时间段就不会被任何样本覆盖,测得的结果会虚低——一个被写锁卡住的数据库看起来反而"很快",因为它只是暂时没人问它要活干。本基准的核心设计目标,正是把等待写锁、排队阻塞的时间真实地计入延迟样本。

具体做法是:事务按照一个固定的到达时刻表运行,这个时刻表不会因为数据库变慢而调整turso-txn-latency的 main.rs 模块注释对此有明确的说明:事务的延迟从它"应当开始"的时刻算起,排在前面的事务造成的等待会被计入样本;否则一个阻塞中的数据库会显得很快,因为它忙的时候干脆没被派活,只有它准备好的事务才会被计时——这恰好掩盖了本基准要测量的停顿。

工作量、加载方式、持久化语义、检查点策略和运行规则五大部分构成了基准的方法论骨架,接下来逐一展开。

快速开始

一键运行(完整实验)

perf/latency目录下执行:

./scripts/run.sh

run.sh的工作分为两阶段(见 run.sh):

  1. 调用scripts/bench.sh构建基准程序并以默认参数跑完全部测试;
  2. 检查系统是否装有uv(绘图所需的 Python 包管理器,缺失时会报错退出),然后调用 plot-latency-ecdf.py 绘制 eCDF 图形。

完整实验的行为如下:

  • 在两个引擎上分别以 1、8、16、32 个连接各跑 3 次,每次负载为 1000 事务/秒;
  • 总耗时约 1 小时,其中大部分时间花在每次运行前让磁盘空闲 1 分钟;
  • 每次运行前都会请求sudo权限,用于对磁盘执行fstrim(告诉驱动哪些块空闲)并清空页面缓存;
  • 运行环境要求:Rust 工具链、用于绘图的uv、以及 Linux(Turso 端依赖 io_uring 后端)。

快速试跑

只想快速看效果时,可以用环境变量压缩规模(详见下文参数表):

CONNECTIONS="1 8" REPEATS=1 DURATION=20 ./scripts/run.sh

这条命令只跑 2 个连接数、每个引擎各 1 次、每次测量 20 秒。

基准程序本身的帮助

直接运行turso-txn-latency --help可查看 harness 自己的全部参数,其中有两个值得关注的选项:

  • --mode immediate:让 Turso 使用 WAL 日志 +BEGIN IMMEDIATE,与 SQLite 的配置方式对齐;
  • --io syscall:让 Turso 使用系统调用 I/O 后端而非 io_uring。

此外,当一次运行未能跟上提供速率(offered rate)时,摘要中会给出 WARNING——此时该次运行画出的曲线展示的是"落后了多少",而不是"延迟是多少"(详见 main.rs 的告警逻辑)。

产物:一次完整实验会得到什么

运行结束后,plot/db/目录下会生成以下文件:

产物说明
plot/latency-ecdf.png.pdf.tikz每个连接数一个 eCDF 面板,SQLite 对 Turso,曲线上标出 p50、p90,另有一条 p99.9 竖线。.tikz是 pgfplots 图片,可在论文中通过\input引入
plot/<engine>-c<N>-r<i>.csv每次运行一个文件,每个事务一行:应开始时刻、是否处于预热期、重启次数、以及 queue/begin/work/commit 各阶段耗时
plot/<engine>-c<N>-r<i>-checkpoints.csv该次运行中每次检查点的开始时刻与耗时
plot/bench.log全部运行的摘要日志,同样输出到终端:分位数、进程自身 CPU 占用、数据库所在磁盘的活动、检查点计时
db/<timestamp>/每次运行的数据库文件,永不删除

从 CSV 的列结构(write_samples)可以看到每个样本携带的完整信息:

engine,mode,connections,run,thread_id,scheduled_ns,warmup,restarts,queue_ns,begin_ns,work_ns,commit_ns,total_ns

其中total_ns即从计划到达时刻到成功 COMMIT 的总延迟(Sample结构体的字段注释见 main.rs);检查点 CSV 的列为engine,mode,connections,run,at_ns,took_ns

方法论:基准是如何设计的

工作量(Workload)

单张表:test_table(id INTEGER PRIMARY KEY, data TEXT)。每个事务插入BATCH_SIZE(默认 10)行,主键 id 从所有连接共享的一个计数器取号(两个引擎分别用NEXT_ID原子计数器实现,见 sqlite_engine.rs 与 turso_engine.rs),因此:

  • 事务之间绝不会触碰同一行,唯一竞争点是引擎自身的写路径;
  • BEGININSERTCOMMIT三条语句在每个连接上只 prepare 一次,解析 SQL 的开销不计入事务延迟(SQLite 端见 sqlite_engine.rs,Turso 端见 turso_engine.rs,注释明确说明这是为了避免把每次Connection::execute的重复解析计费给事务)。

加载方式(Load):开环 + 抗协调遗漏

  • 开环:到达时刻在运行开始前一次性确定(Pacer持有预生成的due: Vec<Duration>时刻表,见 main.rs),事务在其到达时刻"到期",无论当时是否有空闲连接;哪个连接空闲就领取下一个到期事务。
  • 延迟口径:延迟从到期时刻算起,因此排在停滞写者后面的事务,其排队时间计入样本。
  • 到达过程:默认为RATE速率下的泊松过程(指数间隔来自一个带种子的生成器,保证每个引擎、每次重复看到的到达时刻表完全相同);ARRIVALS=fixed则把间隔固定为精确的1/RATE
  • 速率语义RATE是所有连接的总量——更多连接意味着同时在途的事务更多,而不是负载更大。这正是 SQLite "同一时刻只能有一个写者"体现得最明显的地方。
  • 预热:前WARMUP秒的事务会被记录但打上 warmup 标记,在所有摘要与图形中排除(绘图脚本会跳过warmup == 1的行,见 plot-latency-ecdf.py)。
  • 分阶段采样:每个样本携带到期时刻、重启次数、以及 queue(等待连接)、begin、work(插入)、commit 四段时间。

值得注意的实现细节:Pacer的时钟在第一个连接请求工作时才启动(started_at()使用OnceLock,见 main.rs),因此打开连接与 prepare 语句的开销不会挤占第一批事务的时隙。为了不把线程调度的误差算成数据库的延迟,等待到期采用"睡眠 + 最后 300 微秒自旋"的方式(wait_until);Turso 端在 tokio 计时器之上又叠加了一层处理——tokio 计时器每毫秒才 tick 一次,可能晚醒最多 1 毫秒,因此 tokio 睡眠提前 1.5 毫秒结束,最后一段交给线程睡眠加自旋(turso_engine.rs)。

持久化(Durability)

两个引擎都以PRAGMA synchronous = FULL运行,即每次提交都以 fsync 收尾。这意味着对比的是两条持久提交路径之间的差异,而不是两种持久化设置之间的差异。

SQLite 端的配置

  • WAL 模式,通过rusqlite每个 OS 线程一个连接;
  • 使用BEGIN IMMEDIATE。延迟的BEGIN会在第一条INSERT时才获取写锁,若期间其他连接已提交,锁升级会立刻以SQLITE_BUSY_SNAPSHOT失败,事务必须回滚后由应用重试;提前取锁则把等待放进 begin 阶段,由 SQLite 的 busy handler 消化,这正是 SQLite 官方文档为多写者应用推荐的做法(实现见 sqlite_engine.rs)。
  • busy 超时 60 秒,使用默认 busy handler(--timeout默认值 60000 毫秒,见 main.rs)。

Turso 端的配置

  • MVCC(PRAGMA journal_mode = mvcc),每个 OS 线程一个连接、各自持有独立的单线程 tokio runtime,使用BEGIN CONCURRENT,Linux 上默认 io_uring 后端(DEFAULT_IO在 Linux 上为io_uring,见 main.rs)。
  • 并发事务互不阻塞,只在提交时串行化——提交即追加日志记录并 fsync(见 README 描述)。
  • 发现快照过期的事务会重启(restart),重启次数计入其延迟;由于行不相交,正常情况下不应发生重启,摘要中会报告实际发生的次数。Turso 写入线程捕获turso::Error::BusySnapshot后执行ROLLBACK并重新进入事务循环(turso_engine.rs),每笔样本的restarts字段记录该事务的重启次数。

这里有一个值得留意的工程细节(turso_engine.rs 的注释):Turso 端刻意不使用共享的多线程 tokio runtime,而是每个连接一个 current-thread runtime。原因是在 syscall I/O 后端下,事务执行过程中没有任何 yield 点,占住 worker 的任务会让 runtime 的计时器无人驱动,其他连接可能错过时隙甚至睡到运行结束。

检查点(Checkpointing)

两个引擎都按照"生产服务器"的方式做检查点:

  • 写者自身的自动检查点关闭:SQLite 设wal_autocheckpoint = 0,Turso 设mvcc_checkpoint_threshold = -1
  • 由一条独立连接每隔CHECKPOINTER毫秒(默认 1000)执行PRAGMA wal_checkpoint(PASSIVE)
  • Turso 使用被动检查点模式(passive checkpoint),把已提交的版本排入 B-tree 而不阻塞写者;
  • 每次检查点的耗时都会被上报,因此可以把写者停顿与造成停顿的检查点一一对应。

实现层面,两个引擎各自有一个检查点线程/异步任务:SQLite 端在独立连接上周期执行PRAGMA wal_checkpoint(PASSIVE)(sqlite_engine.rs);Turso 端在独立连接上执行相同 pragma 并排空结果行,错误(通常是 Busy)意味着本轮未完成,下一轮会重试(turso_engine.rs)。Turso 的setup还根据模式组合启用被动检查点:仅在Concurrent + Passive组合下调用Builder::experimental_mvcc_passive_checkpoint(true)(turso_engine.rs),而--mvcc-checkpoint-threshold可覆盖默认的 MVCC 自动检查点阈值(默认约 4.12 MB,见 main.rs)。若--checkpointer 0,则各写者自行在提交路径上自动检查点,即引擎开箱即用的行为。

运行规则(Runs)

  • 每次运行从自己的全新空数据库文件开始,测量 60 秒(DURATION),前 5 秒为预热(WARMUP);
  • 每次运行前:先sync,再对数据库所在挂载点执行sudo fstrim,随后让磁盘空闲IDLE(默认 60)秒,最后清空页面缓存(echo 3 | sudo tee /proc/sys/vm/drop_caches,见 bench.sh)。原因是消费级 SSD 会在空闲时排空写缓存、做垃圾回收;不清洗的话,本次运行会继承前几次运行的写积压;
  • 每个引擎 × 连接数组合重复REPEATS(默认 3)次;
  • 每次运行写出独立的样本文件;每次运行的摘要(包括进程自身 CPU 占用与数据库所在磁盘的活动)输出到 stderr 与plot/bench.log

磁盘与 CPU 统计来自内核:磁盘统计通过解析/proc/diskstats按设备主次设备号匹配(main.rs),CPU 统计来自getrusage(RUSAGE_SELF)(main.rs)。摘要会打印 p50/p99/p99.9/max 分位数、实际达到的吞吐、进程 CPU 占用占单核与全部硬件线程的百分比、磁盘写入量/每写耗时/繁忙占比、以及检查点 p50/max/总耗时(report)。

配置参数全表

所有设置都是环境变量,由scripts/bench.sh读取,run.sh会调用它,因此两个脚本都接受这些变量:

变量默认值含义
RATE1000每秒提供的事务数,为所有连接的总额
CONNECTIONS"1 8 16 32"要运行的连接数列表,每个对应一个图形面板
REPEATS3每个引擎和连接数的运行次数;每次运行写出独立 CSV
IDLE60每次运行前(fstrim之后)让磁盘空闲的秒数,避免本次运行继承之前运行的写积压
DURATION60每次运行测量的秒数
WARMUP5测量开始前运行的秒数
BATCH_SIZE10每个事务插入的行数
CHECKPOINTER1000独立连接两次检查点之间的毫秒数;0表示让各写者自行检查点
ARRIVALSpoissonpoisson用指数间隔围绕1/RATE分布到达时刻;fixed则精确间隔1/RATE
SEED1泊松时刻表的种子;相同种子保证每次运行到达时刻一致
OUTplot/CSV 与图形输出目录;运行拒绝覆盖已存在的 CSV
DB_DIRdb/<timestamp>/数据库文件目录;每次运行在那里获得独立文件

bench.sh通过cargo build --release -p turso-txn-latency构建基准程序,并通过仓库根目录的scripts/cargo-target-dir脚本定位构建产物(尊重CARGO_TARGET_DIR设置),随后按连接数 × 引擎 × 重复次数三层循环逐个运行(bench.sh)。每次运行前会检查输出文件是否已存在——存在即报错退出,避免覆盖旧数据(这是"Nothing is ever deleted"原则的一部分)。日志第一行会记录本次会话的完整配置:rate connections repeats idle arrivals seed db

绘制 eCDF 图形

绘图脚本 plot-latency-ecdf.py 通过uv run执行(run.sh会把所有非 checkpoints 的 CSV 文件作为参数传入):

uv run plot-latency-ecdf.py sqlite-c1.csv sqlite-c8.csv turso-c1.csv turso-c8.csv \ -o latency-ecdf.png -o latency-ecdf.pdf -o latency-ecdf.tikz

脚本要点:

  • 图形语义:x 轴为延迟(对数刻度,毫秒),y 轴为"延迟不超过该值的事务占比";每条曲线在 p50、p90 处有标记,p99.9 用虚线竖线标出并标注数值,曲线右端即该引擎最慢的事务。
  • 面板布局:每个连接数一个面板横向排列、共享坐标轴,在同一面板内对比两个引擎,跨面板观察并发数的影响。
  • 同配置多次运行合并:同一引擎、同一连接数的多次运行样本会被合并为一条曲线(read_series(engine, mode, connections)分组并np.concatenate)。
  • 颜色方案:SQLite 与 Turso 使用 Okabe-Ito 色盲安全配色(橙#E69F00与蓝#0072B2),这也是系统论文常用的配色;遇到未知引擎时回退到备选色板。
  • LaTeX 输出.tikz/.tex输出生成 pgfplots 的groupplots图片,按\linewidth自适应尺寸,论文中可直接\input(要求 pgfplots 的groupplots库与\pgfplotsset{compat=1.18})。
  • 可选参数--column可选择绘制total_ns(默认)、queue_nsbegin_nswork_nscommit_ns中的任意一列,用于分解延迟构成;--name ENGINE=NAME可自定义图例名称(如turso=Limbo)。
  • 曲线点采样采用"均匀网格 + 从最慢样本反向的 log 网格"双网格抽稀,确保最末尾的几个事务也能被单独画出来(Curve.points)。

读取摘要输出

每次运行的摘要(写入终端与bench.log)格式如下,各字段含义与告警条件都可从 report 对应:

[turso/concurrent] N transactions in 60.0s, 1000/s achieved [turso/concurrent] p50 0.23ms p99 1.05ms p99.9 2.10ms max 4.87ms [turso/concurrent] cpu: user 3.2s sys 1.1s 7% of one core over 60.0s, 0.2% of 32 hardware threads [turso/concurrent] disk nvme0n1: 4500 writes, 22.5 MB written, 0.50ms per write, busy 1% of the run [turso/concurrent] checkpointer: 60 checkpoints, p50 0.4ms max 1.2ms total 30ms

(以上数值仅为示意格式,非实测数据。)需要特别留意两种告警:

  1. WARNING: gave up after running Nx past the planned time. This database cannot sustain RATE transactions/s, so the tail here is cut short rather than measured.—— 运行超时被截断,尾部数据缺失;
  2. WARNING: only reached X/s of the Y/s offered. The latencies are real, but this database is past saturation.—— 实际吞吐低于提供速率的 95%,数据库已过饱和,此时曲线代表的是"落后程度"而非真实延迟。

Turso 端还会额外打印一行(turso_engine.rs):

[turso] io backend io_uring, checkpoint mode Passive, 0 transaction restarts

用于核对 I/O 后端、检查点模式与重启次数。

基准的工程结构一览

该基准是工作区成员perf/latency(见根目录 Cargo.toml),包名为turso-txn-latency,二进制入口为main.rs(Cargo.toml),依赖rusqlitetokioturso(工作区路径bindings/rust)、claplibc。核心模块划分:

  • main.rs:参数解析、Pacer到达时刻表、Sample/Checkpoint/Run数据结构、CPU/磁盘统计、摘要报告与 CSV 写出;
  • sqlite_engine.rs:SQLite 端——WAL、BEGIN IMMEDIATE、busy handler、独立检查点连接;
  • turso_engine.rs:Turso 端——MVCC、BEGIN CONCURRENT、快照过期重启、tokio current-thread runtime、被动检查点;
  • plot-latency-ecdf.py:eCDF 绘图与 pgfplots 生成。

适用前提与限制

  • 平台:需要 Linux(Turso 端默认 io_uring 后端,DEFAULT_IO在非 Linux 平台回退为syscall,磁盘统计在非 Linux 上不可用)。
  • 权限:完整流程需要sudofstrim与清空页面缓存);无 sudo 环境可跳过这两个步骤,但要注意测量结果会受磁盘写积压影响。
  • 工具链:Rust(cargo)、uv(绘图)。
  • 时长:默认完整实验约 1 小时,其中大部分是每次运行前的 60 秒磁盘空闲期;快速试跑请用CONNECTIONS="1 8" REPEATS=1 DURATION=20这类参数组合。
  • 结果解读:只有能跟上提供速率(achieved ≥ 95% × RATE)的运行,其曲线才反映真实延迟分布;过饱和运行的曲线应视为掉队程度而非延迟。

【免费下载链接】tursoA SQL database in Rust: SQLite-compatible, now also speaking Postgres (experimental). The LLVM of databases.项目地址: https://gitcode.com/GitHub_Trending/tu/turso

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 17:53:14

PDFPatcher PDF 工具箱新手指南

PDFPatcher PDF 工具箱新手指南 【免费下载链接】PDFPatcher PDF补丁丁——PDF工具箱&#xff0c;可以编辑书签、剪裁旋转页面、解除限制、提取或合并文档&#xff0c;探查文档结构&#xff0c;提取图片、转成图片等等 项目地址: https://gitcode.com/GitHub_Trending/pd/PDF…

作者头像 李华
网站建设 2026/9/13 17:52:55

解决Node.js连接MySQL 8.0认证协议不兼容问题

1. 问题现象与背景分析最近在本地开发环境搭建Node.js后端服务时&#xff0c;遇到了一个典型的数据库连接问题。当我尝试用mysql2包连接新安装的MySQL 8.0数据库时&#xff0c;控制台抛出了如下错误&#xff1a;ER_NOT_SUPPORTED_AUTH_MODE: Client does not support authentic…

作者头像 李华
网站建设 2026/9/13 17:52:28

15个Verilog文件造出一颗GPU:tiny-gpu极简并行架构拆解

15个Verilog文件造出一颗GPU&#xff1a;tiny-gpu极简并行架构拆解 【免费下载链接】tiny-gpu A minimal GPU design in Verilog to learn how GPUs work from the ground up 项目地址: https://gitcode.com/GitHub_Trending/ti/tiny-gpu 如果你只能给一颗GPU写11条指令…

作者头像 李华
网站建设 2026/9/13 17:47:28

AI问答系统核心技术解析与实践指南

1. 什么是AI问答系统AI问答系统是一种基于人工智能技术的交互式信息检索工具&#xff0c;它能够理解用户用自然语言提出的问题&#xff0c;并从知识库或数据源中提取相关信息&#xff0c;生成准确、连贯的回答。这类系统通常结合了自然语言处理(NLP)、机器学习和知识图谱等技术…

作者头像 李华