首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >UDP 还是 TCP?以太网温湿度记录仪在高并发轮询下的丢包率基准测试

UDP 还是 TCP?以太网温湿度记录仪在高并发轮询下的丢包率基准测试

原创
作者头像
HONSOR盛世宏博
发布于 2026-09-22 11:50:49
发布于 2026-09-22 11:50:49
1380
举报

UDP 还是 TCP?以太网温湿度记录仪在高并发轮询下的丢包率基准测试

标签:#物联网 #Modbus #TCP/IP #UDP #POE供电 #腾讯云 #Wireshark #Python #InfluxDB #以太网温湿度传感器 #网口温湿度记录仪 #机房监控

一、为什么要做这次基准测试

前几篇我们把以太网温湿度变送器/记录仪接进了 Modbus TCP 采集管线,也提到部分工业记录仪同时支持 Modbus UDP。现场落地时,平台侧第一个问题往往是:

几百个网口温湿度记录仪,集中轮询时,用 TCP 还是 UDP?

直觉上,UDP 省去握手和重传,应该更快、更轻;但实际工程中,高并发轮询下的丢包、连接表耗尽、NAT 超时、应用层重试,才是决定因素。本文给出一套可复现的基准测试方法、指标定义、测试数据和选型结论。

二、被测对象与测试环境

2.1 被测设备(DUT)

  • 工业以太网温湿度记录仪 ×3 台,RJ45,PoE 供电(802.3af)
  • 固件支持双协议:Modbus TCP(502/tcp)、Modbus UDP(502/udp)
  • 保持寄存器映射:40001 温度×10、40002 湿度×10、40003 露点×10
  • 管理面:Web 配置、SNMP 只读

2.2 采集/压测端

  • 边缘网关:ARM Cortex-A53 四核,1GB RAM,Linux 5.10
  • Python 3.11,pymodbus 3.x,自研轮询框架
  • 网卡直连工业 PoE 交换机,与被测设备同二层网段,MTU 1500
  • 旁路抓包:镜像端口 + Wireshark/tshark,另存 pcap 供离线分析

2.3 网络路径

代码语言:javascript
复制
压测端 ──千兆铜缆── PoE交换机 ──百兆/千兆── 记录仪

为模拟机房汇聚场景,额外加了一台二层汇聚交换机,DUT 跨 VLAN 经三层网关回程——但本测试保留二层近距场景,排除广域网变量;跨城/跨网段场景见后文外推。

三、测试方法与指标定义

3.1 轮询模型

  • 单轮遍历 N 个设备(本测 N=3,扩展至模拟 200 逻辑端点)
  • 每端点每轮读 3 个保持寄存器(read_holding_registers, addr=0, count=3)
  • 轮询周期目标:T_cycle,或固定发送速率 R_req = N / T_cycle
  • 并发模型:多线程 / 异步事件循环两种

因为物理 DUT 只有 3 台,高并发通过逻辑端点复制 + 回路代理放大:

  • 方式A:真实设备,轮询进程开 K 个并发任务,每个任务绑定设备 IP,测量真实响应
  • 方式B:软件模拟记录仪(socat/自研 UDP/TCP echo-modbus stub)监听环回/辅助网卡,放大到 200 虚拟端点,测量管线能力

本报告区分两者:现场决策以方式A为准,管线容量以方式B为准。

3.2 关键指标

  • 应用层成功率​ SR_app = 成功解析响应数 / 发起请求数
  • 传输层丢包率​ PL_trans:pcap 中请求未产生响应报文的比例(UDP),或 TCP 重传率、连接失败率
  • 端到端时延​ P50/P95/P99:发出请求到拿到解析值
  • 连接开销(TCP):ESTABLISHED 套接字数、TIME_WAIT 堆积、accept/connect 失败
  • 服务端侧压力:DUT CPU/任务栈、看门狗复位次数、并发连接上限
  • 退化行为:高负载下是否丢旧连接、是否拒绝新连接、是否静默丢弃

3.3 测试矩阵

维度

取值

传输层

TCP / UDP

客户端并发

1, 8, 32, 64 任务

轮询速率

50 / 200 / 500 req/s 聚合(对N归一化)

超时

TCP connect 3s, response 500ms~2s

重试

无/1次/指数退避

持续时间

10min 稳态 + 突发冲击

四、实现要点(Python)

4.1 TCP 轮询(异步)

代码语言:javascript
复制
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 对象;连接池见后。

4.2 UDP 轮询(自研报文,避免库抽象)

Modbus UDP 不是标准 pymodbus 客户端默认路径,需自行封装 MBAP-less 或带 MBAP 载荷:

  • 工业记录仪常见两种:裸 Modbus PDU over UDP;或完整 Modbus TCP ADU(MBAP+PDU)over UDP。测试前用 Wireshark 确认。
  • 下面按常见固件:UDP 载荷 = Modbus TCP ADU(7B MBAP + PDU)。
代码语言:javascript
复制
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:

代码语言:javascript
复制
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 侧必须自己实现重试/超时,否则丢包即数据缺口。

4.3 指标采集

  • tshark 实时管道:tshark -i mir -f "tcp port 502 or udp port 502" -T fields ...
  • 离线:pcap 导入,按事务ID/五元组关联请求响应
  • 应用层埋点:prometheus client 暴露 counter gauge,或写 InfluxDB line protocol 本地缓冲

五、测试结果

5.1 物理 DUT,N=3,单网关近端

并发任务

协议

聚合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溢出

物理设备瓶颈先出现在固件并发连接/任务队列,而非传输层。

5.2 模拟管线,200 逻辑端点

并发

协议

目标速率

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 在拥塞时靠滑动窗口/退避,应用层成功率下降平滑;UDP 在套接字缓冲区满时静默丢弃,应用层若不重试,数据缺口直接扩大。
  • 高并发短连接 TCP 场景下,连接建立/拆除开销显著:每轮新建连接时,DUT 侧出现 SYN 队列丢弃,客户端 TIME_WAIT 堆积。改为长连接复用后,TCP 吞吐提升 2~3×
  • UDP 无连接,但应用层需维护事务表、超时重传、去重;实现复杂度反噬工程收益。

5.3 DUT 固件边界(现场最重要)

  • 多数工业记录仪 Modbus TCP 从站:并发连接数 1~5,部分可配至 8;超连接后 RST 或丢弃新连接
  • Modbus UDP 实现成熟度低:部分固件仅响应最后收到的请求上下文,事务ID不校验;多客户端时响应错配
  • PoE 供电下,持续高轮询(<1s周期)使MCU负载升高,触发看门狗复位——测试中发现2款固件在>200轮询/min/台时出现复位
  • 建议:平台侧收敛轮询,边缘网关本地缓存,别把记录仪当高并发服务打

六、Wireshark 排错要点

  • TCP:过滤 tcp.analysis.retransmission, tcp.analysis.rtt, modbus.exception_code
  • UDP:过滤 udp.length < 12, 自研事务ID关联,统计请求无响应比例
  • 确认 ADU:Modbus TCP over UDP 时,MBAP 中 length 字段、unit id 正确
  • PoE 侧:交换机端口 CRC/误码、双工不匹配导致 TCP 重传放大;ethtool -S 交叉验证
  • 别把平台侧超时当丢包:抓包点在网关出口,区分链路丢包与应用层放弃

七、工程选型结论

7.1 默认选 TCP Modbus TCP,理由

  • 可靠传输,平台逻辑简单:连接管理 + 超时 + 可选重试
  • 长连接复用后,高并发下资源可控,DUT 侧压力小
  • 防火墙/NAT/ACL 语义清晰,502/tcp 策略好管
  • 跨网段/VPN/SD-WAN 场景,TCP 连接态便于排错

7.2 何时考虑 UDP

  • 同二层、低时延、节点数大、轮询周期粗(≥1s)、且平台接受偶发数据缺口
  • 边缘网关本地聚合,应用层已实现重传/补采,DUT 固件确认支持多事务上下文
  • 带宽极度受限或卫星/无线链路,TCP 队头阻塞代价高——但此时更应上报告制(Trap/MQTT)而非拉取

7.3 高并发轮询的工程化做法(比选协议更重要)

  1. 收敛轮询频率:环境量变化慢,1~5s 周期足够;别按毫秒级控制环路设计动环采集
  2. 分片采集:按机房/VLAN 分边缘网关,本地轮询后上送变更/MQTT,中心不直连数百设备
  3. 长连接池:每 DUT 1 连接,异步复用,backoff 退避,熔断降级
  4. 批量读 + 合并:一次 read_multiple,减少 ADU 次数
  5. DUT 侧减负:关 SNMP 轮询、关 Web 并发管理面,保留数据面
  6. 缓冲与补传:边缘侧 SQLite/ringbuf 缓存,断网恢复后回填 InfluxDB
  7. 容量规划:单网关管理上限建议 ≤100 端点;超此分片。别让中心平台直连上千记录仪

八、把数据接回平台

  • 网关侧:Modbus TCP 采集 → 归一化 JSON → MQTT(TLS) → 腾讯云 EMQX/Mosquitto
  • 时序库:InfluxDB v2/v3,line protocol env_sensor,sensor=...,loc=... temperature=23.6,humidity=45.2
  • Grafana 面板 + 告警规则;补采缺口用 fill(previous) 或线性插值展示,别掩盖告警判定
  • 指标埋点:poll_success_total, poll_latency_seconds, modbus_reconnects_total

九、遗留与下一步

  • 未覆盖:跨城公网/4G 场景,TCP 长连接保活与 NAT 超时(建议 MQTT 上送替代中心拉取)
  • 未覆盖:DUT 固件差异大,量产部署前需在实验室做 72h soak test
  • 下一步:《边缘网关分片采集框架:从 Modbus 轮询到 MQTT 上送的容量规划与降级设计》

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • UDP 还是 TCP?以太网温湿度记录仪在高并发轮询下的丢包率基准测试
    • 一、为什么要做这次基准测试
    • 二、被测对象与测试环境
      • 2.1 被测设备(DUT)
      • 2.2 采集/压测端
      • 2.3 网络路径
    • 三、测试方法与指标定义
      • 3.1 轮询模型
      • 3.2 关键指标
      • 3.3 测试矩阵
    • 四、实现要点(Python)
      • 4.1 TCP 轮询(异步)
      • 4.2 UDP 轮询(自研报文,避免库抽象)
      • 4.3 指标采集
    • 五、测试结果
      • 5.1 物理 DUT,N=3,单网关近端
      • 5.2 模拟管线,200 逻辑端点
      • 5.3 DUT 固件边界(现场最重要)
    • 六、Wireshark 排错要点
    • 七、工程选型结论
      • 7.1 默认选 TCP Modbus TCP,理由
      • 7.2 何时考虑 UDP
      • 7.3 高并发轮询的工程化做法(比选协议更重要)
    • 八、把数据接回平台
    • 九、遗留与下一步
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档