跳转至

基准测试

英文版本为准

本页译自英文,由模型翻译。若与英文原文存在差异,以英文原文为准。

从 RabbitMQ 队列走到你的处理器,每条消息都要花时间:订阅的流、解码、分发、ack。这一页说明它花了 多少,并把这份开销拆成两半:本 crate 在它所包装的 lapin 客户端之上加了多少,运行时又在这之上 加了多少。

同一个进程用三个循环跑同一个场景,它们各自只差一件事:消息由谁承载。其余一切都保持相同:连接、 订阅拿到的 channel、队列的声明参数、预取窗口、确认的位置、解码成同一个类型、载荷字节、tokio 运行时和构建。这套流程属于框架本身,写在 RustStream 基准测试页 上;这一页公布它在这台机器上得出的结果。

数字

三个交错轮次中的最佳值,括号里依次是中位的一轮和最差的一轮。越大越好。

表格由浏览器从上一次运行写下的文档读出,所以这一页上没有任何会过期的副本。

「裸客户端」是手写的循环直接驱动 lapin。「本 crate」是同一个循环跑在 ruststream-lapin 自己 身上:broker、队列描述符、订阅的流、投递自带的 ack 和它的发布者,外面没有任何服务。「完整服务」 才是用户写的东西:#[subscriber] 处理器加上 app 和运行时,用的还是同一个发布者。

所以「本 crate 相对裸客户端」就是这个仓库里那层适配的代价,这个数字由本仓库负责。从它到「完整 服务相对裸客户端」的那一步,是运行时在 RabbitMQ 上额外要的代价:框架自身的开销由核心仓库测量并 公布,而这一行说明它落在这个传输上是什么样子。

两行对应 RabbitMQ 自带的两种队列实现,各自把这些代价放在不同的传输旁边衡量。经典队列把消息留在 内存里,一帧就结清一次投递。仲裁队列让每条消息走一遍 Raft 并落盘之后才交出去,这么贵的传输几乎 不给它上面的东西留下显形的余地。

标为「无法区分」的开销,是该列与裸客户端之间的差值小于各自多次运行之间离散范围的那一个。低于 运行噪声的数字读起来像是从未测到过的精度,所以不公布。

标为「受 Broker 限制」的一行,是裸客户端把这次运行花在等服务器、而不是干活上的那一行。这个标记 来自测量,不是猜测:轮次之外的一次探测测出服务器回答一个请求要多久,当一次投递所需的往返加起来 不少于一条消息所花时间的一半时,才打上这个标记。测到的往返时延公布在下面的机器那一节里,算式 可以自己核对。AMQP 的一次投递不用等自己的往返:服务器主动推送消息体,也不回应确认。要等的是确认 换回来的额度,而预取窗口把这份等待摊到窗口里的每一条消息上。

同一次运行的机器可读形式在 benchmarks/results.json, 框架的站点用它拼出跨 Broker 的汇总表。

crate 自身的代码

第二张表是一条消息在代码里的开销,是数出来的,不是计时得来的:指令数由 callgrind 统计,内存分配 次数由 DHAT 统计。每个场景都是用户会写的那种服务,跑在 LapinBroker 上,连到 docker compose 环境 里的节点。服务在启动时声明一个经典队列,用和上面的对比相同的预取窗口来消费它,并且跑在单线程的 tokio 运行时上。队列由另一个线程在服务处理任何消息之前填好,填充要等节点确认了每一条消息才结束。

被统计的是服务线程上的一切:框架的分发、编解码器、本 crate 的代码,以及 lapin 客户端在这个线程上 做的工作。lapin 在自己的线程上读写套接字,那个线程不计入。每一行的内存分配大多来自客户端:每一次 确认、每一次发布,都会在服务线程上创建 promise 和一个内部任务。

指令数和分配次数都是稳态下每条消息的值:1000 次投递的运行和 2000 次投递的运行之间的斜率。最后一列 是启动服务并处理第一次投递一次性付出的开销:建立连接、打开 channel、声明队列、注册消费者。这些数字 是绝对值,框架自身的开销也算在内;框架单独的开销由核心库在它的 基准测试页面上公布。

真实的节点按自己的节奏应答,所以服务线程等待套接字的次数每次运行都不一样。同一份代码运行七次,指令数 最多相差 2.4%,内存分配最多相差一个块。每个场景的分配下限比它测到的最大值高出千分之一,分配次数超过这个 下限时 just bench-code 失败。加上 --baseline=main 时,指令数多出百分之五以上也算失败;不带基线的运行 不限制指令数。改变开销的合并请求要附上自己的数字。

机器

构建标志和数字一起公布,因为它们会改变这些数字。用 -C target-cpu=native 构建出的二进制给出的 结果,换一台机器就复现不了,所以这条 recipe 在构建前先把这个变量清空。

这些数字不代表什么

这里只有一个消费者、一个队列、一个很小的消息体,以及一个跑在回环地址上的节点。它测的是一次投递 在这个 crate 里的开销,不是 RabbitMQ 能扛多少。这里的一行也不能拿去和另一个 Broker 公布的一行 比较:不同的传输在每条消息上做的事并不一样。

预取窗口决定了这些绝对数值。一个每条消息都确认、同时只允许一条未结清投递的消费者,每条消息都要 花掉一个到服务器的往返,三个循环就都落在这个往返的速度上。这些运行用的宽窗口,才让传输之上的 那些代价有机会显现出来;窗口一动,每一列一起动。

每次运行都声明自己的队列,跑完就删掉,所以没有哪个循环会去处理上一次运行留下的东西。消息体发布时没有 要求持久化:经典队列把它们放在内存里,而仲裁队列那一行带着 RabbitMQ 自己的持久化,因为仲裁队列 没有别的模式。

一次运行的测量窗口从第一次投递开始,到最后一条消息上的活干完为止,三个循环都是这样。每个循环都 在那一刻之后才确认投递,所以一次运行携带的数百万次确认里的这一次,在每个循环里都落在数字之外。

这些数字是一台机器在某一天的快照。 它们由人手工重测,测量期间机器只跑这一项:这一页要讲的差值,比与其他任务共用的机器上的噪声还小。

自己跑一遍

just bench

这条 recipe 从 docker-compose.test.yml 起停节点,跑完两个场景,然后把测到的结果写回 docs/benchmarks/results.json。它要花几分钟,并且需要整台机器。消息条数不是固定的:一次试探运行 会把它定下来,使得每一次被测量的运行在所在机器上都不短于五秒。

just bench-code

这条 recipe 从 docker-compose.test.yml 起停节点,在 valgrind 下统计代码表,并重写同一份文档里的 code 部分。它要花不到一分钟,需要 valgrind 和基准测试运行器: cargo install --locked gungraun-runner --version =0.19.4。