我自己做过几个能源管理项目,最大的感受是:刚开始跑得很顺,等设备点位超过几百个、业务规则加了几十条之后,“采集、存储、展示都在一个工程里”的写法就开始拖后腿。改一处协议解析要重新部署整个后端,数据库里一张超大流水表把历史报表查询拖到几十秒,告警和计费逻辑又耦合在一起,牵一发动全身。后来我接触到 MyEMS 这套开源架构,它用微服务把数据采集、清洗、规范化、聚合、计费、告警拆成独立进程,数据流按时序数据模型组织,整体上是一个“时序数据驱动”的能源数字化底座。这篇文章不打算复述官方文档,而是从我实际使用的角度,讲清楚 MyEMS 在微服务解耦和时序数据处理上到底做了什么,哪些设计值得借鉴,部署和二次开发时要躲开哪些坑。正在做能源管理平台选型、或者想把单体能耗系统改造为微服务架构的朋友,可以参考。
1. 能源数字化场景下,为什么我会选择 MyEMS 而不是从零造轮子
1.1 一套能源管理系统到底要管什么
能源数字化不是一个“装个表、看个数”那么简单的事。一个典型的工厂、园区或商业综合体,要管电、水、天然气、蒸汽、冷热量,还要做分项计量,把照明、空调、动力、特殊用电分开统计。再往下走,还有计费分摊、异常告警、能耗报表、碳排放折算等一堆业务。数据链路从表计到平台,再到管理层看板,每一步都有坑。
我在早期项目里见过最普遍的情况是:硬件厂家提供一个采集网关,把 Modbus 数据读到自己的平台上,然后通过接口把数据同步给我们。这种方式在几十个点位的时候很稳定,一旦到了几百上千个点位,网关的转发瓶颈、数据丢包、时区错位全来了。更麻烦的是,平台侧所有功能模块耦合在一起,改一个展示字段都可能影响采集任务,发布一次要整个系统停机。
MyEMS 进入视野的时候,我其实并没有指望它能直接覆盖所有项目需求,但我看重它的架构思路:把能源数据当作一条完整的流水线来处理,每个环节独立成服务。这种“开源架构 + 微服务解耦”的组合,正好解决了我之前单体项目里最头疼的扩展性问题。
1.2 MyEMS 的技术栈与模块全景
MyEMS 的核心服务采用 Python 和 FastAPI 编写,管理后台用 Angular,展示页面也是前端工程化方式组织,数据存储默认落在 MySQL/MariaDB 上。如果只看技术栈,它并不算新,真正的亮点在模块拆分。
在采集层,它不是一个“全协议采集器”,而是按协议拆成多个独立服务:Modbus 一个服务、BACnet 一个服务、M-Bus 一个服务、OPC UA 一个服务,充电桩和光伏也有各自的采集模块。每个采集服务都是一个独立进程,负责把仪表数据写入原始数据库表。
在处理层,它有数据清洗、数据规范化、数据聚合三个独立模块。清洗负责去掉空值、跳变值;规范化负责把各类仪表读数折算成统一口径的能耗数据;聚合负责按小时、日、月、年生成统计结果。再往上,计费、规则引擎、报表、故障检测这些业务能力也各占一个服务。服务之间通过数据库表和 REST API 衔接,而不是把一个庞然大物揉在一起。
这种设计带来的直接好处是:新增一种设备协议,只需要在采集层加一个服务或者驱动,上层 API、展示、报表都不需要动;某个服务资源吃紧,也可以单独扩容。
1.3 微服务不是银弹,MyEMS 的切分标准更值得借鉴
我见过不少团队做微服务改造,最后改出来的是“分布式单体”:服务切了十几个,但互相调用关系搅成一团。问题不在微服务本身,而在于切分的标准不对。按功能菜单切,按团队组织切,按数据库表归属切,最终都会导致边界模糊。
MyEMS 的切分逻辑是跟着数据生命周期走的:数据产生、数据采集、数据清洗、数据规范化、数据聚合、数据应用。每个阶段只对上一阶段的数据负责,只向下一阶段暴露接口。这个边界非常清楚,不存在“这个功能到底该放哪个服务”的纠结。
我后来在自己的项目里借鉴这套思路时,把团队里最容易争论的“告警到底算采集还是算应用”问题一句话就敲定了:告警要消费聚合后的数据,所以它一定在应用层。边界一旦清晰,开发和排障的效率会明显提升。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 时序数据才是能源数字化的命脉:MyEMS 的数据模型与存储策略
2.1 从“设备表”到“读数表”再到“聚合表”的三层结构
很多做能源平台的人容易把数据模型想简单了,以为一张“设备—读数”的表就能走遍天下。实际操作中你会发现,设备档案、原始读数、标准化能耗、聚合统计,这四类数据的使用方式完全不同,混在一层里是灾难。
设备档案是相对静态的,用 meters、equipment 这类表维护,记录仪表编号、安装位置、所属分项、倍率、通信参数。原始读数是时序数据,每一条记录包含采集时间、数值、质量标志位,这一层的数据量最大,写入最频繁。标准化能耗数据是把原始读数经过单位换算、时段归整后,形成的统一口径数据,按小时或更高粒度组织。聚合数据则是进一步按日、月、年生成的统计结果,专门服务报表和看板。
MyEMS 把这几层分得很清,采集服务只写原始读数,清洗和规范化服务负责生成标准能耗,聚合服务只做冗余计算。这样的好处是每一层的读写互不干扰,出了问题可以知道卡在哪个环节,不用全线排查。
2.2 用数据量说话:为什么原始表不能直接支撑报表
我经常和团队强调一个概念:在设计时序数据存储之前,先算清楚一年会产生多少行。拿一个中型园区举例,500 个计量点,每 15 分钟采集一次,一天是 500 × 96 = 48000 条,一年就是 1752 万条。如果换成每 5 分钟采集一次,一年直接超过 5000 万条。
在千万级甚至亿级流水表上做月度报表,如果直接聚合原始数据,数据库会非常吃力。更合理的做法是分层聚合:原始数据保留最近几个月或者一年用于明细追溯,小时、日、月数据提前算好放在聚合表里,查询时优先走聚合表,只有需要下钻明细时才去扫原始表。
MyEMS 的规范化服务和聚合服务正是在干这件事。规范化服务把采集到的离散读数归整到统一时间段上,比如把 10:03:17、10:08:22 这样的采样点归到 10:00 到 11:00 这个小时桶里;聚合服务再把小时数据汇总成日、月、年数据。这套流程跑完之后,报表查询和告警计算基本不会触碰原始流水表,性能自然上来了。
2.3 存储选型:MySQL 分区表、TimescaleDB,还是专用时序库
MyEMS 默认使用 MySQL,官方文档也提供了完整的建表脚本。我前几个项目就是用 MySQL 跑的,把原始读数表做成按时间分区,配合索引优化,几十个G的数据量也可以接受。MySQL 的运维成本最低,团队里任何人都会维护,这是它最大的优势。
如果项目点位更多、写入压力更大,TimescaleDB 是个很顺滑的升级方向。它是 PostgreSQL 扩展,基本兼容原生 SQL,支持自动分区和连续聚合,开发人员几乎不用改代码就能把历史数据管理起来。
真正需要引入 InfluxDB、TDengine 这类专用时序库的场景,通常是高频采集加并发写入特别大的情况,比如秒级采集、几千个点位同时上报。这时候普通关系库的写入吞吐和存储压缩都跟不上。但要注意,专用时序库多一套技术栈,查询语法和运维方式都和 MySQL 不同,团队如果没有积累,不要为了“时序数据库”这个标签盲目上。我的判断标准很简单:一年数据量在亿级以下,MySQL 分区表管够;亿到十亿级,考虑 TimescaleDB;再往上或者要跑复杂实时分析,再谈专用时序库。
3. 从“两两直连”到“API 驱动”:解耦改造实录
3.1 微服务之间到底怎么通信才不算“伪解耦”
微服务改造里最迷惑人的地方是通信方式。很多人以为用了 HTTP 接口就是解耦,用了消息队列就是异步,但实际项目里经常出现 A 服务直接读 B 服务的数据库表,两个服务部署时还要互相依赖,这就是“伪解耦”。
MyEMS 给我最大的启发,是它可以接受“用数据库表作为服务间的中间数据层”。采集服务往原始表里写数据,清洗和规范化服务从原始表里读数据处理后,再写到标准表里,聚合服务接着读标准表。这种模式看起来不如消息队列高级,但它有一个很大的优点:数据链路上每一步都有中间产物,哪个环节出问题可以单独重跑,不会因为某一次网络抖动就把整条链路卡死。
真正需要走消息队列或者 Webhook 的,是那些“事件驱动”的场景,比如告警触发后的通知推送。数据流的搬运,用数据库表加定时任务反而更可靠、更好排查。
3.2 一个典型 Modbus 采集服务是如何被独立出来的
以 Modbus 电表采集为例,一个独立采集服务需要做这几件事:读配置、建连接、轮询寄存器、解析数据、单位换算、写库。配置里要定义设备地址、寄存器地址、寄存器类型、数据类型、倍率、轮询间隔。
协议解析这部分是最值得解耦的地方。每类仪表都可以封装成独立的驱动类,对外暴露统一的读取接口,返回标准结构,比如时间戳、仪表编号、数值、单位。采集服务主流程不关心底层是 Modbus 还是 BACnet,它只负责调度驱动、处理异常、批量写库。这样新增一种电表,通常只是写一个新的驱动类,或者在一份点位配置里加几行。
我当时照着这个思路,在自己的采集程序里定义了类似下面的驱动接口:
python复制class ProtocolDriver:
def connect(self):
raise NotImplementedError
def read_value(self, point):
raise NotImplementedError
def close(self):
raise NotImplementedError
class ModbusRtuDriver(ProtocolDriver):
def read_value(self, point):
# 解析 point 里的从站地址、寄存器号、数据类型和倍率
raw_value = self.client.read_holding_registers(
point.address, point.length
)
return raw_value * point.scale
这个模式看起来简单,实际用起来非常顺手。逻辑都在采集服务内部,上层 API 只认“这个表最新读数是多少”,完全不关心协议细节。
3.3 我把单体能耗系统拆成三个服务后的实际变化
之前有个项目,客户原有的能耗系统是 Java 单体,设备管理、采集、报表、告警写在一个工程里。每次加一个新型号的电表,都要走一次完整发布流程,回归范围特别大。最夸张的一次,因为改了一个报表模板的字段,结果把采集任务的定时器也影响了,数据停采了一晚上。
后来我按 MyEMS 的思路做了重构,拆成三个服务:data-collector 负责协议采集和原始数据入库,energy-api 负责对前端提供统一查询接口,alert-engine 独立跑告警规则计算。三个服务之间通过数据库和接口契约衔接,不再互相调用内部方法。
改造完之后的效果很明显。采集服务升级协议驱动时,API 服务和告警服务不用重启;报表查询全部落到聚合表,原先几十秒的月报查询变成一秒左右;告警服务自己挂了也不影响数据采集。整个发布流程从原来的“牵一发动全身”变成了“单独灰度、独立回滚”。这件事让我确信,微服务拆得好不好,关键不是服务数量,而是每个服务的生命周期是不是足够独立。
4. 部署 MyEMS 时最容易翻车的几个环节(含排查经验)
4.1 环境准备:Python、Node、MySQL 的版本组合
MyEMS 部署并不复杂,但环境版本要把控好。我见过很多人在 Python 依赖这步被卡住,尤其是 PyYAML、paho-mqtt、pika 这几个包,版本不一致会出现 import 报错。建议所有服务都放进独立的 Python 虚拟环境,不要用系统 Python 直接装。
前端部分,admin 和 web 是两个独立工程,构建时需要 Node.js。Node 版本太新或者太旧都可能让 npm install 失败或者构建产物异常,我一般固定用 LTS 版本,比如 16 或 18。安装依赖的时候如果网络比较慢,可以换成国内镜像源,你如果之后需要可以搜一下相关配置,问题不大。
数据库用的是 MySQL 8 或 MariaDB,这里有两个细节特别值得注意。第一,字符集要建库时就指定 utf8mb4,否则后面写中文备注和地址字段时容易出乱码。第二,时区设置要统一。服务器时区、数据库会话时区、Python 连接参数里的 serverTimezone 要一致,否则查询出来的时间会差 8 个小时,报表用错时间会很闹心。
4.2 初始化顺序和服务启动顺序
MyEMS 的数据库脚本是按模块拆分的,包含基础表、采集相关的表、聚合相关的表、计费相关的表等。初始化的时候必须按照依赖关系来执行,先建立基础表,再建立业务表,不然启动服务时会报“table not exists”。
启动顺序上也有讲究。首先要保证数据库已经就绪,然后启动 API 服务,因为 API 启动时会做一批配置校验;确认 API 能正常访问之后,再启动采集服务。如果一上来就把所有服务同时拉起来,采集服务可能会因为数据库线程池还没就绪而报错,虽然一些服务有重连逻辑,但日志里刷一堆红字会干扰排查。
进程管理我建议用 systemd 或者 supervisor,不要用 nohup 加后台符。一个很现实的例子:采集服务因为网络断开退出,nohup 不会帮你拉起来,数据链就断了,而且往往要等到当天报表对不上数才发现。用 systemd 的话,可以配置 Restart=always,进程掉了一两秒就自动拉起,加上健康检查,至少能保证服务意外退出后能快速恢复。
4.3 容器化部署的几个真实问题
如果选择用 Docker Compose 部署,最容易踩的坑是 mysql 容器没有做数据卷映射。容器一旦重建,数据库里的点位配置和历史数据全没了,这在测试环境还能接受,生产环境就是事故。所以第一件事就是把 MySQL 的数据目录挂到宿主机持久卷上。
容器之间编排时,要用 depends_on 加 condition 判断数据库健康。单纯 depends_on 只能保证启动顺序,不能保证数据库已经能接受连接。配合 healthcheck,比如定时执行 SELECT 1,才比较稳妥。
多个采集服务同时写库时,要注意写入频率不能过于集中。每个采集器都配置成同一秒开始轮询,数据库可能会有短暂的锁等待。解决办法是把轮询周期的起始时间稍微错开,比如 Modbus 从第 5 秒开始,BACnet 从第 15 秒开始,这样写入压力分散开,数据库连接池也不容易被打满。
5. 给想深度使用 MyEMS 的人:扩展与重构的进阶建议
5.1 源码里可以动的和不能动的
二次开发之前,先弄清楚哪些地方适合做扩展,哪些地方不要碰。采集协议模块是比较值得动手改的,比如增加新仪表协议、调整读寄存器的逻辑、优化单位换算。报表模板、告警规则、页面展示字段这些地方做定制也比较安全,因为改动范围相对局部。
不太建议动的是核心表结构和基础 API 路径。MyEMS 上层很多功能是基于既有表结构实现的,贸然改表会牵连计费、报表、前端展示,而且后面想同步官方新版本时,会发现这些改动让你无法平滑升级。我自己遇到过一个团队,为了加一个扩展字段直接改了 meter 表的列,结果导致官方聚合脚本跑不过,最后不得不回滚。
比较稳妥的做法是,尽量通过新增配置项或新增关联表的方式做扩展,而不是修改官方表结构。如果真要动核心模块,先做好补丁记录,每次升级前把 diff 拉出来看一遍,再决定怎么合并。
5.2 对接新协议或新设备的完整路径
接一台自定义 TCP 协议的电力仪表时,先把数据链路拆开看:通信参数、数据点定义、协议解析、数据入库、页面展示,每一段都对应不同的代码位置。我的习惯是先写一个驱动类,把协议解析逻辑封装好,然后用一段很小的测试脚本直接读取仪表数据,确认解析结果正确,再接入采集服务。
驱动的返回值尽量遵循统一结构,比如:
python复制{
"point_id": "meter_01_voltage",
"timestamp": "2024-06-18 14:30:00",
"value": 220.5,
"unit": "V",
"quality": 0
}
这样写的好处是,上层规范化和聚合服务完全不关心这台仪表用的什么协议,只管消费统一结构的数据。接入新设备的步骤就是:定义数据点,写驱动,配置采集服务,添加设备档案,验证数据入库,最后配置展示和告警。我建议在正式批量接入前,先拿一台仪表从采集到页面显示完整跑通,链路通了再批量扩展,效率会高很多。
5.3 把硬编码告警改成规则驱动的经验
很多能源系统里的告警最开始都是硬编码的,比如在采集代码里写死“如果电流大于多少就发告警”。这种写法在点位少的时候还行,点位一多,改阈值要改代码重新发布,非常痛苦。
后来我把告警逻辑改成规则驱动,规则表放在数据库里,字段包括:指标、比较符、阈值、生效时间段、通知方式、是否启用。规则引擎定时读取规则,对聚合后的时序数据做评估,触发后走邮件、Webhook 或者微信服务号消息推送给对应负责人。
这样改造之后,运营人员自己就能在管理页面新增、修改阈值规则,完全不用碰代码。规则驱动看起来只是把 if 换成了读表,但语义上完全不同,一是阈值和代码解耦了,二是规则可以做版本记录,三是新规则上线不用发布服务。这个思路在 MyEMS 的规则引擎里也有体现,本质上就是让业务配置和程序逻辑分离。
5.4 从 MyEMS 学到的思路,换一个场景仍成立
我后来复盘这件事,发现 MyEMS 带来的最有价值的东西,不是那一堆可以免费用的模块,而是两句话:数据要分层,服务要解耦。数据分层是指把原始层、标准层、聚合层、应用层分开,每层只处理自己关心的数据粒度;服务解耦是指采集、清洗、聚合、告警这些能力按生命周期独立部署,避免一个故障拖垮整条链路。
这套方法论放到设备物联网、楼宇自控、工业数字化项目里都适用。哪怕你最后不用 MyEMS 这个框架,只借鉴它划分微服务边界的方式,也能少走很多弯路。我现在的习惯是,接到一个能源或者 IoT 类项目,先问自己三个问题:数据从哪里来,数据要经过哪些标准化处理,数据最终要被哪些业务消费。把这三个问题的答案画出来,架构基本就出来了。
