4. 历史数据存储实现:使用 OPC UA SDK 实现历史数据存储、配置与部署

好,咱们进入实战环节。前面讲了那么多理论,说白了,历史数据存储这件事,最终还是要落到代码上。我个人习惯是,先搞清楚 SDK 提供了哪些工具,再动手写。不然你吭哧吭哧写半天,发现 SDK 里早就有现成的了,那多尴尬。

4.1 选哪个 SDK?我的建议

OPC UA 的 SDK 不少,商业的、开源的都有。我这些年用过好几款,踩过一些坑。如果你做的是工业现场的项目,我建议优先考虑 UA-.NETStandard 这个开源库。为什么?

  • 社区活跃:遇到问题,基本上搜一下就有答案。我曾经卡在一个历史数据读取的边界问题上,折腾了两天,最后在 GitHub 的 issue 里找到了解决方案。
  • 功能完整:它原生支持历史数据相关的服务集,包括 HistoryReadHistoryUpdate。你不用自己从头造轮子。
  • 跨平台:Windows、Linux 都能跑。我有个项目,上位机是 Windows,数据采集网关是 Linux 的 ARM 板,一套代码两边用,省了不少事。

当然,如果你用的是商业 SDK,比如 Unified Automation 的,那它的封装更友好,性能也更好。但原理是一样的,咱们今天讲的是通用方法。

4.2 历史数据存储的核心:实现 HistoryUpdate

要实现存储,你得让你的 OPC UA 服务器告诉客户端:“嘿,我支持存历史数据。” 这需要在服务器端实现 HistoryUpdate 方法。

说白了,就是客户端发一个请求过来,说“我要把这条数据存起来”,你的服务器得知道怎么接住这个请求,然后写到数据库里。

来看一段核心代码,我用 C# 写,因为工业场景里 C# 用得最多:

// 这是服务器端处理历史数据更新的核心方法
protected override ServiceResult OnHistoryUpdate(
    RequestHeader requestHeader,
    HistoryUpdateDetailsCollection historyUpdateDetails,
    out HistoryUpdateResultCollection results)
{
    results = new HistoryUpdateResultCollection();

    foreach (var updateDetails in historyUpdateDetails)
    {
        // 判断是更新单个节点还是多个节点
        if (updateDetails is UpdateDataDetails updateData)
        {
            // 这里拿到要更新的节点 ID
            var nodeId = updateData.NodeId;
            
            // 遍历要写入的历史数据值
            foreach (var value in updateData.UpdateValues)
            {
                // 把数据写入你的存储后端,比如 SQLite、时序数据库
                WriteToDatabase(nodeId, value);
            }

            // 返回成功状态
            results.Add(new HistoryUpdateResult()
            {
                StatusCode = StatusCodes.Good,
                OperationResults = new StatusCodeCollection { StatusCodes.Good }
            });
        }
        else
        {
            // 不支持的更新类型
            results.Add(new HistoryUpdateResult()
            {
                StatusCode = StatusCodes.BadHistoryOperationUnsupported
            });
        }
    }

    return ServiceResult.Good;
}

嗯,这里要注意。上面的代码只是一个骨架。实际项目中,你还要考虑并发写入、事务回滚、数据校验等问题。我曾经在一个项目里,因为没做并发控制,结果两个客户端同时写同一个节点的历史数据,直接把数据库搞乱了。从那以后,我写历史存储一定会加锁或者用队列。

4.3 配置与部署:别小看这一步

代码写完了,怎么让它跑起来?配置和部署往往是新手最容易忽略的环节。你想想看,一个 OPC UA 服务器,如果配置不对,客户端连都连不上,还谈什么历史数据?

4.3.1 配置文件里要有什么?

我一般会在 appsettings.json 里放这些配置:

配置项 说明 我的建议值
Server.EndpointUrl 服务器监听的地址 opc.tcp://localhost:4840
History.StorageType 存储后端类型 SQLiteInfluxDB
History.MaxHistoryAge 历史数据保留时长 30 天(根据现场需求定)
History.SamplingInterval 数据采样间隔(毫秒) 1000(1秒一次,够用了)
Security.Policy 安全策略 Basic256Sha256

这里有个坑。我曾经把 SamplingInterval 设成了 100 毫秒,想着数据越密越好。结果呢?数据库写入压力太大,CPU 直接飙到 90%。后来我才明白,工业现场的数据变化没那么快,1秒一次完全够用。除非你是在做振动分析那种高频场景。

4.3.2 部署时的注意事项

部署到现场,和你在自己电脑上跑,完全是两码事。我总结了几条经验:

  • 证书问题:OPC UA 的安全机制依赖证书。你在开发环境可以用自签名证书,但到了现场,一定要用正式的 CA 证书。不然客户端会报“证书不受信任”的错误。
  • 防火墙:OPC UA 默认用 4840 端口。记得让现场 IT 人员把这个端口放开。我遇到过好几次,代码没问题,就是端口被墙了,折腾半天才发现。
  • 日志:一定要开日志。我习惯把日志级别设成 Information,这样既能看到关键信息,又不会刷屏。出问题时,日志就是你的救命稻草。

核心要点:历史数据存储的实现,关键在两点——一是正确实现 SDK 的 HistoryUpdate 接口,二是合理配置存储参数。代码可以复用,但配置必须根据现场情况调整。

4.4 一个完整的存储流程示例

咱们把整个流程串起来看看。假设你有一个温度传感器,节点 ID 是 ns=2;s=Temperature。客户端每隔 5 秒读一次当前温度,然后通过 HistoryUpdate 写到服务器里。

服务器收到数据后,会调用我们刚才写的 OnHistoryUpdate 方法。在这个方法里,我们把数据写入 SQLite 数据库。表结构大概是这样的:

CREATE TABLE HistoricalData (
    Id INTEGER PRIMARY KEY AUTOINCREMENT,
    NodeId TEXT NOT NULL,
    Value TEXT NOT NULL,
    Timestamp DATETIME NOT NULL,
    Quality INTEGER NOT NULL
);

嗯,这个结构很简单。实际项目中,你可能还要加一些索引,不然数据量大了查询会慢。我有个项目,历史数据表里存了上亿条记录,没加索引前,查一个月的数据要 30 秒,加了索引后,3 秒就出来了。

4.5 验证:怎么知道存对了?

代码写完了,配置也搞好了,怎么验证?我一般用 UA Expert 这个工具。它免费,功能也全。

操作步骤很简单:

  1. 用 UA Expert 连上你的服务器。
  2. 找到你要测试的节点,右键选择“History Read”。
  3. 设置时间范围,比如“过去 1 小时”。
  4. 点击读取,看看能不能拿到数据。

如果读不到数据,别慌。先检查服务器日志,看看有没有报错。我遇到过最蠢的一次,是数据库连接字符串写错了,结果数据根本没写进去,查了半天才发现。

小技巧:在开发阶段,可以把历史数据先写到内存里,用 ConcurrentDictionary 存着。这样调试起来快,不用每次都得连数据库。等逻辑调通了,再换成真正的数据库。

4.6 避坑指南

最后,分享几个我踩过的坑,希望能帮你省点时间:

  • 时间戳问题:OPC UA 的时间戳是 UTC 格式的。如果你服务器在东八区,客户端在零时区,时间对不上,查数据就会乱。我建议统一用 UTC 存,展示的时候再转本地时间。
  • 数据质量:别忘了存 Quality 字段。它表示数据是否可信。比如传感器断线了,质量就是 Bad。如果你不存这个,回溯数据时看到一堆异常值,根本不知道是设备坏了还是数据错了。
  • 性能瓶颈:如果数据量特别大,比如每秒上千个点,SQLite 可能扛不住。这时候可以考虑用时序数据库,比如 InfluxDB 或 TimescaleDB。我有个项目,从 SQLite 换到 InfluxDB 后,写入性能提升了 10 倍。

好了,这一章的内容就到这。说白了,历史数据存储这件事,技术本身不难,难的是把各种细节考虑周全。你只要把 SDK 的接口搞清楚,配置弄对,再注意一下我提到的那些坑,基本就能搞定。