本页译自英文,由模型翻译。若与英文原文存在差异,以英文原文为准。
基准测试
ruststream-sqlx 在每条消息上的开销:与裸 sqlx 循环对照的吞吐量,作为插件的 outbox,以及它自身代码的指令数和内存分配次数。
与裸 sqlx 循环对照
第一张图在测试环境的 PostgreSQL 上把每个场景用三种方式运行。裸 sqlx 循环 是手写的循环,它在以同样方式构建的连接池上,按同样的顺序执行 Broker 对同一张表执行的那些语句;这些语句取自方言,因此两者不会走样。手动驱动 crate 用一个自己的循环驱动本 crate 的订阅或发布者,没有运行时。RustStream 服务 是用户写的服务:一张 #[derive(Inbox)] 表、一个 #[subscriber] 处理器和应用。第一项差值是本 crate 在它所执行的语句之外的开销,第二项是运行时在其上增加的开销。
每个循环画成一条横条,从最佳一轮延伸到最慢的一轮,圆点标出最佳一轮,缺口标出中位一轮。坐标轴的零点是裸循环的最佳一轮,圆点在零点右侧表示更慢。空心圆点表示该循环与裸循环的差异小于各轮的波动范围。图表下方的表格列出最佳一轮,括号里是中位数。
每条消息至少要付出一次领取、一次结算和一次提交,每一项都是一次到数据库的往返。标为 受数据库限制 的行,其裸循环在每条消息上至少有一半时间在等待这些往返。这时本 crate 和运行时的工作发生在裸循环同样要付出的等待之内,该行给出的是它们开销的下限,而不是对开销的测量。
作为插件的 outbox
outbox 可以与任意 Broker 配合,所以把它作为普通应用的一项开销来测量:同一个应用的三个变体,经 ruststream-fred 运行在测试环境的 Redis Pub/Sub 和 PostgreSQL 上。不用 outbox 是用户不使用 outbox 时写的应用。手写的 outbox 是同一个应用,在每次发布前插入记录,再在处理器中取出并标记记录,用裸 sqlx 实现。crate 自带的 outbox 是同一个应用,使用本 crate 的注册表、两个中间件层和重发。与不用 outbox 对照,图表显示跟踪一条消息本身的开销;与手写的 outbox 对照,它显示本 crate 的机制在用户无论如何都会执行的语句之外增加了多少。
代码的开销
开销图统计一条消息在服务线程上的开销:稳态下 callgrind 测得的指令数和 DHAT 测得的内存分配次数,连接测试环境的 PostgreSQL。计数包括运行时、本 crate,以及 sqlx 驱动程序在该线程上的编码和解码;数据库服务器是另一个进程,不计入。冷启动 是启动服务一次性付出的开销:打开连接池的连接、Broker 的启动检查和第一次投递。outbox 的计数让同样的应用运行在 MemoryBroker 上,因此其中不含任何传输的开销。
结果
- 裸 sqlx 循环
- 手动驱动 crate
- RustStream 服务
- 各轮,从最佳到最慢
- 最佳一轮
- 中位一轮
- 轮次的波动范围
- 无法区分
ruststream-sqlx 0.7.0 on ruststream 0.7.1, AMD Ryzen 9 7900X 12-Core Processor, Linux 7.2.11-x64v3-xanmod1-1, postgres:17-alpine, mysql:8.0, redis:7-alpine in Docker on the host network, without durability; SQLite bundled with sqlx, on a local file in WAL mode; the outbox over MemoryBroker in the code table and over Redis Pub/Sub in the wall clock, 测量于 2026-10-18. 这些数字是怎样测得的
- 无法区分
- +2.1% (受数据库限制)
- 无法区分
- 无法区分 (受数据库限制)
- 无法区分
- 无法区分 (受数据库限制)
- -3%
- -2.5%
- 无法区分
- 无法区分
- 无法区分
- 无法区分 (受数据库限制)
- 无法区分
- 无法区分 (受数据库限制)
- 无法区分
- 无法区分 (受数据库限制)
- 无法区分
- 无法区分 (受数据库限制)
以表格显示数字
| 场景 | 裸 sqlx 循环 | 手动驱动 crate | RustStream 服务 | 额外开销 |
|---|---|---|---|---|
| row lock claim, JSON decode into a small struct, delete each | 6,149 msg/s (6,103) | 6,136 msg/s (5,986) (比裸循环 无法区分) | 6,018 msg/s (5,996) | +2.1% (受数据库限制) |
| row lock claim, reply inserted through this crate's default publisher, delete each | 5,150 msg/s (4,984) | 5,103 msg/s (5,092) (比裸循环 无法区分) | 5,007 msg/s (4,937) | 无法区分 (受数据库限制) |
| row lock claim in batches of 64, delete each | 29,198 msg/s (29,006) | 29,043 msg/s (28,629) (比裸循环 无法区分) | 28,724 msg/s (28,676) | 无法区分 (受数据库限制) |
| lease claim, JSON decode into a small struct, delete each | 3,125 msg/s (3,098) | 3,219 msg/s (3,193) (比裸循环 -3%) | 3,202 msg/s (3,177) | -2.5% |
| advisory lock claim, JSON decode into a small struct, delete each | 4,611 msg/s (4,572) | 4,558 msg/s (4,462) (比裸循环 无法区分) | 4,565 msg/s (4,295) | 无法区分 |
| by-name subscription, row lock claim, delete each | 6,089 msg/s (5,910) | 6,025 msg/s (5,773) (比裸循环 无法区分) | 5,980 msg/s (5,784) | 无法区分 (受数据库限制) |
| row mode, the row decoded by the driver, delete each | 5,805 msg/s (5,771) | 5,769 msg/s (5,662) (比裸循环 无法区分) | 5,759 msg/s (5,742) | 无法区分 (受数据库限制) |
| repository publish, one insert each | 26,723 msg/s (26,721) | 27,814 msg/s (27,485) (比裸循环 无法区分) | 27,640 msg/s (26,771) | 无法区分 (受数据库限制) |
| routed publish, one insert each | 29,182 msg/s (27,701) | 27,947 msg/s (26,567) (比裸循环 无法区分) | 27,295 msg/s (26,896) | 无法区分 (受数据库限制) |
- 不用 outbox
- 手写的 outbox
- crate 自带的 outbox
- 各轮,从最佳到最慢
- 最佳一轮
- 中位一轮
- 轮次的波动范围
- 无法区分
- 无法区分
- +76.2%
- +75.9% (比手写 无法区分)
- +86.3%
- +86.5% (比手写 无法区分)
- +84.9%
- +85.2% (比手写 无法区分)
以表格显示数字
| 场景 | 不用 outbox | 手写的 outbox | crate 自带的 outbox | 额外开销 | 比手写 outbox |
|---|---|---|---|---|---|
| outbox plugin, an untracked message through both layers | 53,058 msg/s (52,720) | - | 53,160 msg/s (52,178) | 无法区分 | - |
| outbox plugin, a tracked publish from outside a handler | 66,275 msg/s (66,203) | 15,761 msg/s (15,425) (比不用 outbox +76.2%) | 15,961 msg/s (15,395) | +75.9% | 无法区分 |
| outbox plugin, a tracked delivery fetched and marked | 64,200 msg/s (61,442) | 8,821 msg/s (8,600) (比不用 outbox +86.3%) | 8,682 msg/s (8,635) | +86.5% | 无法区分 |
| outbox plugin, a tracked round trip: recorded, delivered, fetched and marked | 54,357 msg/s (53,776) | 8,191 msg/s (8,143) (比不用 outbox +84.9%) | 8,044 msg/s (8,025) | +85.2% | 无法区分 |
AMD Ryzen 9 7900X 12-Core Processor, x86_64, 427-5912 MHz, 12 physical, 24 logical, 62.4 GiB, unknown, Linux 7.2.11-x64v3-xanmod1-1, 1.99.0, 3.25.1, bench, inheriting release (opt-level = 3, lto = fat, codegen-units = 16, debug = line-tables-only), ruststream-sqlx inbox,outbox,postgres,mysql,sqlite,chrono; ruststream macros,json,memory; ruststream-fred default. ruststream-sqlx 0.7.0, ruststream 0.7.1, 测量于 2026-10-18.
以表格显示数字
| 场景 | 指令数 | 内存分配 | 冷启动 |
|---|---|---|---|
| row lock claim, JSON decode into a small struct, delete each | 44,796.1 | 32 | 334,632 / 355 |
| row lock claim, reply inserted through this crate's default publisher, delete each | 66,613.4 | 44 | 337,677 / 353 |
| row lock claim in batches of 64, delete each | 14,281.5 | 12.6 | 334,178 / 358 |
| lease claim, JSON decode into a small struct, delete each | 51,541.4 | 28 | 397,875 / 393 |
| advisory lock claim, JSON decode into a small struct, delete each | 66,800.8 | 47.1 | 421,572 / 494 |
| by-name subscription, row lock claim, delete each | 47,474.3 | 32 | 356,127 / 359 |
| row mode, the row decoded by the driver, delete each | 43,674.9 | 32 | 338,529 / 364 |
| repository publish, one insert each | 23,890.9 | 15 | 144,459 / 128 |
| routed publish, one insert each | 23,429 | 15 | 145,266 / 129 |
| outbox plugin, an untracked message through both layers | 7,238.2 | 6 | 251,239 / 250 |
| outbox plugin, a tracked publish from outside a handler | 23,232.9 | 15 | 183,040 / 161 |
| outbox plugin, a tracked delivery fetched and marked | 51,016.7 | 30 | 280,164 / 271 |
| outbox plugin, a tracked round trip: recorded, delivered, fetched and marked | 75,639.3 | 42.1 | 328,380 / 311 |
下面的计数与它们各自的对照循环并列:inbox 对照裸 sqlx 循环,outbox 对照不用 outbox 的应用和手写 outbox 的应用。
| 场景 | 服务,指令数 | 裸循环,指令数 | 服务,内存分配 | 裸循环,内存分配 |
|---|---|---|---|---|
| row lock claim, JSON decode into a small struct, delete each | 44,796.1 | 40,310.6 | 32 | 33 |
| row lock claim, reply inserted through this crate's default publisher, delete each | 66,613.4 | 60,551.5 | 44 | 45 |
| row lock claim in batches of 64, delete each | 14,281.5 | 12,300.7 | 12.6 | 12.4 |
| lease claim, JSON decode into a small struct, delete each | 51,541.4 | 47,133 | 28 | 28 |
| advisory lock claim, JSON decode into a small struct, delete each | 66,800.8 | 58,844.8 | 47.1 | 47.1 |
| by-name subscription, row lock claim, delete each | 47,474.3 | 40,384.5 | 32 | 33 |
| row mode, the row decoded by the driver, delete each | 43,674.9 | 39,836.3 | 32 | 33 |
| repository publish, one insert each | 23,890.9 | 20,830.8 | 15 | 12 |
| routed publish, one insert each | 23,429 | 20,830.8 | 15 | 12 |
| 场景 | 指令数(不用 outbox / 手写 / outbox) | 内存分配(不用 outbox / 手写 / outbox) |
|---|---|---|
| outbox plugin, an untracked message through both layers | 6,788.2 / - / 7,238.2 | 6 / - / 6 |
| outbox plugin, a tracked publish from outside a handler | 742.2 / 22,349.9 / 23,232.9 | 2 / 16 / 15 |
| outbox plugin, a tracked delivery fetched and marked | 4,877.7 / 47,021.6 / 51,016.7 | 7 / 30 / 30 |
| outbox plugin, a tracked round trip: recorded, delivered, fetched and marked | 6,821.3 / 68,347.8 / 75,639.3 | 6 / 39 / 42.1 |
吞吐量
在每种数据库上,用一个以 workers(n) 挂载的订阅排空一张已填满的表,连接池有 16 个连接,分别使用逐条投递和每批 64 条。PostgreSQL 和 MySQL 采用行锁方式。SQLite 没有行锁,因此采用租约方式。裸循环在一个连接池上运行 n 个任务,每个任务领取、读取并删除,直到领取返回空。本 crate 的循环在同一个连接池上运行 n 个订阅,每个订阅使用自己的 Broker。服务运行一个订阅,由它为所有 worker 领取。outbox 的单元格在 outbox 的三个变体中,都以 workers(n) 挂载中继和接收端。
- 裸 sqlx 循环
- 手动驱动 crate
- RustStream 服务
- 各轮,从最佳到最慢
- 最佳一轮
- 中位一轮
- 轮次的波动范围
- 无法区分
ruststream-sqlx 0.7.0 on ruststream 0.7.1, 测量于 2026-10-18. 这些数字是怎样测得的
- 无法区分
- 无法区分
- 无法区分
- +18.5%
- 无法区分
- +19%
- 无法区分
- 无法区分
- 无法区分
- 无法区分
- 无法区分
- +21.6%
以表格显示数字
| 场景 | 裸 sqlx 循环 | 手动驱动 crate | RustStream 服务 | 额外开销 |
|---|---|---|---|---|
| 逐条投递, workers(1) | 6,606 msg/s (6,534) | 6,498 msg/s (6,345) (比裸循环 无法区分) | 6,519 msg/s (6,489) | 无法区分 |
| 逐条投递, workers(4) | 21,422 msg/s (20,667) | 20,365 msg/s (20,352) (比裸循环 无法区分) | 17,465 msg/s (16,553) | +18.5% |
| 逐条投递, workers(8) | 35,388 msg/s (35,358) | 35,013 msg/s (34,770) (比裸循环 无法区分) | 28,664 msg/s (28,526) | +19% |
| 每批 64 条, workers(1) | 31,095 msg/s (30,671) | 30,127 msg/s (30,047) (比裸循环 无法区分) | 29,910 msg/s (29,658) | 无法区分 |
| 每批 64 条, workers(4) | 100,358 msg/s (94,995) | 96,291 msg/s (93,160) (比裸循环 无法区分) | 94,292 msg/s (93,376) | 无法区分 |
| 每批 64 条, workers(8) | 154,114 msg/s (153,177) | 154,622 msg/s (151,889) (比裸循环 无法区分) | 120,889 msg/s (119,422) | +21.6% |
- 裸 sqlx 循环
- 手动驱动 crate
- RustStream 服务
- 各轮,从最佳到最慢
- 最佳一轮
- 中位一轮
- 轮次的波动范围
- 无法区分
ruststream-sqlx 0.7.0 on ruststream 0.7.1, 测量于 2026-10-18. 这些数字是怎样测得的
- 无法区分
- +16.4%
- 无法区分
- +25.5%
- 无法区分
- +17.8%
- 无法区分
- 无法区分
- 无法区分
- 无法区分
- 无法区分
- 无法区分
以表格显示数字
| 场景 | 裸 sqlx 循环 | 手动驱动 crate | RustStream 服务 | 额外开销 |
|---|---|---|---|---|
| 逐条投递, workers(1) | 7,626 msg/s (7,409) | 6,791 msg/s (6,498) (比裸循环 无法区分) | 6,376 msg/s (6,329) | +16.4% |
| 逐条投递, workers(4) | 17,830 msg/s (16,166) | 16,316 msg/s (15,688) (比裸循环 无法区分) | 13,281 msg/s (13,131) | +25.5% |
| 逐条投递, workers(8) | 21,885 msg/s (21,792) | 19,070 msg/s (15,962) (比裸循环 无法区分) | 17,994 msg/s (17,839) | +17.8% |
| 每批 64 条, workers(1) | 36,152 msg/s (35,674) | 35,752 msg/s (34,773) (比裸循环 无法区分) | 35,228 msg/s (35,088) | 无法区分 |
| 每批 64 条, workers(4) | 107,344 msg/s (107,082) | 102,923 msg/s (86,929) (比裸循环 无法区分) | 99,924 msg/s (95,386) | 无法区分 |
| 每批 64 条, workers(8) | 158,847 msg/s (132,613) | 147,257 msg/s (139,066) (比裸循环 无法区分) | 143,767 msg/s (120,226) | 无法区分 |
- 裸 sqlx 循环
- 手动驱动 crate
- RustStream 服务
- 各轮,从最佳到最慢
- 最佳一轮
- 中位一轮
- 轮次的波动范围
- 无法区分
ruststream-sqlx 0.7.0 on ruststream 0.7.1, 测量于 2026-10-18. 这些数字是怎样测得的
- +3.3%
- +3.3%
- +4.2%
- +63.9%
- +3%
- +65.5%
- +5%
- 无法区分
- 无法区分
- 无法区分
- 无法区分
- 无法区分
以表格显示数字
| 场景 | 裸 sqlx 循环 | 手动驱动 crate | RustStream 服务 | 额外开销 |
|---|---|---|---|---|
| 逐条投递, workers(1) | 14,073 msg/s (13,850) | 13,609 msg/s (13,565) (比裸循环 +3.3%) | 13,611 msg/s (13,599) | +3.3% |
| 逐条投递, workers(4) | 14,453 msg/s (14,266) | 13,846 msg/s (13,728) (比裸循环 +4.2%) | 5,218 msg/s (4,723) | +63.9% |
| 逐条投递, workers(8) | 14,244 msg/s (14,181) | 13,814 msg/s (13,733) (比裸循环 +3%) | 4,916 msg/s (4,754) | +65.5% |
| 每批 64 条, workers(1) | 33,167 msg/s (33,147) | 31,521 msg/s (31,281) (比裸循环 +5%) | 33,092 msg/s (31,648) | 无法区分 |
| 每批 64 条, workers(4) | 34,067 msg/s (33,800) | 35,105 msg/s (33,255) (比裸循环 无法区分) | 34,839 msg/s (33,646) | 无法区分 |
| 每批 64 条, workers(8) | 35,446 msg/s (34,634) | 34,829 msg/s (33,519) (比裸循环 无法区分) | 35,544 msg/s (35,537) | 无法区分 |
- 不用 outbox
- 手写的 outbox
- crate 自带的 outbox
- 各轮,从最佳到最慢
- 最佳一轮
- 中位一轮
- 轮次的波动范围
- 无法区分
ruststream-sqlx 0.7.0 on ruststream 0.7.1, 测量于 2026-10-18. 这些数字是怎样测得的
- +85.2%
- +85.8% (比手写 +3.7%)
- +50.8%
- +51.9% (比手写 无法区分)
- +37.2%
- +39.7% (比手写 +3.9%)
以表格显示数字
| 场景 | 不用 outbox | 手写的 outbox | crate 自带的 outbox | 额外开销 | 比手写 outbox |
|---|---|---|---|---|---|
| 逐条投递, workers(1) | 54,941 msg/s (52,927) | 8,128 msg/s (8,097) (比不用 outbox +85.2%) | 7,826 msg/s (7,663) | +85.8% | +3.7% |
| 逐条投递, workers(4) | 51,743 msg/s (51,332) | 25,440 msg/s (25,275) (比不用 outbox +50.8%) | 24,866 msg/s (24,810) | +51.9% | 无法区分 |
| 逐条投递, workers(8) | 50,650 msg/s (45,681) | 31,813 msg/s (31,242) (比不用 outbox +37.2%) | 30,560 msg/s (29,582) | +39.7% | +3.9% |
机器
| 处理器 | AMD Ryzen 9 7900X 12-Core Processor |
|---|---|
| 架构 | x86_64 |
| 频率 | 427-5912 MHz |
| 核心数 | 12 physical, 24 logical |
| 内存 | 62.4 GiB |
| 内存速度 | unknown |
| 操作系统 | Linux 7.2.11-x64v3-xanmod1-1 |
| 数据库 | postgres:17-alpine, mysql:8.0, redis:7-alpine in Docker on the host network, without durability; SQLite bundled with sqlx, on a local file in WAL mode; the outbox over MemoryBroker in the code table and over Redis Pub/Sub in the wall clock |
| 往返时间 | 17.4 us (median of 2000 `SELECT 1` round trips on one Postgres connection) |
| 编译器 | 1.99.0 |
| sqlx | 0.9.0 |
| 构建配置 | bench, inheriting release (opt-level = 3, lto = fat, codegen-units = 16, debug = line-tables-only) |
| 特性 | ruststream-sqlx inbox,outbox,postgres,mysql,sqlite,chrono; ruststream macros,json,memory; ruststream-fred default |
| RUSTFLAGS | none (the recipe clears RUSTFLAGS, so the numbers are not tied to this CPU) |
| Valgrind | 3.25.1 |
数字来自本版本的 docs/benchmarks/results.json,由基准测试命令写入,并作为 benchmarks/results.json 与页面一起发布。方法论说明它们是怎样测得的,数字说明可以从中得出什么结论。