
关键词:UDP 数据传输、RJ45 温湿度记录仪、机房环境实时采集、丢包实测、PoE 供电、高并发采集、网络抖动、数据完整性、边缘缓存、InfluxDB 写入 标签:#物联网 #Modbus #TCP/IP #UDP #POE供电 #Wireshark #Python #InfluxDB #以太网温湿度传感器 #网口温湿度变送器 #机房监控 #边缘计算
前几篇覆盖了 TCP/UDP 双模式架构和丢包排查。本篇把视角拉回单一 UDP 模式下的实测——在真实机房环境中,RJ45 温湿度记录仪(PoE 供电、网口上报)纯 UDP 方案到底能不能用?能用到什么规模?丢包率在什么水平?
选择 UDP 的动机:
但 UDP 的代价是明确的:不保证送达。所以实测的核心问题是——在你的网络环境下,丢包率是多少?丢包对业务的影响是否可接受?
┌─────────────────────┐
│ 核心交换机 │
│ (24-port GbE) │
└──────────┬──────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
┌────────▼────────┐ ┌────────▼────────┐ ┌────────▼────────┐
│ 接入交换机 A │ │ 接入交换机 B │ │ 接入交换机 C │
│ (PoE, 24-port) │ │ (PoE, 24-port) │ │ (PoE, 24-port) │
└───┬───┬───┬────┘ └───┬───┬───┬────┘ └───┬───┬───┬────┘
│ │ │ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼
[记录仪×8] [记录仪×8] [记录仪×8]
RJ45/PoE RJ45/PoE RJ45/PoE参数 | 规格 |
|---|---|
设备型号 | RJ45 网口温湿度记录仪(PoE 供电) |
网络接口 | 10/100M 自适应,RJ45 |
供电 | IEEE 802.3af PoE(Class 1,约 3.5W) |
上报协议 | UDP 主动上报,目标端口 9000 |
上报周期 | 可配置:2s / 5s / 10s |
数据帧 | 自定义二进制帧,32 字节/帧 |
帧结构 | 头(2B) + 设备ID(2B) + 序号(2B) + 时标(4B) + T(2B) + H(2B) + 状态(1B) + CRC16(2B) + 保留(15B) |
采样精度 | 温度 ±0.3℃,湿度 ±2%RH |
本地存储 | 内置 Flash,约 10 万条记录(环形缓冲) |
参数 | 配置 |
|---|---|
硬件 | 边缘服务器(4C8G,千兆网卡) |
OS | Linux 5.10 |
采集进程 | Python asyncio,单进程 + SO_REUSEPORT |
接收缓冲 | SO_RCVBUF = 4MB |
内核参数 | rmem_max = 16MB |
后端存储 | InfluxDB 2.x,batch 写入(500 点/批,5s flush) |
测试项 | 设备数 | 上报周期 | 持续时长 | 关注指标 |
|---|---|---|---|---|
T1:基线 | 8 台 | 5s | 24h | 丢包率、CPU、内存 |
T2:密度 | 24 台 | 5s | 24h | 同上 + 交换机端口缓冲 |
T3:高频 | 24 台 | 2s | 12h | 同上 + 网络抖动 |
T4:压力 | 48 台 | 2s | 12h | 系统极限、丢包分布 |
T5:扰动 | 24 台 | 5s | 24h | 注入网络扰动后的恢复 |
扰动类型:
1. 交换机端口 flap:每 2h 随机 shutdown/ no shutdown 一个接入端口(持续 10s)
2. 广播风暴模拟:从测试端口发送广播包(100Mbps,持续 30s)
3. 链路拥塞:iperf3 UDP 打满接入交换机上联带宽(持续 1min)
4. PoE 断电模拟:断开接入交换机电源 30s(模拟市电闪断)"""
benchmark_collector.py - 实测数据采集与统计
"""
import asyncio
import time
import struct
import csv
from dataclasses import dataclass, field
from collections import defaultdict
@dataclass
class DeviceStats:
dev_id: str
expected_seq: int = 0
received: int = 0
lost: int = 0
out_of_order: int = 0
duplicates: int = 0
last_seq: int = 0
last_ts: float = 0.0
rtt_samples: list = field(default_factory=list)
first_seen: float = 0.0
last_seen: float = 0.0
class BenchmarkCollector:
def __init__(self, csv_path="benchmark_results.csv"):
self.devices: dict[str, DeviceStats] = {}
self.csv_path = csv_path
self.start_time = time.time()
self.total_rx = 0
self.total_lost = 0
self.packet_sizes = []
def parse_and_track(self, data: bytes, addr: tuple, recv_ts: float):
if len(data) < 32:
return None
# 解析帧
header, dev_id, seq, ts, temp_raw, hum_raw, status, crc = \
struct.unpack_from('>HHHIHHBH', data, 0)
if header != 0xAA55:
return None
dev_key = f"{addr[0]}:{dev_id}"
stats = self.devices.setdefault(dev_key, DeviceStats(dev_id=dev_key))
# 首次收到
if stats.received == 0:
stats.first_seen = recv_ts
stats.expected_seq = seq
# 序号跟踪
if seq == stats.last_seq:
stats.duplicates += 1
elif seq > stats.last_seq:
# 检查是否有丢失
gap = seq - stats.last_seq - 1
if gap > 0 and stats.last_seq > 0:
stats.lost += gap
self.total_lost += gap
stats.last_seq = seq
else:
# 乱序(序号回绕或重传)
stats.out_of_order += 1
stats.received += 1
self.total_rx += 1
stats.last_ts = recv_ts
# 时标偏差(设备时间 vs 接收时间)
device_time = ts # Unix epoch
clock_offset = recv_ts - device_time
return {
"dev_id": dev_key,
"seq": seq,
"temp": temp_raw / 10.0,
"hum": hum_raw / 10.0,
"status": status,
"clock_offset": clock_offset,
"recv_ts": recv_ts
}
def generate_report(self):
elapsed = time.time() - self.start_time
report = {
"elapsed_s": elapsed,
"total_devices": len(self.devices),
"total_rx": self.total_rx,
"total_lost": self.total_lost,
"overall_loss_rate": self.total_lost / max(1, self.total_rx + self.total_lost),
"devices": {}
}
for dev_id, stats in self.devices.items():
expected_total = stats.received + stats.lost
loss_rate = stats.lost / max(1, expected_total)
uptime = stats.last_seen - stats.first_seen if stats.first_seen > 0 else 0
report["devices"][dev_id] = {
"received": stats.received,
"lost": stats.lost,
"loss_rate": loss_rate,
"duplicates": stats.duplicates,
"out_of_order": stats.out_of_order,
"uptime_s": uptime,
"packets_per_sec": stats.received / max(1, uptime)
}
return report
def save_csv(self):
with open(self.csv_path, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["dev_id", "received", "lost", "loss_rate", "dup", "ooo", "pps"])
for dev_id, stats in self.devices.items():
uptime = stats.last_seen - stats.first_seen if stats.first_seen > 0 else 1
writer.writerow([
dev_id, stats.received, stats.lost,
stats.lost / max(1, stats.received + stats.lost),
stats.duplicates, stats.out_of_order,
stats.received / max(1, uptime)
])
# 运行
async def run_benchmark(duration_s=3600):
collector = BenchmarkCollector()
class Protocol:
def datagram_received(self, data, addr):
collector.parse_and_track(data, addr, time.time())
loop = asyncio.get_event_loop()
transport, _ = await loop.create_datagram_endpoint(
Protocol, local_addr=("0.0.0.0", 9000)
)
await asyncio.sleep(duration_s)
transport.close()
report = collector.generate_report()
collector.save_csv()
# 打印摘要
print(f"=== 实测报告({duration_s}s)===")
print(f"设备数: {report['total_devices']}")
print(f"总接收: {report['total_rx']}")
print(f"总丢失: {report['total_lost']}")
print(f"整体丢包率: {report['overall_loss_rate']*100:.4f}%")
for dev, stats in report["devices"].items():
print(f" {dev}: 丢包率={stats['loss_rate']*100:.4f}%, "
f"重复={stats['duplicates']}, 乱序={stats['out_of_order']}, "
f"速率={stats['packets_per_sec']:.2f}pps")测试 | 设备数 | 周期 | 时长 | 总发包 | 接收包 | 丢包数 | 丢包率 | 备注 |
|---|---|---|---|---|---|---|---|---|
T1 | 8 | 5s | 24h | 138,240 | 138,238 | 2 | 0.0014% | 2 个包在交换机维护窗口丢失 |
T2 | 24 | 5s | 24h | 414,720 | 414,701 | 19 | 0.0046% | 分布在 3 台设备上 |
T3 | 24 | 2s | 12h | 518,400 | 518,367 | 33 | 0.0064% | 高频下略高 |
T4 | 48 | 2s | 12h | 1,036,800 | 1,036,521 | 279 | 0.0269% | 开始出现微突发丢包 |
T5 | 24 | 5s | 24h | 414,720 | 414,103 | 617 | 0.149% | 含扰动注入 |
T1~T4 丢包特征:
- 丢包集中在特定时间段(非均匀分布)
- 与交换机 MAC 表老化刷新、STP 拓扑变化时间吻合
- 无单设备持续丢包,说明设备侧发送正常
- 丢包 burst 模式:连续丢失 2~5 个包,然后恢复正常
T5 扰动丢包细分:
- 端口 flap(10s):丢包 8~15 个/次,设备重连后恢复
- 广播风暴(30s):丢包 50~80 个,交换机广播抑制生效
- 链路拥塞(1min):丢包 20~40 个,QoS 未配置时 UDP 被挤
- PoE 断电(30s):丢包 = 断电期间全部(约 6 个/设备),恢复后正常指标 | T1 (8台) | T2 (24台) | T3 (24台/2s) | T4 (48台/2s) |
|---|---|---|---|---|
CPU (采集进程) | 2% | 5% | 12% | 22% |
内存 (RSS) | 45MB | 52MB | 58MB | 71MB |
网络 RX | 0.1 Mbps | 0.3 Mbps | 0.7 Mbps | 1.4 Mbps |
软中断 CPU | 1% | 3% | 7% | 15% |
InfluxDB 写入 | 1.6 点/s | 4.8 点/s | 12 点/s | 24 点/s |
结论:48 台 × 2s 周期时,系统资源消耗仍在安全范围内,但软中断占比开始升高,提示网卡多队列/RPS 可能需要优化。
抓包统计(T4 期间,端口 9000):
- 捕获包数:1,036,803(tcpdump 计数)
- 应用层接收:1,036,521
- 差值:282(tcpdump 比应用多,说明 libpcap 缓冲捕获了部分应用未读走的包)
- 内核丢弃(nstat UdpRcvbufErrors):277
- 吻合度:282 ≈ 277 + 少量时间差确认:丢包发生在内核套接字接收缓冲区溢出,而非网络传输中丢失。
T4 中 48 台 × 2s 周期,峰值 PPS 约 24(不算高),但仍有丢包。原因是:
优化:SO_RCVBUF 调至 8MB + 内核 rmem_max 调至 32MB,T4 丢包率从 0.027% 降至 0.003%。
T3 高频场景下,InfluxDB batch 写入偶尔超时(默认 10s),导致采集进程 asyncio 等待,间接增大接收延迟。
优化:
实测中发现部分设备运行 24h 后,时标偏差达 3~8s(无 NTP 同步)。UDP 帧中的设备时标不可靠。
对策:采集端以接收时间(recv_ts)为写入时间戳,设备时标仅作参考。对时序要求严格的场景,设备需支持 NTP 或 PTP。
T5 中 PoE 断电模拟暴露问题:部分记录仪在供电恢复后,UDP 上报序号从 0 重新开始,导致采集端误判为大量丢包。
对策:
dev_id + 时标 连续性,而非单纯看序号。 规模 | 设备数 | 上报周期 | 丢包率(优化后) | 结论 |
|---|---|---|---|---|
小型 | ≤24 | 5s | <0.005% | ✅ 完全可用 |
中型 | 24~48 | 5s | <0.01% | ✅ 可用,需调优 |
中型高频 | 24~48 | 2s | <0.02% | ⚠️ 可用,需监控 |
大型 | >48 | 5s | 需实测 | ❓ 建议 TCP 兜底或双模式 |
工具 | 用途 |
|---|---|
tcpdump + Wireshark | 抓包分析、丢包定位 |
nstat / netstat -su | 内核 UDP 丢弃计数 |
ss -lunpem | 套接字接收队列积压 |
iperf3 | 网络带宽压测 |
influx_inspect | InfluxDB 数据完整性校验 |
自研 benchmark_collector.py | 端到端丢包统计 |
交换机 CLI | 端口计数、PoE 状态、MAC 表 |
实测证明:在局域网、独立 VLAN、设备带本地存储的前提下,纯 UDP 方案在 50 台以内规模、5s 上报周期下丢包率 <0.01%,工程上完全可用。关键不在协议本身,而在网络基础设施质量、内核参数调优、应用层兜底设计三者配合。
如果规模更大或可靠性要求更高,回到上一篇的双模式方案——TCP 做基线,UDP 做高频,本地存储做兜底,三层保障。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。