跳转到内容

诚实解读 Aeron® 基准测试

Aeron® 基准测试本身是个好工具,问题几乎总出在我们解读它的方式上。最常见的错误:把某个选定速率下的延迟当成容量来引用。这是两种完全不同的说法,团队往往就在这个落差里骗了自己。

这一页要纠正三种具体的误读:

  1. 把”基准峰值”当成”消息上限”。 基准从不主动去找上限。你给它速率,它给你延迟。真正的上限——那个拐点(延迟越过 SLA 的地方)——基准压根没测。
  2. burst=10 的数字当生产结果引用。 burst 旋钮把流量预先攒成一个生产里根本不存在的最佳情形——没人会在实时行情源上装个”按 10 条打包”的开关。漂亮数字有一部分就是这个旋钮造出来的。
  3. 跟着复读”DPDK 比 Java driver 快约 60× / 200×”。 这个比值拿的是健康的 DPDK 点去比已经饱和的 Java 点。让两者都跑在拐点之下,差距就塌缩成一个小倍数——AWS 自己 2025 年的基准也证明了这一点(见 DPDK 神话)。

这些不意味着基准坏了,也不是说跑过的测试白费了。工具本身没问题,问题在于从它身上推出了过头的结论

cluster 基准(remote-cluster-benchmarks)和 MDC 基准(remote-echo-mdc-benchmarks)都是闭环延迟测量台。压测生成器会:

  1. 以你指定的固定目标速率--rate)向 cluster(或 MDC publication)发消息。
  2. 在 offer 时用 clock.nanoTime() 给每条消息打时间戳。
  3. 往返延迟(ingress → 共识/commit → service → egress)测进一个 HdrHistogram。
  4. 报告该速率、该消息大小下的 P50 / P99 / P99.9 / Max

它是块秒表,不是台容量计。它回答的是*“在速率 R、大小 S、MTU M、到达形态 B 下,延迟分布长什么样?“——一个精确、可复现的问题。它直接回答”这台机器最多能扛多少?“*

它测的测的
给定速率/大小下的延迟你自己流量的真实饱和上限
一个人为的到达过程(限速 + 打包)真实的突发 / 可合并的生产到达
transport + 共识的热路径你应用的业务逻辑、GC、序列化
一次只测一种消息大小混合大小的真实流量
固定窗口内的稳态冷启动、failover、开盘尖峰

把每一行基准结果当成一句带全部限定语的话来读,不能只看数字:

如果我把这套确切配置按速率 R、消息大小 SMTU M、到达形态 burst=B 跑一个稳态窗口,那么往返延迟分布是 P50/P99/P99.9 = …,X% 的消息到达。”

“那么”之前的一切都是你选的输入。数字只是这些输入喂出来的结果,不是这台机器的固有属性。

基准擅长的用途:

  • 给芯片 / NIC / MTU / driver 做相对排名——只要每个输入都固定(同样的 R/S/B/MTU 下比 c8a、c8i、c8g)。这是它最强、最可信的用法。
  • 找拐点——前提是你拿真实的消息大小和到达形态去扫速率:P99.9 越过 SLA 的速率,就是那种流量下的可用上限。
  • 回归检测——某次代码/配置改动有没有在固定工作点上挪动延迟?
  • 尾部刻画——在指定负载下,看 NAK/重传/排队在哪里冒出来。
  • 可复现、可独立验证的同类对比(这正是测量台要固定速率的原因——见下文 AWS 的方法论)。

基准不是:

  • 不是容量/吞吐上限。 “1M,✅” ≠ “上限是 1M”。上限是拐点,而这一行没找到它。
  • 不是生产延迟预测——除非你的到达形态、大小、MTU 都和这次运行相符。一个 burst=10、32B 的头条数字,在均匀节奏、470B 的生产源上不会复现。
  • 不是 driver 的”速度倍数”。 DPDK 和 Java 的差距不是一个恒定倍数,而是拐点位置和尾部的差异,只有一方已经饱和时才显得巨大。
  • 不是整机数字。 它不含你的业务逻辑、序列化、GC、磁盘、failover——而这些全都会落进用户真正感受到的端到端延迟里。
  • 不是突发下的保证。 稳态 P50 对开盘尖峰只字未提;那要看突发形态下的 Max/P99.9。

一句话准则: 流量形态是你自己发明的,基准告诉你的是这种形态下的延迟;而流量形态本身,是生产告诉你的。绝不要拿前者当后者的容量来引用。

BURST_SIZE → 压测生成器里的 -Dio.aeron.benchmarks.batch.size。它不是 Aeron 的代码路径——不管它取几,Aeron 的 offer() 和 driver 的 TermScanner 都逐字节一模一样。这个旋钮只改offer 的节奏

batch.size = N → 连续 offer N 条消息,然后等下一个速率 tick。
  • batch.size=1(均匀): offer → 等 → offer → 等。media-driver 的 Sender 在下一条到来前就把每一帧排空了 → 每个 datagram 一条消息 → 接收端付满每 datagram(每秒包数)的代价 → 它很早就饱和
  • batch.size=10(突发): 十次 offer 之间没有间隔 → 这十帧共处于同一个 term buffer → TermScanner.scanForAvailability() 把它们合并进一个 ≤ MTU 的 datagram → 接收端只做约 1/10 的每-datagram 工作 → 同样的速率轻松通过

合并这个机制在生产里 100% 真实。但硬凑一批固定 10 条的那个旋钮,只存在于基准里。所以 burst=10 的数字是一次最佳情形、预先合并的测量。生产源本是均匀到达,你却拿 burst=10 的”峰值”当容量,就会错出一个大倍数。

一个旋钮把 5 毫秒的灾难变成 56 微秒

Section titled “一个旋钮把 5 毫秒的灾难变成 56 微秒”

这不是假想。我们特意做过一次严谨的 A/B 来了结这个问题——同一套 4 节点测试床(c8i.8xlarge、单 AZ placement group、隔离并 pin 核、关 C-states),同样的 1M msg/s × 32B cluster 负载,每组 MTU 都是 8192,而且交错着跑(每轮把所有配置各跑一遍,重复 3 次),这样 session 漂移就没法冒充成结果。唯一变动的只有 BURST_SIZE 这个旋钮。

Java media driver,burst=1(均匀节奏)——一台饱和的机器:

指标结果
P504,698 – 7,737 µs
P9913,214 – 15,385 µs
P99.913,623 – 16,974 µs
Max高达 20,677 µs
状态饱和——撞到了包速率的墙

中位数五到八毫秒,P99 尾部约 14 ms。作为消息传输这是不及格的——单个接收线程每秒派发不了 1M 个独立 datagram,于是进料队列一路堆到失控。

同一台机器、同样的代码、同样的硬件——把 BURST_SIZE=1 → 10

指标结果
P5056.4 – 125.5 µs
P9972.8 – 188.2 µs
P99.990.0 – 197.5 µs
Max267 – 505 µs
状态脱离饱和——稳稳扛住整个 1M/s

中位数从 ~5–8 ms 塌到 ~56 µs——改善约 60–130×,最好的一轮进到亚 100 µs,P99.9 尾部从约 14 ms 降到 ~90–197 µs。没换更快的 NIC,没上 DPDK,没加核,没改一行代码。 变的只是一个决定客户端什么时候调用 offer() 的旋钮。魔法全在这里:burst=10 时大约十条 32 B 帧合并进一个 datagram,于是 leader 的接收端只做约 1/10 的每-datagram 工作,翻过了原本快淹死它的那堵墙。

2b. MTU:什么时候有用,什么时候毫无作用

Section titled “2b. MTU:什么时候有用,什么时候毫无作用”

MTU(AERON_DPDK_MTU,或 channel 的 mtu/term 配置)是合并用来装东西的那个盒子的大小。巨型 MTU(8192 / 9001)只有在有东西可装时才有用。这是最容易被误解的旋钮,因为MTU 和 burst 是相乘关系——MTU 定合并的天花板,burst 决定你能不能够到它。

TermScanner.scanForAvailability() 会把尽可能多的、能塞进 min(senderWindow, MTU) 的驻留帧打进一个 datagram。所以合并比受两个独立上限约束:

  • MTU能塞下几帧(一个天花板)。
  • Burst / 到达形态 → sender 扫描时实际驻留了几帧(供给量)。

你拿到的是 min(装得下的, 在场的)。盒子再大,每次只塞一件东西也白搭。

情形 A——burst=1(均匀,只有一帧驻留):MTU 几乎不起作用。 均匀 offer 时,Sender 在下一帧到来前就把当前帧排空了,所以每次扫描只有约 1 帧驻留。这一帧不管 MTU 多大都照样进 datagram——8192 字节的 MTU 只装一帧 32B,线上就是 32 字节,跟 1408 字节的 MTU 没两样。均匀情形下加大 MTU 并不会降低你的 datagram(PPS)速率,因为 datagram 数量是由消息条数决定的,不是 MTU。这就是为什么面对均匀源的饱和,“直接开巨型帧”往往一点用都没有(MTU 甚至会稍微拖累均匀情形的延迟尾部:buffer 更大,缓冲就多那么一点。)

情形 B——burst=10 / 连续(多帧驻留):MTU 是真杠杆。 现在多帧共处,datagram 会装到 MTU 上限。MTU 直接决定合并比,且随 消息大小 vs MTU 缩放:

消息大小每个 1408B MTU 装几帧每个 8192B MTU 装几帧每个 9001B MTU 装几帧
32B~40~230~250
288B~4–5~26~29
1344B1(勉强塞下)~6~6

(近似值——要减去 Aeron/UDP/IP 的封装开销;帧按 32 字节对齐。)

从这张表能读出两点:

  • 32B、突发下,MTU 影响巨大: 40:1 对 250:1 的 datagram 缩减。巨型帧把天花板抬高一大截。
  • 1344B 下,1408B 的 MTU 根本合并不了(一帧勉强塞满),所以在你同时抬高 MTU 之前,burst 什么都给不了你。这正是为什么 1344B 那些行在 MTU 1408 下即便上了 DPDK 也会在 1M “饱和”,却在 8192 或更低速率下通过——那里缺的杠杆是 MTU,不是 burst。

陷阱: 一个漂亮的头条数字通常把两个旋钮都拧到了最佳:小消息 + 大 MTU + 突发。也就是 32B / 8192 / burst=10 → 约 250:1 合并 → 每-datagram 天花板一下子好躲了 250 倍。生产里消息更大、MTU 是默认值、或者均匀到达,这三样任占一样都吃不到这个乘数,会早得多就饱和。

经验法则:

  • 均匀(burst=1)流量: MTU 对你的 datagram/PPS 上限几乎无关——别指望巨型帧救你。要么修到达形态(micro-batch),要么抬高天花板(DPDK)。
  • 突发/连续流量: MTU 是一等杠杆,消息相对 MTU 越小,它的收益越大。把基准的 MTU 消息大小都对齐生产,否则合并比(连带峰值)迁移不过去。

2c. 如何正确地把”burst”公式化

Section titled “2c. 如何正确地把”burst”公式化”

到目前为止,“burst”指的都是基准的 batch.size 旋钮。但当你给一个真实系统做容量规划时,burst=10 这个单位就用错了。基准旋钮问的是*“我这一个客户端连续 offer 几条消息。”* 生产要问的是管道另一头:“一个排空间隔内能有多少消息落到接收端,我的 buffer 扛不扛得住这一坨、会不会丢包?” 这是两个不同的量,而只有第二个能预测你在现场真正看到的几十毫秒尾部。

burst 是同时到达的条数,不是速率

Section titled “burst 是同时到达的条数,不是速率”

现实里最大的突发来源是N 个独立生产者在同一瞬间开火——而不是某一个客户端的节奏。我们做过根因分析的一起生产事故就是教科书级的例子:

  • 8 条 replay 流,各自都是稳定的长生命周期 session,偏偏帧对齐了,同时爆发。
  • 基线 45.9 pkts/ms;叠加起来的尖峰冲到 658 pkts/ms——17.2× 基线
  • 单个 Aeron 接收线程只能串行排空,于是在一个 211 ms 的窗口里 57,165 个包堆在了 OS socket buffer 里 → 最坏延迟 57.7 ms,45,259 次慢事件。

网络本身没问题(transit 19 µs,p99 28.5 µs),没有哪个环节”太慢”。就是 N 个源撞到了一起,这一坨超过了一个排空周期能清掉的量,于是排起了队。这才是生产里”burst”的含义:一次叠加,用**一个服务间隔内到达了多少包(或多少字节)**来度量。这一节是微突发基准测试展开的接收端视角。

把 burst 建模成:当到达短暂超过排空时,接收端必须扛住的那个队列:

峰值到达速率 λ_peak = Σ (每个源的峰值速率) [msgs/s] ← 叠加所有源
排空速率 μ = 接收端能派发的 msgs/s [msgs/s] ← 拐点
突发时长 T_b = 这一坨持续多久 [s] ← 来自你的 trace / 对齐
要吸收的积压 Q_burst = max(0, (λ_peak − μ) × T_b) [msgs] ← 真正要紧的数字
buffer 必须覆盖 B_bytes ≥ Q_burst × 每条消息平均线上字节数 [bytes]
最坏排队延迟 L_q ≈ Q_burst / μ [s] ← 例如 57k / (1M/s) ≈ 57 ms
  • λ_peak 是个总和,不是单条流。N 个同步的源、各自峰值 r,那么 λ_peak ≈ N·r。那次事故的尖峰就是 8 个源叠加达到 17× 基线。永远做叠加;只看单源速率会骗了你。
  • Q_burst 才是要算出来的东西。 它是 λ_peak > μ 这段时间里堆起来的消息数。你的 socket buffer / Aeron 接收窗口 / term buffer 只要装不下 Q_burst,就会丢包 → NAK → 重传 → 冒出那条多毫秒的尾巴。这直接连到 buffer 规划:Q_burst 来定 buffer,而不是按平均速率。接收路径 bufferbuffer 规划计算器
  • L_q 才是你真正会测到的尾部。 57,165 msgs ÷ ~1M msgs/s ≈ 57 ms——公式把观测到的事故复现了出来。这才是要摆到产品团队面前的数字,不是基准中位数。

两笔各自独立的预算——别混为一谈

Section titled “两笔各自独立的预算——别混为一谈”

有两种”突发容量”,由不同的东西决定:

  1. 传输/流控的突发预算(发送端)。在 NIC egress 限速器(AWS token-bucket)上,基线之上有一笔固定的比特突发额度(比如一个深 1 ms的桶,在 5 Gbps 实例上就是 5,000,000 比特),而恢复时间 = 突发额度 ÷ 每毫秒的富余比特。这笔预算按比特算,跟 MTU 无关;MTU 只决定这笔固定比特预算里能塞几条消息(32 B 的 Aeron 帧头是每条各算一份、摊不掉的,所以又小又单帧的消息几乎占不到便宜)。它在发送端堆起的队列,见发送端排队与突发限速
  2. 接收吸收预算(接收端)。就是上面的 Q_burst——受 socket buffer + 接收窗口 + 排空速率 μ 约束。上面那起事故里崩掉的正是这个。

你实际的突发耐受度取两者的最小值 就算 5 Gbps 的 egress 额度再慷慨,只要接收端的单线程排空 + socket buffer 吸不下由此产生的那一坨,也是白搭——事故里发生的就是这一幕。

不是 batch.size。是 Q_burst = (λ_peak − μ)·T_b,其中 λ_peak 叠加了所有同时开火的生产者μ 是接收端实测的排空速率(拐点),T_b 是 trace 里那一坨的时长。把 socket buffer / 接收窗口 / term buffer 定到 ≥ Q_burst × 每条线上字节数,让 λ_peak 保持在 发送端比特预算 和 接收吸收预算 两者的最小值之下,并拿 L_q ≈ Q_burst/μ 当作你的最坏尾部去引用。缩小 Q_burst 有两个杠杆:把 μ 抬上去(把 subscription 拆到 K≥2 个接收线程/driver、上 DPDK),或者把 λ_peak/T_b 压下来(给生产者去同步、给它们的发送错峰加抖动,别让 N 个源帧对齐)。

3. 限速器 vs 开环——以及生产究竟是什么

Section titled “3. 限速器 vs 开环——以及生产究竟是什么”

基准的速率限速器(那个”等下一个 tick”)是为了让各次运行可比才存在的。它在生产里不存在——你没法叫交易所按 300K/s 发行情。

生产是开环的:数据来了你就 offer()。约束不来自时钟,而来自背压(publication 窗口满时 offer() 返回 < 0 / BACK_PRESSURED)。真正的生产设计决策是你的溢出策略,而这一点基准从来不去触碰:

策略何时适用代价
阻塞 / 自旋重试要求无损(下单、有序命令)背压必须往上游传;否则延迟尾部无界
合并 / 丢到最新行情数据 / 状态快照(只有最新的要紧)丢掉中间值(对行情源通常没关系)
有界缓冲 + 策略吸收微突发,满了才施加策略务实的默认选择

“去掉 sleep” ≠ “burst=10”。 去掉 sleep 是撤掉速率控制、改跑开环;这时合并变成了饱和自然带出来的副作用(受 MTU 上限约束,往往不止 10:1),而任何排队延迟都会落进你测到的延迟里。那是一次吞吐实验,不是受控速率下的延迟实验。

要让基准能预测你的部署,别再优化头条,开始贴合现实:

  1. 对齐到达过程,而不只是速率。
    • 最好:回放一段抓下来的生产 trace(真实的到达间隔与突发)。
    • 否则:给突发性建模。别因为 burst=10 好看就默认用它。
  2. burst=1(均匀)作为保守下限报出来,把 burst=10 标注成”要流量确实突发或者你加了应用层 micro-batch,才达得到”。两个都报,各自标清楚。
  3. 消息大小和 MTU 都对齐生产。 合并比随 size:MTU 缩放(32B 进 1408B ≈ 40:1;470B ≈ 3:1;1344B 可能压根合并不了)。
  4. 测尾部,别只看中位数。 突发形态下的 P99.9 / Max 才是 NAK/重传和队列堆积冒头的地方——那才是你真正的风险,不是 P50。
  5. 如果生产是均匀节奏、你又想要更高的数字,那就得靠设计: 在客户端加一个有界 micro-batch(凑够 N 条攒够 T µs 就 offer,谁先到算谁——两头都要封顶,这样一段冷清期也不会把某条消息卡住)。然后去测那个客户端,而不是那个人造旋钮。
  6. 把和 SLA 相关的冷路径也纳进来: 启动、leader failover/RESIGN、快照、开盘尖峰。
  7. 在你的目标 ρ、拐点之下去测。 定容量时让正常的微突发永远碰不到背压;一旦碰到,就把它当过载信号看,而不是稳态。

准备你自己的负载——即便没有历史数据

Section titled “准备你自己的负载——即便没有历史数据”

你能做的最有价值的一件事,就是别再喂给基准一个人造形态,改喂你自己的形态。

如果你有生产流量——搭一个回放:

  1. 把到达时间戳抓下来,不只是条数。要紧的是到达间隔分布突发结构。来源有:应用层 publish 时间戳(最好,和 offer() 同一层)、ingress 流的 pcap/tcpdump、或带每条接收时间的 feed-handler 日志。
  2. 消息大小分布也一并抓(小心跳 vs 大快照)。一张大小直方图比单个平均值管用得多。
  3. 忠实回放——直接拿抓下来的 (timestamp, size) trace 去驱动,把它压成固定的 --rate + burst。要是你的测量台没法回放 trace,至少把 trace 拟合成一个速率一个真实的突发系数,跑那个,并注明是近似。
  4. 放大也要诚实。 预期 3× 增长,就按 3× 回放(把到达间隔压缩),但突发结构要保住——别只抬一个平坦速率,那会把真正搞垮你的突发给抹平了。
  5. 回放最坏的那个窗口,不是平均的一天。 回放你开盘 / failover / 重试风暴的那一分钟。

如果你是个没有数据可回放的新项目,那就把真相框在一个区间里,别去猜某一个点:

  1. 两端永远都要跑: burst=1(均匀节奏)= 你的悲观下限(这里达标,那个速率下对任何到达形态你都安全);burst=high / 开环 = 你的乐观上限。真实生产落在两者之间。按下限定容量,别按上限。
  2. 找拐点,别信头条。burst=1 和你最靠谱的那个消息大小下扫 --rate,记下 P99.9 越过 SLA 的位置。那个拐点才是你站得住脚的容量数字。
  3. 从第一性原理去估到达: 拿你的业务峰值(开盘时的每秒订单数、最活跃品种的每秒 tick 数),乘一个安全系数(金融峰值常用 2–3×),并且在没被证伪之前一律当它是突发的——真实的事件流会成团,不像 Poisson 那么平滑。消息大小从你的 schema 来(编一条有代表性的 SBE/proto 消息、量出它的线上大小),别靠猜。
  4. 先留出余量,再测进去。 在你估的峰值下把目标定到 ρ < ~0.7,上线,然后从第一天起就给生产装上探针(publish 时间戳、offer() 背压计数、driver 周期时间阈值),让你出来的模型在几周内就被真实 trace 换掉。
  5. 溢出策略要在上线前就定,别拖到第一起事故——无损阻塞 vs 合并丢弃 vs 有界缓冲。没有数据时,速率估错了会怎样比速率估得准不准更要紧。

心态上的转变: 没有历史数据时,你的任务不是憋出一个惊艳的数字——而是给出一个你站得住脚的安全下限burst=1 拐点),外加一份尽快用真实数据替换掉假设的计划。

基准做相对比较——芯片、NIC、MTU、driver——是真有用的,前提是你固定住到达形态,并且把它当相对量、而不是绝对量来读

要做:

  • 在同样的速率/大小/burst/MTU 下比机器,给芯片排序(比如 c8a、c8i、c8g)——基准在排序上靠得住。
  • 给每个规格找拐点: 用你的生产消息大小burst=1(或你的 trace)去扫 --rate,记下 P99.9 越过 SLA 的位置。那个拐点——不是头条 P50——才是这个规格在那种流量下的可用上限。
  • 按余量定容量: 挑那个拐点稳稳高于你峰值到达速率的规格(目标 ρ < ~0.7),让突发有地方可去。
  • 按你的 SLA 给尾部加权。 一台 P50 更低、突发下 P99.9 却更肥的机器,对延迟敏感路径反而可能更差。
  • 算整台机器的账。 Cluster 得给 conductor/sender/receiver/archive/共识/service 分核;一个”更快”却逼出跨 NUMA 或超线程争用的规格,可能反倒输。

不要:

  • 不要拿一个 burst=10、32B、1M 的头条去定容量——除非你的生产真就那么突发、那么小。那是最佳情形。
  • 不要把”饱和”当成”这机器太慢”。 它通常只是说这个速率在这个大小/MTU/burst 下撞穿了每-datagram 或每-frame 的天花板——同一台机器换个 MTU、burst 或速率就可能过(比如 1344B 在 1M 饱和、在 100–300K 就过了)。
  • 不要把一种大小外推到另一种。 合并比和每-frame 成本都跟大小有关。

核心告诫:基准结果 ≠ 消息上限。 一行写着”1M msg/s,P50 42 µs,✅ 100%“,意思是*“在我们要求的速率(1M)、这个大小/MTU/burst 下,延迟是 42 µs、没丢包。”* 它等于”1M 就是上限”。上限在你的流量形态对应的那个拐点上——可能远低于 1M(均匀、大消息),也可能高于它(突发、小消息)。永远为你自己的工作负载找拐点;绝不拿头条当容量来引用。

出事通常就出在这一步。工程师一句”我们测到 1M/s 下亚 100 µs”,几次会开下来,就变成了对客户的 SLA——“每秒一百万条消息、亚 100 µs”,然后生产差了 50×,酿成信誉危机。基准数字是你选定条件下的实验室读数;产品承诺则必须是你在最糟的生产日也守得住的话。两者几乎从来不是同一个数字。

绝不把一个原始数字直接递给产品团队。先把它翻成一句带齐所有条件的话,再单独给出可承诺的那一版:

❌ 不该说✅ 应该说
”Aeron 能做到 1M msg/s、亚 100 µs。""在实验室最佳情形下(小消息、预先打包、不含业务逻辑),我们看到 1M/s 时约 56 µs。对着你的流量形态,我们能承诺的目标是 P99.9 < X µs @ Y msg/s,并留出到 Z 的余量。"
"P50 是 42 µs。""中位数在拐点之下的负载时约 42 µs;但对你的 SLA 要紧的是 P99.9 尾部(A µs),以及突发下的最坏 Max(B)。"
"它没饱和。""它能维持 Y msg/s、留着 ρ ≈ 0.6 的余量;就你的消息大小而言,一过约 K msg/s 它就饱和,延迟进入毫秒级。“

在尾部、在真实工作点、带余量地承诺

Section titled “在尾部、在真实工作点、带余量地承诺”
  • 要报 P99.9(和 Max),绝不能只报 P50。 产品和客户感受到的是尾部——42 µs 的中位数配上 14 ms 的 P99.9,那是个 14 ms 的产品,不是 42 µs 的。
  • 在你规划的工作点(ρ < ~0.7)上承诺,别在拐点上承诺。 拐点是它崩的地方;你要承诺的是拐点之下那个从容的速率。
  • 承诺 burst=1 / 你自己 trace 的数字,别承诺 burst=10 头条(除非配得上它的 micro-batch 你已经做出来并上线了——那就去承诺那套系统实测的数字)。
  • 余量要写明。 内部目标得比对外 SLA 留出一个安全系数,因为生产里有 GC、业务逻辑、failover 和吵闹的邻居,基准里都没有。

每一条性能承诺都得带齐它的五个限定语,不然那就不是承诺,是传闻:速率、消息大小、MTU、到达形态(burst/trace)、分位数。“P99.9 < 200 µs @ 300K msg/s、288 B、MTU 8192、按生产 trace 到达”是一句你能测、也守得住的话;“亚 100 µs”不是。

  1. 实验室最佳情形(头条):“传输在理想条件下跑到什么程度”——用于竞争定位,绝不拿来当 SLA。
  2. 可承诺目标(你的 burst=1/trace 拐点,再减去余量):“我们为你的工作负载承诺什么。“这是 SLA 的输入。
  3. 崩溃点(拐点 / 饱和速率):“它从哪里开始劣化”——好给容量规划和告警划一条真正的红线。
  • 饱和悬崖。 延迟不是线性的——它先一路平着,一过拐点就跌下悬崖(56 µs → 5 ms)。告诉他们这里有一道硬边、它在哪、以及你会把工作点定在它下面。
  • DPDK”快 60ד陷阱。 有人抛出一个巨大的加速比时,说清楚那多半是饱和假象,拐点之下诚实的差距是 ~1.3–1.5×——所以 DPDK 买来的是上限和尾部的稳定性,不是一个能拿去对客户承诺的通用延迟倍数。

给产品团队的一句话: “这是它在实验室能做到的(最佳情形),这是我们对你真实流量会承诺的(带余量、看尾部),这是它崩的地方。请引用中间那个——连同它的条件——千万别引用第一个。“

6. “DPDK 比 Java driver 快 8× / 60× / 200ד的神话

Section titled “6. “DPDK 比 Java driver 快 8× / 60× / 200ד的神话”

内核旁路的 DPDK driver 确实比 Java/C media driver 明显更快、抖动更低。但那些吓人的比值是牛头不对马嘴的假象——拿一个健康的 DPDK 点去比一个已经饱和的 Java 点。这话你不用光听我们的,因为 AWS 自己 2025 年的 Aeron 基准恰好把这一点摆了出来。

来自 AWS Industries 的文章”Aeron on AWS: 2025 Performance Benchmark Results”(全部在 c6in.16xlarge、Cluster Placement Group、MTU 1408 上):

Aeron Cluster @ 100k msg/s(P99,µs):

DriverP99
OSS – Java136
OSS – C139
Premium(内核旁路 / DPDK)98

→ 在 100k,Premium 的 P99 好约 1.4×。不是 8×,不是 59×,就 1.4×

Aeron Cluster @ 1 百万 msg/s(P50 / P99 / P99.9,µs):

DriverP50P99P99.9
OSS – Java3,3018,4799,306
OSS – C4,9488,5778,987
Premium(内核旁路 / DPDK)106143158

→ 在 1M,OSS 已经跌下拐点(P99 8,479µs = 8.5 ms),Premium 还平着(143µs)。AWS 自己把这说成*“59× 的性能提升”*。

把这两张表并排一读,整个故事就摆在那儿: 同样两个 driver,在 100k 差 1.4×、在 1M 差 59×。driver 没在这两行之间变快 40×。是 OSS driver 饱和了,DPDK driver 没有。 “59ד不是速度倍数——它是一个越过了拐点的系统一个还在拐点之下的系统之间的距离。AWS 甚至把设置也点明了:1M 那个数字意在反映*“真实的峰值和突发速率”,而”生产系统通常会超额配置,好避开网络饱和”*——换句话说,你不会在生产里把 OSS 跑到那个饱和点;你会配在拐点之下,那里差距很小。

AWS 的 Transport(非 cluster)表讲的是一模一样的故事:@100k,OSS-Java P99 32µs vs Premium 29µs(基本打平);@1M,OSS-Java P99 57µs vs Premium 39µs(~1.5×)。博客自己那句总结——“Premium 比 OSS 高出 33%(Transport)和 29%(Cluster)“——才是诚实的、拐点之下的倍数:大约 1.3×,离头条那个 59× 远着呢。

碰到任何 DPDK-对-标准 的说法,解读准则就一条: 先问*“这个工作点上,两个 driver 是不是都在各自拐点之下?”* 是的话,预期一个个位数的小倍数差异。要是标准 driver 饱和了、旁路的没有,那个巨大比值就是饱和假象——它告诉你的是 OSS 拐点在哪,不是传输快了多少。内核旁路也不是白来的(专用核、hugepages、NIC/PCI 绑定、和平台相关的搭建),所以该拿它去掂量的是你到底要不要更高的上限、或者更紧的尾部——而不是去信一个抽象的”快 8ד。

  • 这一行背后的到达形态burst、限速还是开环)我清楚吗?
  • 消息大小 + MTU 跟生产对齐了吗?
  • 这是 P50 头条 还是 P99.9 尾部——我的 SLA 在乎的是哪个?
  • 这台机器在这个速率下是在拐点之下、还是已经在饱和?(状态标了”Saturated”或劣化吗?)
  • 我做的是同类对比吗(两个 driver 都没饱和、大小/速率/burst 都一样)?
  • 我为自己的流量找过拐点了吗,还是拿头条当容量在引用?
  • 我选的规格,在峰值到达时留了 ρ < ~0.7 的余量吗?
  • Q_burst 时我把所有同时开火的生产者都叠加进去了吗(而不是用单源速率),我的 socket buffer / 接收窗口 / term buffer ≥ Q_burst × 每条线上字节数 吗?
  • 我是不是正要把这个数字递给一个非工程师?如果是,我把它翻成可承诺目标了吗(看尾部、在工作点、带条件)——而不是甩出实验室头条?

这一页讲的是怎么解读数字。至于 driver 如何把字节送上线路的那套机制——TermScanner、流控、丢包恢复——请去 The Aeron Files,我们不在这里重新推导。至于 Q_burst 往下喂进去的 buffer 规划,见接收路径 bufferbuffer 规划计算器发送端排队与突发限速