吞吐与延迟模型
一个用于预测 峰值 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)。 |
| D | followers | 数量 | 每条日志 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 是单线程 sendmsg 的 syscall 速率,由内核发送路径的每核开销决定(syscall 进出 + UDP/IP 协议栈
- NIC 驱动入队)。这一开销取决于 CPU 微架构(IPC、时钟、缓存/内存延迟)与平台的 NIC 路径 —— 因此 S 因厂商而异,必须在目标平台上 实测,不能凭空假定:
| 平台 | 典型 CPU | S(单线程 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()= 一次sendmsgsyscall。- 日志通道走 MDC:
SendChannelEndpoint.send对每个 follower 目的地各写一次 —— 于是每条日志 datagram 都要花 D 次 syscall(D = follower 数)。 - 单个线程封顶在 S ≈ 400K
sendmsg/s(syscall 路径,每次 ~2.5µs)。加核或换更大的机器都抬不动 它 —— 因为它是单线程的。
有两个单线程阶段负责把数据送上网线,各自被绑定在独立的核上:ConsensusModule.appendMessage(把
每条消息一次性写入那唯一的日志 term buffer)→ Sender(负责传输这一个 buffer)。Sender 是更慢的
那一环。
峰值 TPS 公式
Section titled “峰值 TPS 公式” (S − C) × M_pkt peak_TPS = ───────────────── D| 项 | 含义 | 典型值 |
|---|---|---|
| S | 单线程 sendmsg 上限 | ~400,000 /s |
| C | 从预算中被共识控制流量挤占的 syscalls | ~150–175K(noop 空转);~75K(sleep-ns) |
| D | followers(每条 datagram = D 次 syscall) | 2(3 节点集群) |
| M_pkt | 每 datagram 消息数 | floor(usable_MTU / framed_msg) |
Framing(对小消息尤为关键):
framed_msg = align32(msg_size + 32) # 128B → align32(160) = 160Busable_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)”| msg | framed | MTU 1408 → peak TPS | MTU 8192 → peak TPS |
|---|---|---|---|
| 32B | 64B | 21 msg/pkt → ~2.5M | 127 → ~15M |
| 128B | 160B | 8 → ~960K | 50 → ~6.0M |
| 224B | 256B | 5 → ~600K | 31 → ~3.8M |
| 512B | 544B | 2 → ~240K | 15 → ~1.8M |
| 1344B | 1376B | ~1 → ~120K | 5 → ~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 发往 | D | C | framed 单元 → M_pkt | 是否成为瓶颈? |
|---|---|---|---|---|---|
| Client | ingress 消息 → 仅 leader | 1 | 0(不参与 Raft) | 原始 app msg(128B) | 极少 —— 约为 leader 容量的 2× → ρ≈0.5 |
| Leader | Raft 日志 → 所有 follower(MDC) | = followers(2) | ~160K(commit/append 包) | 日志帧(app msg + cluster hdr,更大) | 是 —— 瓶颈 |
| Follower | SM / ack → 仅 leader | 1 | 小 | 极小的控制帧(~KB/s) | 从不 —— 它的瓶颈在 接收端,不在发送端 |
leader 之所以是唯一的瓶颈,在于它 三个旋钮上一起吃亏:既要缴共识税(C≈160K,而 client 缴 0),又 要扇出到 最多 的目的地(D = follower 数,而非 1),还封装 最大 的单元(一条日志条目比原始 msg 更大,故每 datagram 装得下的消息更少)。client 与 follower 只是在同一公式里拿到了更友好的取值。两点 补充:(1) 这只是 发送路径 —— 每个角色还各有一条带自身限制的 接收路径,而 follower 的瓶颈恰在 它的接收路径(摄入日志),所以 sender 公式根本不是它的上限;(2) M_pkt 因角色而异,因为各角色的 framed 大小不同。
峰值 TPS 不等于延迟
Section titled “峰值 TPS 不等于延迟”公式给出的是 吞吐上限(一个速率),它对单条消息的延迟只字未提。延迟由 ρ = 输入负载 / 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µs;
128B/1408 @ 800K 为 ρ≈1.0 → ~173ms。
MTU 关系
Section titled “MTU 关系”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(发送路径),按投入由小到大:
- 在 cluster + archive 上启用 sleep-ns / backoff 空转策略 —— 合并共识控制包,削减 C (~160K → 75K),腾出约 12% 的预算。纯配置改动、只能部分见效。见 智能批处理与空转策略。
- MTU 8192 —— 即 M_pkt 乘数。纯配置改动(需巨帧)。首要手段。
- 消息批处理 —— 每条 cluster 消息多装些 app 载荷 → 有效的每包消息数更高。
- DPDK media driver —— 内核旁路、轮询模式、突发 TX,用一次 ~50–150ns 的用户态 ring 写取代 ~2.5µs
的
sendmsg→ S 从 ~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 线程,
换个角度看成一条突发排空队列),而接收端的视角则是
微突发模型。
本站与 Adaptive Financial Consulting Limited 或 Aeron 项目无任何关联,未获其背书或赞助。 Aeron 是 Adaptive Financial Consulting Limited 的注册商标。
Aeron 是 Adaptive Financial Consulting Limited 在英国及其他国家/地区的商标。