)
Python asyncio 高并发采集同时轮询数百台 POE 供电的以太网温湿度变送器Modbus TCP/IP添加图片注释不超过 140 字可选物联网 #Modbus #TCP/IP #UDP #POE供电 #Wireshark #Python #InfluxDB #以太网温湿度传感器 #网口温湿度变送器 #机房监控前面把双供电切换、Wireshark 排障、PoE 功率分配、浪涌防护、EMC 电磁兼容、多协议固件开发全部讲透了。这一篇回到采集端——当你面对的不是几台、几十台而是数百台以太网温湿度变送器分布在多个库房、多个楼层、多个园区每台都在跑 Modbus TCP你该怎么写采集程序先说一个很多人踩过的坑用同步 pymodbus 写了一个 for 循环依次轮询 200 台设备每台超时 3 秒。 跑起来发现一轮轮询下来要 10 分钟数据还没入库下一轮又开始了。 改成多线程ThreadPoolExecutor(200)结果200 个线程同时建 TCP 连接交换机 MAC 表被打爆一半设备 TCP 握手超时日志里全是 ConnectionResetError。 再改成 asyncio以为万事大吉——结果 pymodbus 的异步客户端在 3.x 版本里连接管理有坑并发一高就报 ModbusIOException还不容易复现。一、为什么 asyncio 是高并发采集的正确选择1. 三种并发模型对比模型并发方式连接数上限上下文切换适用场景同步串行单线程逐个轮询1无设备 10 台多线程每个设备一个线程~200OS 限制高内核调度设备 100 台但线程切换开销大多进程每个进程独立CPU 核数 × N极高不推荐用于 I/O 密集asyncio单线程事件循环协作式调度数千极低用户态设备数百~数千台核心优势asyncio 是单线程的不存在线程切换开销不存在 GIL 争抢不存在锁竞争。所有 I/O 操作TCP 读写在等待时让出控制权事件循环调度其他任务。一台设备等待响应的 50ms 里可以切换去处理另外几十台设备的请求。2. 但 pymodbus 的 asyncio 支持有坑pymodbus 版本asyncio 支持问题2.x有 AsyncModbusTcpClient较稳定但已停止维护3.0 – 3.3重构了 asyncio 实现连接池、重连逻辑有 bug3.4逐步修复需要仔细验证本文基于 pymodbus 3.6并给出绕过坑点的写法。二、架构设计数百台设备的采集拓扑1. 逻辑分层┌─────────────────────────────────────────────────────────────┐ │ 采集服务单进程 asyncio │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 设备管理器DeviceManager │ │ │ │ · 维护设备列表IP、端口、从站地址、采集周期 │ │ │ │ · 按区域/库房分组 │ │ │ │ · 健康状态跟踪在线/离线/响应时间 │ │ │ └───────────────────┬─────────────────────────────────┘ │ │ │ │ │ ┌───────────────────▼─────────────────────────────────┐ │ │ │ 连接池ConnectionPool │ │ │ │ · 每台设备一个持久 TCP 连接或按需创建 │ │ │ │ · 连接健康检测心跳/超时 │ │ │ │ · 自动重连指数退避 │ │ │ │ · 并发限制Semaphore 控制同时活跃请求数 │ │ │ └───────────────────┬─────────────────────────────────┘ │ │ │ │ │ ┌───────────────────▼─────────────────────────────────┐ │ │ │ 采集调度器Scheduler │ │ │ │ · 按设备采集周期调度非阻塞 │ │ │ │ · 错峰采集避免同时发起所有请求 │ │ │ │ · 优先级关键库房优先 │ │ │ └───────────────────┬─────────────────────────────────┘ │ │ │ │ │ ┌───────────────────▼─────────────────────────────────┐ │ │ │ 数据管道DataPipeline │ │ │ │ · 质量位标记good/bad/timeout │ │ │ │ · 变化过滤deadband │ │ │ │ · 批量写入 InfluxDB / 转发 Kafka │ │ │ └─────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘2. 关键设计决策决策点选择理由连接模型每设备一个长连接Modbus TCP 无会话开销长连接避免握手延迟并发度Semaphore 限制 ~100 并发请求避免交换机/设备 TCP 栈过载采集调度错峰 随机抖动避免 200 台同时发起请求超时处理单次超时 2s连续 3 次失败标记离线快速失败不阻塞其他设备重连策略指数退避 1s/2s/4s/8s上限 60s避免重连风暴数据写入批量异步写入 InfluxDB减少 I/O 次数三、核心代码实现1. 连接池带健康检测import asyncio import time from dataclasses import dataclass, field from typing import Dict, Optional from pymodbus.client import AsyncModbusTcpClient from pymodbus.exceptions import ModbusIOException, ConnectionException dataclass class DeviceConfig: host: str port: int 502 slave_id: int 1 poll_interval: float 5.0 timeout: float 2.0 retry_limit: int 3 dataclass class DeviceState: client: Optional[AsyncModbusTcpClient] None connected: bool False last_poll: float 0.0 last_success: float 0.0 consecutive_failures: int 0 response_times: list field(default_factorylist) quality: str unknown class ConnectionPool: def __init__(self, max_concurrent: int 100): self.devices: Dict[str, DeviceState] {} self.configs: Dict[str, DeviceConfig] {} self.semaphore asyncio.Semaphore(max_concurrent) self._lock asyncio.Lock() def add_device(self, name: str, config: DeviceConfig): self.configs[name] config self.devices[name] DeviceState() async def get_client(self, name: str) - Optional[AsyncModbusTcpClient]: 获取或创建设备的 Modbus TCP 客户端 state self.devices[name] config self.configs[name] if state.client is not None and state.connected: return state.client # 需要新建连接 async with self._lock: # 双重检查 if state.client is not None and state.connected: return state.client # 关闭旧连接 if state.client is not None: try: state.client.close() except Exception: pass state.client AsyncModbusTcpClient( config.host, portconfig.port, timeoutconfig.timeout, retries1, retry_on_emptyTrue, ) try: await state.client.connect() state.connected state.client.connected if state.connected: state.consecutive_failures 0 return state.client else: state.connected False return None except Exception as e: state.connected False return None async def release(self, name: str): 释放信号量在请求完成后调用 pass # 信号量在 poll_device 中管理 async def close_all(self): 关闭所有连接 for name, state in self.devices.items(): if state.client is not None: try: state.client.close() except Exception: pass state.client None state.connected False2. 采集调度器错峰 并发控制class Scheduler: def __init__(self, pool: ConnectionPool, influx_writerNone): self.pool pool self.influx influx_writer self.running False self._tasks: Dict[str, asyncio.Task] {} async def start(self): 启动所有设备的采集任务 self.running True for name in self.pool.configs.keys(): self._tasks[name] asyncio.create_task( self._device_loop(name) ) # 等待所有任务 await asyncio.gather(*self._tasks.values(), return_exceptionsTrue) async def stop(self): 停止所有采集任务 self.running False for task in self._tasks.values(): task.cancel() await asyncio.gather(*self._tasks.values(), return_exceptionsTrue) await self.pool.close_all() async def _device_loop(self, name: str): 单个设备的采集循环 config self.pool.configs[name] state self.pool.devices[name] # 错峰启动随机抖动 0-5 秒 await asyncio.sleep(hash(name) % 5) while self.running: try: await self._poll_device(name) except asyncio.CancelledError: break except Exception as e: # 记录异常但不退出循环 pass # 等待下一个采集周期 await asyncio.sleep(config.poll_interval) async def _poll_device(self, name: str): 执行单次采集 config self.pool.configs[name] state self.pool.devices[name] async with self.pool.semaphore: # 限制并发数 client await self.pool.get_client(name) if client is None or not client.connected: state.quality bad state.consecutive_failures 1 return t0 time.monotonic() try: # 读取保持寄存器 40001-40002温度、湿度 resp await asyncio.wait_for( client.read_holding_registers( address0, count2, slaveconfig.slave_id ), timeoutconfig.timeout ) elapsed (time.monotonic() - t0) * 1000 # ms if resp is None or resp.isError(): raise ModbusIOException(fBad response: {resp}) # 解析数据 temp resp.registers[0] * 0.1 humid resp.registers[1] * 0.1 # 更新状态 state.last_poll time.time() state.last_success time.time() state.consecutive_failures 0 state.response_times.append(elapsed) if len(state.response_times) 100: state.response_times.pop(0) state.quality good # 写入 InfluxDB if self.influx: await self.influx.write_point( measurementtemperature_humidity, tags{device: name, host: config.host}, fields{ temperature: temp, humidity: humid, response_ms: elapsed, quality: 0, } ) except asyncio.TimeoutError: state.consecutive_failures 1 state.quality bad except (ModbusIOException, ConnectionException, OSError) as e: state.consecutive_failures 1 state.quality bad # 连接可能已断开标记重连 state.connected False try: client.close() except Exception: pass state.client None3. InfluxDB 异步写入from influxdb_client.client.influxdb_client_async import InfluxDBClientAsync class AsyncInfluxWriter: def __init__(self, url: str, token: str, org: str, bucket: str): self.client InfluxDBClientAsync(urlurl, tokentoken, orgorg) self.bucket bucket self._queue asyncio.Queue(maxsize10000) self._task None async def start(self): self._task asyncio.create_task(self._flush_loop()) async def stop(self): if self._task: self._task.cancel() await asyncio.gather(self._task, return_exceptionsTrue) await self.client.close() async def write_point(self, measurement: str, tags: dict, fields: dict): 非阻塞写入队列 point { measurement: measurement, tags: tags, fields: fields, time: int(time.time() * 1e9), } try: self._queue.put_nowait(point) except asyncio.QueueFull: # 队列满丢弃最旧的数据 try: self._queue.get_nowait() except asyncio.QueueEmpty: pass self._queue.put_nowait(point) async def _flush_loop(self): 批量写入 InfluxDB batch [] last_flush time.monotonic() while True: try: # 等待数据超时则刷新 point await asyncio.wait_for(self._queue.get(), timeout1.0) batch.append(point) # 批量条件达到 500 条或 5 秒 if len(batch) 500 or (time.monotonic() - last_flush) 5.0: await self._flush(batch) batch.clear() last_flush time.monotonic() except asyncio.TimeoutError: if batch: await self._flush(batch) batch.clear() last_flush time.monotonic() except asyncio.CancelledError: if batch: await self._flush(batch) break async def _flush(self, points: list): 批量写入 InfluxDB from influxdb_client import Point influx_points [] for p in points: pt Point(p[measurement]).time(p[time]) for k, v in p[tags].items(): pt pt.tag(k, v) for k, v in p[fields].items(): pt pt.field(k, v) influx_points.append(pt) try: await self.client.write_api().write( bucketself.bucket, recordinflux_points ) except Exception as e: # 写入失败记录日志不重试避免阻塞 pass4. 主程序async def main(): # 从配置文件加载设备列表 devices load_devices_from_config(devices.yaml) # 创建连接池 pool ConnectionPool(max_concurrent100) # 添加设备 for name, cfg in devices.items(): pool.add_device(name, DeviceConfig(**cfg)) # 创建 InfluxDB 写入器 influx AsyncInfluxWriter( urlhttp://localhost:8086, tokenyour-token, orgarchive, bucketenv_monitor ) await influx.start() # 创建调度器 scheduler Scheduler(pool, influx) try: await scheduler.start() except KeyboardInterrupt: pass finally: await scheduler.stop() await influx.stop() if __name__ __main__: asyncio.run(main())四、性能优化要点1. 并发度调优参数建议值依据max_concurrentSemaphore50–150取决于交换机 MAC 表大小、设备 TCP 栈深度单设备采集周期5–60s温湿度变化慢5s 足够连接超时2s现场网络 RTT 1ms2s 足够区分故障批量写入大小500 条/批InfluxDB 推荐批量写入2. 错峰策略# 方案 1随机抖动 await asyncio.sleep(random.uniform(0, 5)) # 方案 2按设备哈希均匀分布 offset (hash(name) % 100) / 100 * poll_interval await asyncio.sleep(offset) # 方案 3按区域分批 # 区域 A 设备第 0-2 秒 # 区域 B 设备第 2-4 秒 # 区域 C 设备第 4-6 秒3. 连接复用 vs 按需创建策略优点缺点长连接复用无握手延迟响应快占用交换机端口表设备重启后连接失效按需创建资源占用少每次握手 ~1ms高并发时累积延迟混合推荐长连接 健康检查 自动重连实现稍复杂五、常见返工点问题后果正确做法同步 for 循环轮询采集周期过长用 asyncio 并发无限并发无 Semaphore交换机/设备过载限制并发数不处理连接断开采集静默失败检测断开标记离线触发重连不限制队列大小内存暴涨队列满时丢弃旧数据不批量写入 InfluxDBI/O 瓶颈批量异步写入不记录响应时间无法定位慢设备记录 RTT用于性能分析不设置超时单设备卡死阻塞全局每次请求设超时不处理 CancelledError任务取消时资源泄漏捕获并清理资源六、一句话总结数百台设备的并发采集核心不是能同时连多少台而是如何优雅地管理连接生命周期、控制并发度、处理故障、批量写入。 asyncio 提供了正确的并发模型但 pymodbus 的坑需要你绕过去——连接池、信号量、指数退避、批量写入这四件事做好了200 台设备的采集周期可以稳定在 5 秒以内。要不要下一篇直接出 《以太网温湿度传感器 asyncio 高并发采集框架部署实施版》内容可以直接包含完整代码仓库结构、Docker Compose 部署、Prometheus 监控指标、Grafana 面板、设备配置 YAML 模板、性能压测报告模板可直接用于现场部署。Python asyncio 高并发采集同时轮询数百台 POE 供电的以太网温湿度变送器Modbus TCP/IP物联网 #Modbus #TCP/IP #UDP #POE供电 #腾讯云 #Wireshark #Python #InfluxDB #以太网温湿度传感器 #网口温湿度变送器 #机房监控前面把双供电切换、Wireshark 排障、PoE 功率分配、浪涌防护、EMC 电磁兼容、多协议固件开发全部讲透了。这一篇回到采集端——当你面对的不是几台、几十台而是数百台以太网温湿度变送器分布在多个库房、多个楼层、多个园区每台都在跑 Modbus TCP你该怎么写采集程序先说一个很多人踩过的坑用同步 pymodbus 写了一个 for 循环依次轮询 200 台设备每台超时 3 秒。 跑起来发现一轮轮询下来要 10 分钟数据还没入库下一轮又开始了。 改成多线程ThreadPoolExecutor(200)结果200 个线程同时建 TCP 连接交换机 MAC 表被打爆一半设备 TCP 握手超时日志里全是 ConnectionResetError。 再改成 asyncio以为万事大吉——结果 pymodbus 的异步客户端在 3.x 版本里连接管理有坑并发一高就报 ModbusIOException还不容易复现。一、为什么 asyncio 是高并发采集的正确选择1. 三种并发模型对比模型并发方式连接数上限上下文切换适用场景同步串行单线程逐个轮询1无设备 10 台多线程每个设备一个线程~200OS 限制高内核调度设备 100 台但线程切换开销大多进程每个进程独立CPU 核数 × N极高不推荐用于 I/O 密集asyncio单线程事件循环协作式调度数千极低用户态设备数百~数千台核心优势asyncio 是单线程的不存在线程切换开销不存在 GIL 争抢不存在锁竞争。所有 I/O 操作TCP 读写在等待时让出控制权事件循环调度其他任务。一台设备等待响应的 50ms 里可以切换去处理另外几十台设备的请求。2. 但 pymodbus 的 asyncio 支持有坑pymodbus 版本asyncio 支持问题2.x有 AsyncModbusTcpClient较稳定但已停止维护3.0 – 3.3重构了 asyncio 实现连接池、重连逻辑有 bug3.4逐步修复需要仔细验证本文基于 pymodbus 3.6并给出绕过坑点的写法。二、架构设计数百台设备的采集拓扑1. 逻辑分层┌─────────────────────────────────────────────────────────────┐ │ 采集服务单进程 asyncio │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 设备管理器DeviceManager │ │ │ │ · 维护设备列表IP、端口、从站地址、采集周期 │ │ │ │ · 按区域/库房分组 │ │ │ │ · 健康状态跟踪在线/离线/响应时间 │ │ │ └───────────────────┬─────────────────────────────────┘ │ │ │ │ │ ┌───────────────────▼─────────────────────────────────┐ │ │ │ 连接池ConnectionPool │ │ │ │ · 每台设备一个持久 TCP 连接或按需创建 │ │ │ │ · 连接健康检测心跳/超时 │ │ │ │ · 自动重连指数退避 │ │ │ │ · 并发限制Semaphore 控制同时活跃请求数 │ │ │ └───────────────────┬─────────────────────────────────┘ │ │ │ │ │ ┌───────────────────▼─────────────────────────────────┐ │ │ │ 采集调度器Scheduler │ │ │ │ · 按设备采集周期调度非阻塞 │ │ │ │ · 错峰采集避免同时发起所有请求 │ │ │ │ · 优先级关键库房优先 │ │ │ └───────────────────┬─────────────────────────────────┘ │ │ │ │ │ ┌───────────────────▼─────────────────────────────────┐ │ │ │ 数据管道DataPipeline │ │ │ │ · 质量位标记good/bad/timeout │ │ │ │ · 变化过滤deadband │ │ │ │ · 批量写入 InfluxDB / 转发 Kafka │ │ │ └─────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘2. 关键设计决策决策点选择理由连接模型每设备一个长连接Modbus TCP 无会话开销长连接避免握手延迟并发度Semaphore 限制 ~100 并发请求避免交换机/设备 TCP 栈过载采集调度错峰 随机抖动避免 200 台同时发起请求超时处理单次超时 2s连续 3 次失败标记离线快速失败不阻塞其他设备重连策略指数退避 1s/2s/4s/8s上限 60s避免重连风暴数据写入批量异步写入 InfluxDB减少 I/O 次数三、核心代码实现1. 连接池带健康检测import asyncio import time from dataclasses import dataclass, field from typing import Dict, Optional from pymodbus.client import AsyncModbusTcpClient from pymodbus.exceptions import ModbusIOException, ConnectionException dataclass class DeviceConfig: host: str port: int 502 slave_id: int 1 poll_interval: float 5.0 timeout: float 2.0 retry_limit: int 3 dataclass class DeviceState: client: Optional[AsyncModbusTcpClient] None connected: bool False last_poll: float 0.0 last_success: float 0.0 consecutive_failures: int 0 response_times: list field(default_factorylist) quality: str unknown class ConnectionPool: def __init__(self, max_concurrent: int 100): self.devices: Dict[str, DeviceState] {} self.configs: Dict[str, DeviceConfig] {} self.semaphore asyncio.Semaphore(max_concurrent) self._lock asyncio.Lock() def add_device(self, name: str, config: DeviceConfig): self.configs[name] config self.devices[name] DeviceState() async def get_client(self, name: str) - Optional[AsyncModbusTcpClient]: 获取或创建设备的 Modbus TCP 客户端 state self.devices[name] config self.configs[name] if state.client is not None and state.connected: return state.client # 需要新建连接 async with self._lock: # 双重检查 if state.client is not None and state.connected: return state.client # 关闭旧连接 if state.client is not None: try: state.client.close() except Exception: pass state.client AsyncModbusTcpClient( config.host, portconfig.port, timeoutconfig.timeout, retries1, retry_on_emptyTrue, ) try: await state.client.connect() state.connected state.client.connected if state.connected: state.consecutive_failures 0 return state.client else: state.connected False return None except Exception as e: state.connected False return None async def release(self, name: str): 释放信号量在请求完成后调用 pass # 信号量在 poll_device 中管理 async def close_all(self): 关闭所有连接 for name, state in self.devices.items(): if state.client is not None: try: state.client.close() except Exception: pass state.client None state.connected False2. 采集调度器错峰 并发控制class Scheduler: def __init__(self, pool: ConnectionPool, influx_writerNone): self.pool pool self.influx influx_writer self.running False self._tasks: Dict[str, asyncio.Task] {} async def start(self): 启动所有设备的采集任务 self.running True for name in self.pool.configs.keys(): self._tasks[name] asyncio.create_task( self._device_loop(name) ) # 等待所有任务 await asyncio.gather(*self._tasks.values(), return_exceptionsTrue) async def stop(self): 停止所有采集任务 self.running False for task in self._tasks.values(): task.cancel() await asyncio.gather(*self._tasks.values(), return_exceptionsTrue) await self.pool.close_all() async def _device_loop(self, name: str): 单个设备的采集循环 config self.pool.configs[name] state self.pool.devices[name] # 错峰启动随机抖动 0-5 秒 await asyncio.sleep(hash(name) % 5) while self.running: try: await self._poll_device(name) except asyncio.CancelledError: break except Exception as e: # 记录异常但不退出循环 pass # 等待下一个采集周期 await asyncio.sleep(config.poll_interval) async def _poll_device(self, name: str): 执行单次采集 config self.pool.configs[name] state self.pool.devices[name] async with self.pool.semaphore: # 限制并发数 client await self.pool.get_client(name) if client is None or not client.connected: state.quality bad state.consecutive_failures 1 return t0 time.monotonic() try: # 读取保持寄存器 40001-40002温度、湿度 resp await asyncio.wait_for( client.read_holding_registers( address0, count2, slaveconfig.slave_id ), timeoutconfig.timeout ) elapsed (time.monotonic() - t0) * 1000 # ms if resp is None or resp.isError(): raise ModbusIOException(fBad response: {resp}) # 解析数据 temp resp.registers[0] * 0.1 humid resp.registers[1] * 0.1 # 更新状态 state.last_poll time.time() state.last_success time.time() state.consecutive_failures 0 state.response_times.append(elapsed) if len(state.response_times) 100: state.response_times.pop(0) state.quality good # 写入 InfluxDB if self.influx: await self.influx.write_point( measurementtemperature_humidity, tags{device: name, host: config.host}, fields{ temperature: temp, humidity: humid, response_ms: elapsed, quality: 0, } ) except asyncio.TimeoutError: state.consecutive_failures 1 state.quality bad except (ModbusIOException, ConnectionException, OSError) as e: state.consecutive_failures 1 state.quality bad # 连接可能已断开标记重连 state.connected False try: client.close() except Exception: pass state.client None3. InfluxDB 异步写入from influxdb_client.client.influxdb_client_async import InfluxDBClientAsync class AsyncInfluxWriter: def __init__(self, url: str, token: str, org: str, bucket: str): self.client InfluxDBClientAsync(urlurl, tokentoken, orgorg) self.bucket bucket self._queue asyncio.Queue(maxsize10000) self._task None async def start(self): self._task asyncio.create_task(self._flush_loop()) async def stop(self): if self._task: self._task.cancel() await asyncio.gather(self._task, return_exceptionsTrue) await self.client.close() async def write_point(self, measurement: str, tags: dict, fields: dict): 非阻塞写入队列 point { measurement: measurement, tags: tags, fields: fields, time: int(time.time() * 1e9), } try: self._queue.put_nowait(point) except asyncio.QueueFull: # 队列满丢弃最旧的数据 try: self._queue.get_nowait() except asyncio.QueueEmpty: pass self._queue.put_nowait(point) async def _flush_loop(self): 批量写入 InfluxDB batch [] last_flush time.monotonic() while True: try: # 等待数据超时则刷新 point await asyncio.wait_for(self._queue.get(), timeout1.0) batch.append(point) # 批量条件达到 500 条或 5 秒 if len(batch) 500 or (time.monotonic() - last_flush) 5.0: await self._flush(batch) batch.clear() last_flush time.monotonic() except asyncio.TimeoutError: if batch: await self._flush(batch) batch.clear() last_flush time.monotonic() except asyncio.CancelledError: if batch: await self._flush(batch) break async def _flush(self, points: list): 批量写入 InfluxDB from influxdb_client import Point influx_points [] for p in points: pt Point(p[measurement]).time(p[time]) for k, v in p[tags].items(): pt pt.tag(k, v) for k, v in p[fields].items(): pt pt.field(k, v) influx_points.append(pt) try: await self.client.write_api().write( bucketself.bucket, recordinflux_points ) except Exception as e: # 写入失败记录日志不重试避免阻塞 pass4. 主程序async def main(): # 从配置文件加载设备列表 devices load_devices_from_config(devices.yaml) # 创建连接池 pool ConnectionPool(max_concurrent100) # 添加设备 for name, cfg in devices.items(): pool.add_device(name, DeviceConfig(**cfg)) # 创建 InfluxDB 写入器 influx AsyncInfluxWriter( urlhttp://localhost:8086, tokenyour-token, orgarchive, bucketenv_monitor ) await influx.start() # 创建调度器 scheduler Scheduler(pool, influx) try: await scheduler.start() except KeyboardInterrupt: pass finally: await scheduler.stop() await influx.stop() if __name__ __main__: asyncio.run(main())四、性能优化要点1. 并发度调优参数建议值依据max_concurrentSemaphore50–150取决于交换机 MAC 表大小、设备 TCP 栈深度单设备采集周期5–60s温湿度变化慢5s 足够连接超时2s现场网络 RTT 1ms2s 足够区分故障批量写入大小500 条/批InfluxDB 推荐批量写入2. 错峰策略# 方案 1随机抖动 await asyncio.sleep(random.uniform(0, 5)) # 方案 2按设备哈希均匀分布 offset (hash(name) % 100) / 100 * poll_interval await asyncio.sleep(offset) # 方案 3按区域分批 # 区域 A 设备第 0-2 秒 # 区域 B 设备第 2-4 秒 # 区域 C 设备第 4-6 秒3. 连接复用 vs 按需创建策略优点缺点长连接复用无握手延迟响应快占用交换机端口表设备重启后连接失效按需创建资源占用少每次握手 ~1ms高并发时累积延迟混合推荐长连接 健康检查 自动重连实现稍复杂五、常见返工点问题后果正确做法同步 for 循环轮询采集周期过长用 asyncio 并发无限并发无 Semaphore交换机/设备过载限制并发数不处理连接断开采集静默失败检测断开标记离线触发重连不限制队列大小内存暴涨队列满时丢弃旧数据不批量写入 InfluxDBI/O 瓶颈批量异步写入不记录响应时间无法定位慢设备记录 RTT用于性能分析不设置超时单设备卡死阻塞全局每次请求设超时不处理 CancelledError任务取消时资源泄漏捕获并清理资源六、一句话总结数百台设备的并发采集核心不是能同时连多少台而是如何优雅地管理连接生命周期、控制并发度、处理故障、批量写入。 asyncio 提供了正确的并发模型但 pymodbus 的坑需要你绕过去——连接池、信号量、指数退避、批量写入这四件事做好了200 台设备的采集周期可以稳定在 5 秒以内。