Modbus TCP的"静默故障"——从CRC错误到连接表耗尽

干了这么多年Modbus,我发现一个规律:Modbus很少"突然坏掉",它通常是慢慢"退化"的。

退化的时候不报警、不报错,就是数据偶尔旧一点、轮询周期比配置的慢一点、操作工说"数据好像不太对"。等你发现的时候,已经不是调一个参数能解决的问题了。

而且更麻烦的是,Modbus RTU和Modbus TCP的退化机制完全不一样。用排查RTU的思路去查TCP,方向就错了。

RS485的退化:物理层和时序问题

RS485串口链路的退化,几乎都是物理层或者时序问题。

RS485是一个共享介质,协议本身没有冲突检测机制。Modbus RTU严格遵循主从架构——只有主站发起请求,同一时刻只有一个从站响应。

当物理层开始退化——线缆损伤、终端电阻缺失、面板之间的地电位差、大电流线缆与RS485线并行敷设——主站看到的是CRC错误。请求帧发出去是干净的,但从站收到的是噪声。从站要么静默丢弃这一帧,要么回复一个损坏的响应。

关键不是"有没有CRC错误",而是"错误出现在哪里"。

CRC错误均匀分布在总线上所有设备——说明问题在共享介质上:线缆损伤、终端电阻缺失或重复、面板间地电位差。

CRC错误集中在一两台设备上——说明问题在那几台设备:UART无法维持配置的波特率、收发器拉低了总线电压、无源RS485转换器不满足电气规范。

CRC错误出现在每天固定的时间段——说明是感应噪声。一根30米的RS485线缆和460V电机馈线共用线槽,电机的启动瞬态会在RS485线上感应出电压。把线缆远离大电流线路、用屏蔽双绞线、屏蔽层单端接地,问题就解决了。

把超时时间调大?解决不了这个问题。

还有一个经常被忽略的问题:从站地址重复。

同一个RS485段上如果两个设备地址相同——设备更换的时候特别容易出现,新设备出厂默认地址可能已经被占用了——两个设备会同时响应同一个请求。信号冲突,主站收到的响应要么是错的,要么是CRC校验失败。

这个问题在大型RS485总线上非常普遍,但排查的时候很容易被忽略。

Modbus TCP的退化:完全不同的机制

Modbus TCP和Modbus RTU共享同一个应用层协议,但底层传输机制完全不同。失败机制也完全不同。

Modbus TCP最常见的失效模式是连接表耗尽。

什么是连接表?每个Modbus TCP从站设备(PLC、网关、仪表)内部都维护一张TCP连接表。这张表里记录了当前所有连接到这个设备的客户端信息——客户端的IP、端口、TCP会话状态。

Modbus TCP标准规定从站最多支持5个并发连接。但实际实现中,不同厂家的支持数量不一样。有的支持5个,有的支持8个,有的只支持1个。

当连接表满了,新的连接请求会被拒绝。但如果程序没有正确处理这个拒绝——比如重试逻辑写得不对——就会不断发起新的连接请求,连接表里的条目越积越多,最终整个设备的TCP栈崩溃。

连接表耗尽的典型表现是什么?

设备在网络上能ping通,端口502也是开放的,但Modbus请求就是没有响应。你用调试工具去连,有时候能连上,有时候连不上。连上之后读数据正常,过一会儿又断了。

排查的时候,如果你用RTU的思路去查——检查CRC、检查波特率、检查终端电阻——完全找不到方向。因为问题根本不在串口层。

怎么查连接表耗尽?

用Wireshark抓包,看TCP层的行为。正常情况下,客户端发起连接、传输数据、关闭连接,一个完整的生命周期。如果看到大量"SYN"包但没有对应的"SYN-ACK"回复,说明服务端的连接表满了,新的连接请求被丢弃了。

还有一个更隐蔽的变体:半开连接。

客户端发起了TCP连接,但程序异常退出,没有正常关闭连接。从站那边看到的连接是"已建立"状态,但客户端已经不在了。这个连接会一直占用连接表的一个条目,直到TCP的超时机制把它清理掉。

如果程序频繁启动和退出,半开连接会迅速耗尽连接表。

怎么避免?

第一,客户端程序要正确管理TCP连接的关闭。异常退出的时候,操作系统会清理掉连接,但如果代码里有try-catch吞掉了异常,连接可能不会被正常关闭。

第二,保持长连接,不要频繁建立和断开。Modbus TCP的TCP握手开销比RTU的帧间隔大得多。如果每次读数据都新建连接、读完就关,连接表会被快速消耗。

第三,设置合理的Keep-Alive参数。让操作系统自动清理掉那些已经死掉的半开连接。

第四,客户端程序实现连接池,而不是每次操作都新建连接。连接池里维护固定数量的长连接,复用这些连接进行所有操作。

TCP的另一个退化信号:轮询周期悄悄变长

还有一个不容易发现的退化——轮询周期比配置的越来越长。

程序配置的轮询周期是200ms。刚开始跑的时候,实际一圈大概210ms。运行了三个月之后,实际一圈变成了500ms。

程序没改过,设备没换过,网络没动过。为什么变慢了?

原因可能有好几个:

原因一:日志文件太大。 每一条Modbus请求都写日志,日志文件一天涨1GB。程序写日志要打开文件、写入、关闭。文件越大,这些操作越慢。

原因二:死轮询列表。 有些设备已经离线了,但程序还在轮询它们。每次轮到这些设备,程序都要等超时。如果超时设的是500ms,有5台离线设备,每一圈就多花2.5秒。

原因三:数据库索引缺失。 采集到的数据写入SQLite数据库,时间戳字段没有索引。刚开始数据库小,查询很快。数据量涨到几百万条之后,每次查询要几十秒。

原因四:内存泄漏。 超时事件对象没有被正确释放,积压了几万个对象。程序遍历这些对象找新事件,越跑越慢。

这四个原因,排查方法不一样。日志文件看大小,死轮询列表看设备在线状态,数据库查询时间用工具测,内存泄漏用内存分析工具。不要瞎猜,看数据。

四个必须监控的指标

Modbus TCP退化的时候,如果你不监控这几个指标,就发现不了。

交易成功率。 发出的请求中,成功收到响应的比例。正常应该在99%以上。低于95%就要查。

响应时间。 每个请求从发出到收到响应的耗时。记录平均值和最大值。如果平均值没变但最大值在变大,说明偶尔有请求被阻塞了。

CRC错误数(RTU)/ TCP重传数(TCP)。 这是底层质量的直接反映。

轮询周期完成时间。 配置的轮询周期是200ms,实际完成时间是不是200ms?如果偏差超过20%,说明有问题。

关于工具

排查Modbus退化的时候,有一个可以模拟各种异常场景的环境会很方便。ProtoForge支持17种工业协议,可以在电脑上模拟不同响应延迟、不同连接状态、不同数据异常的设备。验证程序的容错逻辑之前,先用虚拟设备把异常场景跑一遍。

GitHub:suoten/ProtoForge
Gitee:suoten/ProtoForge

posted @ 2026-09-18 09:42  硕腾  阅读(5)  评论(0)    收藏  举报