3. 实时数据采集模块开发

实时数据采集模块是SCADA能源管理系统与现场设备之间的“神经末梢”。它负责周期性地从PLC、电表、传感器等设备读取寄存器或点位数据,将原始值转换为工程量,再交给上层做实时展示、报警判断和历史存储。本节围绕一个可运行的采集服务,拆解多线程轮询、断线重连、数据缓存与异常处理四个核心机制,并给出Python与Java两种实现思路的对照。

一、采集服务的整体结构

一个典型的采集服务由四层组成:设备连接层负责建立Modbus TCP、IEC 104或OPC UA会话;轮询调度层按点位表分组、按周期触发读取;数据处理层完成原始值到工程量的换算、质量戳标记;输出层把数据推入内存队列,供入库线程和实时推送线程消费。采集线程与入库线程必须解耦,否则数据库抖动会直接拖慢轮询节奏。

实时数据采集服务结构 设备连接层 Modbus / IEC104 轮询调度层 分组 / 周期触发 数据处理层 换算 / 质量戳 输出层 队列 / 推送 断线重连 指数退避 1s→2s→4s→8s 最大30s,重连后补读 数据缓存 环形缓冲 10000点 满时丢弃最旧,保最新 异常处理 超时/校验错/非法地址 标记质量戳 Bad 采集线程池 → 入库线程 / 实时推送线程 线程解耦,队列削峰,避免数据库抖动拖慢轮询

二、多线程轮询:分组与周期设计

轮询不是“一个设备一个线程”那么简单。若现场有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,避免污染历史曲线。

缓存不是越大越好。10000点约占用几MB内存,若现场有5000个点位、1秒周期,10秒就会写满。应根据“最大允许断线时长 × 点位频率”估算容量,并设置丢弃告警。

五、异常处理:分类与质量戳

采集异常分三类:通信异常(超时、连接拒绝、校验错)、协议异常(非法地址、非法功能码)、数据异常(超出量程、跳变过大)。三类异常的处理方式不同:通信异常触发重连;协议异常记录错误并跳过该点,不重连;数据异常保留原始值但标记Uncertain,供上层判断。

异常类型典型原因处理动作质量戳
通信超时网络中断、设备无响应重连 + 退避Bad
校验错误电磁干扰、线路质量差重试1次,仍失败则重连Bad
非法地址点位表配置错误记录错误,跳过该点Bad
超量程传感器故障、量程设置错保留值,告警Uncertain

异常处理必须可观测:每个通道维护成功次数、失败次数、最后错误信息、最后成功时间。这些指标暴露给监控页面,运维人员一眼就能看出哪条通道在反复重连。日志中要避免每周期打印正常数据,只记录状态变化和异常,否则日志会被淹没。

六、Python与Java实现对照

Python适合快速原型和中小规模采集,用threading加pymodbus即可在半天内跑通。但GIL限制了多核并行,高并发场景建议用asyncio或multiprocessing。Java适合大规模、长周期运行,ScheduledExecutorService加j2mod或Eclipse Milo(OPC UA)更稳健,线程模型和异常体系更完善。两者在架构上应保持一致:分组轮询、退避重连、环形缓存、质量戳,这样上层业务代码无需关心采集端语言。

采集服务的验收标准不是“能读到数”,而是:断线后能在30秒内自动恢复;恢复后数据不丢关键累计量;队列满时服务不崩溃且能告警;异常点位有明确质量戳,不污染历史库。

把上述机制落地后,采集模块就具备了工业现场所需的韧性。下一阶段的数据存储与报警判断,都建立在这些带质量戳的实时数据之上。