1. 项目整体设计:从传感器到时序库的一条数据高速公路
手里的环境监测设备越来越多,分布在楼内各个角落,每台设备持续上报温度、湿度、电压和电流。早期我用普通消息队列加传统关系型数据库跑这套东西,在数据量小的时候还算凑合,等设备上了三十多台、每秒几十条消息进来以后,问题就来了:查询某段时间的温度曲线越来越慢,磁盘占用也失控,历史数据清理得靠人工写脚本。后来把链路整体换成了 MQTT 协议 + EMQX 消息服务 + InfluxDB 时序数据库,整个数据采集存储体系清爽了很多。如果你也在折腾物联网设备数据采集、环境监控、工业设备状态上报这类项目,这套组合值得参考。
先说清楚这套方案到底解决什么问题。EMQX 负责把设备的消息接进来,它天生就是干这个的——支持海量设备同时连接、支持 MQTT 协议的完整特性,设备端只需要一条简单的发布订阅链路就能把数据送上来;InfluxDB 负责把这些带时间戳的测量值存下来,它针对"按时间顺序写入、按时间范围查询"的场景做了深度优化,写入吞吐和压缩率都比普通关系型数据库高一个量级。两者中间通过规则引擎或自定义消费者连接,就构成了一条"设备上报 -> 消息中间件 -> 时序存储 -> 查询分析"的完整管道。
这个方案适合谁?适合手里有几十到几千台设备的开发者、做 IoT 平台架构选型的技术负责人、以及刚开始接触时序数据的后端工程师。你不需要一开始就上大数据全家桶,一个 EMQX 加一个 InfluxDB,单机能扛住非常可观的吞吐。这套链路我在实际项目里跑了一年多,整体稳定,下面把设计思路、选型理由、配置过程和踩过的坑一次讲清楚。
1.1 为什么是 EMQX,而不是自己写 MQTT Broker 或直接用消息队列
很多人会问:既然设备数据要经过消息中间件,我直接用现成的消息队列不就行了?还真不行。MQTT 协议和通用消息队列的定位差别很大。设备端往往处于弱网环境、IP 会变、连接不稳定,MQTT 设计之初就考虑了这些:它基于发布/订阅模型,有 QoS 等级控制消息可靠性,有遗嘱消息(Last Will)让服务端感知设备异常下线,有会话延续机制让设备重连后不丢消息。这些特性在通用消息队列里要么缺失,要么需要你用大量业务代码去补。
EMQX 是 MQTT Broker 里我用下来最顺手的,它的核心优势是连接层做得非常扎实,基于 Erlang/OTP 的高并发模型,单节点撑几万连接毫无压力,集群后更是能到百万级。另一个很关键的点是它的规则引擎,内置了数据桥接能力,可以直接把 MQTT 消息经过简单 SQL 转换后写入 InfluxDB,省掉一条自研消费链路。这点后面实操部分详细讲。
1.2 整体架构与数据流向设计
我的这套链路核心流向如下:
设备侧传感器通过 MQTT 客户端按固定主题上报数据,主题设计为 emqx/env/node-1/data 这种带层级的形式,方便后续规则匹配。EMQX 收到消息后,经过规则引擎匹配主题并提取字段,把数据桥接到 InfluxDB 数据库。存储层设计为原始数据表加聚合统计表,原始数据保留短周期,长期统计靠连续查询自动降采样。展示层后续接 Grafana 可以完全可视化。
两条写入路线值得对比:
一是 EMQX 规则引擎直写 InfluxDB,适合标准场景,设备消息字段格式固定、无需复杂清洗时最省事,吞吐高延迟低,运维只需要管好 EMQX 配置。
二是写一个独立的消费者程序,订阅 MQTT 主题再批量写入 InfluxDB,当需要做复杂解析、多数据源关联、调用外部 API 补全设备信息时比较灵活。
我实际项目里两条都用了:常规环境数据走规则引擎直写,特殊设备协议的解析走消费者程序。下面逐个环节拆开讲。
2. EMQX 部署与关键配置解析
2.1 部署方式选择:Docker 一键起,集群后续再扩
EMQX 的部署没什么玄学。单机验证用 Docker 最省事,一条命令就能把服务拉起来:
docker run -d --name emqx -p 1883:1883 -p 8083:8083 -p 18083:18083 -e EMQX_DASHBOARD__DEFAULT_PASSWORD=your-password emqx/emqx:5.8.0
这里端口需要解释一下:1883 是 MQTT 默认 TCP 端口,设备客户端连接就靠它;8083 是 MQTT over WebSocket 端口,给浏览器客户端或某些小程序用的;18083 是 Dashboard 管理界面端口,规则引擎、认证配置都在这个界面上完成。如果你本地已经占了端口,自行调整映射对应关系即可。
生产环境建议直接上 Docker Compose 编排 EMQX 加 InfluxDB,后面整套链路一条命令拉起。我做项目时习惯先用 Docker 快速验证,确认配置无误后,再用 Compose 固定整套环境供团队使用。在前面的标题里我列了 docker-compose.yml 的片段,这里完整展开:
services: emqx: image: emqx/emqx:5.8.0 container_name: emqx restart: always ports: - "1883:1883" - "8083:8083" - "18083:18083" environment: - EMQX_DASHBOARD__DEFAULT_PASSWORD=your-password volumes: - emqx-data:/opt/emqx/data - emqx-etc:/opt/emqx/etc
influxdb: image: influxdb:1.8 container_name: influxdb restart: always ports: - "8086:8086" environment: - INFLUXDB_DB=iot_data - INFLUXDB_HTTP_AUTH_ENABLED=true - INFLUXDB_ADMIN_USER=admin - INFLUXDB_ADMIN_PASSWORD=your-influx-password volumes: - influxdb-data:/var/lib/influxdb
启动后用 docker compose up -d 即可。这里我把 InfluxDB 的 HTTP 认证也顺手开了,后面会细讲为什么不要把库裸奔在局域网里。
2.2 认证授权:不要把设备接入裸奔在网络上
很多刚接触 MQTT 的开发者喜欢把 EMQX 默认配置直接拿来用,设备端连接 Broker 时不带任何用户名密码。这在内网测试没问题,一旦放到稍微正式一点的环境,我就强烈建议开启认证。
EMQX 5.x 的 Dashboard 里,访问控制 -> 认证 -> 创建认证器,选择"用户名密码"类型,然后录入设备账号密码。设备端连接时带上账号信息即可。这里有个细节:每个设备尽量用独立的 client_id 和独立的账号,方便后续做设备维度的消息追踪和审计。我在项目里给十几台传感器各建了一个账号,虽然管理起来多几步操作,但出了问题能直接定位到具体设备,排查效率高很多。
另外要注意 EMQX 的 Dashboard 默认密码,新版本安装后会要求你修改。这个密码一定不要用默认值,我见过不少把 Dashboard 暴露在公网上结果被改配置的案例。生产环境还建议把 Dashboard 端口通过防火墙限制到内网访问,或至少用反向代理加一层认证。
2.3 规则引擎与数据桥接:EMQX 最核心的竞争力
EMQX 的规则引擎允许你用类 SQL 的语法处理消息流,匹配主题、提取字段、执行动作。它工作在消息发布的路径上,设备消息到达 Broker 后,先经过规则引擎过滤和转换,再把结果交给配置好的动作处理。这比传统的外部消费者模式少一跳网络开销。
在 Dashboard 中创建规则时,核心是写一条 FROM 语句。比如我想匹配所有环境数据主题并提取 payload 字段,规则 SQL 大致为:
SELECT payload.temperature AS temperature, payload.humidity AS humidity, payload.voltage AS voltage, payload.current AS current, payload.device_id AS device_id FROM "env/+/data"
这里的 + 是 MQTT 主题的通配符,代表任意一层。规则写到这还不算完,还要指定动作。EMQX 5.x 的动作类型里有 InfluxDB 数据桥接,配置时填 InfluxDB 地址、端口、数据库名、账号密码,然后在"数据写入"模板里声明 measurement 名称、哪些字段作为 tags、哪些作为 fields、时间戳取哪个字段。配置完成后,规则引擎会自动把消息转换成 InfluxDB 的 Line Protocol 写入。
规则引擎的调试功能一定要用起来。EMQX Dashboard 里规则列表页旁边有"调试"入口,可以模拟一条消息跑一遍规则,看提取出来的字段对不对、SQL 有没有解析报错。我在最初配置时就是靠这个功能,把字段名映射错误一个个暴露出来的。写完规则后先调试再上线,节省大量时间。
3. InfluxDB 时序数据建模与存储设计
3.1 measurement、tag、field 怎么选:影响查询性能和磁盘占用
InfluxDB 的数据模型只有四个核心概念:measurement、tag、field、time。measurement 类似关系库的表名,time 是时间戳,tag 是带索引的标签,field 是具体的测量值。查询时 tag 可以用来快速过滤,field 是真正存储数值的地方。
选择 tag 和 field 有一条核心准则:需要经常按这个维度分组或过滤的离散值放 tag,连续变化的数值放在 field。比如设备 ID、设备类型、所在房间号,这些适合放 tag;温度、湿度、电压、电流,这些必须放 field。另外 tag 的值不要设计得太离散,比如把一条消息的 request_id 这种唯一标识塞进 tag,索引会急剧膨胀,存储和查询性能都会受到明显影响。
我最终的 measurement 设计是 env_data,tags 里放 device_id 和 node_type,fields 里放 temperature、humidity、voltage、current 四个数值。这样按设备查询某时间段的曲线、按节点类型统计数据都非常快。
3.2 保留策略和连续查询:自动清理与自动降采样
保存原始数据当然有用,但不可能永久保留。关系型数据库没有内建的过期清理机制,InfluxDB 的保留策略(Retention Policy,简称 RP)天然解决这个问题。
我的做法是双 RP 策略:原始数据 env_data 的原始保留策略设置为 30 天,超出自动删除;同时创建一个长期保留策略 rp_1y,用于存放降采样后的聚合数据,保留 365 天。降采样由连续查询(Continuous Query,简称 CQ)自动完成,InfluxDB 每隔一段时间自动跑一次聚合统计,把上一小时内的数据按设备维度聚合成平均值写入长期表。
在 InfluxDB 1.x 里创建库和策略的命令如下:
CREATE DATABASE iot_data CREATE RETENTION POLICY "rp_30d" ON "iot_data" DURATION 30d REPLICATION 1 DEFAULT CREATE RETENTION POLICY "rp_1y" ON "iot_data" DURATION 365d REPLICATION 1 CREATE CONTINUOUS QUERY "cq_1h_env" ON "iot_data" BEGIN SELECT mean("temperature") AS "avg_temp", mean("humidity") AS "avg_humidity" INTO "rp_1y"."env_1h" FROM "env_data" GROUP BY time(1h), "device_id", "node_type" END
这个设计的思路是:做实时查看和报警用原始 30 天数据,做趋势分析和月度报告用长期聚合数据。双策略既控制了磁盘成本,又保留了足够长的历史窗口。注意 CQ 的 GROUP BY time 窗口大小要和 INTO 的表保持一致,否则数据写到了错误的位置。
3.3 存储容量估算:先算清楚能撑多久
时序数据库的容量规划比关系型数据库更直观,因为写入模型是 append-only,数据量增长基本可预测。以我的项目为例:每台设备 5 秒上报一条消息,JSON 序列化后约 180 字节,转成 InfluxDB Line Protocol 加上时间戳和文本开销,按 300 字节估算比较稳妥。
单台设备一天的数据量大约是 86400 / 5 * 300 = 5184000 字节,约 5MB。一个月的原始数据约 150MB,这还是单台设备。三十台设备就是 4.5GB 每月。加上 InfluxDB 的索引开销和内部碎片冗余,实际磁盘占用再乘 1.5 到 2 倍比较保险。配合 30 天保留策略,整体磁盘占用可以稳定在一个可控范围。
实际部署时,InfluxDB 的存储目录默认在 /var/lib/influxdb,Docker 部署时需要挂载宿主机目录,否则容器删除数据全没。这块建议一开始就规划好磁盘容量,并且配置日志轮转和监控告警。
4. 实操全链路:从设备采数到 InfluxDB 落库
4.1 模拟设备端:用 Python 发布 MQTT 消息
没有真实传感器的话,可以用脚本模拟设备端消息。我建议用 paho-mqtt 库,它是最主流的 Python MQTT 客户端,安装一条命令搞定。模拟脚本的逻辑很简单:每隔 5 秒生成一组随机温湿度数据,发布到 EMQX 的 env/node-1/data 主题。
import json import time import random from paho.mqtt import client as mqtt_client broker = "192.168.1.100" # 改成你的 EMQX 地址 port = 1883 username = "device-1" password = "device-pass-1" topic = "env/node-1/data" client = mqtt_client.Client(client_id="device-1") client.username_pw_set(username, password) client.connect(broker, port, 60) client.loop_start() for _ in range(720): payload = { "device_id": "node-1", "node_type": "env-sensor", "temperature": round(random.uniform(20.0, 30.0), 2), "humidity": round(random.uniform(40.0, 70.0), 2), "voltage": round(random.uniform(4.8, 5.2), 3), "current": round(random.uniform(0.1, 0.5), 3), "ts": int(time.time() * 1000) } result = client.publish(topic, json.dumps(payload), qos=1) status = result.rc if status != mqtt_client.MQTT_ERR_SUCCESS: print(f"publish failed: {status}") time.sleep(5) client.loop_stop() client.disconnect()几个关键点说一下。QoS 我选的是 1,表示消息至少送达一次,这对环境监测类数据足够可靠,又不像 QoS 2 那样有四次握手开销。payload 里的 ts 字段我用毫秒时间戳,这个在写入 InfluxDB 时要对应声明精度,不然时间戳会被误解为纳秒导致时间显示成 1970 年。客户端连接后的 loop_start 必须在 publish 之前调用,它负责维持网络心跳和消息收发的后台循环,遗漏这一步会看到连上了但消息发不出去。
另外 paho-mqtt 的 publish 是异步接口,返回的是发布结果对象而非发送结果,真正确认发布完成靠回调或等待。如果追求严格确认,可以注册 on_publish 回调看 mid 是否匹配,但环境监测场景下 QoS 1 配合定期检查磁盘数据就可以了。
4.2 方案 A:EMQX 规则引擎直写 InfluxDB
设备消息到了 EMQX,接下来就是把数据灌进 InfluxDB。方案 A 的核心配置在 EMQX Dashboard 的"规则"页面,我的步骤记录如下:
第一步,创建规则。在规则引擎页面新建规则,规则 SQL 填写上文提到的 SELECT 语句,FROM 指定 env/+/data。这个模糊匹配可以覆盖所有设备主题,后续新增设备不需要改动规则。
第二步,配置动作。在规则 SQL 下方编辑动作,选择"数据桥接 - InfluxDB 写入"。新建 InfluxDB 桥接资源,填写地址、端口、数据库、账号密码。然后配置写入模板,measurement 填 env_data,tag 映射选择 device_id 和 node_type,field 映射选择 temperature、humidity、voltage、current,时间戳字段选择 payload 里的 ts,精度声明为毫秒。
第三步,启用规则并验证。保存规则后回到规则列表,点击测试按钮,手动输入一条模拟消息跑一遍,检查输出和动作执行是否成功。确认没问题后,在 InfluxDB 端查询确认数据落库。
这个方案的优点是端到端延迟极低,消息进入 EMQX 后直接转换写入,中间不经过任何自建服务。缺点是字段映射在 EMQX 界面里配置后,要调整格式就得去界面改规则。对于字段格式稳定、设备类型固定的场景,我是强烈推荐这个方式。
4.3 方案 B:自定义消费者订阅 MQTT 再写入 InfluxDB
当消息需要更灵活的处理时,比如要从消息内容里去查设备档案、要拼接多个数据源的信息、要按业务规则拆分路由,我建议用方案 B。写一个常驻 Python 服务,订阅通配主题,收到消息后解析逻辑完全由自己掌控。示例消费者如下:
import json from paho.mqtt import client as mqtt_client from influxdb import InfluxDBClient broker = "192.168.1.100" port = 1883 topic = "env/#" influx = InfluxDBClient(host="127.0.0.1", port=8086, username="admin", password="your-influx-password", database="iot_data") def on_message(client, userdata, msg): try: data = json.loads(msg.payload) json_body = [{ "measurement": "env_data", "tags": { "device_id": data.get("device_id"), "node_type": data.get("node_type") }, "time": data.get("ts"), "fields": { "temperature": data["temperature"], "humidity": data["humidity"], "voltage": data["voltage"], "current": data["current"] } }] influx.write_points(json_body, batch_size=200) except Exception as e: print(f"parse or write error: {e}, raw: {msg.payload}") client = mqtt_client.Client() client.username_pw_set("consumer", "consumer-pass") client.on_message = on_message client.connect(broker, port, 60) client.subscribe(topic, qos=1) client.loop_forever()注意 influx.write_points 的 batch_size 参数实际控制的是 InfluxDB 客户端内部攒批写入的条数,每次达到 200 条才真正发一次 HTTP 请求,能有效降低写入开销。方案 B 的瓶颈通常不在 InfluxDB 而在 Python 单线程处理能力,设备量上到几百台时可以考虑用 asyncio 版客户端或加进程池。
4.4 查询校验:怎么确认数据真的落库了
无论用方案 A 还是方案 B,配置完成后都必须做查询验证。InfluxDB 1.x 提供了 influx CLI 和 HTTP API 两种方式,我自己习惯先用 CLI 快速验证:
influx -host 127.0.0.1 -port 8086 -database iot_data -username admin -password your-influx-password
进入交互式终端后执行:
SHOW MEASUREMENTS SELECT * FROM "env_data" ORDER BY time DESC LIMIT 10
如果能看到刚发的消息记录,链路基本就通了。然后可以进一步验证规则引擎的字段映射是否正确:检查各字段是否都有数值,没有的字段说明规则 SQL 的字段名写错了。HTTP API 方式适合快速脚本验证:
curl -G 'http://127.0.0.1:8086/query?db=iot_data&u=admin&p=your-influx-password' --data-urlencode 'q=SELECT * FROM "env_data" LIMIT 5'
还有一种情况要注意:数据确实写进去了,但 Grafana 上查出来的时间是 1970 年。这说明写入时时间戳精度没有对齐,InfluxDB 默认按纳秒处理,如果你在 EMQX 桥接里声明的是毫秒而实际传的是秒,或者反过来,时间偏移就会非常大。统一在源头把 ts 字段格式固定成毫秒,各个地方声明精度也用毫秒,就不会有这种问题。
5. 常见问题与排查技巧实录
5.1 设备连接不上 EMQX
这个问题的排查路径比较固定。先确认网络层通不通,在设备端 telnet 一下 1883 端口看 TCP 能不能连通;通了再看认证配置,EMQX 开启了认证,客户端没带正确的用户名密码会被拒绝连接;再看 client_id 是否冲突,两个设备用了相同的 client_id,后者会把前者踢下线,表现为连接频繁断开。我遇到过一种隐蔽场景:设备端设置的 keepalive 是 60 秒,但局域网偶尔抖动超过这个时间,EMQX 判定设备失联主动断开。解决方法是把 keepalive 调到 120 秒,同时在 EMQX 侧适当放宽最大连接时长。
5.2 消息发了但 InfluxDB 没数据
遇到最多的是规则引擎 SQL 没有匹配到目标主题。EMQX 规则 SQL 的 FROM 子句用的是 MQTT 主题过滤表达式,如果你在消息里发布到 env/node-1/data,但规则里写的是 env/node-1/data/extra ,那当然永远匹配不上。容易忽略的是通配符的写法:MQTT 的单层通配符是 +,多层是 #,拿 SQL 思维去写容易出错。
字段映射不匹配也是高频问题。设备端发布 JSON 里字段名是 temp,规则 SQL 和 InfluxDB 桥接模板里却写了 temperature,那 InfluxDB 里对应字段就是空的。此时去 EMQX 规则的调试页面模拟一次消息,输出结果一目了然。
5.3 字段类型冲突导致写入失败
InfluxDB 对同一个 field 的数据类型有严格要求,第一次写的是 float,后续写了字符串或整数,写入会直接报错。我踩过的一个例子:某天设备端程序异常,把 voltage 字段发成了字符串,结果这条消息以及后续所有包含 voltage 的写入全部失败,表里出现了一段数据空洞。
解决方法是把所有数值字段在设备端统一强转成 float 再发送,并且在消费者解析时再做一次类型转换兜底。已经脏掉的数据只能删掉重建字段名或删除对应序列后重新写入。
5.4 时间戳精度问题导致的诡异时间
时间戳精度问题在前面提过,这里再补一个场景:使用 EMQX 规则引擎的 InfluxDB 桥接时,如果你不显式指定 payload 里的时间戳字段,EMQX 默认会用服务器收到消息的时间写入。这本身没问题,但如果你在设备端也带了 ts 字段而没有在模板中启用它,就会出现双时间字段,InfluxDB 只认 time 字段,ts 只会被当作普通 field 存储。所以配置时要想清楚:统一用服务器时间,还是统一用设备时间。我建议优先用服务器时间,因为设备端时钟漂移是普遍现象,尤其电池供电的设备,RTC 不准会导致数据写入顺序混乱。
5.5 磁盘无声无息占满
时序数据库最大的隐藏坑,就是保留策略没配置或配置失效。InfluxDB 1.x 的数据库默认保留策略是 autogen,duration 是 0,也就是永不过期。很多人建库后直接往里写,忘了改默认 RP,半年后磁盘满了才来排查。我建议建库后第一件事就是显式创建自己想要的 RP 并设置为默认,同时监控 InfluxDB 存储目录的磁盘使用率,提前设好阈值告警。
另外 CQ 也有自己的计算周期,InfluxDB 默认每 10 秒检查一次,但实际的 GROUP BY 窗口是 1 小时,聚合数据的产出会有一定延迟。如果下游报表依赖 CQ 结果,要接受这个延迟,不要拿实时逻辑去要求它。
5.6 查询越来越慢的优化路径
查询慢最常见的原因是时间范围过大且没有 tag 过滤条件。比如 SELECT * FROM env_data 丢到 Grafana 里拉全量数据,数据量上来以后必然卡。我的优化路径是:先确保查询 SQL 里带 WHERE time 范围,再尽量带上 device_id 这个 tag 过滤条件;再确认查询涉及的时间区间是否跨越了太多 shard,InfluxDB 按时间分片存储,范围太大会扫描多个分片;最后对于需要长期统计的查询,不要查原始表,去查 CQ 已经聚合好的 1 小时表,扫描的数据量少几个数量级。
6. 扩展方向与实际项目体会
链路跑通之后,往上扩展就很自然了。Grafana 加 InfluxDB 数据源,把温湿度曲线、电压波动、设备在线率全部可视化,这一步的配置比想象中简单,因为 InfluxDB 和 Grafana 的集成已经非常成熟,数据源填地址和库名就能拉数据。报警方面可以用 Grafana Alerting,针对异常温度、连续多次上报失败等规则发告警,无需再引入额外的监控组件。
EMQX 侧如果设备量继续增长,单节点不够,可以横向扩展成集群,多个节点的 MQTT 消息通过集群内部路由互通,前端加负载均衡即可。InfluxDB 1.x 的集群能力比较弱,写入瓶颈真的到了,一般先用更高配置的机器和批量写入扛,或考虑按业务拆库拆分。到了这一步,架构复杂度才开始上升,但对于绝大多数场景,单机 InfluxDB 配合合理保留策略,能撑很久。
我在实际使用中还有个体会,就是调试链路时一定要有端到端的模拟工具。设备端脚本、MQTT 客户端、EMQX 规则调试、InfluxDB 查询,每个环节都可能出错,只有把每个环节单独验证通了,再串联起来,才不会在链路不通时一头雾水。这套组合最大的价值,是让数据管道本身变得透明——设备有没有上报、消息有没有到 Broker、数据有没有落库、查询是否正确,每一步都有清晰的检查手段。
如果你正在评估物联网数据方案,我建议不要一上来就追求复杂的流处理框架。先用 EMQX 加 InfluxDB 把链路跑通,数据能看到、能查到、能报警,这套地基就是扎实的。后续真有更高需求,再往流处理、实时计算方向演进也完全来得及。最后分享一个小技巧:所有配置改动都通过 Docker Compose 和配置文件管理,不要只做一次性的手动配置,否则环境销毁重建后,所有规则和桥接都要重新点一遍界面,那种痛苦谁经历过谁知道。