跳转到内容

吞吐与延迟模型

一个用于预测 峰值 TPS、厘清 MTU 关系、并说明 为何峰值 TPS 不等于延迟 的模型,它源自 Aeron 驱动的发送路径,并经我们的基准测试验证。用它来回答一个问题:“配置 X 在负载 L 下会不会跑到 饱和?届时延迟又是多少?”

记号 —— S、μ、λ、ρ(以及为何 ρ 是比值而非速率)

Section titled “记号 —— S、μ、λ、ρ(以及为何 ρ 是比值而非速率)”
符号名称单位含义
S每线程峰值 sendmsg/s速率(/s)硬件/发送路径常数,Intel m7i 上 ≈ 400,000/s因 CPU 家族而异(见下)—— 须按平台实测。
C共识控制 syscalls/s速率(/s)被控制包挤占的预算(~160K noop、~75K sleep-ns)。
Dfollowers数量每条日志 datagram 的目的地数(每个目的地 = 一次 syscall)。3 节点集群为 2。
M_pkt每 datagram 消息数数量floor(usable_MTU / framed_msg)。即 MTU 乘数。
μ(mu)容量 = peak_TPS速率(msg/s)(S−C)/D × M_pkt,即”它 跑多快”。128B/1408 下 ≈ 790K。
λ(lambda)输入负载速率(msg/s)你实际打入的负载量,如 800K。
ρ(rho)利用率无量纲λ / μ,即工作点。

ρ 不是峰值 sendmsg 速率。 它是一个纯数字 = 输入负载 ÷ 容量:ρ = 0.5 表示跑在容量的 50%;ρ = 1.0 表示负载等于容量;ρ > 1 则已过载。S 是速率(μ 的一个输入),ρ 是比值(λ/μ)。同一台机器、 同样的消息,负载不同 ρ 就不同(128B/1408:500K → ρ≈0.63,800K → ρ≈1.01 —— μ 没变,只是 λ 变了)。

要降低 ρ 有两条路:抬高 μ(放大 MTU → 更多 M_pkt,或上 DPDK → 更高的 S),或降低 λ(少发一 点)。口诀: S 与 μ 是速度表的顶格,λ 是你此刻的车速,ρ 则是你正踩在红线的百分之几。

S 因 CPU 家族而异 —— 下面这张峰值表仅适用于 Intel m7i

Section titled “S 因 CPU 家族而异 —— 下面这张峰值表仅适用于 Intel m7i”

S 是单线程 sendmsgsyscall 速率,由内核发送路径的每核开销决定(syscall 进出 + UDP/IP 协议栈

  • NIC 驱动入队)。这一开销取决于 CPU 微架构(IPC、时钟、缓存/内存延迟)与平台的 NIC 路径 —— 因此 S 因厂商而异,必须在目标平台上 实测,不能凭空假定:
平台典型 CPUS(单线程 sendmsg,标准路径)
Intel(m7i —— 我们的集群基准)Sapphire Rapids 8488C~400K/s(实测基准,标准 NIO sendmsg
AMD(m7a / m8a)EPYC Genoa / Turin大概率 更高(IPC 更强、加速频率更高)—— 请实测
Graviton(m7g / c8g)ARM Neoverse V1/V2不一样 —— 取决于代次与时钟

变化的方向并不通用(取决于时钟、x86 与 ARM 的内核路径、ENA 代次)。通用的是公式及其形状 —— 只有 常数 S 会平移,而它一移动,每一个峰值数字随之改变,从而 决定拐点落在何处

  • S 更低(如某些 Graviton)→ peak_TPS 更低 → 在更低的负载下就饱和,拐点来得更早。 在 m7i 上 ρ≈0.6 尚且安全的配置,换到低 S 平台可能已是 ρ≈1。
  • S 更高(如 EPYC)→ 相同 MTU 下余量更大。

由此可得: (1) 下面这张实测表只适用于 Intel m7i —— 换用其他芯片请重新推导 S;(2) MTU 这一手段 在各平台通用(M_pkt 与 CPU 无关),但 你有多需要它 则因平台而异;(3) 在引用任何峰值前,务必先在 自己的目标平台上实测 S。

瓶颈:一个线程,每 MTU、每 follower 一次 syscall

Section titled “瓶颈:一个线程,每 MTU、每 follower 一次 syscall”

leader 靠 单个驱动 Sender 线程 复制 Raft 日志。源码可见:

  • sendData()scanLimit = min(availableWindow, mtuLength) → 每次 doSend() 至多发一个 MTU, 而一次 doSend = 一次 datagramChannel.write() = 一次 sendmsg syscall
  • 日志通道走 MDCSendChannelEndpoint.send 对每个 follower 目的地各写一次 —— 于是每条日志 datagram 都要花 D 次 syscall(D = follower 数)。
  • 单个线程封顶在 S ≈ 400K sendmsg/s(syscall 路径,每次 ~2.5µs)。加核或换更大的机器都抬不动 它 —— 因为它是单线程的。

有两个单线程阶段负责把数据送上网线,各自被绑定在独立的核上:ConsensusModule.appendMessage(把 每条消息一次性写入那唯一的日志 term buffer)→ Sender(负责传输这一个 buffer)。Sender 是更慢的 那一环。

(S − C) × M_pkt
peak_TPS = ─────────────────
D
含义典型值
S单线程 sendmsg 上限~400,000 /s
C从预算中被共识控制流量挤占的 syscalls~150–175K(noop 空转);~75K(sleep-ns)
Dfollowers(每条 datagram = D 次 syscall)2(3 节点集群)
M_pkt每 datagram 消息数floor(usable_MTU / framed_msg)

Framing(对小消息尤为关键):

framed_msg = align32(msg_size + 32) # 128B → align32(160) = 160B
usable_MTU = MTU − ~40 (UDP/Aeron header)
M_pkt = floor(usable_MTU / framed_msg)

实测表(S=400K,C=160K → 日志预算 240K/s,D=2 → 120K datagrams/s,Intel m7i)

Section titled “实测表(S=400K,C=160K → 日志预算 240K/s,D=2 → 120K datagrams/s,Intel m7i)”
msgframedMTU 1408 → peak TPSMTU 8192 → peak TPS
32B64B21 msg/pkt → ~2.5M127 → ~15M
128B160B8 → ~960K50 → ~6.0M
224B256B5 → ~600K31 → ~3.8M
512B544B2 → ~240K15 → ~1.8M
1344B1376B~1 → ~120K5 → ~600K

该模型能反推出每一次运行: 128B/1408 ≈ 960K 峰值 vs 实测 ~790K 上限 / 725K 达成(ρ≈1,饱和)✅; 128B/8192 ≈ 6M → 800K 仅为 ρ≈0.13(永不饱和)✅;224B/1408 ≈ 600K < 800K(必然饱和)✅。

同一公式适用于每个角色 —— leader 只是在 D、C 帧大小上一起吃亏

Section titled “同一公式适用于每个角色 —— leader 只是在 D、C 和 帧大小上一起吃亏”

每个 Aeron 端点都有一个驱动 Sender 线程,所以这本质上是 单线程 sender 容量 公式,对三种角色都 适用 —— 你只需代入 各角色特定 的取值。会变的有三项,而其中一个角色几乎用不上它:

角色其 sender 发往DCframed 单元 → M_pkt是否成为瓶颈?
Clientingress 消息 → 仅 leader10(不参与 Raft)原始 app msg(128B)极少 —— 约为 leader 容量的 2× → ρ≈0.5
LeaderRaft 日志 → 所有 follower(MDC)= followers(2)~160K(commit/append 包)日志帧(app msg + cluster hdr,更大)是 —— 瓶颈
FollowerSM / ack → 仅 leader1极小的控制帧(~KB/s)从不 —— 它的瓶颈在 接收端,不在发送端

leader 之所以是唯一的瓶颈,在于它 三个旋钮上一起吃亏:既要缴共识税(C≈160K,而 client 缴 0),又 要扇出到 最多 的目的地(D = follower 数,而非 1),还封装 最大 的单元(一条日志条目比原始 msg 更大,故每 datagram 装得下的消息更少)。client 与 follower 只是在同一公式里拿到了更友好的取值。两点 补充:(1) 这只是 发送路径 —— 每个角色还各有一条带自身限制的 接收路径,而 follower 的瓶颈恰在 它的接收路径(摄入日志),所以 sender 公式根本不是它的上限;(2) M_pkt 因角色而异,因为各角色的 framed 大小不同。

公式给出的是 吞吐上限(一个速率),它对单条消息的延迟只字未提。延迟由 ρ = 输入负载 / peak_TPS 支配,分三个区间:

区间ρ延迟尾部
安全ρ < ~0.7≈ 固定的流水线穿越时间(metal leader ~150µs;大型 VM ~5ms)SD 紧凑
拐点~0.7 < ρ < ~1.0排队延迟急剧攀升,逼近 ~220ms 上限尾部急剧拉长、SD 变宽 ❌
饱和ρ > 1.0每条消息都恒定在 buffer_window / drain ≈ 220ms均匀、确定 ⚠️
  • 拐点以下,延迟与吞吐无关。 离上限再远也不会让消息更快 —— 延迟就是 各固定阶段耗时之和 (client→ingress、ingress→append、append→wire、follower ack ~25µs、commit)。峰值只经由 ρ 起作用。
  • 上限唯有经由 ρ 才触及延迟。 当 ρ→1,排队延迟 ≈ service_time · ρ/(1−ρ) 爆炸式增长,并被有界的 日志窗口封顶在 termLength/2 ÷ drain ≈ 220ms
  • “可控在一个 SD 之内”只有在安全区间才站得住脚。 当 ρ 逼近 1,尾部会脱离均值急剧拉长,均值/SD 便 不再能刻画你实际感受到的延迟。请以 ρ < ~0.7 为设计目标,而不是”只要低于峰值就行”。

举例 —— 同样一条 128B 消息,仅因 ρ 不同延迟就相差约 1000×:128B/8192 @ 800K 为 ρ=0.13 → ~150µs128B/1408 @ 800K 为 ρ≈1.0 → ~173ms

MTU 正是公式里的 每 syscall 消息数乘数(M_pkt) —— 抬高 sender 上限最省事的一根杠杆。把 1408 提到 8192,M_pkt 抬高约 6×(128B:8→50),于是同一份 syscall 预算能承载约 6× 的负载 → ρ 下降约 6× → 越过 拐点。

已经证明: 在 128B / 800K 下,MTU1408 处于 ρ≈1,尾部被钉在 ~173ms;MTU8192 把 ρ 压到 ~0.13,表现 始终平稳,tp999 ~150–320µs,0% 饱和。MTU 8192 是此处的最大有效值(Aeron 要求 MTU ≤ termLength/8;内部 64k IPC 通道上限为 8192 —— 8896/9000 会被拒绝)。

MTU1408 已经达标,还要不要切到 8K?

Section titled “MTU1408 已经达标,还要不要切到 8K?”

这取决于 一个决定性问题:巨帧(NIC MTU ≥ 9001)在每一跳上是否都可靠支持?

MTU 8192
优点~6× 余量(ρ 深入安全区,为负载增长与拐点上一道保险);即便在拐点以下,尾部也 更低、更平稳(syscall 更少 → sender 争用更轻);sender CPU 占用更低。低负载下也没有延迟惩罚 —— Aeron 会把当下可发的数据发出(至多凑满一个 MTU),并 不会 干等着把包填满。
缺点要求 端到端 巨帧 —— 任一跳被钳回 1500 → 要么分片,要么 静默黑洞(DF 被丢弃);一旦丢一个 8K 包,就丢 ~50 条消息,而非 ~8 条(在 有损 链路上更糟;在零丢包的同 VPC 路径上则无关紧要);配置面也更大(Aeron MTU 与 OS/NIC MTU 必须一致,否则会静默分片)。

决策规则:

  • 巨帧可靠支持(AWS 同 VPC / 集群放置组) ρ 已逼近拐点(0.7–0.9)→ 切到 8K(低风险地换来 余量与尾部收益)。
  • 1408 下 ρ < 0.5 巨帧支持存疑 / 需跨 VPC-peering、TGW、VPN、本地机房 / 链路有损 → 留在 1408(更简单,无黑洞风险;本就安全时 8K 的收益微乎其微)。

关于哪些巨帧支持可以依赖、哪些不能,见 AWS 硬性限制

抬高上限本身(当 M_pkt 也不够用时)

Section titled “抬高上限本身(当 M_pkt 也不够用时)”

若你需要的超出了 MTU 所能给的,就得抬高 S(发送路径),按投入由小到大:

  1. 在 cluster + archive 上启用 sleep-ns / backoff 空转策略 —— 合并共识控制包,削减 C (~160K → 75K),腾出约 12% 的预算。纯配置改动、只能部分见效。见 智能批处理与空转策略
  2. MTU 8192 —— 即 M_pkt 乘数。纯配置改动(需巨帧)。首要手段。
  3. 消息批处理 —— 每条 cluster 消息多装些 app 载荷 → 有效的每包消息数更高。
  4. DPDK media driver —— 内核旁路、轮询模式、突发 TX,用一次 ~50–150ns 的用户态 ring 写取代 ~2.5µs 的 sendmsgS 从 ~400K 抬到 ~5–15M+/s(10–30×)。运维负担重(hugepages、NIC 绑定);留给真 需要数百万 TPS 的场景。与 MTU 一样,它也只拔掉 sender 上限 —— 随后 ingress 窗口 / 带宽 / append 线程会接棒成为瓶颈。见 Media Driver 选型:Java vs C
  • peak_TPS = (S−C)/D × M_pkt,其中 S ≈ 400K sendmsg/s,单线程(Intel m7i)。能反推所有运行结果。
  • MTU 就是 M_pkt 乘数 —— 抬高上限最省事的办法;8192 足以越过 128B/800K。
  • 峰值 ≠ 延迟。 ρ≈0.7 以下,延迟 = 固定的流水线穿越时间(SD 紧凑);0.7–1.0 是 拐点,尾部脱离 均值急剧拉长、逼近 ~220ms;> 1.0 则恒定钉在 ~220ms。请把工作点控制在 ρ < 0.7。
  • 切到 8K —— 若巨帧可靠、或你已逼近拐点;留在 1408 —— 若本就安全(ρ < 0.5)且巨帧支持存疑。
  • 加核 / 换更大的机器都没用(单线程)。只有 M_pkt(MTU / 批处理)或 S(DPDK)管用。

这是从容量与延迟切入的视角;它的姊妹篇是 Sender 队列与突发整形(同一个单 Sender 线程, 换个角度看成一条突发排空队列),而接收端的视角则是 微突发模型