1. 项目全貌与需求拆解
1.1 为什么会有openrig
做工业设备监控这么多年,一个绕不开的痛点就是"每套系统都是定制项目"。钻机、修井机、泥浆泵这些大型装备,表面上看都是标准设备,但到了现场一摸底,每个井队的仪表配置都不一样:有的用老式模拟量传感器,有的用Modbus RTU采集器,还有的上了OPC UA网关。以前的做法是给每个现场单独写一套采集程序,来一台设备就写一遍联调,既费人力又难维护。开源的通用监控平台倒是不少,但大多是围绕IT基础设施设计的,拿到工业现场用要改很多东西。
openrig这个项目,我最初的定位很明确:做一套专门面向钻机装备的开放数据采集与状态监控框架。它不绑定具体硬件品牌,不预设固定点位表,而是提供一套从传感器到数据库再到可视化的完整链路,让现场工程师能通过改配置文件,而不是改代码,就能接入一个新的井场。项目的名字也很直白,open代表开放,rig就是钻机,合起来就是"开放的钻机监控"。如果你正在做装备物联网改造、设备数据上云,或者只是想给车间的几台老设备做个数字化台账,openrig的思路都可以直接借鉴。
1.2 核心需求与目标用户
在动手写代码之前,我先梳理了几个硬性需求,这些需求直接决定了架构选型的方向:
- 协议多样性要兜得住:现场既有传统4-20mA模拟量,也有Modbus RTU/TCP、PLC的S7协议,还可能有个别设备只提供HTTP接口。采集层必须能做到一站接入,不能为了一个特殊协议单独搭一套服务。
- 点位配置要能动态生效:井队换设备、加传感器是家常便饭,如果每改一个点位都要改代码重新发布,项目根本运营不下去。点位表必须外置,最好用表格或配置文件就能维护。
- 断网不能丢数据:井场网络环境不稳定,卫星链路经常抖动。边缘侧必须有一层本地缓存,网络恢复后再自动补传。
- 报警要能分级:不是所有越限都要弹窗,设备轻度波动只需要记录,严重到可能停机才需要推送消息。报警规则需要可配置。
- 展示层要快、要直观:现场值班室看的是实时状态和趋势,办公室看的是日报统计,数据展示要开箱即用,别让用户自己写前端。
这套需求下来,适合参考openrig的读者大致有三类:一是装备制造企业的售后技术团队,需要给客户提供设备运行数据报告;二是油田、矿山现场的自动化工程师,想把分散的传感器统一收拢到一个平台;三是做工业物联网创业的开发者,需要一个稳定的底座作为二次开发基础。
1.3 为什么不用现成的商用平台
市面上其实有不少成熟的组态软件和工业物联网平台,用起来省心,但有两个绕不过去的坎:一是按点位收费,一个中大型井场动辄几百个点位,年费不是小数目;二是闭源黑盒,后期想接入一个平台没支持的自定义协议,只能走厂商排期,现场根本等不起。
openrig选择走开源路径,核心价值在于"可掌控"。协议解析可以自己扩展,数据库结构完全开放,告警规则可以按客户要求随意调整。当然开源不等于所有东西都要自己造轮子,底层仍然依赖成熟的生态组件,比如用EMQX做消息接入、用TDengine存时序数据、用Grafana出图表,这些后来都被证明是靠谱的选择。
提示:如果项目周期极短、点位少于50个、现场没有特别奇怪的协议,用现成商用平台反而效率更高。openrig的价值在设备数量多、协议杂、需要长期演进的情况下才真正显现。
2. 系统架构与数据链路设计
2.1 整体拓扑,数据怎么从传感器流到大屏
把openrig拆开看,数据流是一条非常清晰的单向链路:传感器信号 -> 采集网关 -> 消息总线 -> 数据处理 -> 数据库 -> API/可视化。
最底层的传感器和执行器,通过有线或无线方式接到采集网关。网关这里有两种角色:如果现场已经有成熟的PLC或RTU,openrig负责从这些控制器里"取数";如果是什么控制器都没有的裸传感器,网关就直接承担IO采集任务。
取到的原始数据统一转成标准消息格式,推送到内部的EMQX消息总线。这一层是整个架构的关键解耦点,往后不管是做实时计算还是做历史存储,都只是订阅/消费消息的问题,不会反过来影响采集端。数据处理服务从消息总线拉取数据后,做清洗、单位换算、阈值判断,再决定写库还是触发告警。最终,Grafana从TDengine查询数据做可视化,同时提供一个RESTful API给第三方系统调取。
这张图的重要之处在于每个环节都有明确的界面,谁出问题排查谁,互不干扰。
2.2 采集层设计:一个驱动框架吃掉所有协议
采集层是openrig里工程量最大的模块,也可以说是最体现设计功力的地方。我没有为每种协议单独写一个采集程序,而是抽象了一个"驱动框架":每个协议实现统一接口,对外暴露start、stop、read三个方法,框架负责管理驱动的生命周期、数据上报速率和异常重连。
# 采集驱动统一接口示例 class BaseDriver: def __init__(self, config): self.config = config self.running = False def start(self): raise NotImplementedError def stop(self): raise NotImplementedError def read(self): raise NotImplementedErrorModbus RTU驱动、S7驱动、OPC UA驱动、HTTP轮询驱动都继承这个基类。新增一种协议时,只需要实现这三个方法,再写一个对应的配置文件加载器,就能被框架直接管理。这样设计的主要原因在于采集层是这类项目变更最频繁的部分,把变化隔离在驱动内部,主体框架就能保持稳定。
另一个设计细节是上报速率不能按秒一刀切。温度、液位这种缓变量,5秒采一次已经足够;振动、泵压这种需要捕捉瞬态变化的量,可能需要500毫秒采一次。openrig的点位配置里有一个独立的sampling字段,驱动框架按照点位配置分组调度,保证慢变量不浪费带宽,快变量不丢失细节。
2.3 中间消息层:为什么选EMQX而不是自研队列
采集端和数据端之间放一个消息中间件,这个决定是我在整个项目里最坚持的,没有之一。
早期版本为了部署简单,我图省事让采集服务直接写数据库,结果踩了大坑:一旦数据库连接不稳定,采集线程全被阻塞,一批数据直接丢在内存里。后来改成所有数据先进消息队列,采集服务只负责往队列里推,数据处理服务再从队列里拉,两边彻底脱钩,哪怕数据库重启,消息也原封不动地积压在队列里,恢复后继续消费。
具体中间件选型时,我在RabbitMQ、Kafka、EMQX之间做过对比。最终选EMQX是基于三个考虑:一是它原生支持MQTT协议,而MQTT是工业网关最普遍的对外协议,采集服务直接订阅远端MQTT数据源非常顺滑;二是它支持共享订阅,可以方便地做消费者水平扩展;三是它在嵌入式设备上的生态比较成熟,很多传感器厂家出厂就支持MQTT上报。
下表是当时的选型对比,给同样在纠结的人一个参考:
| 能力维度 | RabbitMQ | Kafka | EMQX |
|---|---|---|---|
| 工业设备接入友好度 | 一般,需适配AMQP | 差,客户端较重 | 高,原生支持MQTT |
| 断线消息积压能力 | 中等 | 强,基于分区日志 | 强,支持离线消息 |
| 边缘部署资源占用 | 中等 | 较高 | 低,适合小机器 |
| 水平扩展 | 一般 | 极强 | 强,支持集群 |
注意:选择EMQX之后,记得开启保留消息和离线消息功能。现场网关经常掉线重连,没有这两个功能,设备一断一合期间的数据就找不回来了。
2.4 存储方案:时序数据与时序数据库的匹配
工业设备监控产生的数据,99%是带有时间戳的数值序列,这类数据用传统关系型数据库存会很尴尬:写入频率高导致IO瓶颈,数据量大后查询变慢,还要自己实现数据降采样和过期清理。
openrig选择TDengine来做时序存储。第一眼看中它是因为"超级表"模型非常契合钻机场景——每台钻机建一张超级表,不同井队的设备作为子表挂在其下,查询时可以自动按时间维度做聚合分片。比如一台钻机上上百个点位,写进同一张超级表的不同子表里,按时间段拉取对比趋势时,一条SQL就够了,性能和写法都省心。
点位实时值并没有直接写时序库,而是放在Redis里做最新值缓存。因为大屏展示页会高频轮询每个点位的当前值,如果每次都去查TDengine,虽然也能查到,但会把时序库的查询压力拉高。用一个主动推送的机制把最新值更新到Redis,前端界面直接读Redis,实时性有保障,历史查询才走时序库。
3. 核心功能模块的落地实现
3.1 点位配置管理:Excel驱动一切
点位配置是openrig最贴近现场的一项设计。现场自动化工程师不一定熟悉编程,但几乎都会用Excel。为了方便他们独立维护点位表,openrig设计了一套基于Excel的配置方式:一张点位清单包含区域、设备、点位名称、数据类型、驱动类型、寄存器地址、采样周期、报警阈值等字段,启动加载时自动解析,生成对应的采集任务和存储结构。
# 配置目录结构示意 config/ ├── devices/ │ ├── drilling_rig_01.yaml │ └── mud_pump_01.yaml ├── drivers/ │ ├── modbus_rtu.yaml │ └── s7_comm.yaml └── alarm_rules/ └── default_rules.yaml之所以用YAML作为Excel机制的底层载体,是因为YAML天然适合表达层级关系,一个点位有哪些属性、属于哪个分组、挂在哪个设备下,一目了然,比Excel更简洁。实际工作流是:现场工程师在Excel里维护点位表,改完后通过Web上传页面导入,后端校验合格后自动生成设备配置,实现热更新,不需要重启服务。
这里有个实际踩过的坑:点位名称一定要设置成英文字段加中文描述双份字段。数据库列名和映射用英文,界面上显示用中文。如果直接在数据库字段里用中文,后期做报表、对接第三方系统时编码问题会搞得人想砸电脑。
3.2 数据解析与质量处理
数据从驱动读到,到最终写入时序库,中间要经过一个数据质量管道。这个管道包含三个环节:格式标准化、单位换算、无效值剔除。
格式标准化解决的是"同一物理量不同表达"的问题。比如压力这个参数,有的设备上报的单位是kPa,有的设备是MPa,有的甚至直接给一个数字,量纲写在描述里。管道里内置一个单位字典,按点位配置对原始值做换算,统一转成国际单位存储。
无效值剔除主要针对三类常见情况:传感器断线时返回的负极大值、接线松动导致的数据跳变、设备停机时上报的无意义数据。剔除的逻辑不是简单的丢弃,而是给数据打质量标签,正常数据标记为good,异常标记为bad或uncertain。这个标签会一直跟随数据进入时序库,查询时可以根据需要过滤。这样设计的原因是,有些时候现场的"坏数据"恰恰是判断设备故障的线索,简单丢弃反而会让后续分析失去依据。
3.3 告警引擎:分级分渠道推送
告警模块的触发逻辑其实不复杂,难点在减少无效打扰。一开始我设计成超限就推送,结果现场一天能收几百条微信消息,值班长直接把通知关了,真正的险情反而被淹没。
升级后的告警规则引入两个概念:持续时间与恢复延迟。持续时间是指越限状态必须维持多少秒才触发告警,用于屏蔽瞬时尖峰;恢复延迟是指告警解除后,至少要等待多久才能再次触发同一告警,防止设备在阈值边缘反复震荡导致告警风暴。
-- 告警规则表示例 -- threshold: 报警阈值 -- duration: 持续时间,单位秒 -- delay: 恢复延迟,单位秒 CREATE TABLE alarm_rule ( id INTEGER PRIMARY KEY, device_id VARCHAR(64), point_id VARCHAR(64), alarm_level VARCHAR(16), operator VARCHAR(8), threshold FLOAT, duration INT DEFAULT 5, delay INT DEFAULT 120 );告警级别按严重程度分为三级:提示、警告、紧急。提示级只记录不推送,用于趋势关注;警告级推送值班室和班组群;紧急级直接触发电话语音告警和现场声光报警联动。分级之后,值班人员的精力才真正聚焦在高风险事件上。
3.4 可视化看板:不只是画曲线
openrig的可视化层基于Grafana二次开发,但不只是换个Logo做几个面板那么简单。针对钻机场景,我在看板上做了三块核心内容:
第一块是设备总览,用一张钻机简化示意图标注关键点位,哪个位置传感器有问题,直接在图上变色闪烁,比翻列表直观得多。第二块是报警统计,按班组、按时间段、按设备类型三个维度展示报警次数和处置时长,这个看板是我在项目上线后应现场管理要求加的,因为考核人员需要量化每个班组对报警的响应效率。第三块是运行趋势对比,支持任意选两台设备,把相同点位的时间序列叠加显示,这个功能在比对两台同型号泥浆泵的运行状态时非常好用。
数据返回路径上,我优化了两个点:一是历史趋势查询的冷数据走TDengine的预计算聚合,默认按原始精度存储,按分钟、小时、天自动降采样;二是页面上的实时卡片都走WebSocket推送,不用轮询,浏览器端负载更低,数据刷新也几乎无延迟。
4. 实际操作:从零部署一套openrig平台
4.1 服务器选型与环境准备
以3台钻机、大约600个点位接入为例,我建议的最低配置是4核8G内存,100G SSD硬盘,单节点部署全部服务。为什么是这个规格,因为EMQX和TDengine本身都是轻量级服务,内存大头其实在Grafana和数据处理服务上,600个点位按5秒一个采集周期算,消息吞吐量也才每秒几百条,单节点完全扛得住。
操作系统建议用Ubuntu 22.04 LTS或Debian 12,原因是这两个系统的软件源里能直接装到较新的Docker版本,后面部署容器化服务省心。openrig的部署是基于Docker Compose的,所有服务都容器化,主机上只需要装好Docker和Compose插件。
# 安装基础环境(以Ubuntu为例) sudo apt update && sudo apt install -y docker.io docker-compose-plugin sudo systemctl enable --now docker提示:生产环境一定不要用Docker的host网络模式跑TDengine,会绕开容器网络隔离,端口冲突和权限问题会让你浪费大量排查时间。建议用compose文件里的网络配置,让服务间通过服务名相互访问。
4.2 配置文件详解
整个openrig的部署配置集中在docker-compose.yaml和.env两个文件里。docker-compose.yaml定义了6个核心服务,下面逐个说明配置意图:
- emqx:消息中间件,映射1883端口用于采集网关接入,18083为Web管理界面,开启离线消息和持久化会话。
- tdengine:时序数据库,数据目录挂载到宿主机磁盘,必须持久化,否则容器重建就丢光历史数据。
- redis:最新值缓存和告警去重使用,追加appendonly配置确保重启不丢。
- collector:采集服务,既是MQTT消费者也是Modbus/PLC采集发起端,环境变量里指定EMQX地址。
- processor:数据处理服务,消费数据消息,执行清洗、单位换算、告警判定,写时序库。
- grafana:可视化服务,内置数据源和看板模板,第一次启动自动加载配置。
# docker-compose.yaml 核心片段 services: emqx: image: emqx/emqx:5.4.1 environment: EMQX_ALLOW_ANONYMOUS: "true" EMQX_PERSISTENT_SESSION: "true" ports: - "1883:1883" - "18083:18083" volumes: - emqx_data:/opt/emqx/data tdengine: image: tdengine/tdengine:3.2.3.0 volumes: - td_data:/var/lib/taos environment: TAOS_NUM_OF_CNODES: "1" grafana: image: grafana/grafana:10.2.0 environment: GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD} volumes: - grafana_data:/var/lib/grafana ports: - "3000:3000".env文件里存放公共配置,包括数据库账号密码、告警邮箱、企业微信机器人Webhook地址,还有平台自身的监听端口。密码和密钥建议用环境变量注入而不是写死在Compose文件里,防止配置信息泄露到代码仓库。
4.3 数据接入与点位导入流程
平台跑起来之后,最关键的步骤是接入第一台设备的点位。
首先在设备管理页面录入钻机的基本信息和通信参数,比如IP、端口、从站地址。然后准备点位Excel模板,模板表头包含:区域编码、设备编码、点位名称、点位编码、驱动类型、寄存器地址、数据类型、单位、采样周期、告警级别、告警阈值、持续时间。这些字段不要随意改,后端校验器会做字段名严格匹配。
上传之后立即校验三步:驱动类型是否在已安装列表里、寄存器地址格式是否符合协议规则、告警阈值与数据类型是否兼容。校验通过后点"下发配置"按钮,配置会自动分发到采集服务并热加载生效。
验证接入是否成功的标志不仅仅是能看到实时数值,更重要的是看两条记录:一条是采集驱动日志里的"点位加载成功",一条是数据处理日志里的"首次数据落库"。看到这两条,就说明链路彻底打通了。接着去Grafana的探针页面确认数值的物理合理性——这个步骤很多新手容易漏掉,监控平台跑起来显示一个爆炸数据,往往不是设备真的爆炸,而是点位映射错位或量纲没换算。
5. 部署上线后的常见问题与排查实录
5.1 设备离线不是真离线,是心跳机制不够健壮
项目上线第一周就遇到一个诡异问题:大屏上总有一两台设备显示离线,但现场人员过去看,设备明明在正常运转,本地仪表盘数值也在跳动。
排查后发现根因不在设备侧,而在openrig的心跳判断机制。最初的判断标准是"超过30秒没有收到该设备的任何消息就判定离线",但现场有部分点位上报周期原本就设置为60秒,慢变量点位多的设备,整体数据上报频率自然偏低,于是被误判。
修复方案是区分"点位活跃"和"设备活跃"两个概念。只要该设备下任意一个点位在30秒内有数据,就认为设备在线;只有全部点位都沉默超过心跳阈值,才判定离线。同时,在采集网关侧增加一个设备级心跳消息,由网关每隔10秒主动上报,不依赖业务点位数据。这样既避免误报,又能真正确认网络链路是否通畅。
5.2 数据曲线频繁跳尖峰,是滤波参数没调
部署初期,现场反馈泥浆泵的压力曲线每隔十几分钟就出现一个尖锐脉冲,从正常的25MPa瞬间跳到35MPa又落回。第一反应是压力变送器质量问题,换了一个新变送器后现象依旧。
后来排查到问题出在Modbus采集驱动的时间戳逻辑上。驱动取数时使用了采集程序处理时刻作为数据时间戳,而Modbus RTU链路上如果一条报文在传输层重发了两次,采集程序可能把这次重发的旧数据也当成新数据上报了,旧数据的真实发生时刻跟程序处理时刻对不上,写入时序库后表现为一个虚假的瞬时尖峰。
修复方法是在驱动里引入数据版本号机制。每台从站设备维护一个上次成功获取值的寄存器和时间戳,新上报的数据必须同时满足"寄存器地址匹配"和"数值变化时间有效"两个条件才被接受,否则丢弃并记一条质量标签。同时在前端展示滤波器上配置一阶惯性滤波,设置时间常数为0.5秒,双管齐下后曲线恢复平滑。
5.3 告警风暴把值班手机震麻了
有一口井在交接班期间连续收到170多条报警消息,值班手机什么都干不了。打开告警日志一看,是同一个低压告警点,在阈值线上下反复穿越,触发、恢复、再触发,几秒钟一轮循环。
告警引擎里的持续时间和恢复延迟已经生效了,为什么还会这样?后来发现恢复延迟参数默认是120秒,但对于某些快变点位,设备本身在阈值附近波动,恢复还没到时间又冲上去,计数器永远在重置。
针对这类点位,我把报警规则做了自动化抑制:当一个点位在5分钟内触发超过5次同类告警,自动进入静默模式,只记录事件不推送消息,直到连续10分钟数据恢复正常才解除静默。本质是加了一个简单的"告警熔断"机制,而不是一味调大延迟。上线后告警量直接降了两个数量级,同时真正重要的事故报警一条没漏。
5.4 历史数据跨天查询,时段错乱
值班长报告一个很影响信任度的BUG:前一天23点到次日凌晨1点的数据,在日报曲线里显示到了8点之后的位置。检查数据库存储后发现时间戳本身没有问题,问题出在展示层的时区配置。
TDengine服务默认使用系统时区,而Grafana默认使用UTC时区。数据写入TDengine时带的是北京时间的时间戳,但显示时被Grafana按UTC转换,整整偏移了8小时。修复方式是统一约定:所有服务一律使用UTC时间戳存储,展示层由Grafana自身的时区设置做转换。把Grafana的默认时区改成Asia/Shanghai,TDengine容器挂载宿主机的/etc/localtime后,跨天查询再没出过时区错位。
注意:时区问题在容器化部署里非常隐蔽,因为你单独看任何一层日志时间都是对的,放在一起就乱了。建议在架构图阶段就明确时区约定,不要等到用户反馈之后再补。
6. 一些让我改变做法的实际经历
项目进入稳定运行期后,我重新审视了整个架构,有两点感受特别深。
第一点,监控平台的真正难点不是技术而是流程。数据链路搭建、点位配置、告警规则,这些技术工作一周就能完成。但要让现场值班人员真正信任这套系统、把报警当成一回事,花了将近两个月。中间经历了一次误报漏报的信任危机,后来靠每天与现场联合复盘报警日志、按他们的反馈调整阈值才逐步建立信任。
第二点,自动补数据不是万能的。早期为了追求数据完整性,在网络恢复后按时间戳补传积压数据。但后来发现,补传的旧数据会覆盖时序库里的"真实空缺",导致统计报表与实际生产记录有出入。我的应对方案是把补传数据打上"backfill"标签,默认不参与日报统计,只有在管理员确认需要纳入趋势分析时才手动放开。这比无脑补传要稳妥得多。
最后分享一个配置小心得:在用Excel维护点位表时,一定养成"每次修改后同步导出存档"的习惯。openrig支持配置版本回滚,但前提是你得先把配置快照存下来。我记得有一次现场同事误操作把整张点位表清空了,如果没有前一天导出的备份,几百个点位的信息就得一个一个重新录入。从那以后,我写了个定时任务,每天晚上自动把设备配置和点位配置打包备份到对象存储,安全感一下就上来了。
openrig这个项目的价值,不在于它做出了多惊艳的功能,而在于它把工业设备数据接入这件事从"项目级定制"变成了"配置式交付"。如果你手头正好有设备需要做监控,按这个思路搭一套,省下的时间一定对得起折腾的功夫。