机房里的采集服务器重启一次,或者交换机闪断几分钟,PDU 的电量曲线上就出现一段平地。等你把网络恢复,那段时间的数据是再也补不回来的------因为大部分采集脚本的逻辑都是「读一次,发一次,发完就丢」。
这个问题在月度报表和 PUE 测算里会被放大。少掉几十分钟的采样点,日均功率的均值就偏了。我在项目里处理过一次,客户拿着月度 PUE 报表问为什么比上个月好了 0.03,查了两天才发现是月初那台采集机重启丢了一段数据。
先想清楚丢数据发生在哪几个环节
一条典型的采集链路是这样的:
lua
PDU 计量模块
|-- RS485 总线(Modbus RTU)
|-- 串口服务器 / 采集网关
|-- 采集脚本(Python / Go)
|-- 时序库(InfluxDB / Prometheus)
|-- 展示层(Grafana)
断链可能发生在任意一段。RS485 那一段断,网关拿不到数;网关到采集机这一段断,脚本读串口超时;采集机到时序库这一段断,写失败。
三段的处理方式不一样,得分开看:
断链位置
表现
能不能补
处理思路
RS485 总线
读寄存器超时
不能,PDU 不留存历史
重试 + 标记缺采
网关到采集机
socket 超时
不能
重试 + 切备用链路
采集机到时序库
写入报错
能,数据已在本地
落盘缓存 + 补传
本文要讲的是第三段。前两段的数据确实没了,第三段的数据其实一直在采集机内存里,只是没发出去就被覆盖了。
缓存层的设计:别用数据库,先用一个文件
我一开始用的是 SQLite,后来换成了纯文件追加写(append-only),原因很简单:写入频率高、格式固定、不需要查询,SQLite 的事务开销在这个场景下是浪费。
结构大概是这样:
python
import json, os, time, threading
from pathlib import Path
CACHE_DIR = Path("/var/lib/pdu-collector/cache")
CACHE_DIR.mkdir(parents=True, exist_ok=True)
class SpoolWriter:
def __init__(self, max_size_mb=64):
self.fp = None
self.max_bytes = max_size_mb * 1024 * 1024
self.lock = threading.Lock()
self._rotate()
def _rotate(self):
ts = time.strftime("%Y%m%d-%H%M%S")
self.path = CACHE_DIR / f"spool-{ts}.jsonl"
self.fp = open(self.path, "a", encoding="utf-8")
def write(self, record: dict):
line = json.dumps(record, ensure_ascii=False, separators=(",", ":"))
with self.lock:
if self.fp.tell() > self.max_bytes:
self.fp.close()
self._rotate()
self.fp.write(line + "\n")
self.fp.flush()
os.fsync(self.fp.fileno())
几个设计点值得说:
- 每条记录必须带采集时间戳,且这个时间戳是采集时刻打上的,不是补传时刻。补传时如果用了发送时间,整段曲线会被压平到一个点上。
- fsync 在这不能省。断电场景下,页缓存里的数据一样会丢,那这层缓存就没意义了。
- 按大小滚动而不是按时间,便于后面按文件粒度做补传和清理。
写入的记录长这样:
json
{"ts":1760500000,"dev":"rack-a-pdu-01","phase":"L1","v":221.4,"i":8.32,"pf":0.97,"kwh":1247.6}
补传逻辑:幂等是关键
补传时常出的问题是重复写入导致曲线出现尖刺。时序库里同一个时间戳写两次,如果用的是覆盖语义,那还好;如果是追加语义,聚合查询就会翻倍。
我的做法是给每条记录生成一个确定性主键:
python
def point_key(rec: dict) -> str:
return f"{rec['dev']}|{rec['phase']}|{rec['ts']}"
补传前先把待补的文件扫描一遍,按 point_key 去重,然后按时间升序批量写。写入时用时序库支持的 upsert 语义(InfluxDB 本身就是按 timestamp + tag 覆盖的,这点很省事)。
补传的流程:
lua
扫描 spool 目录(按文件名升序)
|-- 逐行解析 jsonl
|-- 按 point_key 去重(内存 set,超过阈值改用外部排序)
|-- 分批写入(每批 500~2000 条)
|-- 写成功 → 标记该批行号 offset
|-- 整文件完成 → 移动到 done/ 目录
关键点:补传要记录进度 。一个 64 MB 的 spool 文件可能有十几万条,中途失败如果不记 offset,下次又从头来一遍。我用的是一个同名的 .offset 文件存行号,简单但够用。
还有一点:补传时要限速。断网半小时攒下来的数据一股脑打进时序库,容易把写入线程堵死,影响实时采集。我一般控制在每秒 2000 条以内,剩下的慢慢追。
怎么判断这一段是「缺采」而不是「零值」
这是另一个容易踩的坑。网络断了,采集脚本读不到数,很多实现会填一个 0 进去。这个 0 到了图表上就是一段掉到底的曲线,看报表的人会以为是那段时间真的没用电。
正确做法是区分「缺采」和「实测为零」:
状态
入库方式
图表表现
正常采集
写入实际值
连续曲线
通信失败
不写入,只记一条缺采标记
曲线断开,前端用虚线或灰底表示
实测为零(设备确实关机)
写入 0
曲线贴底
缺采标记我单独存一张小表,字段就三个:设备、起始时间、结束时间。前端画图时把这段时间标成灰色,看报表的人一眼就知道这段是没采到,不是没用电。这个区分在月末对账的时候能省掉大量解释成本。
关于采样间隔和存储量的账
举个实际的数字。一台三相 PDU,采电压、电流、功率因数、有功、电能五个量,三相合计 15 个字段。按 10 秒一次采样:
项目
数值
单台每天点数
8640
单台每天字段数
约 13 万
100 台每天(未压缩)
约 130 MB
保留 13 个月预估
约 40 GB
这个量级用单节点 InfluxDB 扛得住,但要注意把高精度原始数据和高层次聚合数据分开存。我的习惯是原始数据留 90 天,之后降到 5 分钟粒度的聚合值长期保留。
缓存目录也要设上限。断网时间过长,spool 文件会一直涨,把磁盘撑满反而会让采集进程崩掉。上面那段代码里的 max_size_mb 就是干这个用的,超过就滚动新文件,再由清理任务删掉超过保留期的旧 spool。
顺带说一下计量精度对这套链路的意义
采集层做得再完整,如果源头读数本身不准,那后面全是白工。我们在宁波做的这些 PDU,计量精度出厂控制在 ±1% 这一档,每台走一遍标准源比对,数据留在出厂记录里。现场若发现某台读数长期偏离钳表,先怀疑钳表和接线,再怀疑表头。
厂子 2014 年办起来,产品拿的是 CCC、CE(LVD 加 EMC)和 GS 三张证,体系过 ISO 9001。非标单子接得不少,图纸 30 分钟能给、报价 1 小时内给到,让集成商在方案阶段就把参数定死。双电源切换实测压在 7 毫秒以内,切换本身不造成数据断采------这点对做采集的人挺重要,不然每次切换都要处理一段缺采。
车间里负责出厂测试的师傅平均在这行干了二十五年。他们有个做法我一直觉得挺好:计量比对后把标准源读数和被测读数并排抄在记录本上,哪怕差 0.2% 也照抄。这做法看着笨,但真要追溯某台设备的历史表现时,那本子就是凭据。
回到正题。断网丢数据这件事,本质上不是网络问题,是采集程序没有为失败做设计。加一层落盘缓存,补传时保证幂等,缺采和零值分开标记------这三件事做完,机房里的电量曲线就再也断不了。
几个延伸问题
缓存文件会不会把磁盘写满?
会,所以必须设大小上限和保留期。建议 spool 目录单独挂载,容量按「单台日数据量 × 设备数 × 可接受断网天数」估算,再留一倍余量。同时加监控告警,spool 目录增长过快时通知运维。
补传的时候会影响实时采集吗?
会,所以补传必须限速并降优先级。推荐做法是补传走独立的线程和连接,批量控制在 2000 条/秒以内,并监控时序库的写入延迟,延迟抬高时自动降低补传速率。
RS485 那一段断链的数据是不是彻底没了?
在 PDU 本身不带历史存储的前提下,是的。所以这一段要靠缩短轮询周期、部署冗余链路、以及在网关侧做缓存来降低损失,而不是指望事后补传。部分型号支持内部记录极值,可以读出来做参考,但拿不到完整曲线。
采样间隔设多少合适?
看用途。做容量趋势和月度报表,5 分钟粒度足够;做故障溯源和冲击电流分析,需要 1 秒到 10 秒。折中方案是高频采、低频存,原始数据短期保留后再降精度归档。