上一篇(03)只算了分片的一半账--"丢一片要重传整条,消息级失败率被放大 n 倍"。本篇把另一半补齐,并且先用一条逐层剥洋葱的推导链把 1412/1396/1392 这三个数字钉死:1500 B IP MTU - 20 IPv4 - 8 UDP = 1472 B UDP 净荷,AUTOSAR 又把 SOME/IP payload 限到1400 B,减去4 B SOME/IP-TP 头(28 bit Offset + 3 bit Reserved + 1 bit More)得1396 B;再看 Offset 字段以 16 字节为单位,净荷必须 16 对齐,向下取整就是1392 B = 87 × 16--这 4 B 的对齐罚每片只值 0.287%,却决定了"1 MB 发 754 片还是 752 片"。开销账:大消息下 TP 分片与 TCP 流的载荷效率几乎重合(94.18% vs 94.93%,差 0.75 个百分点),差别不在字节而在尾部时延与缓冲区。成功率账:无重传时消息级成功率严格等于 (1-p)^n,1 MB(n=754)在 0.1%/0.5%/1%/2% 单帧丢包下分别是 47.03%/2.28%/0.051%/0.0000%,解析与 20 万次蒙特卡洛最大偏差 0.14 个百分点。重传账是本文最锋利的一刀:同样要做到 99.99% 完成率,1 MB 在 p=1% 下"分片级 ARQ≤3 轮"只要发 761.6 片,而"整条重传"要发 147 万片——差 1936 倍;p=2% 时差 404 万倍。超时账:1 MB 在 10 Mbit/s(10BASE-T1S)上整条要 905.8 ms,8 MB 要 7246 ms,直接越过 vsomeip 硬编码的 5 s 门限;若按"3× 传输时间"配超时,8 MB 在 10/100/1000 Mbit 下应分别是21738/2499/575 ms。缓冲区账:每路重组上下文要吃掉整条消息,2 MB 缓冲池只能同时装 2 条 1 MB 帧;内存受限后的丢弃率服从 Erlang-B,并发 C=1/2/3/5 在 a=0.9 Erlang 下丢弃率47.37%/17.57%/5.01%/0.20%,蒙特卡洛 20 万次到达的最大偏差 0.6%。阻塞账:1 MB 突发在 10 Mbit 链路上让90 个 10 ms 周期的控制报文错过,在 100 Mbit 上仍是 9 个。结论:SOME/IP-TP 不是"把大消息切开发"这么简单--它是"用尾片时延、重组内存、超时门限三样东西,去换一条 UDP 通道上的可靠性",三样都得算。
1. 先把三个数字钉死:1412、1396 与 1392
网上讲 SOME/IP-TP 的文章,最常见的错误是直接甩出一个"单片段最大 1400 字节"或者"1412 字节"就往下走。数字本身不重要,重要的是它从哪来——因为这条链上任何一层变了(Ethernet FCS 算不算、有没有 VLAN Tag、跑 IPv4 还是 IPv6、有没有 MACsec),片长就得跟着变。本篇先做一件事:把每一层开销逐层减掉,让片长自己长出来。
| # | 剥洋葱步骤 | 字节 | 说明 |
|---|---|---|---|
| 1 | 以太网 IP MTU | 1500 | L3 层可用长度上限 |
| 2 | − IPv4 头 | −20 | 无选项,含 TTL/协议/校验和 |
| 3 | − UDP 头 | −8 | 端口各 2 B + 长度 + 校验和 |
| 4 | = UDP 净荷上限 | 1472 | 这是"物理"上限 |
| 5 | − SOME/IP 头 | −16 | Message ID / Length / Request ID / 版本 / 类型 / 返回码 |
| 6 | = SOME/IP payload 上限 | 1400 | AUTOSAR 规范主动收窄(为 IPv6/安全预留),比物理值少 72 B |
| 7 | − SOME/IP-TP 头 | −4 | Offset 28 bit + Reserved 3 bit + More-Segment 1 bit |
| 8 | = 单片段净荷上限 | 1396 | 规范推导值 |
| 9 | 16 B 对齐向下取整 | −4 | Offset 字段以 16 B 为单位,净荷必须 16 对齐 |
| 10 | = 实际每片净荷 | 1392 | = 87 × 16,工程实现值 |
第 6 步是很多人漏掉的一步:AUTOSAR PRS 明确写了"With UDP the SOME/IP payload shall be between 0 and 1400 Bytes",理由是给 IPv6(多 20 B)和未来的安全字段留余量。所以片长不是 1472−16−4=1452,而是被规范砍到 1396。
第 9 步是更隐蔽的一步。SOME/IP-TP 的 Offset 字段是 28 bit,值不是字节数而是16 字节为单位的计数:
Offset(28 bit) = payload 已发送字节数 / 16 # 单位 16 B, mask 0xFFFFFFF0 More-Segment(1 bit) = 0x00000001 # 1 = 后面还有片 Reserved(3 bit) = 0x0000000E # 必须为 0既然每条偏移都是 16 的整数倍,而偏移是"前面所有片长的累加",那么除了最后一片,每片净荷都必须能被 16 整除,否则第二片开始的 Offset 根本无法表达。1396 不是 16 的倍数,1392 是(87 × 16)。这 4 B 的差看起来微不足道(0.287%),但它是所有商用 SOME/IP 协议栈里SOMEIP_TP_SEGMENT_LENGTH = 1392这个常量的来源。
与上一篇口径的差异:03 篇为了单独隔离"分片放大"这一项,用了一个乐观的单片段值 1623 B。本篇按规范链收敛到 1392 B。这不是笔误修正,而是一次口径收敛的量化演示:同样是 1% 单帧丢包,64 KB 消息按 1623 B/片是 41 片、消息级失败率 33.77%(03 篇的原值),按 1392 B/片是 48 片、失败率 38.27%——片长假设变了 16.6%,消息级失败率就跟着变 4.5 个百分点。参数表里的每一个数字都会传播到最终指标,这就是为什么"片长怎么来的"必须写在第一页。
还要注意 Message Type 的映射:一旦 TP 置位,报文类型整体加 0x20。
| 业务类型 | 原始 Message Type | 分片后的 Message Type |
|---|---|---|
| REQUEST | 0x00 | 0x20 |
| REQUEST_NO_RETURN | 0x01 | 0x21 |
| NOTIFICATION | 0x02 | 0x22 |
| RESPONSE | 0x80 | 0xA0 |
| ERROR | 0x81 | 0xA1 |
接收端就是靠这个 0x20 高位判断"这是一个分片,不是一条完整消息",从而进入重组状态机。
2. 实验与参数
单文件exp04.py,固定随机种子20261015,Python 3.12 + matplotlib 3.10,一次运行约 23 秒,产出 5 张图与exp04_out.json。全部模型均为解析式 + 蒙特卡洛交叉复算,蒙特卡洛规模最高 20 万次/档。
| 参数 | 取值 | 含义与依据 |
|---|---|---|
| 随机种子 | 20261015 | 固定种子,全文数字可复现 |
| 每片净荷 | 1392 B | 1400 − 4(TP 头)后按 16 B 对齐 |
| TP 头/分片 | 4 B | Offset 28 bit + Reserved 3 bit + More 1 bit |
| 单片线上字节 | 净荷 + 86 B | 8 前导 + 14 ETH + 4 FCS + 12 IFG + 20 IPv4 + 8 UDP + 16 SOME/IP + 4 TP |
| TCP 对照 | MSS 1460,单片线上 78 B | 16 B SOME/IP 头只出现一次,无 TP 头 |
| 消息档位 | 256 B ~ 8 MB | 覆盖传感器值、标定数据、LiDAR 帧、OTA 包 |
| 链路速率 | 10 / 100 / 1000 Mbit/s | 含 10BASE-T1S 车载多节点链路 |
| 每片处理开销 | 20 µs | 中断 + 拷贝 + 分发,与链路速率无关 |
| 单帧丢包率 p | 0.1% / 0.5% / 1% / 2% | 分片级 ARQ 对照 |
| ARQ 轮数 | ≤3 轮 | 分片级选择性重传 |
| 抖动尖峰 | 2% 概率,logmean 25 ms | 队列瞬时拥塞 |
| 超时门限 | 10 ms ~ 5 s | 含 vsomeip 硬编码 5 s |
| 重组缓冲池 | 256 KB / 1 MB / 2 MB / 8 MB | 内存受限的并发上限 |
| 并行重组台 C | 1 / 2 / 3 / 5 / 8 | M/M/K/K 丢弃率 |
| offered load a | 0.5 / 0.7 / 0.9 / 1.1 Erlang | 重组请求强度 |
3. 分片头账:TP 分片和 TCP 流的效率差只有 0.75 个百分点
很多人第一反应是"分片头当然有开销,TCP 不也分段吗"。这句话对了一半:TP 的确每片多 8 B(4 B TP 头 + 4 B 因为 16 对齐损失的净荷),但它换来的是"一条 UDP 通道上的多路复用"——不同服务、不同实例的分片可以交错走同一条 UDP 五元组,接收端靠 Message ID + Request ID 分流重组。TCP 做不到这一点:同一条 TCP 连接上的字节流是严格有序的,一条大消息会把连接后面的所有小消息一起堵住。
先看账。表 3 是 TP 分片与 TCP 流式在同一 ETH/IPv4 栈下的载荷效率对比(1 Gbit/s 口径列在表 4)。
| payload | TP 分片数 n | 末片大小 | TP 线上字节 | TP 效率 | TP 头开销 | 头占比 | TCP 段数 | TCP 线上字节 | TCP 效率 |
|---|---|---|---|---|---|---|---|---|---|
| 256 B | 1 | 256 | 342 | 74.85% | 86 | 33.59% | 1 | 350 | 73.14% |
| 512 B | 1 | 512 | 598 | 85.62% | 86 | 16.80% | 1 | 606 | 84.49% |
| 1 KB | 1 | 1024 | 1110 | 92.25% | 86 | 8.40% | 1 | 1118 | 91.59% |
| 1392 B | 1 | 1392 | 1478 | 94.18% | 86 | 6.18% | 1 | 1486 | 93.67% |
| 4 KB | 3 | 1312 | 4354 | 94.07% | 258 | 6.30% | 3 | 4346 | 94.25% |
| 32 KB | 24 | 752 | 34832 | 94.07% | 2064 | 6.30% | 23 | 34578 | 94.77% |
| 256 KB | 189 | 448 | 278398 | 94.16% | 16254 | 6.20% | 180 | 276200 | 94.91% |
| 1 MB | 754 | 400 | 1113420 | 94.18% | 64844 | 6.18% | 719 | 1104674 | 94.92% |
| 4 MB | 3014 | 208 | 4453508 | 94.18% | 259204 | 6.18% | 2873 | 4418414 | 94.93% |
| 8 MB | 6027 | 416 | 8906930 | 94.18% | 518322 | 6.18% | 5746 | 8836812 | 94.93% |
三条结论:
- 大消息的效率天花板是 94.18%(TP)/94.93%(TCP),差 0.75 个百分点。也就是说 8 MB 消息上 TCP 只省 70 KB(8906930 vs 8836812),不到 0.8%。"分片头很贵"在字节层面是个伪命题,贵的是别的东西。
- 分片数才是真问题。1 MB 要发754 片,8 MB 要发6027 片。每一片都要经过"发送侧排队 → 序列化 → 接收侧校验 → 查找重组上下文 → 拷贝到消息缓冲 → 更新位图"这条链;我们的参数里给了保守的 20 µs/片,6027 片就是120.5 ms 的纯处理时间,在 1 Gbit/s 链路上(串行化只有 71.3 ms)处理开销反而超过了传输时间。
- 小消息反而被 TP 拖累:256 B 消息如果走 TP 通道,效率只有 74.85%,因为 86 B 的头占了 33.59%。所以正常的实现是按消息配置 TP 与否——小于 1400 B 的服务接口根本不进 TP 路径。
3.1 为什么不干脆用 IP 分片?
既然“每片 86 B 头”看起来是 TP 的额外成本,那为什么不直接用 IP 层分片(让内核把 1 MB 拆成 709 个 IP 分片)?先把这个替代方案的账也算清楚。IP 分片的规则是:每个分片自带 20 B IP 头,但 UDP 头(8 B)与 SOME/IP 头(16 B)只出现一次,首片容量 1500−20−8−16 =1456 B,后续每片 1500−20 =1480 B。
| payload | IP 分片数 | IP 分片线上字节 | IP 分片效率 | TP 分片数 | TP 线上字节 | TP 效率 | 字节差 | p=1% 消息级成功率(IP/TP) |
|---|---|---|---|---|---|---|---|---|
| 4 KB | 3 | 4294 | 95.39% | 3 | 4354 | 94.07% | −1.38% | 97.0299% / 97.0299% |
| 32 KB | 23 | 34126 | 96.02% | 24 | 34832 | 94.07% | −2.03% | 79.3614% / 78.5678% |
| 256 KB | 178 | 272492 | 96.20% | 189 | 278398 | 94.16% | −2.12% | 16.7134% / 14.9641% |
| 1 MB | 709 | 1089722 | 96.22% | 754 | 1113420 | 94.18% | −2.13% | 0.0804%/ 0.0512% |
| 4 MB | 2835 | 4358758 | 96.23% | 3014 | 4453508 | 94.18% | −2.13% | 0.0000% / 0.0000% |
结论有点颠覆:IP 分片在字节上更省(−2.13%)、片数还更少(709 vs 754)、消息级成功率甚至略高(0.0804% vs 0.0512%)。那么为什么车载以太网里几乎没人这么干?因为把上表里所有优势加起来也不到 2%,而代价是灾难级的:
- 任意一片丢失就整条作废,而且没有任何补救通道。IP 层没有“缺失片列表”这种语义,无法选择性重传;只能等重组定时器超时(操作系统级,30 s 量级),而超时前的重组缓冲一直占着。TP 至少能做分片级 ARQ(第 6 节)。
- 交换机/防火墙普遍按策略丢弃非首片。很多设备对
Fragment Offset > 0的报文直接不转发,因为它们不做重组、也不愿意承担首片缺失的复杂度。这条不是理论风险,是部署事实。 - 首片丢失时连 ICMP 都发不出来。ICMP 错误只能基于首片里的 L4 信息生成,首片丢了就没有上下文,发送端感受不到任何信号。
所以 IP 分片是“字节账赢 2%、可观测性和可恢复性输全部”的典型例子。这也是全篇要反复回到的一点:大消息传输方案的选择不在带宽维度上,而在“失败时怎么办”这个维度上。
表 4 把链路速率加进来。注意"传输时间"是串行化 + 处理开销两项之和,这是后面配重组超时的唯一依据。
| payload | n | 100 Mbit 串行化 | 纯处理时间 | 100 Mbit 总时间 | 1 Gbit 总时间 | 10 Mbit 总时间 |
|---|---|---|---|---|---|---|
| 4 KB | 3 | 0.35 ms | 0.06 ms | 0.41 ms | 0.09 ms | 3.54 ms |
| 32 KB | 24 | 2.79 ms | 0.48 ms | 3.27 ms | 0.76 ms | 28.35 ms |
| 256 KB | 189 | 22.27 ms | 3.78 ms | 26.05 ms | 6.01 ms | 226.50 ms |
| 1 MB | 754 | 89.07 ms | 15.08 ms | 104.15 ms | 23.99 ms | 905.82 ms |
| 4 MB | 3014 | 356.28 ms | 60.28 ms | 416.56 ms | 95.91 ms | 3623.09 ms |
| 8 MB | 6027 | 712.55 ms | 120.54 ms | 833.09 ms | 191.80 ms | 7246.08 ms |
在 100 Mbit 上,8 MB 的纯处理开销(120.5 ms)只有串行化时间(712.6 ms)的 17%;到 1 Gbit 上比例反转成169%。这条曲线有个重要的工程含义:当链路升到 1 Gbit 之后,限制大消息吞吐的不再是带宽,而是每片的处理开销。优化方向随之改变——降分片数(增大片长 / 上 jumbo frame)、或者让协议栈批量收发分片、或者干脆把这类大对象挪到 DoIP/TCP 通道上。
4. 16 字节量化账:offset 单位决定了 4 GiB 上限
Offset 字段 28 bit、单位 16 B,那么它能表达的最大偏移是:
max_offset_units = 2^28 = 268435456 个单位 max_offset_bytes = (2^28 − 1) × 16 = 4294967280 B max_message_bytes = max_offset_bytes + 1392 = 4294968672 B ≈ 4.000 GiB也就是说 SOME/IP-TP 理论上能搬4 GiB 级的对象,实际约束从来不是这个字段,而是超时、内存和抖动。1 MB 消息只用到 offset 空间的524262 / 268435455 = 0.195%,离天花板三个数量级。
真正需要盯的是"对齐罚"这一项:如果实现里"忘了"16 对齐,直接按 1396 切,会产生多少额外分片?
| payload | 16 对齐 n(1392) | 不对齐 n(1396) | 多出的片 Δn | 多花线上字节 | 线上字节增量 | 末片浪费 | 100 Mbit 多花时间 |
|---|---|---|---|---|---|---|---|
| 1400 B | 2 | 2 | 0 | 0 | 0.0000% | 1384 B | 0 µs |
| 4 KB | 3 | 3 | 0 | 0 | 0.0000% | 80 B | 0 µs |
| 32 KB | 24 | 24 | 0 | 0 | 0.0000% | 640 B | 0 µs |
| 256 KB | 189 | 188 | 1 | +86 | 0.0309% | 944 B | 26.9 µs |
| 1 MB | 754 | 752 | 2 | +172 | 0.0155% | 992 B | 53.8 µs |
| 4 MB | 3014 | 3005 | 9 | +774 | 0.0174% | 1184 B | 241.9 µs |
结论很克制:对齐的代价是千分之零点三以下,几乎可以忽略。那为什么还要专门写一节?因为它是"顺序敏感"的:不做对齐不是"少切一片",而是最后一小段可能落在 16 B 边界外,导致某一片的 Offset 无法表达,接收端整条重组失败。也就是说,对齐罚的期望成本不是那 0.03% 的字节,而是"某一小类 payload 长度下 100% 失败"这种尾部风险。工程上宁可统一按 1392 切,也不要做"按 payload 长度动态选 1392/1396"的聪明事——省下的 0.03% 带宽不值得一次线上重组失败的代价。
5. 消息级成功率:(1−p)^n 是铁律,而且很难被"重传"绕过
TP 的分片是有代价的:只要有一片没到,整条消息就重组不出来,哪怕其余 753 片全部到齐。所以"无重传"下的消息级成功率精确等于:
P(成功) = (1 − p)^n, n = ceil(P / 1392)n 是指数,所以消息越大越脆。表 6 是解析值与 20 万次蒙特卡洛实测的对照(每档独立抽样)。
| payload | n | p=0.1% 解析/实测 | p=0.5% 解析/实测 | p=1% 解析/实测 | p=2% 解析/实测 |
|---|---|---|---|---|---|
| 4 KB | 3 | 99.7003 / 99.7100 | 98.5075 / 98.5045 | 97.0299 / 97.0835 | 94.1192 / 94.0520 |
| 32 KB | 24 | 97.6274 / 97.6055 | 88.6654 / 88.7820 | 78.5678 / 78.4835 | 61.5780 / 61.7140 |
| 256 KB | 189 | 82.7708 / 82.8135 | 38.7759 / 38.8215 | 14.9641 / 14.9355 | 2.1965 / 2.2280 |
| 1 MB | 754 | 47.0303 / 47.0925 | 2.2835 / 2.2775 | 0.0512 / 0.0600 | 0.0000 / 0.0000 |
| 8 MB | 6027 | 0.2405 / 0.2455 | 0.0000 / 0.0000 | 0.0000 / 0.0000 | 0.0000 / 0.0000 |
解析与实测的最大偏差 0.136 个百分点(32 KB / p=2%),全部落在 20 万次抽样的置信区间内。铁律就是铁律:1 MB 消息在 1% 单帧丢包下,只有千分之零点五能一条成功;8 MB 在 0.1% 丢包下也只剩 0.24%。把 p 从 1% 降到 0.1% 听起来是"十倍改善",但在 6027 片上只把成功率从 0 拉到 0.24%--指数在这一侧是钝的。
这就是"大消息必须配重传"的量化理由,而不是工程直觉。
6. 分片级 ARQ vs 整条重传:1936 倍与 404 万倍
重传有两个层次,很多协议栈只实现后者,代价是灾难级的:
- 整条重传(消息级):整条消息作废,754 片从头再来。期望发送分片数 = n / P(成功)。
- 分片级 ARQ(选择性重传):接收端只报缺失片的 Offset 列表,发送端只补那些片。
表 7 是 p=1% 下的对照(20 万次蒙特卡洛,分片级 ARQ 允许 ≤3 轮补齐)。
| payload | n | 分片级 ARQ 成功率 | 残余失败(ppm) | ARQ 期望发送片数 | 整条重传期望发送片数 | 倍数 |
|---|---|---|---|---|---|---|
| 4 KB | 3 | 100.0000% | 0.0 | 3.0 | 3.1 | 1.02× |
| 32 KB | 24 | 100.0000% | 0.0 | 24.2 | 30.5 | 1.26× |
| 256 KB | 189 | 100.0000% | 0.0 | 190.9 | 1260.0 | 6.62× |
| 1 MB | 754 | 99.9985% | 15.0 | 761.6 | 1.47×10⁶ | 1936× |
| 8 MB | 6027 | 99.9950% | 50.0 | 6087.9 | 1.22×10³⁰ | 2.01×10²⁶ |
把 p 调到 2%(表 8),差距进一步拉开:
| payload | n | 分片级 ARQ 成功率 | ARQ 期望发送片数 | 整条重传期望发送片数 | 倍数 |
|---|---|---|---|---|---|
| 32 KB | 24 | 99.9990% | 24.5 | 39.0 | 1.59× |
| 256 KB | 189 | 99.9975% | 192.9 | 8600.0 | 44.6× |
| 1 MB | 754 | 99.9865% | 769.4 | 3.11×10⁹ | 4.04×10⁶ |
| 8 MB | 6027 | 99.9125% | 6149.9 | 4.58×10⁵⁶ | 7.44×10⁵² |
三件事值得单独拎出来:
- 分片级 ARQ 的代价几乎可以忽略。1 MB / p=1% 下,平均只多发7.6 片(761.6 − 754),即1.0% 的额外带宽,就把完成率从 0.051% 抬到99.9985%(残余 15 ppm)。这是全篇"性价比最高的一笔交易"。
- 整条重传在 p≥1% 的大消息上不是"慢",而是"不可能"。1 MB 在 p=1% 下期望要发 147 万片 ≈2.2 GB流量才能成功一次;p=2% 上升到 3.11×10⁹ 片 ≈4.6 TB流量。任何"丢片就整条重发"的实现,在真实链路丢包率下都应该被判定为不可用,而不是"性能差一点"。
- 就算给整条重传 3 次机会也不够。表 6 里的
succ_full3一列:1 MB / p=1% 只有0.2045%,8 MB / p=0.5% 是0。分片级 ARQ 一次就打到 99.99% 量级。
工程落地建议很直白:TP 通道必须实现"缺失片列表 + 选择性重传",并且把 ARQ 轮数上限设在 3 轮左右——因为每多一轮只多 1% 的分片量,收益却是一个数量级的完成率。如果协议栈只有整条重传,那么它只适合 p<0.1% 且 payload<32 KB 的场景,再大就必须换 TCP 或 DoIP。
7. 重组超时账:5 s 硬编码在哪一层失效
重组超时必须满足一个不等式:超时门限 > 收完最后一片所需的全部时间。这个"全部时间"包含四块:
T_to > 首片到达 → 末片到达的时间 = 串行化时间(线上字节 × 8 / 链路速率) + 每片处理开销 × 分片数 + 发送侧排队抖动 + 重传补齐所需的时间(若配了分片级 ARQ,还要算 2~3 轮)只算前两项(表 4)就能得到一组很有说服力的数字。按稳妥的"3× 传输时间"配置:
| payload | n | 10 Mbit 建议超时 | 100 Mbit 建议超时 | 1 Gbit 建议超时 | 固定 5 s 在 10 Mbit 上够吗 |
|---|---|---|---|---|---|
| 32 KB | 24 | 85.0 ms | 9.8 ms | 2.3 ms | 够 |
| 256 KB | 189 | 679.5 ms | 78.2 ms | 18.0 ms | 够 |
| 1 MB | 754 | 2717.4 ms | 312.5 ms | 72.0 ms | 够 |
| 4 MB | 3014 | 10869.3 ms | 1249.7 ms | 287.7 ms | 够 |
| 8 MB | 6027 | 21738.3 ms | 2499.3 ms | 575.4 ms | 不够(7246 ms > 5000 ms) |
vsomeip 的 5 s 硬编码超时在 10BASE-T1S(10 Mbit/s)上传 8 MB 会必然超时,因为光是把 8.9 MB 线上字节发完就要 7246 ms。这不是配置问题,是数学问题——所以 AUTOSAR 后续版本把 TP 接收超时改成了可配置定时器(R24-11 的更新项之一),而工程上正确的做法是按消息大小和链路速率算出来的函数去配,而不是抄一个常数。
超时也不能配得太保守,因为超时意味着整条作废 + 重组缓冲被白占。抖动模型(基础片间隔 + 2% 概率的 25 ms 级排队尖峰)下,1 MB(n=754)的"最坏片间隔"分布给出了另一侧的门限:
| 超时门限 T_to | 10 ms | 20 ms | 50 ms | 100 ms | 200 ms | 500 ms | 1 s |
|---|---|---|---|---|---|---|---|
| P(最坏片间隔 > T_to) | 100.000% | 99.995% | 84.820% | 14.705% | 0.455% | 0.000% | 0.000% |
| 通过条数(2 万轮) | 0 | 1 | 3036 | 17059 | 19909 | 20000 | 20000 |
把这张表和传输时间放在一起看,门限被夹在一个很窄的窗口里:
- 下界 = 收齐时间:100 Mbit 下 1 MB 要 104.15 ms。低于这个值,正常传输也会被自己超时掐断——10/20 ms 档的 100% 作废率就是这么来的,那不是"链路问题",是配置错误。
- 上界 = 抖动尾部:P99 的最坏片间隔是172.9 ms,超过 200 ms 后作废率降到 0.455%,500 ms 之后基本为 0。
- 推荐值 = max(3× 传输时间, 抖动 P99 × 2):1 MB 在 100 Mbit 上 → max(312 ms, 346 ms) ≈350 ms;在 1 Gbit 上 → max(72 ms, 346 ms) =350 ms(注意:1000 倍带宽并没有让超时门限变小,因为抖动尾部与带宽无关,这是“配网升级换不掉超时预算”的典型例子)。
这里还有一个容易配错且失败方式极隐蔽的耦合:如果开了分片级 ARQ ≤3 轮,超时门限就不只要盖住第一轮,而要盖住全部 3 轮。以 1 MB / 100 Mbit 为例,单轮收齐时间 104.15 ms,三轮共312.5 ms。若把 T_to 配成 150 ms,ARQ 的第 2、3 轮还没等到,重组上下文就已经被销毁——结果是“重传了但永远接不上”,表现为成功率停在第一轮水平,而日志里一切正常。反过来,如果 T_to 配到 2 s 以上,又会把“确实失败”的上下文长期占住缓冲池,把第 8 节的 Erlang-B 丢弃率推高。超时预算必须与 ARQ 轮数联动,这两个参数不能分头配。
注意 8 MB / 10 Mbit 那格的 21738 ms:它同时超过了 5 s 硬编码,也超过了绝大多数车载 ECU 的"周期报文看门狗"窗口。结论是"8 MB 级对象不该走 10BASE-T1S 的 TP 通道",不是"把超时配大一点"。
8. 重组缓冲区账:2 MB 池子只装得下 2 条 1 MB 帧
接收端每建立一个重组上下文,就必须预留整条消息的缓冲区(因为分片可能乱序到达,而且 Offset 允许跳过空洞)。所以内存受限下的并发上限是除法,不是加法:
C_max = floor(M_pool / P_message)| 单条消息 P | 256 KB 池 | 1 MB 池 | 2 MB 池 | 8 MB 池 |
|---|---|---|---|---|
| 32 KB | 8 | 32 | 64 | 256 |
| 256 KB | 1 | 4 | 8 | 32 |
| 1 MB | 0 | 1 | 2 | 8 |
| 4 MB | 0 | 0 | 0 | 2 |
"0"这一格是很多项目第一次做 LiDAR/标定传输时会撞的墙:缓冲池比单条消息还小,那么这条消息永远传不进来,而且失败方式很隐蔽——不是报错,而是"首片到达时申请缓冲失败,静默丢弃",发送端只看到"对端没有 ACK",然后走重传,永远打不通。所以第一个门禁值应该是:
M_pool ≥ P_max × (1 + 冗余系数) # 至少能装下一条最大的消息,系数建议 ≥2第二个门禁值是并发能力。假设重组请求以泊松流到达(每条占用一个重组台),每台服务时间独立指数分布,系统没有排队队列(缓冲区满了直接丢新请求),这就是经典的M/M/K/K 阻塞系统,丢弃率有闭式解 Erlang-B:
E_B(a, K) = 1 / ( 1 + Σ_{k=1..K} (k/a) · (k−1)/a · ... ) # 递推式实现见 exp04.py表 11 是 offered load a=0.9 Erlang 下解析与蒙特卡洛(20 万次到达)的对照。
| 并发台 K | Erlang-B 解析丢弃率 | 蒙特卡洛实测 | 相对偏差 |
|---|---|---|---|
| C=1 | 47.3684% | 47.2580% | −0.23% |
| C=2 | 17.5705% | 17.5855% | +0.09% |
| C=3 | 5.0072% | 4.9770% | −0.60% |
| C=5 | 0.2001% | 0.1990% | −0.57% |
| C=8 | 0.0004% | 0.0000% | 量级一致 |
解析与实测最大偏差0.6%,说明这套"无排队、满即丢"的模型是可信的。表 12 给出不同负载下的敏感性:
| offered load a | K=1 | K=2 | K=3 | K=5 |
|---|---|---|---|---|
| 0.5 Erlang | 33.33% | 7.69% | 1.27% | 0.0158% |
| 0.7 Erlang | 41.18% | 12.60% | 2.86% | 0.0696% |
| 0.9 Erlang | 47.37% | 17.57% | 5.01% | 0.2001% |
| 1.1 Erlang | 52.38% | 22.37% | 7.58% | 0.4472% |
两个可用的工程结论:
- K 从 1 加到 3,丢弃率降 9.5 倍;加到 5 降 237 倍。a=0.9 时丢弃率从 47.37% 一路降到 0.2001%,所以“缓冲池给够 3~5 条最大消息”是一个便宜的起点。
- 负载超过 1 Erlang 时,K 再大也救不了:a=1.1、K=8 仍有 0.0018% 的丢弃率,而 a=0.9、K=5 已经降到 0.2%。限制来源要从"并发数"转向"总带宽/总处理能力"——这正是实际项目里"传得完但发不完"的根因。
9. 阻塞账:1 MB 突发让 10 ms 周期报文丢 90 帧
TP 通道走的是 UDP,而 UDP 没有拥塞控制——一条 1 MB 的消息发出来,链路上是连续 754 片、没有任何间隙的突发。对同一网段里的周期报文来说,这等于"高速公路上突然插进一列 754 节的火车"。
| 链路速率 | 1 MB 突发时长 | 100 B 控制帧自身传输 | 最坏排队时延 | 阻塞倍数 | 错过的 10 ms 周期 | 错过的 1 ms 周期 |
|---|---|---|---|---|---|---|
| 10 Mbit/s | 890.74 ms | 148.8 µs | 890.88 ms | 5987× | 90 帧 | 891 帧 |
| 100 Mbit/s | 89.07 ms | 14.9 µs | 89.09 ms | 5987× | 9 帧 | 90 帧 |
| 1 Gbit/s | 8.91 ms | 1.5 µs | 8.91 ms | 5987× | 1 帧 | 9 帧 |
"阻塞倍数 5987×"是因为控制帧太小(100 B + 86 B 头),被一条 1 MB 消息挡在后面,等待时间是自己传输时间的近 6000 倍。这个比值与链路速率无关(分子分母同比例缩放),所以换更快的链路只能缩短绝对时长,不能改变"大消息必须让路"的结构。
工程上有四种让路策略,量化后的取舍是:
- 分片限速(inter-segment pacing):把 754 片摊到 N 个周期里发,每周期发 k 片。代价是 1 MB 传输时间从 104 ms 拉长到 N×周期;收益是控制帧的最坏排队时延从 89 ms 降到 ~1 个周期(≤10 ms)。这是首选:它对大消息时延的惩罚是可接受的线性代价,对控制报文则是数量级的改善。
- 优先级队列(802.1Q / TSN):把大消息放 BE 队列、控制报文放高优先级队列。100 B 控制帧按 10 ms 周期在 10 Mbit 链路上只占约 1.5% 带宽,只要调度器不打乱顺序,控制帧最坏排队时延可以压到 1 个片以内。
- 换通道:把 1 MB 级对象从 UDP-TP 挪到 TCP/DoIP。代价是放弃"一条 UDP 五元组上的多路复用"(见第 3 节),收益是所有上面这些账都不用算——TCP 的流控和拥塞控制天然给出了 pacing。
- 不做:只在"大消息是低频事件、且同网段没有硬实时控制"的场景可接受。门禁值可以写成一个不等式:
大消息最坏阻塞时长 < 安全相关周期报文的最大允许抖动。按表 13,1 MB 在 10 Mbit 上直接违反(890 ms ≫ 10 ms),在 1 Gbit 上勉强通过(8.91 ms ≤ 10 ms)。
10. 分片问题的抓包诊断法
本篇算的都是“应当发生什么”,现场拿到的却永远是“实际发生了什么”。SOME/IP-TP 有个很好用的特征:它把所有重组信息都放在 4 B 头里,所以抓包几乎能直接读出故障类型。Wireshark 的packet-someip.c把 TP 头拆成someip.tp(TP 标志)、someip.tp.offset(字节偏移)、someip.tp.offset_encoded(原始 16 B 单位值)与 More-Segment 这四条字段,把offset_encoded加进自定义列,就能用一条规则判掉三类故障:
- offset 跳变 = 丢片。因为每片净荷是 1392 B =87 个单位,正常流的
offset_encoded序列就是0, 87, 174, 261, …,恒定步长 87。把当前值与上一个值做差:
| 差值 | 含义 | 后果 | |---|---|---| | 87 | 正常 | — | | 174 | 丢了 1 片 | 该 Offset 前的空洞无法补齐 | | 261 / 348 | 丢了 2 / 3 片 | 突发丢包的典型形态 | | 非 87 倍数 |对端实现未做 16 B 对齐| 兼容性风险,可能整条重组失败(第 4 节) |
- More-Segment 位 = 末片是否到达。末片必须为 0。如果抓到一串 More=1 之后突然开始一条新消息(offset 回到 0),而前一条的 offset 还远未到
(n−1)×87,那就是末片丢失导致整条作废,并且发送端已经开了新一条。 - offset 回退到 0 = 整条重发。如果前一条还没收齐就又看到 offset=0,说明对端用的是"整条重传"而不是分片级 ARQ--这正是第 6 节里期望发送量差 1936 倍的那种实现。抓到这种流,基本可以直接判定"大消息在这个网络上不可用"。
举一个真实可读的例子:1 MB 消息共 754 片,offset_encoded应该在0 → 65511(753×87)之间以 87 步进走完。若抓包里序列走到451×87 = 39237之后就跳到39411(差 174),就说明第 452 片丢了;;此时后面 302 片即使全部到达,重组也永远无法完成,只能等 T_to 超时把整条丢掉。按帧号定位到那一处,再回看该帧前后frame.time_delta有没有出现第 7 节那个 172.9 ms 量级的尖峰,基本就能判定“丢片是链路丢的还是排队丢的”。
最后一条经验:TP 分片的乱序很常见,因为每一片都是独立的 UDP 报文,走的是独立的队列。抓到乱序不要急着判定“链路有问题”——先看offset_encoded的集合是否完整(缺失片是丢包,乱序片只是顺序不同)。两者对重传策略的要求完全不同:乱序只需要缓冲深度,丢包才需要 ARQ。
11. 配置清单:把上面九节变成可以写进需求的值
| 配置项 | 建议值 | 依据 |
|---|---|---|
| 单片段净荷 | 1392 B | 1400 − 4(TP 头)后按 16 B 对齐(第 1 节) |
| Offset 单位 | 16 B,28 bit(0xFFFFFFF0) | 规范字段定义;高位 3 bit 保留、bit0 = More-Segment |
| 进 TP 的阈值 | payload > 1400 B | 小于规范上限的消息直接单帧发,效率更高 |
| 重传机制 | 分片级 ARQ,≤3 轮 | 1 MB/p=1%:多发 1.0% 分片换 99.9985% 完成率(第 6 节) |
| 禁用项 | 整条重传(p>0.1% 且 payload>256 KB) | 期望发送量 1936×~404 万×(第 6 节) |
| 重组超时公式 | max(3×T_x, 2×maxgap_P99) | 1 MB → 350 ms,与链路速率近似解耦(第 7 节) |
| 重组缓冲池 | ≥ 2 × P_max,并发 K ≥ 3 | 2 MB 池 = 2 条 1 MB 帧;K=3 时 a=0.9 丢弃率 5.01%(第 8 节) |
| 大消息分片限速 | 每周期 ≤ ceil(n/N),N ≥ T_x/周期 | 控制帧最坏排队时延从 89 ms 降到 ≤1 周期(第 9 节) |
| 1 MB 可否上 10 Mbit | 否(阻塞 890 ms) | 表 13:违反 10 ms 周期抖动预算 |
| 8 MB 可否上 10 Mbit | 否(传输 7246 ms > 5 s) | 表 9:硬编码 5 s 超时必然触发 |
12. 方法论:三条可迁移的结论
- 参数的来源必须写在第一页。同一个"单片段大小",1623 B 和 1392 B 让 64 KB 消息的消息级失败率从 33.77% 变成 38.27%。任何"效率/成功率"类指标都必须连同它的分片假设一起复述,否则两条结论根本不可比。这是本篇第一节存在的唯一理由。
- 指数区不要用"改善百分比"思考。(1−p)^n 在 n 大的一侧是钝的:p 降十倍只把 6027 片的成功率从 0 抬到 0.24%;而在重传侧,分片级 ARQ 多发 1% 的分片就把完成率抬了三个数量级。"哪一侧是指数区,预算就花在哪一侧"——本篇的指数区在 n 和 K(并发数),不在链路速率。
- "换更快的链路"解决不了所有问题。从 100 Mbit 到 1 Gbit:8 MB 的传输时间从 833 ms 降到 192 ms(好),但每片处理开销从 17% 占比涨到 169%,成为新瓶颈;重组超时的推荐值几乎不动(抖动尾部与带宽无关);阻塞倍数恒定 5987×;唯一随带宽线性改善的只有串行化时间这一项。工程优化前先问一句:我要缓解的那一项,是不是真的挂在带宽上?
13. 本篇的可复现性
- 环境:Python 3.12 + matplotlib 3.10,单文件
exp04.py,随机种子20261015,一次运行约 23 秒。 - 规模:ARQ 蒙特卡洛每档 20 万次抽样;抖动仿真 20000 轮 × 最多 6027 片;M/M/K/K 丢弃率每档 20 万次到达;全部解析模型给出闭式。
- 图表:5 张图全部由脚本直接生成(分片头账 / 16 B 对齐 / 成功率与重传 / 重组超时 / 缓冲与丢弃),
rcParams中文字体 Microsoft YaHei,保存 dpi=140,无手工绘制。 - 关键模型(可手工验算):
import math # 1) 片长推导链 chain = [('IP MTU', 1500), ('IPv4 头', -20), ('UDP 头', -8), ('SOME/IP 头', -16)] seg_spec = 1400 - 4 # 1396 <- 表1 第 8 步(1400 是规范主动收窄的上限) seg_data = (seg_spec // 16) * 16 # 1392 <- 表1 第 10 步 = 87×16 # 2) 线上字节与效率 OVH_UDP = 8 + 14 + 4 + 12 + 20 + 8 + 16 + 4 # 86 B/片 def tp(P, seg=seg_data): n = math.ceil(P / seg) w = P + OVH_UDP * n return n, w, round(P / w * 100, 2) tp(1048576) # (754, 1113420, 94.18) <- 表3 tp(8388608) # (6027, 8906930, 94.18) <- 表3 # 3) 消息级成功率 for P, p in ((1048576, 0.01), (262144, 0.005), (8388608, 0.001)): n = math.ceil(P / seg_data) print(P, n, round((1 - p) ** n * 100, 4)) # 1048576 754 0.0512 <- 表6 # 262144 189 38.7759 <- 表6 # 8388608 6027 0.2405 <- 表6 # 4) offset 上限 (2**28 - 1) * 16 + 1392 # 4294968672 B ≈ 4.000 GiB <- 第 4 节 # 5) Erlang-B 丢弃率 def erlang_b(a, K): inv = 1.0 for k in range(1, K + 1): inv = 1.0 + inv * k / a return 1.0 / inv erlang_b(0.9, 3) # 0.050072 <- 表11 的 5.0072% erlang_b(0.9, 5) # 0.002001 <- 表11 的 0.2001% # 6) 超时门限 def t_timeout(P, rate_mbps, proc_us=20): n, w, _ = tp(P) return round(w * 8 / (rate_mbps * 1e6) * 1e3 + n * proc_us / 1000, 1) t_timeout(8388608, 10) # 7246.1 ms <- 表9(> 5 s 硬门限) t_timeout(1048576, 100) # 104.2 ms <- 表9 t_timeout(8388608, 1000) # 191.8 ms <- 表9- 全部中间结果已落盘
exp04_out.json,本篇全部表格的每一个数字都能在其中找到对应字段(A_overhead/B_align/C_success/D_timeout/E_buffer/F_ipfrag)。 - 复跑命令:
python exp04.py,stdout 即完整 JSON 报告;图片直接覆盖images/04-*.png。
14. 下篇预告
05. UDP 还是 TCP:SOME/IP 的连接管理与超时体系:前面四篇一直在 UDP 这条道上算账,但 SOME/IP 允许同一个服务出现在 UDP 和 TCP 两个端点上,而两者的失效模式完全不同——TCP 有半开连接(对端掉电但本端内核还以为连着)、有 Nagle 与延迟 ACK 的 40 ms 小报文陷阱、有 TIME_WAIT 端口耗尽;UDP 则有"无连接所以无状态可用",但必须自己实现心跳、会话恢复与重复报文抑制。下篇会用同一套方法把三种连接管理方案(纯 UDP + 应用层心跳 / 长连接 TCP / TCP + keepalive)放在连接建立时延、半开连接检出时间、端口与内核资源占用三个维度上做成表,并给出"什么服务该走 UDP、什么服务必须走 TCP"的可门禁判据。同样是一份固定种子、可复跑的单文件实验。