基准测试¶
这个 crate 的订阅者与发布者处在 SQS 客户端和你的代码之间,运行时则在它们之上。两者每条消息都要付出
时间:接收泵、它交出的投递、它的 ack 背后的删除、进入你的处理器的分派。本页给出各自的开销有多大,
参照物是同样的工作用 aws-sdk-sqs 手写一遍。
同一个进程把一个场景跑三遍,是三个循环,各自只差一件事:由什么来承载消息。
- 原生客户端 -
aws-sdk-sqs,由基准测试里的循环直接驱动。 - 本 crate - 同一个循环,跑在这个 crate 的 Broker、订阅、投递、
ack和发布者上;没有处理器, 没有路由器,没有分派。 - 完整服务 - 用户写的那个服务:
#[subscriber]、应用、运行时。
因此两个开销列回答的是两个不同的问题。本 crate 相对原生是这个 crate 的订阅者与发布者相对它们 包装的客户端的代价,这是本仓库要负责的问题。服务相对原生是整条链路的代价;两列之间的落差就是 运行时在这个 Broker 上所占的份额。之所以发布在这里,是因为一个随 Broker 而变的份额说明的是两者如何 衔接,而不是核心本身。
其余一切在三者之间保持相同:AWS 配置、接收参数、接收请求的等待时间、每条消息的删除及其发生的位置、 解码成同一个类型、消息体的字节、tokio 运行时和构建方式。这套流程属于框架本身,写在 “方法”一节里。
测试台是实时测试所用的 LocalStack 模拟器,而不是托管服务。下面每个数字说的都是一次投递在这个 crate 内部的代价,别的什么也没说。
数字¶
三个交错轮次中的最佳值,括号里是中位的一轮。越大越好。
表格在你的浏览器里从最近一次运行写下的文档读取,所以本页没有任何可能过期的副本。
两行的节奏都由往返决定,这正是本页最要紧的一点。一次投递要付出自己的一次 DeleteMessage,因为这
正是这个 crate 的 ack 所做的事,再加上把它那一批带过来的 ReceiveMessage 的十分之一。到测试台的
往返时间是单独测量的,发布在下面,这些往返约占一次投递耗时的一半;往返达到一半的行标为受 Broker 限制。
本 crate 和框架的工作落在原生客户端同样要付的等待里,所以各循环之差是这部分工作代价的下界,而不是对它
的测量。
标为“无法区分”的行,是两边之差小于各自运行之间离散范围的行。低于运行噪声的数字读起来像是从未测得 的精度,所以不发布。
同一次运行的机器可读形式在
benchmarks/results.json,
框架站点用它构建跨 Broker 的汇总表。
crate 自身的代码¶
第二张表是一条消息在本 crate 代码路径上的开销,是数出来的,不是计时得来的:指令数由 callgrind 统计,
内存分配次数由 DHAT 统计。每个场景都是用户会写的那种服务:用服务里同样的构造方式建在 SqsBroker 上,
指向同一个 LocalStack 测试台,并在单线程运行时上启动。在启动和排空之间,由另一个线程往队列里填消息,
这个线程不计入。
计入的是服务线程执行的一切:框架、本 crate,以及 aws-sdk-sqs 和它的 HTTP 栈在这个线程上的工作,
也就是构建并签名每个请求、解析每个应答。客户端自己开的线程不计入,每次往返中模拟器那一侧也不计入。
每次投递都要分摊一次 ReceiveMessage,并付出自己的一次 DeleteMessage,回复还要再加一次
SendMessage,所以每一行的大部分是 AWS 客户端的工作,而不是本 crate 的。
指令数和分配次数都是稳态下每条消息的值:1000 次投递的运行和 2000 次投递的运行之间的斜率。最后一列 是启动服务并处理第一次投递一次性付出的开销:加载 AWS 配置、解析队列、读取它的可见性超时时间。这些 数字是绝对值,框架自身的开销也算在内;框架单独的开销由核心库在它的 基准测试页面上公布。
一批是十条消息,这是一次 ReceiveMessage 最多返回的条数;批里的每次投递仍然各自用一次调用删除。
同一个二进制文件跑四次,指令数相差不到半个百分点,分配次数在 170 万次里最多差三次,这取决于模拟器
如何作答、套接字的读取如何切分。just bench-code 在分配次数超过场景声明的下限时失败,这个下限是测到的
最大计数再加千分之一;加上 --baseline=main 时,指令数多出百分之二以上也算失败。改变开销
的合并请求要附上自己的数字。
机器¶
构建标志与数字一起发布,因为它们会改变这些数字:用 -C target-cpu=native 构建出的二进制,别的机器
无法复现它的结果,所以配方在构建前会清空这个变量。
这些数字不代表什么¶
测试台是模拟器。它在本地回环上用普通 HTTP 讲 SQS 和 SNS 的 API,因此 TLS 握手、凭证刷新和区域延迟 都不在其中。配对依然成立:两边面对的是同一个模拟器,发布的是两者之差。饱和状况则不成立:区域中的 队列按另一种时间尺度作答,这些数字没有一个能说明服务在那里的代价。
两边都请求 20 秒的等待时间,这是 crate 的默认值,也是协议上限。在始终有积压的队列上这个等待不会 走完,因为接收一有可答的内容就会作答;但它决定了空队列的代价,两边若在这一项上不一致,发布出来的 就是长轮询策略,而不是分派代价。
这里只有一个消费者、一个队列、一个小消息体和本地回环上的模拟器。它测的是一次投递在这个 crate 内部 的代价,本页的行也不能与为另一个 Broker 发布的行相比:不同传输每条消息做的工作并不相同。
一次运行测量的窗口从第一次投递开始,到最后一次投递被读完时结束,两边一致,所以结算它的那次删除落在 两边的数字之外。那是一次运行携带的数千次删除中的一次。
FIFO 的数字取自把消息体分散到 64 个消息组的队列。FIFO 队列在某个消息组有投递在途时会压住这个组, 因此只用少数几个组的运行测的是这种压制;组数多到这个程度,接收总有未被触及的消息组可以作答。
这些数字是一台机器在某一天的快照。 它们由人手工重测,测量期间机器只跑这一项:这一页要讲的差值,比与其他任务共用的机器上的噪声还小。
自己跑一遍¶
这条配方从 docker-compose.test.yml 启动测试台,跑完两个场景,停掉测试台,并把测得的结果写回
docs/benchmarks/results.json。它需要几分钟,并且要独占这台机器。消息条数不是固定的:一次探测
运行会把它定下来,使每次被测量的运行在所在机器上至少持续五秒。
这条配方启动同一个测试台,在 valgrind 下统计代码表,停掉测试台,并重写同一份文档里的 code 部分。
它需要几分钟,还要有 valgrind 和基准测试运行器:cargo install --locked gungraun-runner --version =0.19.4。