上一篇(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 附近,峰高对 ρ 极其敏感。
工程结论有三条:
- backlog 必须 ≥ 节点数 (不是"≥ 某个经验值")。198 节点的网络,
listen()的 backlog 至少要 198 + 余量。somaxconn也得跟着调,否则listen()会被静默截断到系统上限。 - 抖动窗口要 ≥ 串行处理时间 (= 节点数 × 单次握手服务时间)。198 节点 1 ms 服务时间 → J ≥ 200 ms 。这个抖动的实现成本极低(随机退避几行代码),但能把
backlog=16的溢出率从 100% 降到 37.53%,把峰值并发从 198 降到 15.8。 - 别把 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 量产问题集。