SCADA 系统在工业现场承担着数据采集与监控的核心职责,但传统的采集程序部署方式一直饱受诟病:现场服务器操作系统版本五花八门,Python 依赖装不上,.NET 版本冲突,一次升级往往要跑遍所有车间。容器化技术恰好能解决这些问题。把采集程序打包成 Docker 镜像后,无论是在边缘工控机还是云端服务器,只要能跑 Docker,镜像拉下来就能工作,环境完全一致。

本文将从架构设计、镜像构建、协议采集实现和生产部署四个层面,完整讲解如何搭建一套容器化的 SCADA 数据采集系统,代码示例以 Modbus TCP 和 OPC UA 两种主流工业协议为主。
一、容器化 SCADA 采集的整体架构设计
在动手写代码之前,先想清楚架构。一个典型的容器化采集系统通常分为四层:设备接入层、采集服务层、消息缓冲层和存储消费层。设备接入层是现场的 PLC、仪表、传感器;采集服务层由多个容器组成,每个容器负责一个网段或一类协议的采集任务;消息缓冲层一般用 Kafka 或 MQTT 做削峰和解耦;存储消费层则对接时序数据库如 InfluxDB、TDengine 或 TimescaleDB。
为什么要强调分层?因为采集容器最容易受网络波动影响,如果采集程序直接写数据库,一次网络抖动就可能导致数据丢失或进程崩溃。引入消息中间件后,采集容器只负责把数据丢进 Kafka,下游消费者挂了也不影响采集,重启后还能从断点继续消费,这是生产环境里非常实用的容错手段。
另外建议把「采集」和「转发」拆成两类容器。采集容器贴近设备侧部署,尽量轻量;转发和清洗容器可以集中部署在云端或机房。这样即使某个车间的边缘节点宕机,影响范围也只限于该车间,符合工业系统故障隔离的设计原则。
二、采集服务的 Docker 镜像构建要点
以 Python 编写的 Modbus 采集程序为例,很多人直接用官方 python 镜像,结果镜像体积超过 1GB,在边缘工控机上拉取一次要等很久。更好的做法是使用多阶段构建,编译依赖在第一阶段完成,最终镜像只保留运行时所需文件。
from pymodbus.client import ModbusTcpClient
import time, json
client = ModbusTcpClient('192.168.1.10', port=502, timeout=3)
def collect():
if not client.connect():
return None
result = client.read_holding_registers(0, count=10, slave=1)
client.close()
if result.isError():
return None
return {'ts': time.time(), 'values': result.registers}
上面是采集程序的核心逻辑,接下来用多阶段 Dockerfile 打包。基础镜像选 python:3.11-slim,配合 alpine 版本的构建阶段,最终镜像可以控制在 150MB 以内。
FROM python:3.11-slim AS builder WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir --prefix=/install -r requirements.txt FROM python:3.11-slim WORKDIR /app COPY --from=builder /install /usr/local COPY . /app CMD ["python", "collector.py"]
有几个细节值得注意。第一,务必在 requirements.txt 中锁定依赖版本号,否则隔几个月重新构建镜像时,pymodbus 大版本升级可能导致 API 全变。第二,采集容器要设置 restart: always 或 restart: unless-stopped,工业现场断电重启后容器能自动拉起,这一点非常关键。第三,时区问题不要忽略,默认镜像是 UTC 时区,采集时间戳和现场时间对不上会带来排查困扰,可以在 Dockerfile 中加上时区配置。
三、Modbus 与 OPC UA 采集的容器化实现
Modbus TCP 的容器化相对简单,网络模式建议使用 host 模式。因为 Modbus 是长连接轮询,bridge 模式下的 NAT 转换在高频采集时可能引入额外延迟,而工控网络对实时性比较敏感。当然 host 模式意味着容器与宿主机共享网络栈,端口冲突需要自行规避,一般建议每个边缘节点只跑一组采集容器。
OPC UA 的容器化稍微复杂一些。开源实现可以用 Python 的 asyncua 库或者 Open62541,若现场使用 KEPServerEX 等商业网关,容器只需作为 OPC UA 客户端连接即可。下面是 asyncua 的采集示例。
from asyncua import Client
import asyncio
async def main():
url = 'opc.tcp://192.168.1.20:49320'
async with Client(url=url) as client:
node = client.get_node('ns=2;s=Channel1.Device1.Tag1')
value = await node.read_value()
print('采集到的值:', value)
asyncio.run(main())有一个高频踩坑点必须提醒:OPC UA 的证书问题。容器每次重建后,如果证书目录没有做持久化,服务端会把新证书视为陌生客户端而拒绝连接。解决办法是用 Docker 卷把证书路径挂载出来,例如 -v /data/certs:/root/.opcua/certs,保证容器重建后身份不变。同理,Modbus 采集的点位配置文件也应该挂载为外部卷,改点位不用重新构建镜像,只需重启容器。
四、生产环境的持久化、监控与边缘部署
容器「随建随删」的特性与工业数据「不能丢」的要求看似矛盾,实际通过合理的卷挂载和数据外发策略可以解决。本地的点位配置、日志、证书全部挂载到宿主机;采集到的数据先写入本地缓冲目录或嵌入式 SQLite,成功发送到 Kafka 之后再删除,形成简单的本地兜底机制。这样即使上联网络中断几小时,恢复后数据依然可以补传。
监控方面,建议给每个采集容器加入健康检查。Docker 的 healthcheck 配置示例如下。
services:
modbus-collector:
image: scada/collector:1.4.2
restart: unless-stopped
network_mode: host
volumes:
- /data/config:/app/config
- /data/buffer:/app/buffer
healthcheck:
test: ["CMD", "python", "/app/healthcheck.py"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15shealthcheck 脚本内部可以检测最近一次采集时间是否超时、Kafka 连接是否正常,配合 Prometheus 加上 node-exporter 和 cadvisor,就能在 Grafana 上看到每个容器的 CPU、内存以及采集链路状态,异常时通过告警通道通知值班人员。
边缘部署时还有两点经验。一是边缘工控机普遍配置不高,尽量用 docker compose 而不是 Kubernetes,单机编排更轻量也更容易维护;二是镜像要提前推送到内网私有仓库,甚至离线导出为 tar 包带到现场,避免工厂网络无法访问外网导致部署卡壳。把这些细节处理好,容器化的 SCADA 采集系统完全可以做到现场无人值守、升级一键回滚,运维成本比传统部署方式低一大截。