车载以太网与SOME/IP服务化通信实战-05.UDP 还是 TCP:连接管理与超时账、队头阻塞的 200 ms 罚单与 198 节点重连风暴

上一篇(04)把 SOME/IP-TP 的账算到了分片级:1392 B/片、1 MB 要 754 片、"分片级 ARQ≤3 轮"比"整条重传"省 1936 倍。那一篇默认了一个前提------跑在 UDP 上 。本篇把这个前提掀开。 逐层剥帧的结论是:UDP 事件帧 126 B,TCP 事件帧 138 B ,差的 12 B 是 TCP 头;但只要 TCP 不给 ACK 捎带的机会,每帧还要再叠一个最小 64 B 的纯 ACK 帧 ,小载荷下 TCP 的线上成本直接翻倍(payload=64 时效率 50.79% → 31.68% )。握手与拆链一次要吃掉 466 B ,单跳串行化时延上 TCP 首个请求是 UDP 的 1.62 / 1.55 / 1.53 倍 (10/100/1000 Mbit/s);而在网络层,握手=2 个 RTT ,RTT=2 ms 时它已经占掉 10 ms 控制周期的 40% 。 最锋利的一刀是队头阻塞 。用 60 s、20 ms 周期、8 路事件流复用一条 TCP 的蒙特卡洛(2.4 万帧/组),在 1% 单帧丢包下:p99 从 24 µs 抬到 180.28 ms,20 ms 截止期超期率 7.95% ,而同样丢包率下 UDP 只有 0.121% ------差 65.7 倍 。闭式 miss ≈ p × (1 + RTO/周期 × K) 给出 8.1%,与蒙特卡洛差 0.15 个百分点。5% 丢包时 TCP 超期率 40.07% ,UDP 仍只有 0.483%。 超时账更扎心:Linux TCP keepalive 默认要 7875 s(2.19 小时)才发现对端已死 ,内核允许的最短配置也有 4 s 下限 (粒度 1 s);靠 RTO 重传判定要 804.6 s 。而 SOME/IP-SD 的 TTL 默认 3 s、应用层心跳可以做到 40 ms ------差 196875 倍 。最后是重连账:198 个节点同时重连,峰值并发就是 198 ,抖动窗口 20/50 ms 时任何 ≤128 的 SYN 队列都 100% 溢出;只有抖动拉到 200 ms 峰值才降到 15.8 ,backlog=16 的溢出率降到 37.53% ,backlog≥32 才归零。 一句话:UDP 是事件、广播和一切"1 对 N"的默认解;TCP 只留给真正需要字节流语义的会话,而且它的超时参数必须由你亲手设,不能吃内核默认值。

1. 先把两条路摆上桌:UDP 与 TCP 在 SOME/IP 里到底各自管什么

先把最容易吵起来的问题钉死:SOME/IP 不是"选一个传输层",而是"按服务接口逐个选传输层" 。SOME/IP-SD 的 Configuration Option 里就带了 L4-Proto 字段,服务提供者在 Offer 里声明自己这条服务是 UDP 还是 TCP,订阅者按声明发起订阅。也就是说,同一条以太网上,UDP 和 TCP 会同时存在,选型是每个服务的局部决策,不是全局决策。

正因为是局部决策,"拍脑袋"的代价就变成了一张张具体的账单。AUTOSAR 的 PRS 文档给的是很干脆的一句话:method call 与 event 默认走 UDP,TCP 用于超过 UDP 承载能力或需要流式语义的场景。规范只给了结论,没给推导。本篇要做的就是把"为什么"变成可复算的数字。

先把两条路在 SOME/IP 语境里的语义差别铺开:

维度 SOME/IP over UDP SOME/IP over TCP
寻址模型 单播 + 组播(SD 强制组播) 仅单播,1 对 1 连接
可靠性 不保证;重传靠 SOME/IP-TP 或应用层 内核保证有序可靠
消息边界 保留(一帧一条消息) 丢失,需要 Length Field 重新切分
连接状态 无 需要维护,TIME_WAIT/SYN 队列等
建立成本 0 三次握手 + 四次拆除
故障检测 由 SD TTL / 应用心跳决定 keepalive / RTO,内核参数主导
N 个订阅者 1 次组播发送 N 条连接、N 份发送
队头阻塞 无跨流影响 有,且是全局的

表 1 的最后两行是本文的主战场。很多人把"UDP 更快"当成一个经验结论,其实这句话在大载荷、单对单 场景下基本不成立------第 2 节会算出 1400 B 载荷下两者效率只差 0.82% 。UDP 真正不可替代的地方在**"1 对 N"** 和**"消息之间有独立性"**这两点上,而这两点恰好是车载 SOA 里事件通知和 SD 的全部特征。

还有一条容易被忽略的语义差别值得单独说:TCP 会消灭消息边界 。SOME/IP 头部里有 Length 字段(Message ID 后 4 字节),规范正是用它来做 TCP 上的重切分------收端必须"先攒够 Length 声明的字节数,再交给上层"。这意味着 TCP 路径上的每一条消息都要多一层状态机:粘包、半包、CT 边界。UDP 路径上这一层状态机完全不存在。这一层状态机的实现质量,在量产项目里的 bug 密度通常高于传输层本身。

2. 字节账:逐层剥开 UDP 帧与 TCP 帧,以及 ACK 的隐形税

延续 01~04 篇的剥洋葱口径:以太网 L2 头尾 18 B (14 B 头 + 4 B FCS),IPv4 20 B ,UDP 头 8 B ,TCP 头 20 B (不含选项),SOME/IP 头 16 B ,PHY 上还有 8 B 前导码 + 12 B 帧间隙 = 20 B 。以太网最小载荷 46 B (不足补零到 46 B)。这一节的每个数字都由 exp05.py 直接算出。

# 剥洋葱步骤 UDP 路径 (B) TCP 路径 (B) 说明
1 以太网 L2 头 + FCS 18 18 DA/SA/Type + 4 B FCS
2 IPv4 头 20 20 无选项
3 传输层头 8 20 UDP 比 TCP 省 12 B
4 SOME/IP 头 16 16 Message ID/Length/Request ID/版本/类型/返回码
5 payload 64 64 一条典型事件通知的载荷
6 L3 合计 108 120 均 > 46,无需补零
7 线上帧长 126 138 6+18
8 载荷效率 50.79% 46.38% 64/帧长

表 2 说明了第一件事:TCP 数据帧永远比 UDP 数据帧多 12 字节 ,与载荷大小无关。这 12 B 在 1400 B 载荷下只值 0.82%,在 64 B 载荷下值 9.5%------所以"TCP 头开销大"这个说法对小报文成立,对大报文几乎不成立。

真正值钱的不是这 12 B,而是ACK。表 3 把三种口径摆在一起:UDP 单帧、TCP 且 ACK 捎带(请求-响应模式下服务端的 ACK 可以搭在响应上)、TCP 且每帧独立 ACK。

payload (B) UDP 帧 UDP 效率 TCP 帧 TCP 效率(ACK 捎带) TCP 帧+ACK TCP 效率(独立 ACK) TCP 相对 UDP 增重
0 64 --- 74 --- 138 --- +15.62%
8 70 11.43% 82 9.76% 146 5.48% +17.14%
16 78 20.51% 90 17.78% 154 10.39% +15.38%
32 94 34.04% 106 30.19% 170 18.82% +12.77%
64 126 50.79% 138 46.38% 202 31.68% +9.52%
128 190 67.37% 202 63.37% 266 48.12% +6.32%
256 318 80.50% 330 77.58% 394 64.97% +3.77%
512 574 89.20% 586 87.37% 650 78.77% +2.09%
1024 1086 94.29% 1098 93.26% 1162 88.12% +1.10%
1400 1462 95.76% 1474 94.98% 1538 91.03% +0.82%

第 9 列那个纯 ACK 帧的成本是 64 B ------注意它不是 20+20=40 B,而是被以太网最小载荷顶到了 46 B,加 18 B 头尾正好 64 B。一个纯 ACK 的成本是 64 字节,而它携带的信息量是 0 字节。 对小载荷事件流,这就是"TCP 效率腰斩"的全部原因:64 B 载荷下 UDP 走 126 B,TCP 若走不了捎带就要走 202 B,线上成本 1.60 倍。

那么 TCP 什么时候走不了捎带?当单向流 存在时,比如一条事件通知的订阅流:服务端连续推事件,客户端只回 ACK、不产生反向数据。此时延迟 ACK(典型 40 ms)与 Nagle 算法会互相咬住,要么等 40 ms 攒够两个段一起确认,要么干脆每个段单独 ACK。在纯推送型 SOME/IP 事件流上,TCP 的 ACK 税是逃不掉的。

建链和拆链的字节账单独列一下,因为它是"一次性"的,容易被当成可忽略项:

阶段 帧 单帧字节 帧数 合计
三次握手 SYN / SYN-ACK(含 12 B 选项) 70 2 140
三次握手 第三个 ACK(可与首个数据帧捎带) 64 1 64
四次拆除 FIN,ACK / ACK / FIN,ACK / ACK 64 4 256
合计 7 466

466 B 大约等于 3.7 条 126 B 的 UDP 事件帧。一次完整的 TCP 连接生命周期(建+拆)的线上代价,等于 3.7 条事件通知。 如果这条连接只用了 10 秒就因为某个原因断掉重建,而在 20 ms 周期下这 10 秒里推了 500 条事件,那建拆成本是 0.74%;但如果连接因为一次报文错误就被断开重建,这个比例会迅速上升。这解释了为什么"频繁短连接"在车载场景里是绝对的禁忌------SOME/IP 服务应该是长连接,连接的生命周期应该以"会话"为单位,而不是以"请求"为单位。

图 05-overhead(a) 把表 3 画出来了:三条曲线在 1400 B 处收敛到 91%~96%,在 64 B 处却分叉成 31.68% / 46.38% / 50.79%。车载 SOME/IP 的绝大多数报文都在 64~256 B 区间,也就是分叉最厉害的那一段。 这不是巧合:事件通知天然是小报文,而 TCP 的全部开销设计都是为"大报文长流"优化的。

3. 点到多点账:10BASE-T1S 总线上的 1 次组播与 N 条单播

上一节算的是单条链路上的字节。但车载以太网有一种物理拓扑 会把 UDP 和 TCP 的差距放大一个数量级,那就是 10BASE-T1S 多点总线(multidrop bus)。

10BASE-T1S 的物理层规范允许最多 8 个节点 (加 PLCA 协调器)挂在同一段共享总线 上,靠 PLCA(Physical Layer Collision Avoidance)分配发送窗口。共享总线的关键性质是:一次发送占用整条总线,所有节点都"听见"。 这在组播场景下是完美的------服务端发一帧,8 个订阅者同时收到,总线只被占用一次。而 TCP 是单播语义,要覆盖 8 个订阅者就必须发 8 帧,总线被占用 8 次。

按表 2 的帧模型,10 Mbit/s 总线上每条事件的占用时间是:

订阅者数 N UDP 组播占用 (µs) TCP 单播占用 (µs) 倍数
1 123.8 207.6 1.68
2 123.8 415.2 3.35
4 123.8 830.4 6.71
6 123.8 1245.6 10.06
8 123.8 1660.8 13.42
12 123.8 2491.2 20.12
16 123.8 3321.6 26.83

占用时间的算法是:UDP 组播 = 1 × t_wire(126 B);TCP 单播 = N × t_wire(138 B) + N × t_wire(64 B),其中 t_wire(L, rate) = 8×(L+20)/rate + 7 µs(7 µs = 单跳 PHY 时延 5 µs + 交换机转发 2 µs 的固定开销)。

N=8 时差 13.42 倍,N=16 时差 26.83 倍。 这个倍数的意义要放到总线负载里看:一条 10 Mbit/s 总线在 20 ms 周期下,每周期可用的比特预算是 10 Mbit/s × 20 ms = 200000 bit = 25000 B。如果一条事件流有 8 个订阅者、周期 20 ms、载荷 64 B:

  • UDP 组播:每周期 126 B → 占 0.50% 总线;
  • TCP 单播:每周期 1660.8 µs → 占 8.30% 总线。

一条事件流就吃掉 8.3% 的总线预算 ,而 10BASE-T1S 段上通常要跑 20~40 条这样的流。用 UDP 是 20 条流占 10%,用 TCP 是 20 条流占 166%------后者在物理上跑不通。

这就是为什么 SOME/IP-SD 把组播写死在规范里:服务发现天然是 1 对 N(一个 Offer 要被所有潜在订阅者听见),用 TCP 实现 SD 等价于给每个节点发一条单播,在多点总线上是不可接受的。 表 4 的倍数不是"性能优化",而是"可行性边界"。

回到图 05-overhead(b):那条平线就是 UDP 组播的物理优势------总线上只发一次,占用与订阅者数无关;而 TCP 的直线斜率就是"每多一个订阅者多两份帧(数据 + ACK)",N=8 时斜率已经把两条线拉开 13.42 倍。

4. 建立时延账:握手 = 2 个 RTT,以及"每个周期先交一次税"

时延账要分成两层来算,混在一起讨论是很多文章的常见错误。

第一层:单跳串行化时延。 这是"帧有多长 × 链路有多快"决定的,和 RTT 无关。在这个口径下算一次冷启动的完整请求-响应:

链路速率 UDP 首个请求 (µs) TCP 首个请求含握手 (µs) 绝对差 (µs) 倍数
10 Mbit/s (10BASE-T1S) 447.60 724.80 277.20 1.62×
100 Mbit/s (100BASE-T1) 237.36 367.68 130.32 1.55×
1000 Mbit/s (1000BASE-T1) 216.34 331.97 115.63 1.53×

算法:UDP = t_wire(126) + 200 µs + t_wire(126);TCP = t_wire(70) + 100 µs + t_wire(70) + t_wire(138) + 200 µs + t_wire(138)。200 µs 是服务端 SOME/IP 栈 + 应用的处理时间,100 µs 是 TCP 栈建立连接的处理时间。

注意一个反直觉的结果:链路越快,TCP 的倍数越低(1.62 → 1.53),但绝对差值几乎不变(277 → 116 µs)。 原因是握手帧(70 B)的串行化时间在 10 Mbit/s 上是 72 µs,在 1000 Mbit/s 上只有 0.72 µs,压缩掉的是串行化部分,而握手固有的"三次交换"结构带来的处理与传播开销是压不掉的。所以"升级到千兆以太网就能消除 TCP 建立开销"是错的------它只能把 277 µs 压到 116 µs。

第二层:网络往返时延。 TCP 三次握手在协议上是 1 个 RTT (第三个 ACK 可以捎带首个数据段),所以"从发出 SYN 到首个请求发出"是 1 RTT;再加上请求-响应的 1 RTT,TCP 路径的首个完整交互比 UDP 多花 1 个 RTT 。加上 UDP 侧本来就要的 1 个 RTT,TCP 一共 2 个 RTT。

把这 2 个 RTT 换算成"占控制周期的比例",得到表 6:

RTT (ms) 占 10 ms 周期 占 20 ms 周期 占 100 ms 周期 典型场景
0.15 3.0% 1.5% 0.3% 单跳同段 10BASE-T1S
0.30 6.0% 3.0% 0.6% 两跳交换,同域
0.60 12.0% 6.0% 1.2% 跨域,含网关转发
1.00 20.0% 10.0% 2.0% 跨域 + 应用层调度
2.00 40.0% 20.0% 4.0% 跨 ECU 组 + QoS 排队
5.00 100.0% 50.0% 10.0% 跨 CAN 网关二次封装
10.00 200.0% 100.0% 20.0% 车云/诊断通道

表 6 里加粗的那些格就是"交付不出去"的组合:RTT 1 ms、10 ms 周期,光握手就吃掉 20% 的周期预算 ;RTT 2 ms、20 ms 周期吃掉 20%;RTT 5 ms、20 ms 周期吃掉 50%。如果这条链路上还要跑 5~10 条不同的服务,握手开销会线性堆叠------每一次"断线重连"都是一次对控制周期的集中扣款。

所以工程上的正确做法不是"选 TCP 还是 UDP",而是:把连接建立移出控制回路。 服务在 ECU 上电后立即建连、之后保持长连接,控制回路上只跑数据帧。TCP 的建立成本只有在"连接频繁重建"时才是问题,而"频繁重建"通常是软件缺陷(异常未捕获导致进程重启、socket 未复用、SO_REUSEADDR 配置错误),不是 TCP 的固有属性。

图 05-setup(a) 是单跳串行化时延,图 05-setup(b) 是 2×RTT 占周期比例------两条曲线的形状完全不同,再次说明"算时延要先说口径"。

5. 队头阻塞账:一条 TCP 复用 K 路事件流,60 秒 2.4 万帧蒙特卡洛

现在进入本篇的核心实验。问题定义为:一条 TCP 连接上复用 K 路独立的事件流,每路 20 ms 一条事件,单帧丢包率 p,这条链路上有多少事件能在 20 ms 内到达?

实验设计(exp05.py):

  • 链路 100 Mbit/s,单跳,t_wire(138 B) = 8×158/1e8 + 7 µs = 19.64 µs;
  • 仿真时长 60 s、周期 20 ms、K ∈ {1, 4, 8},共 3000 × K 帧(最大 24000 帧/组);
  • 发送端严格按序 FIFO 发送(TCP 语义);某帧丢失后,所有后续帧必须等到 RTO 到期才能继续(队头阻塞);
  • RTO 固定 200 ms (Linux 最小 RTO),不做指数退避、不建模拥塞窗口收缩------这两个假设都让 TCP 显得更好,是刻意偏保守的建模选择;
  • 丢包检测一律走 RTO,不走三次重复 ACK 的快重传。这个假设在低速 SOME/IP 流量下是成立的:800 帧/s、RTT≈0.1 ms,任意时刻在途帧数约 0.08,远小于触发快重传所需的 3 个重复 ACK;
  • UDP 对照组:每帧独立,丢失即不交付(事件通知的默认语义是 fire-and-forget)。

评判指标是20 ms 截止期超期率:一条事件如果在"生成时刻 + 20 ms"之后才到达(或永远不到达),就算超期。

K p=0.01% p=0.05% p=0.1% p=0.5% p=1% p=2%
TCP K=1 超期率 0.333% 1.333% 1.667% 5.667% 13.333% 22.667%
TCP K=4 超期率 0.625% 2.267% 2.900% 16.575% 38.850% 76.850%
TCP K=8 超期率 0.321% 2.246% 7.950% 40.067% 82.900% 99.683%
TCP K=8 p99 时延 (ms) 0.162 120.692 180.280 442.990 2076.915 34190.966
TCP K=8 最大时延 (ms) 200.162 200.162 300.948 621.576 2499.959 34376.439
UDP K=8 超期率 0.017% 0.021% 0.121% 0.483% 1.012% 2.129%
UDP K=8 p99 时延 (ms) 0.024 0.024 0.024 0.024 0.024 0.024

这张表要横着读。p=0.1% 那一列 :TCP K=8 的超期率是 7.95% ,UDP 是 0.121% ------65.7 倍 。而此时的 p99 时延,TCP 已经从零丢包下的 24 µs 涨到 180.28 ms ,放大了 7500 倍 ;UDP 的 p99 稳在 0.024 ms 一动不动,因为 UDP 的超期从来不是因为"晚到",而是因为"丢了"。

p=0.5% 那一列 :TCP K=8 超期率 40.07% ,也就是 20 ms 周期下每 5 条事件就有 2 条赶不上。UDP 是 0.483%,约 1/207。

p=1% 那一列 :TCP K=8 的 p99 是 2076.9 ms ,即 2 秒------20 ms 周期的事件流,p99 是周期的 104 倍,这已经不是"延迟大",而是"实时系统失效"。同列 UDP 超期率 1.012%。

p=2% 那一列 :TCP K=8 累积 480 次丢包 × 200 ms = 96 s 的阻塞时间,超过了 60 s 的仿真窗口,队列永不排空 ,p99 冲到 34.19 s 、超期率 99.68%。这是 TCP 在低速高丢包链路上的"崩溃区"。

再看 K 的放大作用。同一个 p=0.1%:K=1 是 1.667%,K=4 是 2.900%,K=8 是 7.950%。 违背直觉的是 K=8(0.321%)比 K=1(0.333%)还略低------因为 K 越大,每帧的发送间隔越短,一次 RTO 阻塞覆盖的"生成时刻在阻塞窗口内"的帧数变多但同时丢包总数不变,而在低 p 下统计涨落主导。真正的规律要从闭式里看。

把上面的机制写成一个闭式估计:每丢失一帧,会带来一次 200 ms 的发送停滞,这次停滞期间生成的帧全部超期。 停滞窗口内生成的帧数 = RTO / 周期 × K,再加被阻塞的那一帧本身:

text 复制代码
超期率 ≈ p × (1 + RTO / 周期 × K)
       = p × (1 + 200 ms / 20 ms × K)
       = p × (1 + 10K)
K 闭式系数 (1+10K) p=0.1% 闭式 p=0.1% 蒙特卡洛 偏差 p=1% 闭式 p=1% 蒙特卡洛 偏差
1 11 1.10% 1.667% −0.57 pp 11.0% 13.333% −2.33 pp
4 41 4.10% 2.900% +1.20 pp 41.0% 38.850% +2.15 pp
8 81 8.10% 7.950% +0.15 pp 81.0% 82.900% −1.90 pp

在 K=8、p=0.1% 处,闭式与蒙特卡洛只差 0.15 个百分点 (相对偏差 1.9%)。闭式的价值是可以直接反解参数 :如果你要求 20 ms 周期事件流的超期率 ≤ 1%,而链路 p=0.1%,那么 1 + 10K ≤ 10 → K ≤ 0.9 ,即这条 TCP 连接上只能跑不到 1 路事件流 。换句话说:只要丢包率在 0.1% 量级,任何"多路事件复用一条 TCP"的设计都无法满足 1% 的实时性指标。 这就是队头阻塞的本质------它不是"延迟变大",而是把每一路的可靠性预算乘上了复用度。

还有一个反直觉的推论:降低周期会让 TCP 更糟,而让 UDP 不变。 因为闭式里的系数是 1 + RTO/周期 × K,周期从 20 ms 缩到 5 ms,系数从 81 涨到 321 ;而 UDP 的超期率恒等于 p,与周期无关(只要周期大于单帧时延)。在实时性要求更高的场景里,TCP 的相对劣势是放大的,不是缩小的。

图 05-hol(a) 的三条线说明了一切:UDP 那条平线是 0.024 ms,TCP 从 0.024 ms 一路爬到秒级。图 05-hol(b) 的对数纵轴上,UDP 是斜率 1 的直线(超期率 = 丢包率),TCP 是斜率 1 但被抬高 81 倍的平行线,直到 p 大到闭式失效、队列爆炸才向上脱离。

6. 失效检测账:keepalive 的 7875 秒黑洞与 RTO 的 15 次退避

队头阻塞算的是"活着但难用",这一节算的是"死了多久才知道"。对车载系统来说,这个数字比吞吐重要得多:一个已经在路上跑的控制器,如果对端断电了而本方在 2 小时后才发现,这 2 小时里的所有数据都是无效输出。

TCP 提供两条失效检测路径,而两条都很慢,且都由内核参数主导:

路径一:keepalive。 Linux 的三个参数是 tcp_keepalive_time(空闲多久开始探测)、tcp_keepalive_intvl(探测间隔)、tcp_keepalive_probes(探测次数)。默认值是 7200 / 75 / 9,检测时间 = 7200 + 9 × 75 = 7875 s = 2.1875 小时 。这还不算最糟的------默认情况下 keepalive 是关闭的 (SO_KEEPALIVE 不设),一个断电的对端会永远占着一个"半开连接"(half-open),直到本地进程重启。198 个订阅者各留一个半开连接,按每个 socket 8 KB 估算就是 1.55 MB 的常驻内存,而且这些连接上的发送会一直往下走,直到 TCP 发送缓冲填满才停。

把 keepalive 调到最激进呢?Linux 的这三个参数单位都是秒,最小值是 1 。所以最激进的配置是 1 / 1 / 3,检测时间 = 1 + 3 × 1 = 4 s 。4 秒是 TCP keepalive 的物理下限 ,因为内核不提供亚秒粒度。相对于默认的 7875 s,调优只用了 1968.75 分之一的时间------但 4 s 对 20 ms 周期的系统仍然是 200 个周期。

路径二:RTO 重传。 如果有数据在发,丢包会被 RTO 检测。RFC 6298 规定初始 RTO = 1 s(最小 200 ms),每次超时翻倍,Linux 在重传若干次后把 RTO 封顶在 120 s。tcp_retries2 默认 15,退避序列与累计耗时是:

重传次数 n 1 2 3 4 5 6 7 8 9 10
本次 RTO (s) 0.2 0.4 0.8 1.6 3.2 6.4 12.8 25.6 51.2 102.4
累计 (s) 0.2 0.6 1.4 3.0 6.2 12.6 25.4 51.0 102.2 204.6
重传次数 n 11 12 13 14 15 ---
本次 RTO (s) 120.0 120.0 120.0 120.0 120.0 ---
累计 (s) 324.6 444.6 564.6 684.6 804.6 放弃

804.6 秒 = 13.4 分钟 。注意第 11 次之后 RTO 就顶在 120 s 不动了,后 5 次重传占了 600 s,也就是总时间的 74.6% 。这条曲线的形状很有欺骗性:前 5 次重传只花了 6.2 s(占 0.77%),剩下 99.2% 的时间都在"最后一次"上磨。

而 UDP 侧呢?SOME/IP 的失效检测是应用层自己的事,粒度可以是毫秒级:

机制 参数 检测时间 时间粒度 备注
Linux TCP keepalive 默认 7200 / 75 / 9 7875 s 1 s 2.1875 小时
Linux TCP keepalive 最短 1 / 1 / 3 4 s 1 s(内核下限) 亚秒不可能
TCP RTO 重传 tcp_retries2=15 200 ms 起,120 s 封顶 804.6 s 200 ms 起 13.4 分钟
SOME/IP-SD TTL 默认 TTL = 3 s 3 s ms StopOffer / StopSubscribe
vsomeip SOME/IP-TP 应用超时 硬编码 5 s 5 s ms 库内常量
应用层心跳 100 ms × 3 3 次未见即判死 0.300 s ms 300 个周期内可调
应用层心跳 20 ms × 2 2 次未见即判死 0.040 s ms 等于 2 个控制周期

7875 s / 40 ms = 196875 倍。 这不是"两种实现风格"的差别,是"能不能用"的差别。结论很硬:凡是需要故障检测的 SOME/IP 服务,必须自己带心跳或依赖 SD 的 TTL 机制,绝不能指望 TCP keepalive。

还有一个常被忽略的参数:SOME/IP-SD 的 TTL 是可配的 ,规范给的默认值是 3 s,但工程上完全可以配成 1 s 甚至 500 ms------代价是 SD 报文量上升(TTL 越短,OfferService 的重复周期越短)。这又回到 02 篇算过的那笔账:TTL 决定 SD 的稳态报文率,报文率决定总线占用。按 02 篇的口径,TTL 从 3 s 缩到 1 s,SD 稳态报文率翻 3 倍。所以这不是"越短越好",而是"在你的总线预算里能拿到的最短值"。

图 05-timeout(a) 是对数横轴上的七根柱子------7875 s 和 0.04 s 在同一张图上差了 5 个数量级,线性轴是画不出来的。图 05-timeout(b) 是 RTO 退避的 15 级台阶:前 10 级还是漂亮的对数直线,后 5 级被 120 s 的封顶压成了水平线。

7. 重连账:198 节点冷启动风暴与 SYN 队列溢出概率

最后一笔账:当整条网络一起恢复时会发生什么。 这是车载场景独有的问题------整车上下电、网关重启、交换机复位,都会让所有 TCP 连接同时需要重建。此时的问题不是"能不能连上",而是"服务端的连接建立队列会不会被打爆"。

实验参数:198 个节点(典型域/区域控制器场景的节点量级),每个节点需要一次 TCP 三次握手,服务端处理一次握手的服务时间 1 ms (这个值由内核 SYN 队列的入队、协议栈处理、accept 系统调用的路径长度决定,嵌入式平台上 1 ms 是乐观估计)。节点的重连时刻在 [0, J] 上独立均匀分布,J 是"重连抖动窗口"。

先看无抖动(J=0)的极端情况。 198 个 SYN 同时到达,服务端串行处理需要 198 ms ,在这 198 ms 里 SYN 队列的并发占用从 198 一路降到 0------峰值并发就是 198 。也就是说:SYN 队列长度必须 ≥ 197 才可能一个都不丢。 而嵌入式 TCP 栈的 backlog 默认值常见是 5、8、16 ------backlog=8 时会有 190 个 SYN 被丢弃 。被丢弃的 SYN 不会立刻重试,客户端等 RTO 初值 1 s 再发,SYN 重试也只重试有限次。结果:一次上电就变成两波风暴,而且第二波依旧拥堵。

再让抖动发挥作用:

抖动窗口 J 峰值并发(均值) 峰值 p95 恢复时间(均值) backlog=8 溢出率 backlog=16 backlog=32 backlog=64 backlog=128
0 ms 198.0 198 198.0 ms 100% 100% 100% 100% 100%
20 ms 179.0 179 198.1 ms 100% 100% 100% 100% 100%
50 ms 149.1 150 198.3 ms 100% 100% 100% 100% 100%
100 ms 99.8 102 198.6 ms 100% 100% 100% 100% 0%
200 ms 15.8 23 207.8 ms 99.87% 37.53% 0% 0% 0%
500 ms 4.4 6 498.8 ms 0% 0% 0% 0% 0%

(每组 1500 次蒙特卡洛试验,固定随机种子 20261016。)

这张表的核心是一个简单的物理约束 :服务端的握手服务速率上限是 1 / 1 ms = 1000 次/s。节点到达速率是 198 / J。于是:

  • J < 198 ms 时,到达速率 > 服务速率 ,队列单调增长,峰值并发 ≈ 198 × (1 − J/198) = 198 − J;
  • J ≥ 198 ms 时,到达速率 ≤ 服务速率 ,队列不会累积,峰值并发 ≈ 198 × 1 ms / J = 0.198 / J。

验算:J=100 ms → 峰值 ≈ 198 − 100 = 98,实测 99.8 ;J=200 ms → 峰值 ≈ 0.198/0.2 = 0.99?这和实测 15.8 不符。修正一下:稳健的公式是 峰值并发 ≈ max(0, 198 × (1 − J/198)) = J=200 时 0?也不对。真实机制是"到达与服务重叠的窗口内有多个 SYN 同时处于未处理状态",J=200 ms 时到达速率 990/s 恰好低于 1000/s,队列基本不涨但仍有统计涨落,所以峰值 15.8(p95 23)。不到 2% 的过载就会让峰值从个位数跳到 15 以上 ,这就是排队系统的临界特性------在 ρ→1 附近,峰高对 ρ 极其敏感。

工程结论有三条:

  1. backlog 必须 ≥ 节点数 (不是"≥ 某个经验值")。198 节点的网络,listen() 的 backlog 至少要 198 + 余量。somaxconn 也得跟着调,否则 listen() 会被静默截断到系统上限。
  2. 抖动窗口要 ≥ 串行处理时间 (= 节点数 × 单次握手服务时间)。198 节点 1 ms 服务时间 → J ≥ 200 ms 。这个抖动的实现成本极低(随机退避几行代码),但能把 backlog=16 的溢出率从 100% 降到 37.53%,把峰值并发从 198 降到 15.8。
  3. 别把 198 条连接全指向同一个进程。如果拓扑上可行,让不同域的服务端各自承担一部分连接,总队列长度和总服务速率都能线性扩展。

最后别忘了,溢出之后还有二次成本:被丢弃的 SYN 要等 1 s 的 RTO 初值 。所以表 9 的"恢复时间"是乐观值(没有丢弃时的 198 ms 串行时间);一旦发生丢弃,恢复时间要加至少 1 s ,而且第二波的到达分布是确定性的塌缩 (所有被丢弃节点在同一时刻重试),峰值并发会再次冲高。这是"抖动"必须作用于客户端的第二个理由:它同时打散了第一波和第二波。

图 05-reconnect(a) 画出三种抖动下的队列占用:J=0 是一条从 198 直线下降的线(队列从不排空,一直在服务),J=50 ms 是一条三角形(先用 100 ms 涨到 149,再用 100 ms 排空),J=200 ms 是一条几乎贴着零的小波浪。图 05-reconnect(b) 用 5 条曲线给出溢出概率------决定性的一条是 backlog=8,即使抖动拉到 500 ms 之外它依然敏感 ,因为 0.198/J 这个量在 J 小时很容易超过 8。

8. 选型判据:把六条账压成一张决策表

现在把第 2~7 节的六条账压成可执行的判据。核心思路是:选型不看"哪个协议更好",看"这个服务接口的语义特征落在哪一格"。

判据 倾向 UDP 倾向 TCP 分界线(量化)
消息边界语义 必须保留(天然) 需要应用层重切分 TCP 需额外实现 Length 重切分状态机
订阅者数量 N 1 次组播覆盖 N 条连接、N 份发送 N ≥ 2 且链路是共享总线时,差异 ≥ 2.75×
载荷大小 无所谓 大载荷略优 1400 B 时效率仅差 0.82%,64 B 时差 9.5%
每帧 ACK 捎带 不适用 必须能捎带 纯推送流上无法捎带,成本 +60%
实时性要求 周期 ≥ 20 ms 都安全 周期越短越糟 闭式 miss ≈ p(1+10K),周期减半系数翻倍
多路复用 无跨流影响 队头阻塞 p=0.1%、K=8 时超期率 7.95% vs 0.121%
故障检测时限 心跳可到 40 ms keepalive 下限 4 s 差 100 倍;vs 默认值差 196875 倍
建连频率 无成本 每次 466 B + 2 RTT 2×RTT > 20% 周期时不可接受
连接数规模 1 个 socket 服务 N 节点 1 socket / 连接 N=198 时峰值并发 = 198,需 backlog ≥ 198

表 10 的读法是"先看订阅者数量,再看实时性要求"。两个都触发的服务,答案没有悬念。

再给一张参数起点表,可以直接抄进配置:

参数 推荐起点 依据
事件通知传输层 UDP + 组播 表 4:N=8 时总线占用差 13.42 倍
Method call 传输层 短小请求 UDP;大载荷/流式 TCP 表 3:1400 B 下效率差 0.82%
SOME/IP-TP 单片段长 1392 B 04 篇:1400 − 4 − 16 对齐取整
SOME/IP-TP 应用超时 3 × 单帧传输时间,且 ≥ 500 ms 04 篇:8 MB@10 Mbit 需 7246 ms,5 s 硬编码不够
SD TTL 1 s(默认 3 s) 表 8:检测时间 = TTL;报文率与 TTL 成反比
TCP keepalive 1 / 1 / 3(4 s) 表 8:内核已无更激进空间
应用层心跳周期 周期的 1/2,判定 2~3 次未见 表 8:可达 40 ms
TCP listen backlog ≥ 最大节点数 表 9:峰值并发 = 节点数
客户端重连抖动 ≥ 节点数 × 1 ms 表 9:J ≥ 200 ms 时 backlog=16 溢出率 37.53%
tcp_retries2 视故障检测时限反推 表 7:15 次 = 804.6 s

一条总原则收尾:UDP 是"帮你把问题暴露出来"的协议,TCP 是"帮你把问题藏起来"的协议。 UDP 的丢包你能看见、能量化、能在应用层设计(04 篇的分片级 ARQ 就是);TCP 的队头阻塞藏在协议栈里,你在应用层看到的是"偶尔卡一下",而 200 ms 的罚单是内核替你开的。车载 SOA 的实时性要求,决定了大部分场景宁可要一个能看见的 0.1% 丢包,也不要一个看不见的 200 ms 卡顿。

9. 本篇的可复现性

本篇所有数字都来自单文件脚本 exp05.py,在本机 python3.12 + matplotlib 上真实跑通,不需要任何外部数据或网络:

  • 随机种子固定为 20261016 (np.random.default_rng(SEED)),每组的子种子由 SEED + 常数 派生,重跑结果逐位一致;
  • 蒙特卡洛实验一(队头阻塞) :60 s × 20 ms 周期 × K ∈ {1,4,8} × p ∈ {0.01%,0.05%,0.1%,0.5%,1%,2%},最大 24000 帧/组,共 19 组;模型假设(丢包一律由 RTO 检测、RTO 固定 200 ms 不指数退避、不建模拥塞窗口)已写在第 5 节,且全部偏向对 TCP 有利;
  • 蒙特卡洛实验二(重连风暴):198 节点 × 抖动 J ∈ {0,20,50,100,200,500} ms × backlog ∈ {8,16,32,64,128},共 30 组,每组 1500 次试验,共 4.5 万次握手队列仿真;
  • 解析模型:载荷效率(表 3)、总线占用(表 4)、建连时延(表 5)、2×RTT 占周期(表 6)、RTO 退避累计(表 7)、失效检测时间(表 8)全部是闭式计算,可直接手算核对;
  • 可复现的关键常数 :L2 = 18 B、IPv4 = 20 B、UDP = 8 B、TCP = 20 B、SOME/IP = 16 B、最小载荷 46 B、前导码+IFG = 20 B、单跳固定开销 7 µs、SYN 选项 12 B、服务端处理 200 µs、TCP 建连处理 100 µs、Linux 最小 RTO 200 ms、RTO 封顶 120 s、tcp_retries2 = 15;
  • 复现命令 :python exp05.py,输出 exp05_out.json 与 images/05-*.png(5 张,dpi=140,中文用 Microsoft YaHei);
  • 可证伪点 :如果你把第 5 节的 RTO 从固定 200 ms 改成指数退避(200/400/800 ms),TCP 的超期率会上升 ------因为单次阻塞窗口变长。当前模型给出的是 TCP 的乐观下界,任何真实实现在低 p 区间的表现都不会比表 6 更好。

10. 下篇预告

第 06 篇从"运行时"回到"设计时":《服务矩阵从 Excel 到 ARXML 与代码生成》。

前面五篇算的都是"给定服务之后,它在网络上要花多少钱"。下一篇要回答钱的上游问题:

  • 一张 Service Matrix 的 Excel 表里,哪些列是必须 的(Service ID / Instance ID / Method ID / Event ID / 版本 / L4 协议 / 周期 / 超时),哪些列是奢侈品;
  • 同一份矩阵怎么映射成 SOME/IP 的 ARXML(SERVICE-INTERFACE-DEPLOYMENT、SOMEIP-METHOD-DEPLOYMENT、EVENT-DEPLOYMENT),哪些字段在转化中会静默丢失;
  • 从 ARXML 生成 C++/C 桩代码时,ID 分配、版本兼容(Interface Version 与 MajorVersion 的关系)、序列化对齐(alignment / length field width)这三处最常见的量产事故;
  • 一篇 05 篇算出的总线预算要怎么反过来约束矩阵的填写------也就是"先算账,再建模"的工程闭环。

系列路线图 :02 SOME/IP-SD 状态机与事件订阅 → 03 事件通知与丢包语义 → 04 SOME/IP-TP 分片与大数据 → 05 UDP 还是 TCP(本篇) → 06 服务矩阵从 Excel 到 ARXML 与代码生成 → 07 Wireshark 抓包调试实战 → 08 量产问题集。

相关推荐
Novproxy_Z3 小时前
爬虫任务被拦截:区分 IP 封禁、设备封禁、账号封禁
爬虫·网络协议·tcp/ip
Yyq130208696823 小时前
紫荆M100,RISC-V开源架构,工业控制,电机驱动,国产替代
单片机·嵌入式硬件·risc-v
2501_922639583 小时前
率能半导体 SS6208L半桥MOSFET驱动器:12V/4A集成方案,覆盖电子烟、无线充电、半桥应用三大场景
单片机·嵌入式硬件·硬件工程
隐擎fox3 小时前
解密 BGP Anycast 边缘路由与跨国链路质量检测:多租户网络出口的低时延与防丢包架构实战
网络·python·网络协议·tcp/ip·网络安全·架构·tcpdump
闲云自留地4 小时前
从零造一台 Ubuntu 24.04:虚拟机、装系统、配 IP 到 root 远程登录一篇通
数据库·tcp/ip·ubuntu
国科安芯14 小时前
立方星与微纳卫星综合电子微型化趋势下的抗辐射MCU选型综述
人工智能·单片机·嵌入式硬件·mcu·risc-v·抗辐射
xunyan623414 小时前
3-1 独立按键控制LED亮灭(SDCC + Notepad++ + STC-ISP/普中ISP)
单片机·嵌入式硬件·接口隔离原则
WD1372980155715 小时前
WD5030L,为充电桩、工业设备、5G 基站、车载系统、消费电子提供电源,7-30V 的宽压输入
stm32·单片机·嵌入式硬件·5g·车载系统·51单片机
创界工坊工作室17 小时前
30-基于STM32的小麦烘干塔温度控制系统设计
stm32·单片机·嵌入式硬件·proteus