3. 实时数据采集模块开发
实时数据采集模块是SCADA能源管理系统与现场设备之间的“神经末梢”。它负责周期性地从PLC、电表、传感器等设备读取寄存器或点位数据,将原始值转换为工程量,再交给上层做实时展示、报警判断和历史存储。本节围绕一个可运行的采集服务,拆解多线程轮询、断线重连、数据缓存与异常处理四个核心机制,并给出Python与Java两种实现思路的对照。
一、采集服务的整体结构
一个典型的采集服务由四层组成:设备连接层负责建立Modbus TCP、IEC 104或OPC UA会话;轮询调度层按点位表分组、按周期触发读取;数据处理层完成原始值到工程量的换算、质量戳标记;输出层把数据推入内存队列,供入库线程和实时推送线程消费。采集线程与入库线程必须解耦,否则数据库抖动会直接拖慢轮询节奏。
二、多线程轮询:分组与周期设计
轮询不是“一个设备一个线程”那么简单。若现场有200台电表,每台开一个线程会导致线程数爆炸、上下文切换开销大。更合理的做法是按通信通道分组:同一串口或同一网关下的设备共享一个采集线程,线程内顺序读取各设备;不同通道之间并行。这样线程数等于通道数,通常控制在8~32个。
每个采集线程维护一个点位分组表,按周期分档。例如:
# 点位分组示例(Python)
POLL_GROUPS = [
{"name": "fast", "period": 1.0, "points": ["P_Total", "U_A", "I_A"]},
{"name": "normal", "period": 5.0, "points": ["E_Day", "PF", "Freq"]},
{"name": "slow", "period": 30.0, "points": ["E_Month", "Temp"]},
]
线程主循环用time.monotonic()记录上次执行时间,每次循环计算各分组是否到期。到期则调用read_points()批量读取,未到期则sleep一个短间隔(如0.05秒)。批量读取比逐点读取效率高得多,Modbus一次可读125个寄存器,应尽量合并连续地址。
Java侧通常用ScheduledExecutorService配合scheduleAtFixedRate,但要注意任务执行时间超过周期时会产生堆积。更稳妥的是用scheduleWithFixedDelay,或在线程内自行计算下次触发时刻,避免任务重叠。
三、断线重连:指数退避与状态恢复
现场网络抖动、设备重启、网关掉线都会导致连接中断。采集服务必须能自动恢复,且不能疯狂重试把设备打挂。推荐指数退避策略:首次失败后等待1秒,第二次2秒,第三次4秒,依次翻倍,上限30秒;连接成功后重置退避计数。
# 断线重连核心逻辑(Python伪代码)
backoff = 1
while not connected:
try:
conn = connect(ip, port, timeout=3)
connected = True
backoff = 1
except Exception as e:
log.warning("connect failed: %s, retry in %ds", e, backoff)
time.sleep(backoff)
backoff = min(backoff * 2, 30)
重连成功后不能直接继续轮询,还要做状态恢复:清空该通道的待读队列,重新建立会话,必要时补读断线期间的关键累计量(如电量)。对于Modbus,重连后建议先读一次设备标识或状态寄存器,确认设备真正就绪,再进入正常轮询。
四、数据缓存:环形缓冲与背压
采集速度与入库速度不匹配时,必须有缓存。最简单的是queue.Queue(maxsize=10000),采集线程put_nowait,入库线程get。当队列满时,采集线程不能阻塞,否则会拖慢轮询;应丢弃最旧数据或直接丢弃当前点并记录丢弃计数。
更精细的做法是环形缓冲:固定长度数组,写指针循环覆盖。它内存可控、无锁或轻量锁即可实现,适合高频点位。缓存中每条记录至少包含:点位ID、时间戳、原始值、工程量、质量戳。质量戳用枚举表示:Good、Bad、Uncertain,断线期间产生的数据标记为Bad,避免污染历史曲线。
五、异常处理:分类与质量戳
采集异常分三类:通信异常(超时、连接拒绝、校验错)、协议异常(非法地址、非法功能码)、数据异常(超出量程、跳变过大)。三类异常的处理方式不同:通信异常触发重连;协议异常记录错误并跳过该点,不重连;数据异常保留原始值但标记Uncertain,供上层判断。
| 异常类型 | 典型原因 | 处理动作 | 质量戳 |
|---|---|---|---|
| 通信超时 | 网络中断、设备无响应 | 重连 + 退避 | Bad |
| 校验错误 | 电磁干扰、线路质量差 | 重试1次,仍失败则重连 | Bad |
| 非法地址 | 点位表配置错误 | 记录错误,跳过该点 | Bad |
| 超量程 | 传感器故障、量程设置错 | 保留值,告警 | Uncertain |
异常处理必须可观测:每个通道维护成功次数、失败次数、最后错误信息、最后成功时间。这些指标暴露给监控页面,运维人员一眼就能看出哪条通道在反复重连。日志中要避免每周期打印正常数据,只记录状态变化和异常,否则日志会被淹没。
六、Python与Java实现对照
Python适合快速原型和中小规模采集,用threading加pymodbus即可在半天内跑通。但GIL限制了多核并行,高并发场景建议用asyncio或multiprocessing。Java适合大规模、长周期运行,ScheduledExecutorService加j2mod或Eclipse Milo(OPC UA)更稳健,线程模型和异常体系更完善。两者在架构上应保持一致:分组轮询、退避重连、环形缓存、质量戳,这样上层业务代码无需关心采集端语言。
把上述机制落地后,采集模块就具备了工业现场所需的韧性。下一阶段的数据存储与报警判断,都建立在这些带质量戳的实时数据之上。