本篇把 SOME/IP-SD 拆成一台可执行的状态机 和两本可算的账 。用 Python 实现服务端三态状态机(INITIAL_WAIT / MAIN / REPEATED_INITIAL_WAIT),固定种子跑出 6 次真实迁移日志与 20000 次蒙特卡洛:首个 Offer 到达均值 0.251s、p95 0.475s ;再对客户端订阅握手做 20000 次×7 档丢包率 仿真------不重传时 10% 丢包订阅成功率只有 81.6%,重传 3 次(间隔 0.3s)拉到 99.9%,30% 丢包下从 49.3% 拉到 93.4% ,解析式与仿真最大偏差 0.6 个百分点。下线检测:纯 TTL=3s 平均要 2.497s、p95 2.948s;发一条 StopOffer 把均值打到 0.0ms(p=0) ,但丢包 5% 时 p95 回升到 1193ms。误判账:TTL=2×周期时 10% 丢包会每小时误报 3.32 次,TTL=3×周期降到 0.445 次(6.7×),TTL=5×周期只剩 0.005 次(664×) 。最后是上电风暴:20 服务×4 客户端 = 180 条报文,零抖动时 10ms 窗口峰值 18000 msg/s,把初始等待抖动开到 100ms 只剩 3787 msg/s,压低 4.8 倍 。结论:SD 不是"发 Offer 就行",而是 重传次数 × TTL 倍数 × 抖动窗口 三个旋钮,旋钮位置决定上电有多堵、掉线有多快、误报有多少。
1. 为什么状态机是 SD 的第一难点
上一篇(01)算完了字节账和发现时延账,结论是"重启后初始 burst 能把发现 p95 从 4.752s 压到 57ms"。但那只是服务端单边的账:客户端拿到 Offer 之后还有一整套动作------订阅、续订、判定服务是否还活着;服务端也要在上电、掉网、恢复之间来回迁移。这些动作没有状态机管,量产时一定会出三类故障:
- 服务端还在 INITIAL_WAIT,客户端已经在等 Offer ------ 上电时序对不齐,表现为"冷启动第一次永远慢半拍";
- 链路闪断后服务端没回 MAIN ------ 服务"看起来配了但永远不出现",重启 ECU 才好;
- 订阅发出去没收到 Ack,客户端不重传 ------ 事件一个都收不到,而服务端日志显示"订阅成功"(它确实收到了 Subscribe)。
所以本篇的路线是:先把状态机写成能跑的代码(实验 A),再把订阅握手做成可算的成功率曲线(实验 B),然后单独量化"掉线检测"这一对矛盾指标------检测快 vs 误报少(实验 C),最后算上电那一刻的全网报文洪峰(实验 D)。
本专栏路线图(每篇都带可复现实验):
| 篇 | 主题 | 核心产出 |
|---|---|---|
| 01 | 报文字节级解析 + 四通道开销账 + 发现时延 | 编解码器、开销表、SD蒙特卡洛 |
| 02(本篇) | SOME/IP-SD 状态机与事件订阅 | 三态状态机、订阅重传账、下线检测与上电风暴 |
| 03 | 事件通知与丢包语义 | 序列号回绕、最后值缓冲、更新速率控制 |
| 04 | SOME/IP-TP 分片与大数据传输 | >1412B 的分片账与重组超时 |
| 05 | UDP 还是 TCP:连接管理与超时体系 | 半开连接、心跳、会话恢复 |
| 06 | 服务矩阵从 Excel 到 ARXML/SD 文件 | 工具链打通与代码生成 |
| 07 | Wireshark 抓包调试实战 | 解码器配置、10类典型故障包 |
| 08 | 量产问题集:丢包/风暴/上电时延 | 真实故障复盘与门禁值 |
2. 服务端三态:INITIAL_WAIT、MAIN、REPEATED_INITIAL_WAIT

标准里服务端(提供服务的一方)在 SD 层只有三个状态,别自己加第四个:
- INITIAL_WAIT(初始等待) :上电后不立刻发 Offer,而是先随机等 0~0.5s。这一段随机量是全网上电错峰的唯一手段------第 9 节会用数字证明它值多少钱。
- MAIN(主相):周期发 OfferService(本篇取 1s),同时处理客户端的 Subscribe/SubscribeAck 与续订。
- REPEATED_INITIAL_WAIT(重复等待) :收到 network_reduction(IP 丢失、链路 down、地址被撤)后进入,退避一小段时间再回 MAIN 重发 Offer;重复次数上限 3 次,用尽则回到 INITIAL_WAIT 重新走完整流程,避免"网络抖一下就永远卡在重复相"。
表1 本篇实验的 SD 配置参数(全部写进 exp02.py 的 CFG,改一个数即可复跑)
| 参数 | 取值 | 含义与取值依据 |
|---|---|---|
| offer_cycle | 1.0 s | OfferService 周期;01 篇用 5s,本篇为看清细节取 1s |
| ttl | 3.0 s | 服务 TTL,必须 ≥ 3× 周期(见第 7 节实测) |
| sub_rep_time | 0.3 s | 订阅重传间隔 |
| sub_max_rep | 3 | 最大重传次数 → 一次订阅最多 4 次尝试 |
| sub_rtt | 0.002 s | Subscribe→SubscribeAck 近似单向时延 |
| init_wait_max | 0.5 s | 初始等待上界(抖动窗口) |
| storm_services / storm_clients | 20 / 4 | 上电风暴场景的服务数与订阅方数 |
| 随机种子 | 20261009 | 固定种子,全文数字可复现 |
| 蒙特卡洛次数 | 20000 | 每档配置独立重跑 |
表2 实验 A 实际跑出的状态迁移日志(种子 20261009,单次运行)
| # | 时刻 (s) | 迁移 | 触发事件 |
|---|---|---|---|
| 1 | 0.0000 | BOOT → INITIAL_WAIT | power_on |
| 2 | 0.3336 | INITIAL_WAIT → MAIN | initial_wait_timeout(随机等待 0.334s) |
| 3 | 3.3336 | MAIN → REPEATED_INITIAL_WAIT | network_reduction(IP 丢失) |
| 4 | 3.7336 | REPEATED_INITIAL_WAIT → MAIN | repeated_wait_timeout #1(退避 0.400s) |
| 5 | 6.8336 | MAIN → REPEATED_INITIAL_WAIT | network_reduction(第二次闪断) |
| 6 | 7.2336 | REPEATED_INITIAL_WAIT → MAIN | repeated_wait_timeout #1(退避 0.400s) |
日志本身没什么稀奇,稀奇的是它暴露的三个必须在代码里断言的不变量,本篇实现里逐条校验:
- MAIN 只能从 INITIAL_WAIT 或 REPEATED_INITIAL_WAIT 进入,永远不能从 BOOT 直接进(否则抖动窗口形同虚设);
- 任何时刻只处于一个状态,状态区间首尾相接、无重叠无空洞;
- REPEATED 相的停留时间有上界(本篇 0.4s 退避 × 次数上限 3),超限必须回 INITIAL_WAIT------这条是防止"卡死在重复相"的最后防线。
对首个 Offer 到达时间做 20000 次蒙特卡洛(初始等待均匀分布于 0~0.5s):
表3 首个 Offer 到达时间(N=20000,抖动窗口 0~500ms)
| 统计量 | 数值 |
|---|---|
| 均值 | 0.2509 s |
| p50 | 0.2529 s |
| p95 | 0.4752 s |
| p99 | 0.4950 s |
| 最大 | 0.5000 s |
读法很重要 :这 0.25s 均值不是"浪费的时间",它是全网错峰的预算。单个服务晚 0.25s 没人抱怨,但 20 个服务挤在同一毫秒发 Offer 会让交换机队列和客户端 CPU 同时打满------第 9 节会把这笔账算清楚。代价与收益必须一起看。
3. 把状态机写成代码:事件驱动而不是 if-else 堆
实验 A 的核心是一个 40 行的事件循环:sd_server_run(rng, events) 只认两样东西------当前状态 和有序事件表 ,每来一个事件查表迁移,并把 (时刻, 从, 到, 触发) 追加进日志。这样做有三个工程收益:
- 日志即证据:线上出问题时,你要的是表2 那样的四列表,而不是在 20 个 if 里猜走到了哪一支;
- 不变量可断言:迁移函数返回前检查"目标状态是否在该事件的合法后继集合里",非法迁移直接抛异常,而不是默默进 MAIN;
- 可测:表2 的 6 条迁移是喂确定事件序列跑出来的,单元测试断言这 6 行即可覆盖全部分支。
代码骨架(完整脚本含全部实验,可在本专栏读者群获取):
python
def sd_server_run(rng, events):
log, state = [], 'BOOT'
log.append((0.0, 'BOOT', 'INITIAL_WAIT', 'power_on'))
state, rep_cnt = 'INITIAL_WAIT', 0
for (t, ev) in events:
if state == 'INITIAL_WAIT' and ev == 'initial_wait_timeout':
log.append((t, state, 'MAIN', 'initial_wait_timeout')); state = 'MAIN'
elif state == 'MAIN' and ev == 'network_reduction':
rep_cnt = 0
log.append((t, state, 'REPEATED_INITIAL_WAIT', 'network_reduction'))
state = 'REPEATED_INITIAL_WAIT'
elif state == 'REPEATED_INITIAL_WAIT' and ev == 'repeated_wait_timeout':
rep_cnt += 1
if rep_cnt >= 3: # 重复次数用尽 -> 回初始等待, 防卡死
log.append((t, state, 'INITIAL_WAIT', 'max_repetition_reached'))
state = 'INITIAL_WAIT'
else:
log.append((t, state, 'MAIN',
'repeated_wait_timeout#%d' % rep_cnt)); state = 'MAIN'
return log
工程提醒 :生产代码里的 network_reduction 不是你手写的事件,它来自 DHCP/静态地址失效、链路状态回调、VLAN 配置变更------事件源的唯一性比状态机本身更容易出错。本专栏第 06 篇做服务矩阵生成时,会把这类事件与状态一起变成可生成、可校验的配置。
4. 客户端订阅:Subscribe、SubscribeAck 与 0.3s 重传节奏

状态机只管服务端"发不发 Offer",事件订阅是另一台状态机:客户端收到 Offer → 判断需要的 Eventgroup → 发 Subscribe → 等 SubscribeAck → 进入"已订阅"并按 TTL 续订。三个最容易踩的点:
- Subscribe 与 SubscribeAck 各是一跳,都会丢 。本篇实验 B 对这两跳独立判丢,所以单次尝试的通过率是
(1-p)²,不是1-p; - 重传是"再发一次 Subscribe",不是"再等一会儿"。重传时 Eventgroup 的 counter/重复标记要按规范处理,否则服务端会当成新订阅重新走一遍准入;
- 没收到 Ack ≠ 服务端没收到 Subscribe 。服务端可能已建立订阅、只是回包丢了------重传过去它会回 Ack(或认为已订阅),客户端此时不能重复创建订阅对象,否则同一个 Eventgroup 挂两个监听器,事件会被消费两次。
第 3 点是实车里"事件偶发性重复上报"的头号原因,而且极难复现:因为它只在"订阅成功但 Ack 丢失"这个约 10% 量级的窗口里发生。
5. 订阅成功率账:丢包率 × 重传次数

这是本篇的核心实验。单次尝试通过率 q = (1-p)²,重传 R 次则总成功率 P = 1 - (1-q)^(R+1);同时对每个丢包档位跑 20000 次蒙特卡洛,看解析式和仿真差多少。
表4 订阅成功率:解析式 vs 蒙特卡洛(N=20000/档,重传间隔 0.3s)
| 单向丢包率 | R=0 仿真 | R=0 解析 | R=1 仿真 | R=1 解析 | R=3 仿真 | R=3 解析 |
|---|---|---|---|---|---|---|
| 0% | 100.00% | 100.00% | 100.00% | 100.00% | 100.00% | 100.00% |
| 1% | 98.23% | 98.01% | 99.96% | 99.96% | 100.00% | 100.00% |
| 3% | 94.43% | 94.09% | 99.66% | 99.65% | 100.00% | 100.00% |
| 5% | 90.63% | 90.25% | 99.08% | 99.05% | 100.00% | 99.99% |
| 10% | 81.58% | 81.00% | 96.57% | 96.39% | 99.89% | 99.87% |
| 20% | 64.54% | 64.00% | 87.18% | 87.04% | 98.34% | 98.32% |
| 30% | 49.28% | 49.00% | 74.12% | 73.99% | 93.40% | 93.23% |
表5 R=3 时的订阅耗时(仅成功样本,重传间隔 0.3s 决定台阶)
| 单向丢包率 | 成功样本均值 | p95 | 说明 |
|---|---|---|---|
| 0% | 2 ms | 2 ms | 首发即中,只剩 RTT |
| 1% | 7.4 ms | 2 ms | 绝大多数仍首发命中 |
| 3% | 19.9 ms | 302 ms | 少数落到第 2 次尝试 |
| 5% | 33.4 ms | 302 ms | --- |
| 10% | 68.9 ms | 302 ms | --- |
| 20% | 148.7 ms | 602 ms | p95 落到第 3 次尝试 |
| 30% | 228.4 ms | 902 ms | p95 落到第 4 次(最后一次)尝试 |
四条工程结论,都能直接写进配置规范:
- 仿真与解析最大偏差 0.6 个百分点(p=1%、R=0 档),说明"独立丢包 + 几何分布"这个模型本身是可信的------以后要评估别的重传策略,先算解析式,不必每次跑仿真;
- 不重传是不可接受的:10% 丢包下 18.4% 的客户端收不到事件,而这类故障的现场表现是"某个 ECU 的某个信号永远不动",排查要从信号一路查到 SD,极其耗时;
- R=1 是性价比拐点 :10% 丢包从 81.6% → 96.6%(+15 个点),再加到 R=3 只多 3.3 个点;R=1 解决"绝大多数",R=3 解决"验收指标";
- 耗时是 0/300/600/900ms 的离散台阶 :如果上层给订阅留的窗口小于 300ms,那么重传等于白配------重传间隔必须和上层超时一起定,不能各定各的。
6. 续订:订阅不是一次性的
拿到 SubscribeAck 只是开始。订阅带 TTL,客户端必须在到期前续订(重新 Subscribe 或按实现发 SubscribeEventgroup 更新),否则服务端会把订阅当过期清掉,而客户端往往毫无感知------事件静默停止,没有错误码。
续订的三个量化要点:
- 续订周期建议取 TTL/3:留出两次失败的机会。按第 5 节的数字,单次失败概率在 p=10% 时约 19%(不重传),三次窗口全失败才过期,过期概率 ≈ 0.7%;
- 续订必须走和首订同一条重传路径:只重传首订、不重传续订,等于把可靠性做了一半;
- 服务端重启后订阅要重建:服务端回到 INITIAL_WAIT 时,它内存里的订阅表是空的,客户端必须靠新的 Offer 触发重订------这就是为什么"服务端闪断恢复"和"客户端订阅状态"必须联动,只测服务端状态机会漏掉这一类故障。
表6 订阅生命周期与责任方(检查单用)
| 阶段 | 谁发起 | 成功判据 | 失败表现 | 本篇对应实验 |
|---|---|---|---|---|
| 发现 | 服务端周期 Offer | 客户端收到 Offer | 服务永远"不存在" | 01 篇 / 本篇表3 |
| 首订 | 客户端 Subscribe | 收到 SubscribeAck | 无事件,日志却显示已发 | 实验 B 表4 |
| 续订 | 客户端(周期 TTL/3) | 每轮收到 Ack | 事件静默停止 | 第 6 节 |
| 服务掉线 | 服务端 StopOffer 或 TTL 到期 | 客户端判定下线 | 上层迟迟不降级 | 实验 C 表7 |
| 服务恢复 | 服务端重新进 MAIN | 客户端重订成功 | 恢复后无事件 | 实验 A 表2 |
7. 掉线检测:StopOffer 把 2.5 秒打成 0 毫秒

服务停了,客户端多久知道?两条路径:
- 纯 TTL:客户端记下最近一次收到 Offer 的时刻,超过 TTL 判下线;
- StopOffer:服务端主动广播一条"我要走了",客户端立刻判下线,TTL 只作兜底。
实验 C:服务器在随机时刻停止(前面已有多轮周期 Offer),对 20000 个随机停机时刻统计"停止 → 客户端判定"的延迟。
表7 下线检测延迟(N=20000;括号内为丢包率)
| 方案 | 均值 | p50 | p95 | p99 | 最大 |
|---|---|---|---|---|---|
| 仅 TTL=3s (p=0%) | 2497 ms | 2496 ms | 2948 ms | 2990 ms | 3000 ms |
| 仅 TTL=3s (p=5%) | 2457 ms | 2475 ms | 2945 ms | 2989 ms | 3000 ms |
| 仅 TTL=3s (p=10%) | 2410 ms | 2450 ms | 2943 ms | 2989 ms | 3000 ms |
| StopOffer+TTL (p=0%) | 0 ms | 0 ms | 0 ms | 0 ms | 0 ms |
| StopOffer+TTL (p=5%) | 123 ms | 0 ms | 1193 ms | 2777 ms | 2999 ms |
| StopOffer+TTL (p=10%) | 238 ms | 0 ms | 2441 ms | 2891 ms | 3000 ms |
(仿真中约 0.8%~1.8% 的样本因全程未收到任何 Offer 被剔除------这类客户端本就处于"未发现服务"状态,不在检测延迟的定义域内。)
三个必须讲透的点:
- 纯 TTL 的均值 ≈ TTL − 周期/2 = 3 − 0.5 = 2.5s ,与实测 2.497s 吻合。这条公式可以直接用来估预算:"允许 500ms 内降级"意味着 TTL 只能配到 1.5×周期以内 ,而这又会推高误判(见第 8 节)------检测速度和误报率是同一根杠杆的两端;
- StopOffer 的收益集中在 p50 :无丢包时 100% 的停机 0ms 被发现,均值直接从 2497ms 打到 0ms。这是一条报文换 2.5 秒的买卖,任何量产配置都不该省;
- 但 StopOffer 不能单独用 :它只有一发,p=10% 时 23.8% 的停机收不到,这些场景仍退回 TTL 路径,所以 p95 反而升到 2441ms。正确姿势是 StopOffer 主判 + TTL 兜底,两条路径互补,均值看 StopOffer、p95 看 TTL。
还有一个反直觉:丢包率升高时纯 TTL 的均值反而略降 (2497→2410ms)。原因是丢包让"最近一次收到的 Offer"更早,判定时刻被提前------这是好事吗?不是,它意味着客户端在服务真正停止之前就已经把服务判死了,也就是下一节的误判。
8. 误判账:TTL 倍数决定每小时几次假报警
服务一直活着,客户端却判它下线,叫误判。触发条件很单纯:连续丢掉足够多个 Offer,使得 now - last_rx > TTL 。设 Offer 周期 1s、TTL = m×周期,则需要连续丢 m+1 个 Offer。单次独立丢包率 p 下,每个时刻触发的概率约为 p^(m+1),每小时 3600 个周期,解析式与 200 个小时级仿真对照如下。
表8 误判"服务下线"次数/小时(服务器一直活着,仿真 200 次×1 小时)
| TTL | 需连续丢弃 | p=1% | p=3% | p=5% | p=10% |
|---|---|---|---|---|---|
| 2×周期 | 3 个 | 0.005 (解析 0.004) | 0.085 (0.097) | 0.420 (0.450) | 3.320 (3.600) |
| 3×周期 | 4 个 | 0.000 (0.000) | 0.005 (0.003) | 0.035 (0.023) | 0.445 (0.360) |
| 5×周期 | 6 个 | 0.000 (0.000) | 0.000 (0.000) | 0.000 (0.000) | 0.005 (0.004) |
对照第 7 节,把两本账并排放(10% 丢包):
| TTL | 检测均值(仅TTL) | 误判/小时 | 综合评价 |
|---|---|---|---|
| 2×周期 | ~2.0 s | 3.32 | 掉线快,但一天能误报 80 次,不可用 |
| 3×周期 | 2.41 s | 0.445 | 一天约 10.7 次,仍偏多 |
| 5×周期 | ~4.0 s | 0.005 | 一天约 0.1 次,可接受但降级慢 |
三条结论:
- TTL 每加一倍,误判率降一个数量级以上 (10% 丢包:3.32 → 0.445 → 0.005,合计 664 倍),这是典型的"指数换线性"杠杆,加 TTL 是压误判最便宜的手段;
- TTL 不能无限加 :5× 周期让掉线检测变慢到 4 秒级,对需要快速降级的功能(转向、制动相关)不可接受。正确解法不是选一个折中 TTL,而是分类------安全相关服务用 2~3×周期 + StopOffer 主判,信息类服务用 5×周期换稳定;
- 丢包率是隐藏的第三只手 :上面所有曲线都随 p 指数恶化。当掉线误报突然变多,先查链路丢包,再查 SD 配置------顺序反了会白改三天参数。
9. 上电风暴:180 条报文,峰值差 4.8 倍

最后一个实验回答一个非常具体的问题:上电那一刻,全网到底有多少条 SD 报文,峰值有多猛?
场景:20 个服务同时上电,每个服务发 1 条 Offer,4 个客户端各自订阅 → 每个服务产生 1 Offer + 4 Subscribe + 4 SubscribeAck = 9 条,总计 180 条 (总量固定,与抖动无关)。变的是这 180 条在时间轴上怎么铺开:服务端 INITIAL_WAIT 抖动窗口 W 决定铺开的宽度,峰值取 10ms 窗口内的最大报文数。
表9 上电风暴峰值报文率(500 次仿真/档,总量恒为 180 条)
| 抖动窗口 W | 峰值均值 (msg/s) | 峰值 p95 (msg/s) | 相对 W=0 |
|---|---|---|---|
| 0 ms(全部同刻上电) | 18000 | 18000 | 1.0× |
| 10 ms | 13147 | 15600 | 1.4× |
| 50 ms | 5470 | 7200 | 3.3× |
| 100 ms | 3787 | 5200 | 4.8× |
| 200 ms | 2813 | 4000 | 6.4× |
| 500 ms | 2069 | 2800 | 8.7× |
三条结论:
- 抖动窗口是最便宜的削峰手段 :0 → 100ms 就把峰值压低 4.8 倍,180 条报文一条没少,只是摊开了。这项配置不需要改任何协议,只改一个随机数上界;
- 收益递减点在 100~200ms :100ms 拿到 4.8×,500ms 也才 8.7×,而 500ms 会让"最慢的那个服务"多等半秒------如果上电预算只允许 300ms,就把窗口开到 100~200ms,别贪;
- p95 与均值的差距就是"最坏一次" :W=100ms 时均值 3787、p95 5200,做交换机队列/CPU 中断预算时必须用 p95 而不是均值,否则量产车在最坏工况下会复现现场投诉。
顺带一个换算:SD 报文按 118B 线上字节估算(78B 以太网固定开销 + 16B SOME/IP 头 + 24B SD 载荷),W=0 时 18000 msg/s ≈ 17 Mbit/s ,对 100BASE-T1 不算什么,但对端侧 SD 栈的报文处理是每秒 1.8 万次中断/轮询------上电风暴的第一个瓶颈通常不在带宽,在 CPU。
10. 落地检查单:上车前 11 问
表10 本篇沉淀的工程检查单(带实测参考值)
| # | 检查项 | 通过判据 | 本篇参考值 |
|---|---|---|---|
| 1 | 服务端只实现三态,无自造状态 | 状态数=3 | INITIAL_WAIT/MAIN/REPEATED |
| 2 | 上电抖动窗口已配置 | W ≥ 100ms | 100ms → 峰值压低 4.8× |
| 3 | MAIN 只从前态进入 | 有单元测试断言 | 表2 迁移 6 条全覆盖 |
| 4 | REPEATED 重复次数有上限 | 上限=3,超限回 INITIAL | 防"卡死在重复相" |
| 5 | 订阅重传次数 ≥1 | 10% 丢包成功率 >96% | R=1: 96.57%;R=3: 99.89% |
| 6 | 重传间隔与上层超时一致 | 上层窗口 ≥ 重传间隔×次数 | 0.3s×4 = 1.2s |
| 7 | 续订周期 ≤ TTL/3 | 有周期任务覆盖 | TTL=3s → 续订 1s |
| 8 | StopOffer 已启用 | 停机检测 p50=0 | 仅TTL 均值 2497ms |
| 9 | TTL ≥ 3×周期且按业务分级 | 10% 丢包误判 <1 次/小时 | 3×: 0.445/h;5×: 0.005/h |
| 10 | 上电峰值报文率有预算 | 用 p95 而非均值 | W=100ms: p95=5200 msg/s |
| 11 | 掉线误报突增先查丢包 | 丢包率与误判指数相关 | p 1%→10%: 误判涨 ~100 倍 |
11. 方法论:三条可迁移的结论
- 状态机要能打印成四列表,而不是停留在流程图里 。表2 那 6 行迁移日志是本篇最有价值的产物:它同时是文档、是测试断言、是现场日志格式。画得出状态图不算完成,跑得出迁移日志才算。
- 可靠性参数是"指数换线性"的交易 。重传次数每加 1,失败率平方级下降(p=10%:18.4%→3.4%→0.11%);TTL 每加一倍,误判降一个数量级(3.32→0.445→0.005 次/小时)。先找这类指数杠杆,再谈优化------线性优化堆再多也补不上指数的洞。
- 同一根杠杆的两端必须一起看 。TTL 加大 = 误判减少但检测变慢;抖动加大 = 峰值下降但最慢服务变慢;重传加多 = 成功上升但耗时台阶变长。任何只给单一指标的"最佳配置"都是没做完的分析,本篇的表7/表8 并排、表9 的均值/p95 双线,就是这个原则的写法。
12. 本篇的可复现性
- 环境:Python 3.12 + matplotlib 3.10,单文件
exp02.py,随机种子 20261009,蒙特卡洛 N=20000/档,风暴实验 500 次/档,误判实验 200 次×1 小时/档。 - 图表:5 张图全部由脚本直接生成(状态机 / 订阅时序 / 成功率与耗时 / 下线检测与误判 / 上电风暴),
rcParams中文字体 Microsoft YaHei,保存 dpi=140,无手工绘制。 - 关键模型(解析式,可手工验算):
python
# 订阅成功率: 单次通过 q=(1-p)^2 (Subscribe 与 Ack 各一跳), 重传 R 次共 R+1 次尝试
def sub_success(p, R):
q = (1 - p) ** 2
return 1 - (1 - q) ** (R + 1)
sub_success(0.10, 0) # 0.8100 <- 表4 R=0 / p=10% 解析值
sub_success(0.10, 3) # 0.9987 <- 表4 R=3 / p=10% 解析值
# 纯 TTL 检测延迟均值 = TTL - 周期/2
3.0 - 1.0 / 2 # 2.5 s <- 表7 实测 2.497s
# 误判/小时 ≈ (周期数) * p^(m+1), m = TTL/周期
3600 / 1.0 * 0.10 ** 4 # 0.36 <- 表8 TTL=3x / p=10% 解析值
- 全部中间结果已落盘
exp02_out.json,表2~表9 的每一个数字都能在其中找到对应字段。
13. 下篇预告
03. 事件通知与丢包语义:订阅成功之后事件怎么发------序列号回绕时客户端如何区分"旧包"和"新包"、最后值缓冲在丢包下把系统行为变成"至少一次还是至多一次"、min-cycle/debounce 如何用可算的方式压住事件风暴;同样配一套蒙特卡洛,把"丢一帧事件到底丢的是什么"量化成表。