

近期在调试一套工业环境监控系统时,遇到了关于 Modbus 连接保持的问题。现场部署了一批 恒湿净化设备(包括 @恒湿消毒净化一体机 等),主要用来维持特定环境的温湿度指标。在长时间运行后,发现设备偶尔会掉线。把 SNMP 和 Modbus TCP 做成"双协议并行"之后,系统上线初期一切正常,但运行两周后开始出现间歇性连接拒绝、SNMP 超时、Modbus 寄存器读不全的诡异现象。排查到最后,根因不是代码 bug,而是端口冲突 + MCU 资源耗尽 + 网络栈行为叠加——这篇把完整复盘写出来。
时间线:
Day 1–3:正常,86 台传感器 Modbus TCP 轮询 OK,SNMP Trap 秒级推送
Day 4–7:偶发,约 3–5 台传感器 Modbus 读超时,重试后恢复
Day 8–10:加剧,每天上午 10 点左右集中爆发,持续 15–30 分钟后自愈
Day 11–14:全天不稳定,约 20% 传感器 Modbus 连接被 RST,SNMP Trap 丢包率飙升诡异之处:
snmpwalk 超时 检查项:
✅ 交换机端口无 error、无 CRC
✅ 无广播风暴(交换机开启风暴抑制)
✅ POE 供电正常(电压 48V,电流正常)
✅ 网线无松动
✅ VLAN 配置正确,无 ACL 拦截→ 网络层排除。
检查项:
✅ 并发 30,每台 30s 一轮
✅ 超时 1.5s,重试 3 次
✅ 没有频繁建连/断连(长连接复用)→ 轮询参数排除。
抓包发现的关键帧:
1. TCP 三次握手正常
2. Modbus 请求发出
3. 传感器响应:TCP ZeroWindow(接收窗口为 0)
4. 网关重试 → TCP Retransmission
5. 传感器:TCP RST, ACK → 连接被重置
同时段 UDP:
SNMP Trap 帧到达,但传感器没有响应 snmpwalk 的 GetRequest关键线索:TCP ZeroWindow + SNMP 无响应 → MCU 侧接收缓冲区满了,处理不过来。
系统信息页:
CPU: 168MHz (STM32F407)
RAM: 192KB (实际可用 ~128KB,其余被 LWIP + 应用占用)
TCP Connections: 4/4 (已满!)
UDP Sockets: 2/2 (已满!)
Free Heap: 2.1KB (危险水位)根因浮出水面:MCU 资源耗尽,TCP 连接池满了,新的 Modbus 连接被 RST;SNMP 的 UDP 接收缓冲区也满了,GetRequest 被丢弃。
场景还原:
网关 A(Modbus 轮询) → 长连接,正常
网关 B(SCADA 系统) → 也连了同一批传感器(之前没发现)
运维笔记本 → 偶尔 snmpwalk,每次新建 TCP 连接(SNMP over TCP?不,但 Web 管理用 TCP:80)
→ 4 个 TCP 连接槽位被占:
Slot 1: 网关 A 长连接(正常)
Slot 2: 网关 B 长连接(之前不知道)
Slot 3: Web 管理界面(有人开着没关)
Slot 4: SNMP Trap 的某种内部 TCP 回调(固件 bug)为什么之前没发现:网关 B 是旧系统,一直连着这些传感器做 SCADA 展示,但没人告诉新系统。两套系统同时轮询,连接池直接打满。
LWIP 配置:
PBUF_POOL_SIZE = 16
PBUF_POOL_BUFSIZE = 256
场景:
上午 10 点,恒湿机启动 → 温湿度波动 → 触发 SNMP Trap
→ 86 台传感器同时发 UDP Trap
→ 每台 Trap 帧 82 bytes + IP/TCP/UDP 头 ≈ 150 bytes
→ 86 × 150 = 12.9KB,看起来不大
但!LWIP 的 pbuf 是固定大小的:
16 个 pbuf × 256 bytes = 4KB 总池
Modbus TCP 响应需要 pbuf(每个连接约 2–3 个 pbuf)
SNMP Trap 需要 pbuf(每个 UDP 帧 1 个 pbuf)
同时到达时,pbuf 池耗尽 → Modbus 响应分配不到内存 → ZeroWindow上午 10 点恒湿机启动 → 局部气流扰动 → 温湿度快速变化
→ 传感器采样频率自动提高(部分型号有"快速响应模式")
→ 采样间隔从 1s 变成 200ms
→ 内部计算负载增加
→ LWIP 接收中断被延迟
→ TCP 窗口更新不及时 → ZeroWindow1. 找到网关 B(SCADA),停止对这 86 台传感器的轮询
2. 传感器 Web 管理页面加会话超时(15 分钟自动踢出)
3. 固件升级:修复 SNMP Trap 内部 TCP 回调的 bug(联系厂商)// 调整前
#define PBUF_POOL_SIZE 16
#define PBUF_POOL_BUFSIZE 256
#define MEMP_NUM_TCP_PCB 4
#define MEMP_NUM_UDP_PCB 2
#define TCP_WND 4096
#define TCP_SND_BUF 4096
// 调整后
#define PBUF_POOL_SIZE 32 // 翻倍
#define PBUF_POOL_BUFSIZE 512 // 翻倍(适应 Modbus TCP 大帧)
#define MEMP_NUM_TCP_PCB 2 // 限制 TCP 连接数,宁可拒绝也不要撑死
#define MEMP_NUM_UDP_PCB 4 // UDP 多给点(Trap 用)
#define TCP_WND 2048 // 缩小窗口,避免接收方缓冲区溢出
#define TCP_SND_BUF 2048
#define TCP_MSS 256 // 减小 MSS,分片更细关键思路:不是"越大越好",而是限制 TCP 连接数 + 给 UDP 更多 pbuf + 缩小 TCP 窗口避免撑死接收方。
# 网关侧加 SNMP Trap 接收速率限制
import asyncio
from asyncio import Semaphore
TRAP_RATE_LIMIT = 20 # 每秒最多处理 20 个 Trap
trap_semaphore = Semaphore(TRAP_RATE_LIMIT)
async def on_trap_received(frame):
async with trap_semaphore:
await process_trap(frame)
await asyncio.sleep(0.05) # 50ms 间隔,给 MCU 喘息时间恒湿机启动策略从"统一启动"改为"分批启动":
- 8 台恒湿机,每台间隔 2 分钟
- 避免所有传感器同时进入"快速响应模式"
- 边缘网关在恒湿机启动时段临时降低轮询频率(从 30s → 60s)复盘过程中,整理了现场常见的"端口冲突"类型:
冲突类型 | 现象 | 根因 | 解决 |
|---|---|---|---|
多主站抢 TCP 连接 | Modbus 间歇性 RST | MCU 连接池满 | 限制主站数量,或用 RTU 转 TCP 网关做"一主多从" |
SNMP Trap 和 Modbus 抢 pbuf | ZeroWindow + Trap 丢包 | LWIP 内存池不足 | 调大 pbuf + 限制 Trap 速率 |
Web 管理和 Modbus 端口冲突 | 打开 Web 页面后 Modbus 变慢 | 同一 TCP 栈,Web 占连接 | 加 Web 会话超时,或 Web 走独立端口 |
SNMP GetRequest 和 Trap 共用 UDP 端口 | snmpwalk 超时 | 固件 bug:GetRequest 处理阻塞 Trap 发送 | 固件升级,或 Get 和 Trap 用不同端口 |
交换机端口安全限制 | 传感器 MAC 被交换机封锁 | 交换机端口安全策略(MAC 数量限制) | 调整交换机端口安全策略 |
nmap -sT -p 1-65535 <sensor_ip> 确认开放端口和协议 nc 或 telnet 同时开 N 个连接到 502 端口,看第几个被拒 SNMP 和 Modbus TCP 双协议并行不是"开两个端口"那么简单,MCU 的 TCP 连接池、LWIP 的 pbuf 内存池、以及外部系统的"隐形连接"都会成为瓶颈。 排错时别只看应用层,Wireshark 的 ZeroWindow 和 RST 才是真正的线索。上线前做连接数压测和 pbuf 监控,比事后救火省十倍时间。
下一篇候选:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。