☰
采集周期拉长到分钟级之后(上):低频采集的调度设计与补错机制
2026/10/13 7:04:31 网站建设 项目流程

一、低频采集的难点,与高频完全不是一回事

大多数人默认"低频采集简单"——一分钟才采一次,能有什么压力?这是个危险的误判。

高频与低频的差别:

维度高频采集低频采集

核心压力

吞吐(每秒采多少点)

数据完整(缺没缺、对不对)

失败表现

丢点、卡顿、程序阻塞、内存暴涨

断线、缺口、时钟错位

时间尺度

秒级就暴露问题

跑几个月才暴露问题

接入面

通常单一系统、少数设备

站点多、设备杂、协议杂、网络差

数据价值

波形细节

连续性与完整性

主战场

缓冲、GC、落库

调度、补数、容错

一句话总结:高频采集的第一指标是吞吐,低频采集的第一指标是数据完整率。

低频数据的用途通常是能耗统计、产量核算、环保上报、趋势分析。这类场景里,缺一段数据的后果远大于多采一段——一个月里断了两小时,这个月的能耗报表就是错的,而且事后很难解释。

还有一个容易被忽略的放大效应:低频系统的运行周期是年为单位。一个每天漂移 5 分钟的设计,跑三个月就完全错位,但前两周你根本看不出来。低频系统的问题,几乎都有"延迟暴露"的特征。

二、调度:漂移是怎么一点一点累积出来的

2.1 一个真实的错误写法

最遇到的低频采集写法是这样:

while (true) { Collect(); // 采集,耗时不定 await Task.Delay(period); // 睡一个周期 }

问题在于:下一次的起点是"上一次结束的时刻",不是"应该触发的时刻"。

假设单轮采集耗时 200ms(通信往返 + 解析),周期 60 秒:

  • 每一轮实际周期 = 60.2 秒
  • 一天 1440 轮,累计滞后 = 1440 × 0.2s =288 秒 ≈ 4.8 分钟
  • 一个月后,相位偏移约2.4 小时

你以为在"整分"采集,一个月后实际采在"整分 + 2.4 小时"的相位上。

这里有一个必须澄清的点:很多人把这个漂移归咎于"定时器不准",这是错的。

Windows 默认定时器分辨率约 15.6ms,Task.Delay的单次误差确实存在,但它是随机抖动,不累积。真正会累积的是采集耗时——因为你把"完成时刻"当成了下一轮的基准。

更麻烦的是:多个站点如果各自独立跑这个循环,每个站点耗时不同,偏移量就不同。这才是致命的地方——跨站点的数据从此对不齐,做关联分析时两个站点的"8:00 数据"实际相差好几分钟。

单站点的漂移危害相对小一些,但依然存在:按小时统计能耗时,采集点从:00漂到:04:48,落在小时边界附近的数据归属会出错,峰谷电价时段统计直接失真。

2.2 正确做法:对齐到绝对时刻

不要用"上次 + 周期",要用基于固定基准时刻(epoch)的对齐算法:

DateTime NextAligned(DateTime nowUtc, DateTime epochUtc, TimeSpan period) { long n = (nowUtc.Ticks - epochUtc.Ticks) / period.Ticks; return new DateTime(epochUtc.Ticks + (n + 1) * period.Ticks, DateTimeKind.Utc); }

关键在epoch 必须是一个固定时刻(比如2020-01-01T00:00:00Z),不能用程序启动时刻。

原因很实际:如果用启动时刻做基准,程序每次重启相位都会变。重启三次,就有三个不同的相位,前后的数据对不上。用固定 epoch,无论重启多少次,08:00:00永远是08:00:00这一槽。

2.3 三个"时刻",必须分开存

这是低频采集里最容易埋雷的地方。一条数据其实涉及三个时间:

时刻含义用途

计划时刻(Bucket)

这一轮对齐后的槽位时间

归档、聚合、跨站点对齐

实际时刻(Actual)

真正取到数据的时刻

排查延迟、判断新鲜度

设备时标(DeviceTime)

设备自己打的时标(若有)

追溯真实发生时间

工程师混用这三个时刻,是低频采集最常见的事故源。

典型错误:有的设备能返回自带时标,有的不能,于是代码里"有的用设备时标、有的用采集时标"。结果是同一张表里混了两种时基,跨站点对齐彻底失效,而且这种错误不会报错,只会让报表悄悄变错。

我的处理原则:

  1. 归档一律用计划时刻(Bucket),它是唯一确定、跨站点可对齐的
  2. 实际时刻作为独立字段存,用来算采集延迟、判断数据新鲜度
  3. 设备时标能拿就拿,但只作为追溯字段,绝不参与聚合
  4. 拿不到设备时标的数据,在质量位上标记,让下游知道这条数据的时基是采集侧的

2.4 并发、超时与设备承受能力

低频系统通常要接几十上百个站点,串行采集一轮可能就要几十分钟——比周期还长。所以必须并发。但要注意三件事:

第一,同一设备必须串行。绝大多数工业仪表、串口设备、RTU 不支持并发请求,并发读会直接返回乱码或超时。

第二,必须设单站点超时。一个站点卡死不能拖垮整轮。超时后记BadTimeout,继续下一个。

第三,并发度不是越大越好。工业现场的网络和网关承载能力往往很差,一口气并发 50 个站点可能把网关打瘫,反而造成大面积超时。建议并发度从保守值(如 4–8)起步,按实测调整,而不是一开始就拉满。

2.5 一个容易被忽略的约束:周期不能短于设备刷新周期

如果你的采集周期是 10 秒,但仪表内部的测量周期是 30 秒,那你采到的三个点其实是同一个值——数据量翻了三倍,信息量没变,还会在趋势图上画出虚假的"台阶"。

这不是 bug,但会污染分析。设计阶段要确认:被采集设备自身的刷新周期是多少?采集周期应大于它,或者接受"重复值"并在数据加工阶段做处理。

三、补数:先想清楚“这个数还能不能补回来”

3.1 两类补数,性质完全不同

类型

数据来源能否补

缓存回传

采集端断网期间存了本地缓存,恢复后上传

能,前提是缓存真的落盘了

设备侧回补

从设备/RTU 的历史记录里读回缺失区间

取决于设备有没有历史存储

关键判断:补数的前提是"数据还有源"。如果设备没有历史存储能力,采集端也没做本地缓存,那断线期间的数据就是永久丢失,再好的补数程序也补不出来。

补数这件事,80% 的工作在设计阶段:

  • 采集端有没有本地持久化队列?(不能只放内存,进程一崩全没)
  • 设备侧有没有历史数据接口?(选型阶段就要问)
  • 缺失多久以内可以自动补?超出怎么办?

3.2 幂等:补数必然带来重复

补数模块最容易出错的地方。补数一定会产生重复写入,所以写入必须是幂等的。

幂等键建议用:(站点ID, 测点ID, 计划时刻 Bucket)。

但是这里有个坑,不同时序库的行为不一样,必须实测确认:

  • InfluxDB:相同 series + 相同 timestamp 写入,行为是覆盖
  • TimescaleDB:可通过唯一约束 +ON CONFLICT DO UPDATE/DO NOTHING控制
  • TDengine:重复时间戳的写入语义与版本和配置相关

另一个必须提前定的策略:同一槽位补到了不同的值,取哪个?

常见三种取法,各有适用场景:

策略说明适用
最后写入的生效

直接用最新的值覆盖

数值型、追求最新的场景

优先设备侧时标

设备自带时标的数据优先级更高

设备时标可信时

非空优先

已有的正常值不被后续补数覆盖

防止补数把已核实的数据改坏

没有唯一正确答案,但必须有明确选择,并且写进文档。最怕的是代码里没定义,行为随数据库默认行为漂移。

3.3 补数要限流、降优先级

断网两小时后恢复,如果一次性全量回补,会发生三件事:

  1. 补数请求和实时采集抢同一条设备连接,导致新一轮实时采集也超时
  2. 大量并发请求把网关压垮,刚恢复的网络又断了
  3. 补数期间 CPU 和磁盘被打满,影响正常业务

正确做法:

  • 补数任务走独立低优先级队列,与实时采集分开
  • 限速:每秒补 N 条,或每次补一个站点的一小段
  • 补数和实时采集争抢同一设备时要排队,同一设备串行
  • 补数窗口有上限:只自动补最近 24 小时,超出的转人工处理
  • 补数失败要有重试上限,不能无限重试

3.4 缺口检测:先知道缺在哪,才谈得上补

补数的第一步是找出缺口。核心逻辑是按计划时刻序列比对已有数据:

public static List<(DateTime From, DateTime To)> FindGaps( DateTime fromUtc, DateTime toUtc, TimeSpan period, IEnumerable<DateTime> existingBuckets) { // 注意:existingBuckets 必须是"对齐后的计划时刻",不能直接拿原始时间戳比较, // 否则毫秒级差异会导致全部判定为缺失。 var have = new HashSet<DateTime>(existingBuckets); var gaps = new List<(DateTime, DateTime)>(); DateTime? start = null; for (var t = fromUtc; t < toUtc; t += period) { if (!have.Contains(t)) { start ??= t; } else if (start.HasValue) { gaps.Add((start.Value, t)); // 半开区间 [start, t) start = null; } } if (start.HasValue) gaps.Add((start.Value, toUtc)); return gaps; }

注意实现细节:have里的时刻必须是经过同样的对齐算法得到的值。如果你拿数据库里带毫秒偏差的原始时间戳去比,会全部匹配不上,缺口率 100%——这是实际会踩的坑。

四、总结:

调度和补数这两块,最容易出问题的就这四处:

1. 用"上次完成时间 + 周期"做调度漂移会累积,且多站点偏移量不同导致跨站点错位。用固定 epoch 对齐。

2. 混用设备时标和采集时标不会报错,只会让报表悄悄变错。归档统一用计划时刻,设备时标仅作追溯。

3. 认为断网期间的数据"补一下就有了"补数的前提是数据还有源。设备无历史存储 + 采集端无落盘缓存 = 永久丢失。这是设计阶段的问题,不是补数程序能解决的。

4. 补数不做幂等补数必然产生重复写入,没做幂等会导致数值型数据被重复计数,能耗、产量全部虚高。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询