设备总掉线却复现不了?把弱网"造"出来:tcconfig 五种网络故障模拟实战
设备批量出货后,客户反馈来了:设备经常掉线。
做物联网平台的都知道这种反馈有多难接。掉线的设备分布在各地,网络环境千差万别,你总不能扛着电脑去客户现场蹲着。而实验室里的网络好得过分------千兆内网、专线出口,设备挂一个月都稳如老狗,什么问题都复现不了。
排查陷入了死局:问题在弱网里,弱网在千里外。
我们的破局思路是:既然设备来不了弱网,就把弱网搬到设备面前。不需要买昂贵的网络损伤仪,Linux 自带的流量控制工具就能干------我们用 tcconfig (tcset 命令)在服务端网卡上注入网络损伤,把五种典型的弱网场景在机房里造了出来:延时、丢包、包重复、包损坏、包乱序。
这篇讲清楚两件事:怎么造,以及造出来之后看到了什么------有些实测结果,和教科书说的不一样。
一、测试环境:一行命令造出一种故障
环境很简单:
- 服务端:EMQX(MQTT 端口 8874)+ 一个内网 Web 服务(80 端口)
- 验证工具:tcping (看 TCP 层通不通、快不快)+ MQTTX(模拟真实设备,看 MQTT 消息收发)
- 损伤注入:tcconfig,直接打在服务端的 eth0 网卡上
【配图 1:损伤注入的位置】
关键点:损伤打在服务端网卡上,对客户端完全透明。客户端以为自己在和一个"网络很烂的服务器"通信------这正是设备在现场的视角。
五种场景的命令模式(注意 --direction 区分入向/出向,可以单独打也可以叠加):
| 场景 | 命令示例 | 备注 |
|---|---|---|
| 延时 | tcset eth0 --direction incoming --delay 100ms --network <客户端IP> --port 80 |
入向出向分开设,叠加后 RTT 线性累加 |
| 丢包 | tcset eth0 --direction incoming --loss 50% --network <客户端IP> --port 80 |
设定值有波动,见下文实测 |
| 包重复 | tcset eth0 --direction incoming --duplicate 50% ... --port 80 |
实测两个工具都观测不到效果 |
| 包损坏 | tcset eth0 --direction incoming --corrupt 50% ... --port 80 |
校验和不过,直接无应答 |
| 包乱序 | tcset eth0 --direction incoming --delay 100ms --reordering 50% ... |
必须配合 --delay,否则报错 |
测完用 tcdel eth0 --all 一键清除。逐个场景说实测结果,重点在和预期不符的地方。
二、丢包:50% + 50% = 70%,以及一次诡异的"假死"
设定值不等于实际值
设 50% 丢包,tcping 实测丢包率在 10%~50% 之间波动------netem 的丢包是概率模型,不是严格的"每两个丢一个"。
更有意思的是叠加效应:入向、出向各设 50% 丢包,直觉上以为还是 50%,实测整体丢包率到了 70%。道理不复杂:一个来回要穿过两道鬼门关,两个方向是独立事件。设 50% 时实际可用率大约只有 (1-0.5)×(1-0.5) = 25%,加上波动,70% 是合理读数。
MQTT 在丢包下的两个极端
轻度丢包时,MQTT 客户端的重发机制会兜底:上报的消息有时秒到,有时延迟几秒到------表现为"能用,但抖"。
但双向 50% 丢包时,我们看到了本次测试最值得警惕的现象:
- 创建 MQTT 连接,连 3 次才成功 1 次
- 连上之后,一开始发的消息都能收到
- 某一次发送失败后,就再也收不到了------翻 EMQX 的日志,broker 明明收到消息并重发了 7 次,客户端一条都没收到
【配图 2:丢包下的"假死"】
这就是弱网设备的典型死法:连接状态是"活着"的,数据通路已经死了。平台上显示在线,数据却永远不更新------后来我们理解客户现场很多"假在线"投诉,机理就在这张图里。
三、延时:5 秒延时,连接先死
延时场景最符合直觉:打 100ms,RTT 就涨 100ms;入向出向各打 100ms,RTT 涨 200ms,线性叠加,tcping 看得清清楚楚。
MQTT 侧的结论更值得记:8874 端口入向延时打到 5 秒 时,客户端连接 EMQX 的失败率明显升高------消息延迟收到是小事,连接握手阶段超时才是大事。
这解释了现场的一个错觉:设备日志里大量"连接失败",很容易误判成"服务器挂了"或"设备坏了"。其实可能只是链路 RTT 超过了客户端的连接超时阈值。弱网的第一受害者不是消息,是握手------MQTT 的 CONNECT/CONNACK、TCP 三次握手、TLS 握手,每一段都掐着超时表。设备端的连接超时参数,必须按最差网络而不是平均网络来设。
四、包损坏与包重复:一个太狠,一个太温柔
包损坏 (corrupt 50%):tcping 直接无应答------校验和不过关的包,协议栈连看都不看就扔了。MQTT 侧的现象是连接耗时变长,并且出现和丢包类似的模式:失败一次之后一直失败。损坏在 TCP 层的表现就是丢包,只是更彻底。
包重复(duplicate 50%):这是唯一一个两个工具都测不出效果的场景。tcping 无感,MQTT 侧 EMQX 日志和平台日志里都只有一条数据。
一开始以为规则没打上,查了 tc 配置明明在。后来想通了:TCP 本身就有去重机制(序列号),重复包在协议栈就被消化了,根本到不了应用层。想测应用层的重复消息,得在应用层自己构造(比如用脚本对同一 topic 发两条一样的消息),靠网络层造不出来。
这也是本次测试的一个方法论收获:网络损伤工具只能模拟网络层的病,应用层的病要在应用层造。
五、包乱序:抓到"后发先到",逼出一个协议改动
乱序场景有个文档里不显眼、不查根本不知道的坑:--reordering 必须配合 --delay 一起用,否则直接报错。原理是乱序靠"把一部分包延迟后再发"实现,没有延时就没有乱序的空间。
tcping 对乱序无感(它对每个连接独立计时)。但 MQTT 侧,我们抓到了教科书上才见过的现象:消息"后发先到"------设备对同一 topic 连续上报,订阅端收到的消息时间戳是倒序的,先发出的那条反而后到。
【配图 3:乱序导致的"后发先到"】
如果是聊天消息,乱序顶多看着别扭。但设备数据不一样:平台如果默认"后到的就是最新的",旧读数会把新读数覆盖掉。电表读数、设备状态、告警信息,覆盖错了都是事故。
这次测试直接推动了协议改动:报文增加指令序列号,平台侧对数值/序号比当前值更小的上报做忽略处理。规则一句话,前提是你得先亲眼看到乱序真的会发生。
六、有效/无效矩阵(直接抄走)
| 工具 | 延时 | 丢包 | 包重复 | 包损坏 | 包乱序 |
|---|---|---|---|---|---|
| tcping | 有效 | 有效 | 无效 | 有效 | 无效 |
| MQTTX | 有效 | 有效 | 无效 | 无效 | 有效 |
用法建议:先用 tcping 确认规则打上了(网络层生效),再用 MQTTX 看应用层表现(业务受什么影响)。两个工具各管一层,谁都不能单独证明"规则生效且业务受影响"。
七、三个教训
1. 复现不了的问题,先把环境造出来
"客户说掉线,我们这里好好的"是 IoT 排障最常见的僵局。破局的从来不是更努力的猜测,而是把故障环境搬进实验室。一台 Linux 机器 + tcconfig,零成本,五种弱网随便造。测试环境的意义不是证明系统没问题,是让问题愿意出现。
2. 设定值 ≠ 实际效果,每个规则都要验证
50% 丢包实际 10%~50% 波动;双向 50% 叠加出 70%;包重复打了跟没打一样;乱序不配延时直接报错。损伤规则设完不算完,先用工具确认它真的发生了,再谈对业务的影响。否则你测的可能是一个根本不存在的场景。
3. 弱网测试的产出应该是设计改动,不是测试报告
这份测试如果只产出一份"有效/无效矩阵",价值有限。它真正的产出是:连接超时参数按最差网络设、协议加序列号、旧值忽略、以及对"假在线"机理的理解。弱网测试测的不是网络,是你的协议和参数在烂网络下配不配活。
写在最后
三篇文章,同一个主题的三个侧面:服务器端,重连波峰打垮集群(凶手是日志);设备端,无退避重连烧光流量;这一篇,是弱网本身怎么复现、怎么测。
设备联网的坑有个共同特点:在好网络里全都是隐形的。它们只在偏远地区的基站下、在 50% 丢包的链路上、在乱序的报文里现形。要么等设备跑出去替你踩雷,要么在机房里把雷造出来自己踩。
选一个。
你们测过弱网吗?用的什么工具,踩过什么坑?评论区交流一下。