
标签:#物联网 #Modbus #TCP/IP #UDP #POE供电 #腾讯云 #Wireshark #Python #InfluxDB #以太网温湿度传感器 #网口温湿度记录仪 #机房监控
前几篇我们把以太网温湿度变送器/记录仪接进了 Modbus TCP 采集管线,也提到部分工业记录仪同时支持 Modbus UDP。现场落地时,平台侧第一个问题往往是:
几百个网口温湿度记录仪,集中轮询时,用 TCP 还是 UDP?
直觉上,UDP 省去握手和重传,应该更快、更轻;但实际工程中,高并发轮询下的丢包、连接表耗尽、NAT 超时、应用层重试,才是决定因素。本文给出一套可复现的基准测试方法、指标定义、测试数据和选型结论。
压测端 ──千兆铜缆── PoE交换机 ──百兆/千兆── 记录仪为模拟机房汇聚场景,额外加了一台二层汇聚交换机,DUT 跨 VLAN 经三层网关回程——但本测试保留二层近距场景,排除广域网变量;跨城/跨网段场景见后文外推。
因为物理 DUT 只有 3 台,高并发通过逻辑端点复制 + 回路代理放大:
本报告区分两者:现场决策以方式A为准,管线容量以方式B为准。
维度 | 取值 |
|---|---|
传输层 | TCP / UDP |
客户端并发 | 1, 8, 32, 64 任务 |
轮询速率 | 50 / 200 / 500 req/s 聚合(对N归一化) |
超时 | TCP connect 3s, response 500ms~2s |
重试 | 无/1次/指数退避 |
持续时间 | 10min 稳态 + 突发冲击 |
import asyncio, logging
from pymodbus.client import AsyncModbusTcpClient
async def poll_one(host, unit, q, sem):
async with sem:
client = AsyncModbusTcpClient(host, port=502)
try:
await asyncio.wait_for(client.connect(), timeout=3.0)
resp = await asyncio.wait_for(
client.read_holding_registers(0, count=3, slave=unit), timeout=1.0)
if resp.is_ok():
regs = resp.registers
val = (regs[0]/10.0, regs[1]/10.0)
await q.put((host, val, None))
else:
await q.put((host, None, "modbus_err"))
except Exception as e:
await q.put((host, None, str(e)))
finally:
client.close()
async def runner(hosts, rate, duration):
sem = asyncio.Semaphore(64)
q = asyncio.Queue()
loop = asyncio.get_event_loop()
end = loop.time() + duration
tasks = set()
sent = 0
while loop.time() < end:
for h in hosts:
t = asyncio.create_task(poll_one(h, 1, q, sem))
tasks.add(t)
sent += 1
# 简单速率整形
await asyncio.sleep(1.0/(rate*len(hosts)))
await asyncio.gather(*tasks, return_exceptions=True)注意:pymodbus 异步客户端每实例一连接,高并发下别复用同一 client 对象;连接池见后。
Modbus UDP 不是标准 pymodbus 客户端默认路径,需自行封装 MBAP-less 或带 MBAP 载荷:
import socket, struct, asyncio
TRANS_ID = 0
def build_req():
global TRANS_ID
tid = TRANS_ID & 0xffff; TRANS_ID += 1
mbap = struct.pack(">HHHB", tid, 0x0000, 0x0006, 0x01)
pdu = b"\x03\x00\x00\x00\x03"
return tid, mbap + pdu
class UdpModbusPoll:
def __init__(self, hosts, timeout=1.0):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.sock.setblocking(False)
self.hosts = hosts
self.pending = {}
self.timeout = timeout
async def run(self, duration):
loop = asyncio.get_event_loop()
transport, _ = await loop.create_datagram_endpoint(
lambda: self, local_addr=("0.0.0.0", 0))
# 简化:直接用 sock.sendto + loop.add_reader
...更务实的做法:用 dpkt/scapy 离线回放,或写同步发送+select 循环。生产侧推荐事件循环 + recvfrom:
async def udp_poll(hosts, q, duration):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setblocking(False)
loop = asyncio.get_event_loop()
pending = {}
sent = 0
end = loop.time() + duration
while loop.time() < end:
for h in hosts:
tid, pkt = build_req()
sock.sendto(pkt, (h, 502))
pending[tid] = (h, loop.time())
sent += 1
# 收一批
try:
while True:
data, addr = sock.recvfrom(1024)
tid = struct.unpack(">H", data[:2])[0]
pending.pop(tid, None)
# 解析 regs
except BlockingIOError:
await asyncio.sleep(0.001)
# 剩余 pending = 未响应UDP 侧必须自己实现重试/超时,否则丢包即数据缺口。
tshark -i mir -f "tcp port 502 or udp port 502" -T fields ... 并发任务 | 协议 | 聚合req/s | SR_app | PL_trans | P95时延 | 备注 |
|---|---|---|---|---|---|---|
8 | TCP | 120 | 99.8% | 重传0.1% | 12ms | 连接复用后更稳 |
8 | UDP | 120 | 99.2% | 0.8%丢响 | 9ms | 局域网, 交换机无拥塞 |
32 | TCP | 400 | 99.1% | 重传0.4% | 28ms | fd/连接池正常 |
32 | UDP | 400 | 96.4% | 3.6%丢响 | 22ms | 应用层无重传时缺口 |
64 | TCP | 800 | 97.5% | 连接排队,重传1.2% | 60ms | DUT最大连接数触顶前正常 |
64 | UDP | 800 | 91.0% | 9%丢响 | 45ms | 客户端sockbuf溢出 |
物理设备瓶颈先出现在固件并发连接/任务队列,而非传输层。
并发 | 协议 | 目标速率 | SR_app | 服务端CPU | 客户端瓶颈 |
|---|---|---|---|---|---|
64 | TCP | 2k req/s | 98.9% | 35% | epoll正常, 连接池命中 |
64 | UDP | 2k req/s | 94% (重试1次后97.5%) | 20% | recvbuf丢包, 需SO_RCVBUF调优 |
256 | TCP | 8k req/s | 95%, TIME_WAIT膨胀 | 50% | 端口耗尽? 否, 同客户端多连接 |
256 | UDP | 8k req/s | 82%原生 / 92%重试后 | 30% | 内核UDP队列表满, ENOBUFS |
关键观察:
tcp.analysis.retransmission, tcp.analysis.rtt, modbus.exception_code udp.length < 12, 自研事务ID关联,统计请求无响应比例 ethtool -S 交叉验证 env_sensor,sensor=...,loc=... temperature=23.6,humidity=45.2 原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。