跳到主要内容
英文版本为准

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

方法论

RustStream 的每个 crate 如何测量自己,使一个 Broker 公布的数字与另一个 Broker 的含义相同。

每个 Broker crate 都遵循下面的流程。这样一来,不同 Broker 公布的数字含义相同。偏离流程的 Broker 会在自己的页面上说明。

三个循环​

一次运行把同一个场景测三遍,三者只有一个区别:消息由什么承载。

  • 裸客户端。 Broker 自己的客户端,直接驱动。
  • Broker crate。 该 crate 自己的消费者和发布者(它的订阅描述符、它产出的订阅者流、它的 确认、它的发布者)由基准测试里的一个循环驱动,上面没有运行时。
  • RustStream。 用户写的那整个服务:处理器、应用、分发。

第一项差值是这个 Broker crate 比它所包装的客户端多付出多少,这由 crate 自己负责。第二项是 运行时在其上多付出多少,而且是在这个 Broker 上;它按 Broker 分别公布,因为各个 crate 与运行时 相接的方式不同:流如何产出、投递是否成批到达、背压如何传到消费者。

  • 同一个客户端,同一套客户端配置。 预取、ack 模式、consumer group、持久化、连接数和 Broker 特有的调优,三个循环完全一致。框架一侧通过 RustStream 配置 Broker,最终生成的客户端设置 仍然必须相同。
  • ack 的位置相同。 RustStream 在处理器返回之后 ack,因此另外两个循环也在同一位置 ack。在裸运行 的末尾批量 ack,测的是另一种协议,而不是另一个框架。
  • 解码成同一个类型。 没有处理器的那两个循环用同样的编解码器把载荷反序列化成同一个结构体,并通过 std::hint::black_box 访问其中一个字段。省掉这一步,最容易得出错误的数字:解码结果没有人 用,优化器就会把解码删掉,裸的一侧于是悄悄不再解码。
  • 载荷逐字节相同。 每个循环消费的消息体都来自同一个生成器。
  • 运行时相同。 tokio 的 flavor、工作线程数和同时在处理的消息条数,在三个循环里都一样。
  • 构建相同。 构建配置、RUSTFLAGS 和分配器一致,可观测性 feature(logging、metrics、 otel)要么每个循环都关,要么每个循环都开。环境里带着 -C target-cpu=native 的机器,产出的数字 别的机器复现不了,因此这些标志与结果一起公布。

运行​

  • 消费者在第一条消息发布之前就已连接。 否则一侧消费的是 Broker 里已有的积压,另一侧收到 的是实时投递,而在多数 Broker 里这是两条不同的路径。
  • 每次运行都用自己的名字。 subject、队列、流或 consumer group 每次运行都新建,这样第 N 次 运行绝不会看到第 N-1 次留下的东西。
  • 计时窗口从收到第一条消息开始,到最后一条完成 ack 结束。 测量的运行之前,先跑一次校准 运行并丢弃结果。建立连接、注册消费者和最初的内存分配属于启动开销,不属于每条消息的开销。
  • 消息条数要让一次运行至少持续五秒,这样启动阶段的瞬态和计时器精度都落在噪声范围内。
  • 各个循环是交替的,不是分块的。 裸、crate、框架,裸、crate、框架,如此往复,共三轮。先把 一个循环整个跑完再跑下一个,会把机器的全部漂移(发热、后台负载、页缓存)算到最后跑的那个 循环头上。

报告​

  • 每个循环都报告三轮:最佳、中位和最差。 机器上的噪声只会让运行变慢,所以最快的一轮最接近 未受干扰的开销,中位的一轮是典型的一轮,最慢的一轮则说明机器离安静有多远。单次运行的单个 数字不算结果。
  • 表格里印出最佳的一轮,括号里是中位数。 最差的一轮留在文档里,不进入单元格:它的用处是 波动范围,也就是最佳的一轮到最差的一轮之间的距离。
  • 小于该波动范围的差异公布为 无法区分, 而不是一个百分比:低于运行间噪声的数值,读起来 像是从未测到过的精度。
  • 饱和的消费者要标注出来。 当裸的一侧整个运行都在等 Broker 时,该行带上 broker-bound。
  • 环境与数字一起公布:CPU 型号与核心数、内核、Broker 如何启动(镜像、容器、主机)、rustc 版本、各 crate 版本、构建配置和编译标志。没有这些,一个数字既无法复现,也无法判断它是否过时。

代码的测量​

每个 crate 都用自己的基准测试命令,生成它的代码开销图。 命令名写在该 crate 的基准测试页面上,在 ruststream 里是 just bench。这条命令需要 valgrind, 以及与该 crate 所依赖版本一致的基准测试 runner。 在 ruststream 里,just bench 5000 让每个场景在五千次投递上测量,而不是一千次:数字更稳, 运行更久;发布的文档仍按默认值测量。

  • 每个场景都是用户写的那个服务,由真实运行时启动,测试用的 harness 不参与编译,测的因此 就是发布出去的代码。数字是绝对值;在 ruststream 里,它就是框架自身的开销。与别人写的 客户端相比,是对照图的事,由 Broker crate 来做。
  • ruststream 的传输是进程内的。 被测的是框架自身的代码,数字不应随套接字、服务器负载或 网络而变。Broker crate 在真实的传输上测量,这给数字带来了什么,写在它的基准测试页面上。
  • 队列在测量区间打开之前就已填满。 场景测的是稳态投递,不含连接、订阅建立,以及它们背后的 首批内存分配。
  • 采集只覆盖测量区间,不越界。 准备和收尾跑在同一个进程里,走的是同一份框架代码,把它们也 算进去的测量,会把填满队列的开销当成清空队列的开销报出来。
  • 每个场景跑三次,各有各的用处。 一条消息,再加两次更长的运行,后一次的投递数是前一次的 两倍:在 ruststream 里是一千条和两千条,Broker crate 的条数写在它的基准测试页面上。 后两次之差是服务跑起来之后一条消息的开销,只投递一条的那次就是冷启动。运行途中不需要关掉 任何东西,正是这一点让这个办法对内存分配计数器也成立,因为它根本没有开关。
  • 每个场景两个数字。 callgrind 给出的指令数是精确的,用来对照;DHAT 给出的内存分配次数是 精确的,由场景声明的下限把住。在 ruststream 里,另有一个基准程序用自己的场景测量时钟时间。 这些时间有噪声,just bench 把它们打印出来,仅供参考。
  • 是对照,不是门槛。 带上 --baseline=main 运行时,基准测试命令把一个分支与目标分支上 同一套基准测试的结果对照:某个场景的指令数增幅超过该 crate 自己的上限,就算失败。 ruststream 的上限是两个百分点,Broker crate 的上限写在它的基准测试页面上。 上限是相对的,所以只在与基线对照时生效。 超出场景声明的内存分配次数,则不论有没有基线都算失败。 pull request 的作者自己运行对照并引用它的数字,而降低了某个场景的改动, 也在同一个 pull request 里降低它声明的下限。冷启动也按同样的规则检查。