1. 数据采集方案的整体设计与思路拆解
1.1 为什么要用Node-RED来完成OPC UA到MySQL的数据通道
做工业现场数据采集的工程师,八成会碰到这样的场景:现场PLC、传感器、仪表的数据需要存到数据库里做历史记录、报表统计或后续分析。传统的做法不外乎两类——买一套组态软件或者工业网关,让厂家帮忙配置好数据转发规则;或者自己写程序,用C#、Python去调OPC UA客户端SDK,再自己拼SQL写进数据库。
这两条路各有各的麻烦。组态软件和网关价格不便宜,而且往往绑定硬件,现场加一个点位都要找供应商改配置,灵活性很差。自己写程序倒是灵活,但开发周期长,调试也麻烦,尤其OPC UA这种带安全策略、证书认证的协议,自己从零搞一遍,光握手和加密就能耗掉两三天。
Node-RED在这类场景里的价值,简单说就是四个字:编排快。它是一个基于流的可视化编程工具,节点之间用连线把数据处理逻辑串起来,OPC UA读取、JSON解析、MySQL写入都有现成的节点可以直接拖。改一个点位、加一条写入规则,在界面上拖拽几下就好,不用重新编译、重新部署。整体下来,从一个空环境到数据能落库,熟练的话半小时到一个小时就能跑通。这正是很多做数据采集、设备监控的同学愿意用它的原因。
另外要提一下选型。OPC UA(OPC Unified Architecture)是工业通信里的标准协议,相比老一代的OPC DA(基于Windows COM/DCOM)优势非常明显:跨平台、内置安全机制、支持数据模型建模,而且不需要再纠结DCOM那套极其脆弱的配置。MySQL则是应用最广的开源关系型数据库之一,运维门槛低,查询分析方便,配合Navicat、Workbench这类工具做可视化查看也很成熟。Node-RED + OPC UA + MySQL这套组合,恰好覆盖了"采集-处理-存储"整条链路,而且每个环节都是成熟方案。
1.2 数据链路的架构设计与核心流转逻辑
先把我这套方案的完整链路画在文字里:
现场设备(PLC/传感器)→ OPC UA服务器 → Node-RED(OPC UA客户端节点读取数据)→ 函数节点做格式化和清洗 → MySQL节点写入数据库表
在实际落地时,OPC UA服务器可能是设备自带的(比如某些高端PLC、支持UA协议的仪表),也可能是通过网关把Modbus、Profinet之类的协议统一映射成OPC UA的(比如KepwareEX配合UA插件,或者Softing、Matrikon的网关产品)。这两种情况对Node-RED来说没有任何区别,因为它只认OPC UA这层协议,不需要关心上游是什么设备,这就大幅简化了数据源的对接工作。
读取方式上,OPC UA提供了两种模式——轮询读取和订阅机制。我在正式环境里强烈推荐用订阅。轮询就是定时用Read服务去读点位,简单直接,但每个周期都要发起请求,点位多了以后对服务器压力比较大,实时性也取决于轮询周期。订阅是客户端向服务器注册一批感兴趣的节点,服务器按设定的发布间隔主动把变化(或周期性的数据)推给客户端,负载更小,实时性也更高。
Node-RED的node-red-contrib-opcua节点库同时支持客户端和服务器端。我们这里主要用客户端节点去连已有的OPC UA服务器,后面我会把配置细节一步步拆开讲。
数据到达Node-RED之后,我习惯先经过一个函数节点做三件事:提取有效值字段、统一时间戳格式、补上点位标识和质量码。因为OPC UA返回的数据结构里,实际值在value字段里,还带着sourceTimestamp、serverTimestamp、statusCode等信息,如果不处理就直接丢给数据库,写入字段会混乱,而且后期做数据分析时时间维度也不统一。
然后再到MySQL写入节点,用一条INSERT语句落库。这里有个优化项:如果采集频率高、点位多,一条条INSERT效率很低,可以把一个周期内的多条记录拼成一条INSERT INTO ... VALUES (...),(...)的批量语句提交,性能提升非常明显。下面章节我会分别讲环境、配置、实操和坑。
2. 环境准备与软件安装,先把基础设施搭好
2.1 Node-RED的本机安装与初始设置
我以Windows环境为例,因为目前还是很多工控工程师的主力系统,其他平台的步骤差别不大。
前提是装好Node.js,建议装12.x以上的LTS版本,太老的版本跑新插件容易出兼容问题。到Node.js官网下载安装包,装完在命令行验证一下:
node -v npm -v然后全局安装Node-RED:
npm install -g node-red安装完成后,在命令行直接敲node-red就能启动,默认监听1880端口。浏览器打开http://localhost:1880,看到拖拽式的流编辑界面就算成功了。
我建议在启动命令里加两个参数,对之后的调试很有帮助:
node-red --safe --userDir D:\nodered_data--safe是进入安全模式,已经部署的流照常运行,但新改动先不生效,等点部署后再加载;--userDir可以指定数据目录,把整个工作区、配置文件集中到某个盘,重装系统或迁移环境时直接把文件夹拷走就行,省得默认目录找半天。
如果公司内网服务器是Linux,更推荐用Docker方式:
docker run -it -p 1880:1880 -v node_red_data:/data --name mynodered nodered/node-red不管用哪种方式,装完之后先确认版本能起来,别急着下一步。
2.2 OPC UA测试服务器的选择与搭建
调试阶段,我不建议直接连现场PLC,风险太大而且出问题很难定位。先用一个OPC UA模拟服务器把整条链路跑通,再接真实设备,这个顺序能省掉很多麻烦。
常用的免费模拟器有三个,我分别说一下适用场景:
Prosys OPC UA Simulation Server:免费的模拟服务器,自带一批模拟点位,界面清爽,支持在线浏览地址空间,非常适合初学者熟悉OPC UA的地址模型和节点概念。不需要License,直接用。
KepwareEX:老牌工业协议网关软件,本身以OPC DA为主,想要OPC UA功能需要装插件(UA Server插件是收费的)。如果现场已经有KepwareEX,调试时可以直接把它当UA服务器用,但纯学习意义不大,因为License和插件都得花钱。
Node-RED自带的OPC UA服务器节点:node-red-contrib-opcua库里有一个Server节点,可以在Node-RED里直接起一个OPC UA服务器,挂几个模拟点位出来。好处是你不用再装别的软件,整个环境就在一个工具里全部搞定,缺点是没有ACO地址空间的图形化浏览界面,新手可能不好理解自定义NodeId的创建方式。
我个人的推荐组合是:学习阶段用Prosys,因为可视化浏览节点树对理解"OPC UA地址空间"这个概念帮助特别大。
Prosys启动后,默认端点地址一般是opc.tcp://你的主机名:53530/opcua/server,里面会预置很多模拟标签。快速验证用的常用节点ID包括:
ns=2;i=1001(随机正弦波,用于观察连续变化)ns=2;i=1005(随机数)
记一下,后面在Node-RED里测连接和订阅会用到。
2.3 MySQL安装建库建表
MySQL的安装方式有MSI图形安装版、ZIP免安装解压版和Docker版。我自己更常用ZIP解压版,因为不污染系统,删的时候也干净。
到MySQL官网下载Community Server的ZIP包,比如mysql-8.x.x-winx64.zip,解压到D:\mysql,然后在D:\mysql下新建一个my.ini配置文件,最小配置如下:
[mysqld] basedir=D:/mysql datadir=D:/mysql/data port=3306 character-set-server=utf8mb4 default-authentication-plugin=mysql_native_password [client] default-character-set=utf8mb4这里utf8mb4是必须的,如果以后采集的数据里可能有中文标签名或注释,用utf8能存但可能有编码问题,utf8mb4是utf8的完整版,兼容性最好。
然后用管理员权限打开命令行,进入D:\mysql\bin,执行:
mysqld --initialize-insecure mysqld -install net start mysql--initialize-insecure会生成一个root空密码的初始实例,所以初始化完第一件事就是改密码:
mysql -u root -p ALTER USER 'root'@'localhost' IDENTIFIED BY '你的密码'; FLUSH PRIVILEGES;接下来建库建表。我用一张最简单的采集表做示例:
CREATE DATABASE IF NOT EXISTS opc_iot DEFAULT CHARACTER SET utf8mb4; USE opc_iot; CREATE TABLE IF NOT EXISTS tag_history ( id INT AUTO_INCREMENT PRIMARY KEY, tag_name VARCHAR(128) NOT NULL COMMENT '点位名称', tag_value DOUBLE NULL COMMENT '点位数值', quality INT NULL COMMENT 'OPC UA质量码', source_time DATETIME NULL COMMENT '设备时间戳', record_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '入库时间', INDEX idx_tag_time (tag_name, record_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='OPC UA点位历史数据表';字段设计上,tag_name用来区分点位,tag_value存实际值,quality存OPC UA状态码,source_time存设备侧发出的时间戳,record_time是数据库写入时间。两个时间字段分开存,后面排查"设备时间和服务器时间不一致"的问题时就能直接对比,不用来回倒推。
表建好后,用Navicat或MySQL Workbench连一下确认能访问,数据库这步就算到位了。
3. 在Node-RED中配置OPC UA节点,实现数据读取
3.1 安装node-red-contrib-opcua节点库
进入Node-RED界面,点击右上角菜单(三横线),选"节点管理",切到"安装"标签页,在搜索框里输入node-red-contrib-opcua,点安装。
装完会看到面板左侧新增一大批节点,归纳一下主要分这几类:
- OPC UA Client相关:
OPC UA Client(用于创建客户端连接)、OPC UA Subscription(订阅一组节点)、OPC UA Reader(单次读取节点值)、OPC UA In(订阅上报的入口节点) - OPC UA Server相关:
OPC UA Server、OPC UA Item(定义服务端点位) - 工具类:
OPC UA Browser(浏览远程服务器地址空间)、OPC UA Events(监听事件)、OPC UA Historizer(历史数据读取)
我们这次用到的核心是Client、Subscription和Reader。这三个的定位区别记一下:Reader是"主动拉一次",Subscription是"订阅后持续推",实际采集用Subscription居多。
如果安装时提示失败,大概率是npm源网络问题,切换一下源再装:
npm config set registry https://registry.npmmirror.com注意改完后要重启一次Node-RED进程再尝试。
3.2 创建OPC UA客户端连接
在左侧节点列表找到OPC UA Client节点,拖到画布上,双击打开配置。
关键配置项我逐个说明:
Endpoint:必填。填OPC UA服务器的地址,格式是opc.tcp://主机IP:端口/路径。注意这里不要填localhost,尤其是Node-RED和OPC UA服务器不在同一台机器时,直接用IP最稳妥。后面排查问题也方便。
Security Policy:安全策略。可选None、Basic128Rsa15、Basic256、Basic256Sha256等。如果OPC UA服务器允许,调试阶段建议选None,省去证书配置。生产环境再根据实际情况选择加密策略并配置证书。
User Name和Password:如果服务器启用了账号认证就填上。Prosys这类模拟服务器默认是不开启认证的,先不填,能连上了再考虑加认证的场景。
连接节点配置好之后,还要再拖一个OPC UA Subscription节点。这个节点的作用是维护一个订阅任务,订阅项可以动态添加。配置里比较重要的是Publishing Interval,这个值决定服务器多久推送一批数据,单位是毫秒。由于实际项目对实时性要求不高——存数据库嘛,一般数据延迟几百毫秒完全能接受——我建议设置为1000ms。设太短比如100ms,数据点多了会频繁拉起MySQL写入,存储压力很大;设太长比如10s,实时监控界面看起来又"卡顿"。
OPC UA Subscription节点还需要指定它挂在哪个Client连接上。在节点配置里选择对应的Client实例即可。
接下来,把OPC UA Client和OPC UA Subscription连线连起来:Client的输出连接到Subscription的输入。这一步的逻辑是:先有客户端连接,才有订阅通道,顺序不能反。
我先说一下,很多新手在这里被卡住,根本问题在于没有理解这三个节点的协作模式——Client负责"建立连接",Subscription负责"承载订阅任务",Reader才是真正"读单个点位"的。如果你直接拖一个Reader节点让它读Prosys的模拟点,而不先连一个Subscription,是读不到持续数据的,因为Reader只执行一次性读取,返回之后就结束了。
3.3 点位订阅与数据输出的实际配置
配置Subscription节点时,除了连接选择,还需要把要订阅的节点添加进去。在Subscription节点配置界面的"subscriptions"区域,点击加号添加一行,填写:
- NodeId:对应OPC UA服务器里节点的标识。格式一般是
ns=2;i=1001或者ns=2;s=Simulation_Random。这里的命名空间(ns)和标识(i或s)是OPC UA地址空间里的核心概念,简单理解,ns是命名空间编号,i是数字节点ID,s是字符串节点ID,具体值和服务器里定义的变量绑定。 - Sampling Interval:采样间隔,服务器从底层设备读取数据的周期,可以设短一些比如100ms。
- Queue Size:队列长度,默认1即可,除非想缓存历史变化。
保存之后,在Subscription节点的输出后面接一个调试节点(debug),部署一下流,看看调试面板。正常的输出应该类似:
{ "payload": 12.345, "nodeId": "ns=2;i=1001", "sourceTimestamp": "2024-01-06T10:01:02.123Z", "serverTimestamp": "2024-01-06T10:01:02.124Z" }或者直接是一个包含value字段的对象,这取决于节点库的具体版本。看到数据能在调试面板里持续变化,就说明OPC UA读取这一侧的管道已经通了。
这里有个非常关键的细节:OPC UA订阅推送过来的数据,Payload并不一定总是数值。对于布尔点位、字符串点位,或者带状态码的点位,结构会不一样。在后面写MySQL之前,必须先用一个Function节点把需要写入的数据标准化。我后面给的函数模板会直接把这个问题解决掉。
4. 数据写入MySQL,从清洗到落库的完整实现
4.1 安装node-red-contrib-mysql并配置连接
MySQL节点有两个版本比较知名:官方维护的node-red-contrib-mysql和mysql-r分支模式。我推荐用node-red-contrib-mysql,因为它提供的节点比较直观:一个mysql节点做连接管理,一个query节点负责执行SQL,还有一个query,multi节点支持批量查询。
安装方法同上,在节点管理的搜索框输入node-red-contrib-mysql,点安装。
拖一个mysql节点到画布,双击配置:
- Host:MySQL所在机器的IP,本地就是
127.0.0.1。 - Port:默认3306。
- User:root或者单独建的采集账号。
- Password:对应密码。
- Database:上一步建的
opc_iot。
我强烈建议生产环境不要用root连接,新建一个专用账号,权限只给到该数据库的增删改查。这样可以避免误操作其他库,也便于后期审计。创建语句如下:
CREATE USER 'opcwriter'@'%' IDENTIFIED BY '密码'; GRANT SELECT, INSERT, UPDATE ON opc_iot.* TO 'opcwriter'@'%'; FLUSH PRIVILEGES;配置连接节点的名字建议直接写成MySQL-opc_iot,节点多了以后好找。
4.2 数据清洗函数的设计与实现
OPC UA订阅输出的原始数据结构因为节点版本不同而有差异,但通常都包含节点ID、值和时间戳。为了稳妥,我习惯写一个Function节点把输出统一成下面这个结构:
{ "tagName": "Simulation_Random", "tagValue": 12.345, "quality": 192, "sourceTime": "2024-01-06T10:01:02.123Z" }函数内容如下(按常见输出结构写的,现场可以根据实际字段名调整):
var raw = msg.payload; var nodeId = raw.nodeId || msg.topic || 'unknown'; var tagName = nodeId.toString(); var tagValue = null; var quality = 0; var sourceTime = null; if (typeof raw === 'object') { // 常见情况:value字段里带有实际值 if (raw.value !== undefined && raw.value !== null) { tagValue = raw.value.value !== undefined ? raw.value.value : raw.value; if (raw.value.sourceTimestamp) { sourceTime = raw.value.sourceTimestamp; } } else if (raw.value === null) { tagValue = null; } else { tagValue = raw; } if (raw.statusCode !== undefined) { quality = raw.statusCode; } } else { tagValue = raw; } // 时间戳缺失时用当前时间 if (!sourceTime) { sourceTime = new Date().toISOString(); } // 去掉命名空间前缀,只保留节点标识作为标签名,方便数据库存储 tagName = nodeId.replace(/^ns=\d+;(i|s)=/, 'node_$1_'); return { payload: { tagName: tagName, tagValue: tagValue, quality: quality, sourceTime: sourceTime } };注意一个处理逻辑:tagName里我换掉了ns=2;i=1001这种带特殊字符的原始标识。虽然MySQL字段可以存这些字符串,但后面在数据库里做查询、统计时用node_i_1001这种格式更顺手,而且避免一些SQL拼接问题。
写完函数之后,拖一个switch节点做一下数据有效性过滤。比如服务器异常时可能推送状态码不等于Good(质量码192),这些数据如果直接入库,会在报告里显示成"负值"或者"异常值"干扰分析。可以在switch节点里按质量码分桶,Good的走正常落库分支,其他质量码的走一个通知分支(接调试节点,或者写个日志表)。
4.3 MySQL写入节点的SQL配置与批量写入实践
把清洗好的输出接到query节点的输入,双击展开配置。
在"SQL"标签页里输入insert语句。这里是动态参数,需要用到node-red-contrib-mysql支持的问号占位符:
INSERT INTO tag_history (tag_name, tag_value, quality, source_time) VALUES (?, ?, ?, ?)然后在函数节点里,payload需要配合成一个数组,或者用params字段传参。我把函数节点最后输出的msg稍作调整:
msg.payload = { tagName: tagName, tagValue: tagValue, quality: quality, sourceTime: sourceTime }; msg.params = [ msg.payload.tagName, msg.payload.tagValue, msg.payload.quality, msg.payload.sourceTime ]; return msg;这样query节点会自动从msg.params里取参数填入SQL占位符。注意不要直接在SQL里拼字符串,不但容易引入语法错误,还有SQL注入风险,虽然Node-RED这边没有外部用户输入,但养成好习惯总没坏处。
部署之后,到数据库里查一下:
SELECT * FROM tag_history ORDER BY id DESC LIMIT 10;能看到数据一条条进来,说明整条链路已经跑通了。
接下来是优化:单条INSERT在大数据量下效率太低。如果订阅了很多点位,或者发布间隔很短,建议用批量写入的方式。做法是先把若干条记录缓存到Node-RED的流变量里,攒够比如20条,再一次性拼成一条多值SQL。
批量SQL的语句风格如下:
INSERT INTO tag_history (tag_name, tag_value, quality, source_time) VALUES ('node_i_1001', 10.1, 192, '2024-01-06T10:01:02.000Z'), ('node_i_1002', 20.2, 192, '2024-01-06T10:01:02.000Z'), ...在函数节点里可以把msg.params改成一个二维数组:
msg.params = [ ['node_i_1001', 10.1, 192, '2024-01-06T10:01:02.000Z'], ['node_i_1002', 20.2, 192, '2024-01-06T10:01:02.000Z'] ];node-red-contrib-mysql节点会自动将二维数组批量提交。实测下来,在单条INSERT写入大约1ms-2ms的场景下,批量20条写入往往只需要3-5ms,吞吐量提升三四倍,CPU占用也低很多。
我做批量缓存的方式是:
- 用一个单独的函数节点维护
context里的数组buffer - 数据到达时push进数组
- 数组长度达到阈值(比如20),把整个数组作为msg.params下发到query节点,并清空buffer
- 如果超时(比如200ms)还没有积满20条,也要把已有数据先刷一次,避免数据延迟过大
这样即保证了吞吐,又没有把延迟拉得太高。
顺便说一句,如果点位非常多(比如几千个)、每秒需要落库几千条记录,那么MySQL的单表插入也会成为瓶颈。这个阶段再考虑时序数据库(InfluxDB、TDengine)或者分批落库方案,但那是后话了,先把基本链路跑通最重要。
5. 常见问题与排查技巧实录
5.1 OPC UA连接不上,端点地址和网络问题
这是我在社区里看到提问最多的一个问题。Node-RED的Client节点报类似Error: connect ECONNREFUSED,说明TCP层就连不通。排查顺序如下:
先用Node-RED所在机器的命令行验证网络通不通:
ping OPCUA服务器IP。如果ping不通,检查两台机器是否在同一网段、防火墙是否拦截。Windows的OPC UA服务器很多默认会监听动态端口,如果你把Prosys装在带防火墙的Windows上,第一次启动时记得允许Java或Prosys通过防火墙。如果ping通但连接还是失败,用telnet或PowerShell测试端口:
Test-NetConnection 192.168.1.100 -Port 53530。端口通不了就查服务器端监听地址——Prosys默认监听0.0.0.0,但KepwareEX的UA服务有可能只监听localhost,需要在服务配置里改成所有接口。如果端口通但OPC UA握手失败,比如报
BadSecurityModeRejected,那就是安全策略不匹配。把Client的安全策略改成None或者和服务端一致。
一个特别容易忽略的坑:OPC UA端点地址里的主机名部分。Prosys启动时会使用你自己的主机名,比如opc.tcp://DESKTOP-ABC123:53530/opcua/server,但你的电脑可能无法解析这个主机名。解决办法是直接用IP地址:opc.tcp://192.168.1.100:53530/opcua/server。
5.2 数据读到了但库表没写入,SQL和参数问题
这类问题调试面板通常不会直接报错,而是显示"transferred: 0"或者没有任何反馈。排查手段:
先在query节点前面加一个debug节点,确认进入query节点的msg里,
params字段是否是预期格式。数组长度、参数个数和SQL占位符是否一致,少一个占位符MySQL会报错,多一个也会报错。检查query节点的输出,把执行结果都接一个debug节点。
node-red-contrib-mysql在执行成功后会返回一个包含affectedRows的payload,如果affectedRows为0,多半是SQL语法问题或者值是NULL导致插入条件不成立。常见的SQL错误还有字段名不对、表名拼错、字符串值少引号。如果用了反引号包裹字段名,注意是反引号(`)不是单引号(')。
如果你的时间戳字段是DATETIME类型,插入的字符串格式需要匹配。MySQL的
source_time字段如果用ISO8601格式(如2024-01-06T10:01:02.000Z),能自动转换,但如果时区差8小时,记得在连接字符串里调整时区设置,或者干脆用MySQL函数在SQL里转换:STR_TO_DATE(?, '%Y-%m-%dT%H:%i:%s.%fZ')。
这类"能读不能写"的排查,核心思路就是分两段:先验证SQL本身能否用Navicat手工执行成功,再把参数逐步代入看是哪一环破坏格式。
5.3 部署后运行一段时间,节点掉线不重连
OPC UA订阅在长时间运行后有可能因为网络抖动、服务器端重启、证书过期等原因断开,导致整个流看起来还在运行,但数据已经不再更新。Node-RED的订阅节点一般有自动重连机制,但我在几个项目里发现它并不总是可靠。
解决方案我在生产环境里给Subscription节点配了一个简单的看门狗:
- 在Subscription节点后面接一个函数节点,检查msg的时间戳(sourceTimestamp或serverTimestamp)。如果距离当前时间超过阈值(比如发布间隔的3倍以上),就认为订阅已经挂掉。
- 在Node-RED里设置一个定时器(inject节点,周期5秒),定期检查这段数据流中最近一次数据到达时间。
- 如果发现数据超时未更新,就调用Client节点的一个特殊能力重连——最简单的方式是在Node-RED里用
context标记一段需要清空重连的连接,然后让Client节点的"connected"事件触发重新初始化。
更稳妥一点的做法是给Subcription节点加一个heartbeat点位:如果OPC UA服务器允许,在订阅里额外加上一个服务器自身的周期计数器节点(很多服务器自带,比如Prosys的ns=2;i=1),只要这个值还在变化,就说明订阅链路活着;如果连它都不动了,说明连接已经断开,这时用node.send发一个重置信号到Client节点的reset端口,强制重建连接。
这个看门狗机制在我的现场项目里非常管用。用了它之后,连续运行数月不掉链子,哪怕中间对端的服务器升级重启过,也能在几十秒内自愈。
5.4 中文乱码和时区问题,老生常谈但总踩坑
数据库编码在五年前还是utf8够用,现在必须用utf8mb4。采集的数据里如果包含中文标签名或注释字段,用utf8有时候会报Incorrect string value错误。所以建库和建表时我都用utf8mb4,注意不是只在建库时指定,连接串里也要用characterEncoding=utf8mb4(如果是JDBC连接)或charset=utf8mb4(Node-RED的mysql连接)。Node-RED的mysql节点默认走的连接协议,字符集可以在MySQL配置里用character-set-server统一指定,所以我在MySQL的my.ini里每次都写死utf8mb4。
时区问题是另一类高频坑。OPC UA推送的时间戳通常是UTC(ISO8601格式结尾带Z),而MySQL本地的CURRENT_TIMESTAMP和你的系统时区有关。如果你发现source_time比实际时间慢了8小时,那是UTC和北京时间(UTC+8)的差异。我建议在Node-RED函数节点里统一把时间戳转成本地时区:
var d = new Date(raw.sourceTimestamp); sourceTime = d.toISOString(); // 存UTC数据库里source_time字段存UTC,record_time字段用MySQL的CURRENT_TIMESTAMP存本地时间。这样两个字段一个对应设备端UTC,一个对应服务器本地时间,调试时一目了然,不会混乱。
5.5 点位数量多、发布间隔短的性能优化建议
如果订阅数百上千个点位,发布间隔设置到100ms,每两秒就攒了几千条记录要落库,MySQL压力会很大。我实测过单台普通配置服务器上,MySQL单表每秒插入几千条一般能顶住,但CPU占用和磁盘IO会明显上去。这时候有几招:
- 降低落库频率:发布间隔从100ms调到500ms或1s,大多数历史分析场景根本不需要毫秒级数据。
- 批量写入:上面的批量SQL方案,把单条INSERT改成多值批量。
- 分区表:按月、按天做MySQL分区,查询时按时间范围过滤更快,但写入性能没太大提升,适合查询优化。
- 引入缓存层:如果数据量实在太大,先写到Redis,周期性地批量刷到MySQL,但这会引入额外的架构复杂度,小项目一般用不到。
先做好2,再考虑4,不要一上来就把架构搞复杂。
6. 库表设计与运维层面的几条建议
6.1 点位映射表和标签元数据管理
如果现场只有三五个点位,直接在采集表里写死tag_name就行。但当点位数量多起来,我会建一张维度表做点位元数据管理,结构类似:
CREATE TABLE tag_meta ( id INT AUTO_INCREMENT PRIMARY KEY, tag_key VARCHAR(128) UNIQUE NOT NULL COMMENT 'Node-RED里的点位标识', tag_display_name VARCHAR(128) NOT NULL COMMENT '中文名称', unit VARCHAR(32) NULL COMMENT '工程单位', description VARCHAR(255) NULL COMMENT '描述', enabled TINYINT DEFAULT 1 COMMENT '是否参与采集' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;点位的业务信息——比如"一号炉温度",单位"℃"——不需要在每一条历史数据里都重复存储,放在tag_meta里维护一次就行。查询时用JOIN关联tag_meta和tag_history,报表里展示的就是业务名称和单位,而不是一串node_i_1001的代码。尤其在给非技术的车间同事做报表时,这个表的意义非常大。
Node-RED侧可以定时(比如每小时)从tag_meta表读一次启用状态,动态决定订阅哪些节点,省得每次增删点位都要改流并重新部署。
6.2 数据保留策略与定期清理
历史日志表最大的特点就是只增不减,时间长了磁盘空间迟早报警。我建议在表设计阶段就规划保留策略,比如"保留90天原始数据,超过90天的按小时聚合后删除"。
MySQL没有内建的TTL功能,写一个简单的存储过程(或者Node-RED定时任务)每天晚上清理过期数据:
DELETE FROM tag_history WHERE record_time < NOW() - INTERVAL 90 DAY LIMIT 10000;加LIMIT是为了避免一条大事务锁表太久影响业务写入,分批清理更温和。
如果数据量真的很大,直接在MySQL里定期DELETE可能性能不好,更建议换用分区表,按天分区,删除旧数据就变成一个ALTER TABLE tag_history DROP PARTITION p20241001的操作,秒级完成。
6.3 用定时任务重算前一天数据质量
最后再分享一个我曾经做过的扩展思路:在Node-RED里加一个定时任务,每天凌晨跑一次汇总查询,生成前一天的日报表,比如"每个点位一天内的最大值、最小值、平均值、超限次数"。这个表可以在MySQL里预创建,Node-RED每天生成一次汇总结果写进去,这样车间看板直接查汇总表就行,不用每次现算几百万条原始数据。
实现起来不复杂:一个inject节点设置成cron触发(Node-RED 2.x支持注入定时调度),后面接query节点执行聚合SQL,再把结果写入汇总表。整个任务就是一条流,不需要额外的定时任务框架。
这个思路扩展性很好——你今天能汇总日报,后续做周报、月报、设备利用率统计,都是在同一套架构上多加几个定时任务而已。
我自己实际运维这套方案时,最大的体会是:Node-RED最大的价值不是"能跑通",而是"好维护、好扩展"。同样是OPC UA数据采集,用C#写一套程序从编码到调试可能要两三天,还要考虑编译、部署、配置文件格式。Node-RED这边半小时跑通管道,后续改一个点位、加一个联动逻辑,界面上几分钟搞定,非开发背景的同事也能上手。这就是它在工业小场景里受欢迎的根本原因。
还有一个让我印象深刻的经验:做数据采集,一定要把质量码和时间戳当做一等公民来对待,不要只盯着值。工业现场的数据不是所有时刻都可靠,设备断电、传感器故障、通信抖动都会产生质量略低的数据。如果你把质量码完整存下来了,后面做分析、做告警时能省很多事;如果当初为了省空间丢掉了,等遇到"数据莫名其妙多了一个尖峰"而你没法溯源时,就悔之晚矣。
整套方案跑通之后,可以继续往这些方向扩展:OPC UA数据转MQTT上云、Node-RED里做简单的阈值告警并推送到微信或钉钉、接入InfluxDB做时序分析、或者对接大屏可视化。数据底座已经打在MySQL里了,上层怎么玩都行。