本页译自英文,由模型翻译。若与英文原文存在差异,以英文原文为准。
方法论
ruststream-sqlx 如何测量自己:三个循环、在真实数据库上的运行、受数据库限制的判定计算、吞吐量网格和代码计数。流程就是框架自己的流程(RustStream 方法论),本页说明它在 SQL 数据库上是什么样子。
三个循环
一个进程把每个场景跑三遍,作为三个循环,它们只有一处不同:承载消息的是什么。
- 裸 sqlx 循环。 直接驱动 sqlx:一个循环在以同样方式构建的连接池上,按同样的顺序执行 Broker 对同一张表执行的那些语句。它的语句取自 Broker 渲染语句所用的方言,因此两个循环执行相同的 SQL。
- 手动驱动 crate。 本 crate,上面什么也没有:Broker 建立连接,
InboxQueue打开一个订阅,一个循环从它的流中取出投递,逐个解码、读取一个字段并确认;或者把Repository和Routed配对成发布者。没有处理器,没有应用,也没有分发。 - RustStream 服务。 用户写的服务:一张
#[derive(Inbox)]表、一个#[subscriber]处理器,以及RustStream::new(..).with_broker(SqlxBroker::new(pool), ..)。
其余一切在三个循环中都相同:表、行、解码成的同一个小结构体、四个连接的连接池、带四个工作线程的 tokio 运行时,以及二进制程序。应答场景通过已连接 Broker 的默认发布者 Routed,把每次投递的应答写入第二张表。
outbox 的场景是同一个应用的三个变体,分别占据这三个位置:不用 outbox 的应用;同一个应用,用裸 sqlx 手写 outbox,使用本 crate 的中间件所取用的那些连接;以及同一个应用,使用本 crate 的 outbox。在最后一个变体中,从处理器之外发布的消息经过 Outbox::wrap。
运行
- 每次运行使用新的表。 每次运行都重新创建表并填满它,然后排空它。发布场景则改为写入它的行。
- 在三个循环中,计时窗口都从第一次处理的投递开始,到最后一次结束,因此启动服务和打开连接池都在窗口之外。发布场景的窗口从第一次发布持续到最后一次。每次处理的投递都让一个门闩减一,最后一次计数唤醒这次运行,因此运行无需轮询表或休眠就能知道表已排空。
- 消息条数来自一次探测,它测量裸循环的速率,因此在任何机器上,测量的运行都至少持续五秒。
- 各循环交替进行:裸、crate、服务,共三轮。每个循环报告它最佳、中位和最差的一轮。以最佳一轮为准,因为噪声只会让运行变慢。
- outbox 运行在 Redis Pub/Sub 上,由一个 worker 供给消息,它最多保留 1024 条未消费的消息,因为订阅的缓冲区装不下时,Pub/Sub 会丢弃消息。
小于任一循环各轮之间波动范围的差异记为 无法区分,从不给出具体数字。往返占一条消息所用时间的至少一半时,该行标为 受数据库限制。这是算出来的。在各轮之前,一次探测在一个连接上为一条服务器作答的语句计时:2000 次 SELECT 1 往返的中位数,与机器信息一起公布。每个场景声明一条消息要付出的往返次数,包括每条语句,以及从连接池取出的每个连接,因为 sqlx 在借出连接前会先 ping 它。往返时间乘以这个次数达到每条消息测得时间的一半时,该行就被标记。
吞吐量网格
每个单元格在 16 个连接的连接池上排空一张填有 20,000 行的表,分别使用一个、四个和八个 worker,分别使用逐条投递和每批 64 条。PostgreSQL 和 MySQL 采用行锁方式;SQLite 运行在 WAL 模式的本地文件上,采用租约方式,这时一次领取就是一条语句。裸循环在一个连接池上运行 n 个任务,每个任务是一个循环,领取、读取并删除,直到领取返回空。本 crate 的循环在同一个连接池上运行 n 个任务,每个任务是一个读取一个订阅的独立 Broker。服务在一个订阅上以 workers(n) 挂载它的处理器;一个 worker 就是普通的挂载,处理完一次投递后才拉取下一次。三轮交替进行,每个循环取最佳、中位和最差的一轮。
代码计数
开销图的每个场景都是用户写的服务,构建时不启用 testing 特性,在单线程运行时上启动,连接测试环境的 PostgreSQL,并在旁边统计裸循环。一个生产者在自己的线程和运行时上,在启动和排空之间填充表,因此填充不计入任何一方。outbox 的场景运行在 MemoryBroker 上,同一时间只有一条消息在途。
指令数来自 callgrind,内存分配来自 DHAT。场景先跑一次投递,再跑 1000 次,然后 2000 次。后两者之差就是一条消息的稳态开销,单次投递就是冷启动。运行时等待服务器而挂起期间,计数暂停:那部分工作取决于服务器的时间,而不是代码。
指令数在多次运行之间仍有波动,因为客户端轮询应答时应答是否仍在途中,会改变一条消息要做多少 sqlx 的读取工作。对同一份代码运行五次,一个场景的总数最多波动约 7%,每条消息的数字最多波动约 15%;从不访问数据库的场景,结果精确到每一条指令都一致。与保存的基线对照的运行,在某个场景的指令数多出百分之十时失败。内存分配没有波动:每个场景声明一个上限,即五次完整运行中的最高计数加 0.1%,任何一次运行超过它都会失败。
自己运行
just bench
这个命令根据 docker-compose.test.yml 启动测试环境,即在主机网络上以 Docker 运行的 PostgreSQL、MySQL 和 Redis,运行每个场景和吞吐量网格,停止测试环境,并重写 docs/benchmarks/results.json。它大约需要半个小时,运行期间机器应专供它使用。RUSTSTREAM_BENCH_SECONDS 设置最短的运行时长,RUSTSTREAM_BENCH_THROUGHPUT_ROWS 设置吞吐量单元格的行数,RUSTSTREAM_BENCH_PAIRS 设置轮数。它会清空 RUSTFLAGS,因此二进制程序不绑定构建它的 CPU。两个命令都按发布版本的方式构建基准测试,启用跨 crate 的 fat 链接时优化,因此这些数字描述的是服务实际交付的代码。
just bench-code
代码计数启动同样的测试环境,需要 valgrind;它会安装锁文件所固定版本的基准测试运行器。just bench-code 5000 让每个场景跑五千次投递,而不是一千次:运行更长,数字更稳定。在 main 上运行 just bench-code --save-baseline=main 记录基线,在分支上运行 just bench-code --baseline=main 与它对照。