前段时间做了个物联网数据采集的小项目,设备端每隔几秒上报一次温湿度、电量和位置信息,一天下来几百万条数据。一开始团队想用 MySQL 存,结果图表查询慢到让人怀疑人生。这种情况在物联网里太常见了,高频率、按时间流动的传感器数据,用传统的关系型数据库去硬扛,基本是给自己挖坑。把 MQTT 和时序数据库组合起来,是目前最省心也最能扛住生产压力的一套方案。这篇文章就围绕 InfluxDB 和 MQTT 的实际组合来写,用 EMQX 作为消息接入端,配合各类 MQTT 客户端完成从设备到存储的完整闭环,给做物联网采集、智能硬件、边缘网关的朋友提供一个能直接落地的参考。
这个组合的定位非常清晰:MQTT 解决设备连接和消息流通的问题,时序数据库解决海量时间序列数据的存储和查询问题。EMQX 负责高并发接入,InfluxDB 负责把数据变成可分析、可展示的曲线。两者之间可以靠规则引擎打通,也可以靠自定义客户端程序打通。方案本身不复杂,但里面有很多琐碎的地方容易踩坑,我把自己的经验和踩过的坑整理出来,争取让读者少走一点弯路。
1. 方案整体设计与选型依据
1.1 组合里每个角色到底在干什么
先梳理一下这套组合里的核心角色。MQTT是一个轻量级的发布订阅消息协议,非常适合低带宽、不稳定网络环境下的设备接入。设备端通过 MQTT 客户端把数据发布到某个主题,服务端或其他客户端订阅这个主题就能收到数据。EMQX是 MQTT 的 broker,相当于一个消息路由器,负责处理设备连接、主题匹配、QoS 语义、鉴权等一堆底层协议细节。InfluxDB是时序数据库,它天生就是为带时间戳的数据设计的,以时间为索引,写入速度快,压缩率高,并且内置了大量时间聚合函数。
MQTT 客户端在整个链路里其实有两种身份。一种是设备端上报数据的角色,比如传感器固件里的 MQTT 客户端,负责把采集到的温湿度、压力、电量等数据定期发布到主题。另一种是服务端订阅和消费数据的角色,比如用 Python 或 Go 写一个常驻进程,订阅设备主题,收到数据后写入 InfluxDB。这个身份区分很重要,后面设计数据流向的时候必须先想清楚自己是哪一端。
1.2 为什么不用 MySQL、Redis 或者 Kafka
很多人一开始会考虑 MySQL,理由很直接:熟悉、生态成熟、身边人都在用。但 MySQL 处理高频时序数据的问题非常明显,每秒几千条写入对 MySQL 已经是压力,加上物联网数据大多数时候不需要事务和复杂关联,用 InfluxDB 这种列式存储加高压缩比的时序库,写入效率能高出很多倍。再说查询,设备连续三天的温度曲线,在 InfluxDB 里是一条range加mean()的查询就搞定,在 MySQL 里要写一堆按时间分组的 SQL,性能还未必好。
Redis 也不是好的持久化存储。它适合做缓存、做实时状态的短暂保存,但作为时序数据主存储,内存成本和数据可靠性都是大问题。Kafka 是流处理平台,吞吐量确实高,但它不是存储和查询引擎,通常在 Kafka 之后还需要一个消费者把数据写入 InfluxDB。对于中小规模的物联网项目,直接让 EMQX 通过规则引擎把数据写到 InfluxDB,去掉 Kafka 这一层,架构简洁,运维成本也低很多。
1.3 这套方案适合什么场景
这套组合最适合的场景是:设备端已经有或者可以接受 MQTT 协议,数据本质上是按时间不断追加的传感器指标,业务上需要按时间范围聚合统计,展示趋势曲线。典型例子包括环境监测、机房动环监控、智能农业大棚、充电桩运营、共享设备管理、工业设备状态采集等。
反过来,如果业务需要强事务、多表关联查询,比如订单系统、财务系统,就别硬套这套方案。时序数据库擅长的是时间范围内的分析,不适合做复杂关系建模。如果项目里数据量小到一天只有几百条,用 MySQL 也完全没问题,没有必要为了技术而技术。选型的原则永远是匹配业务特征,而不是追逐时髦。
2. 环境准备与基础概念
2.1 用 Docker 快速搭建 EMQX
我习惯本地验证用 Docker,干净且能复现。启动 EMQX 的命令很简单,但端口要核对清楚。常用端口包括:1883 是 MQTT 普通端口,8083 是 WebSocket 端口,18083 是 Dashboard 管理控制台端口。我在测试环境一般执行:
docker run -d --name emqx \ -p 1883:1883 \ -p 8083:8083 \ -p 18083:18083 \ emqx/emqx:5.1启动之后,浏览器访问http://localhost:18083,默认账号密码是admin / public,登录后第一件事就是改密码。Dashboard 里可以看到连接数、订阅数、消息速率这些实时指标,这些信息后面排查问题非常有用。
生产环境不建议这样裸奔。至少要把 EMQX 的数据目录挂载到宿主机,配置持久化,并且设置好集群节点。Docker 版本很适合快速上手和功能验证,但如果要上生产,建议用官方推荐的部署方式,并提前把日志、数据目录、配置文件都规划好。
2.2 安装 InfluxDB 2.x 与初始化
InfluxDB 现在主推 2.x,自带完整的 Web UI、Token 鉴权和 bucket 概念,比老的 1.x 好用不少。我这里统一以 2.x 为例。启动命令:
docker run -d --name influxdb \ -p 8086:8086 \ influxdb:2.7首次访问http://localhost:8086会要求初始化:设置管理员用户名、密码、组织名称和初始 bucket。我测试时常设组织为iot,bucket 为iot_data,保留周期设成30d(30 天)。这里的保留周期意味着超过 30 天的原始数据会被自动清理,后面讲到降采样时会再展开。
初始化完成后,InfluxDB 会生成一个 Token,这个 Token 是程序访问数据库的凭证。EMQX 或自定义客户端写入数据时都需要用到,一定要保存好。在 Dashboard 左侧的 Load Data -> API Tokens 里可以重新生成和管理 Token,建议为不同用途创建不同权限的 Token,只给写入权限或只给读取权限,避免一把钥匙开所有门。
2.3 MQTT 主题和 QoS 的基础概念
MQTT 的主题设计直接影响后续规则匹配和维护成本。我常用的主题格式是sensor/{device_id}/data,比如sensor/dev_001/data。这样设备维度清晰,EMQX 规则里可以用通配符sensor/+/data或sensor/#来匹配所有设备。不建议把设备上报的主题简单设计成随机字符串,否则后期做鉴权、统计和桥接都会非常痛苦。
QoS 是 MQTT 里的可靠传输等级,分 0、1、2 三档。QoS 0 最多投递一次,可能丢消息但开销最小;QoS 1 确保至少收到一次,消息可能重复但不会丢;QoS 2 确保正好一次,握手流程最重,传输开销最大。物联网上报数据我通常选 QoS 1,既能保证在线状态下消息不容易丢,又不会像 QoS 2 那样明显增加网络开销和 broker 压力。实时性要求高的告警类数据可以放宽到 QoS 0,但要做好丢数据的心理准备。
3. 核心实操:从设备模拟到数据入库
3.1 用 Python 模拟设备端发布数据
验证整个链路,最简单的方式是用 Python 快速写一个模拟设备。这里我使用paho-mqtt客户端库,循环发布 JSON 格式的数据。代码不复杂:
import paho.mqtt.client as mqtt import json import time import random client = mqtt.Client(client_id="simulator-dev-001") client.connect("127.0.0.1", 1883, 60) client.loop_start() while True: payload = { "device_id": "dev_001", "temperature": round(random.uniform(20.0, 30.0), 2), "humidity": round(random.uniform(40.0, 60.0), 2), "battery": round(random.uniform(60.0, 100.0), 2), "ts": int(time.time() * 1000) } client.publish("sensor/dev_001/data", json.dumps(payload), qos=1) time.sleep(10)这里有三个细节值得注意。第一,payload 统一用 JSON,解析方便,规则引擎处理也方便。第二,消息里带上设备自己生成的时间戳ts,而不是依赖 EMQX 或 InfluxDB 的服务器时间,因为设备上报的数据有时会有网络延迟,客户端生成时间戳更能反映真实采样时刻。第三,client_id要稳定且唯一,尽量用设备标识,这样便于 EMQX 端做会话管理和连接跟踪。
3.2 方案一:用 EMQX 规则引擎直接写入 InfluxDB
如果设备上报的数据不需要复杂的业务处理,我强烈建议直接用 EMQX 的规则引擎把数据桥接到 InfluxDB。这个方案最大的好处是没有中间服务,EMQX 收到 MQTT 消息后直接触发规则,把处理后的数据写入时序数据库,延迟低、组件少。
在 EMQX 5.x 的 Dashboard 里,进入 Data Integration 菜单,创建规则。规则分两部分:匹配消息的 SQL 和触发动作。SQL 大概是这样:
SELECT payload.device_id AS device_id, payload.temperature AS temperature, payload.humidity AS humidity, payload.battery AS battery, payload.ts AS timestamp FROM "sensor/+/data"然后给规则添加一个写 InfluxDB 的动作。不同版本的配置入口名字可能不同,有的叫 InfluxDB Bridge,有的叫 Data Bridge,但核心配置项差不多:
- URL:InfluxDB 的 HTTP 地址,例如
http://127.0.0.1:8086 - Token:初始化 InfluxDB 时创建的那个 Token
- Org:组织名,例如
iot - Bucket:目标 bucket,例如
iot_data - Measurement:写入的 measurement 名称,例如
sensor_data - Precision:时间戳精度,通常选
ms,和设备时间戳精度保持一致 - Tag / Field 映射:把
device_id映射为 Tag,把temperature、humidity、battery映射为 Field
映射这块非常重要。InfluxDB 把数据点中的标签 Tag 当作索引维度,把 Field 当作具体指标数值。设备 ID、区域、类型这些需要分组查询的字段应该设成 Tag,而温度、湿度、电量这些数值应该设成 Field。如果全部塞进 Field,查询时会非常被动。配置完成后,回到 Dashboard 的规则页面能看到命中条数和成功率,这就是排查链路是否通的第一手信息。
3.3 方案二:用订阅客户端写入 InfluxDB
有些项目情况复杂,比如设备消息要经过单位换算、异常剔除、多源数据关联,或者同一份数据要同时发给几个下游系统。这种时候在 EMQX 规则里写一堆处理逻辑会比较吃力,更适合用自定义的 MQTT 客户端订阅主题,再调用 InfluxDB 写入接口。
我用 Python 写过一个类似的服务,核心逻辑很清晰:
import paho.mqtt.client as mqtt from influxdb_client import InfluxDBClient, Point from influxdb_client.client.write_api import SYNCHRONOUS influx = InfluxDBClient( url="http://127.0.0.1:8086", token="my-token", org="iot" ) write_api = influx.write_api(write_options=SYNCHRONOUS) def on_message(client, userdata, msg): data = json.loads(msg.payload.decode()) point = (Point("sensor_data") .tag("device_id", data["device_id"]) .field("temperature", float(data["temperature"])) .field("humidity", float(data["humidity"])) .field("battery", float(data["battery"])) .time(int(data["ts"]), write_precision="ms")) write_api.write(bucket="iot_data", record=point) client = mqtt.Client() client.connect("127.0.0.1", 1883, 60) client.subscribe("sensor/+/data", qos=1) client.on_message = on_message client.loop_forever()这里的代码为便于理解使用了同步写入,实际生产环境我一般会用BATCHING配置,让写入变为批量模式。订阅端方案比规则引擎方案开发量稍大,但好在逻辑透明、便于调试,适合业务逻辑比较重的场景。
两个方案怎么选,不用纠结。数据链路简单、追求稳定高效,选方案一;需要复杂数据处理和二次分发,选方案二。也可以两种混合:设备消息先进 EMQX,一部分规则直接写 InfluxDB,另一部分转发给后端的消费服务,各取所长。
3.4 验证数据对不对
链路跑起来之后,第一件事是去 InfluxDB 里确认数据长什么样子。在 InfluxDB Dashboard 的 Data Explorer 里写一段 Flux 查询:
from(bucket: "iot_data") |> range(start: -1h) |> filter(fn: (r) => r._measurement == "sensor_data") |> filter(fn: (r) => r.device_id == "dev_001")如果查询结果正常,就能看到设备dev_001最近一小时的温度和湿度曲线。这里有一个值得注意的细节:如果时间戳处理不对,所有数据点会堆叠在同一时刻,或者干脆查询不到。后面章节会专门展开排查方法。
4. 性能与可靠性优化细节
4.1 QoS、保留消息、遗嘱消息和自动重连
物联网设备网络环境不稳定,掉线重连是常态,所以可靠性配置比功能本身更值得花心思。设备端发布数据时建议固定使用 QoS 1,这样在线状态下最多发生消息重复,不会因为网络瞬断而大面积丢数据。EMQX 支持持久会话,设备断线重连后,broker 可以把离线期间积压的 QoS 1 消息继续投递给客户端,条件是客户端连接时设置clean_session=False。
遗嘱消息是另一个实用功能。设备上线时可以在 CONNECT 报文里注册一个遗嘱消息,例如发布一条device/dev_001/status,内容为offline。当设备异常掉线时,EMQX 会代替设备发布这条遗嘱消息,服务端就能及时感知设备离线。这个能力在做设备状态监控时非常好用,比单纯依靠心跳超时判断要快得多。
客户端重连的逻辑也不要写成简单的死循环。我见过不少设备因为断网后高频重连,把 broker 打到连接数暴涨。更合理的做法是采用指数退避:第一次重连隔 1 秒,第二次隔 2 秒,第三次隔 4 秒,设置最大间隔上限,比如 60 秒,同时加入随机抖动,避免大量设备同时重连造成雪崩。
4.2 用批量写入代替逐条写入
InfluxDB 写入性能受请求数量影响很大。如果每收到一条 MQTT 消息就同步 HTTP 写入一次,在设备数量多、上报频率高时,程序的 CPU 和内存都会被请求开销拖垮。正确做法是批量写入。InfluxDB Python 客户端提供了批量写入配置,使用BATCHING模式:
from influxdb_client import InfluxDBClient, Point from influxdb_client.client.write_api import ASYNCHRONOUS write_api = influx.write_api( write_options=WriteOptions( batch_size=500, flush_interval=10_000, jitter_interval=1_000, retry_interval=5_000, max_retries=5, ) )这些参数的意思是:积累 500 条数据或者每 10 秒主动刷新一次,不管哪个条件先到就发起写入;写入失败时最多重试 5 次,每次间隔 5 秒,并加入 1 秒抖动,避免重试风暴。这个批量大小和刷新间隔是我在实际项目里验证过比较稳妥的初始值。
Batch 大小不是越大越好。batch 太大,内存占用高,而且第一次写入失败时重试的代价也大。设备上报频率本身不高的情况下,把 flush 时间调短一点比把 batch 调大一点更有效。稳定压倒一切,批量写入的核心目的是降低请求频率,而不是压榨极限吞吐。
4.3 保留策略与降采样
物联网项目的数据增长往往超出预期,所以存储策略必须一开始就想好。InfluxDB 2.x 的 bucket 自带保留周期,例如原始数据 bucket 设置为 30 天保留。但很多时候业务需要把历史数据保留更久,比如一年甚至几年。如果直接把原始数据全量保留一年,存储成本会非常夸张。
通常的做法是分层存储:原始数据保留 30 天,同时用 InfluxDB Task 定期把原始数据聚合成分钟级或小时级的统计数据,写入另一个长期 bucket。例如保留周期 365 天的iot_data_1hbucket,里面存的是每台设备每个小时的平均温度、最高温、最低温、平均湿度等。这样历史趋势查询走聚合数据,明细查询走原始数据,存储占用能降低一个数量级。
InfluxDB 2.x 里用 Task 实现降采样并不复杂,页面上在线编辑 Flux 任务即可,设置every: 1h,每整点调度一次,查询上一个小时的数据并在目标 bucket 内写入聚合结果。核心思路就是:明细数据不过度保留,聚合数据尽量长留。
4.4 安全加固与日常运维
生产环境最怕把默认配置直接暴露到公网。EMQX 的 Dashboard 默认端口和管理员密码必须修改;InfluxDB 的 Token 要按最小权限原则分配,写数据的服务只给 write 权限,查询面板只给 read 权限。网络层面,尽量把 EMQX 和 InfluxDB 放在内网,InfluxDB 不对设备端开放。
传输层安全也很重要。EMQX 默认的 1883 端口是明文 MQTT 协议,设备端如果走公网,建议使用 8883 端口开启 TLS。InfluxDB 也建议开启 HTTPS。不是说自己的测试环境也要这样做,而是方案要具备从测试平滑迁移到生产的能力,前期配置时提前把证书和相关配置的结构留出来,后面上线会省很多事。
日常运维要盯几个指标:EMQX 的连接数、消息速率、规则引擎命中数和失败数;InfluxDB 的写入延迟、磁盘使用率、series 基数。这些指标直接反映系统的健康度。规则引擎的失败数尤其重要,它是最好的预警信号,一旦规则处理链路出了问题,数据会悄悄丢失,而连接数和写入量表面上看都是正常的。
5. 常见问题与排查实录
5.1 EMQX 规则触发了但 InfluxDB 没有数据
这是最常见的坑。遇到之后不要慌,先打开 EMQX Dashboard 的规则页面,看规则命中数和成功数。如果命中数持续增长但成功数为 0,说明是动作配置的问题。常见的失败原因有几个:InfluxDB 地址写错、Token 无效、bucket 不存在、字段映射不对。最容易忽视的是字段类型映射问题,InfluxDB 要求同一个字段不能一会儿是字符串一会儿是数字,如果设备上报的数据里某个字段偶尔是"25"偶尔是25,EMQX 转出去后 InfluxDB 会直接拒绝写入。
如果规则命中数本身就是 0,那问题出在 SQL 过滤条件上。检查主题是否匹配,比如规则写的是sensor/+/data,但设备实际发布到sensor/dev_001/data,就应该匹配。还要注意 payload 里的字段名大小写,EMQX 规则里payload.TEMPERATURE和payload.temperature是完全不同的两个字段,这类问题肉眼很难发现,最好先在调试终端里看一条原始 JSON。
5.2 时间戳导致的数据错乱
时间戳是时序数据最容易出错的地方。设备端生成的时间戳是毫秒还是微秒?InfluxDB 写入时 precision 参数是ms还是us?这两者一旦不匹配,数据写入时会变得非常奇怪。最典型的表现是时间戳当作纳秒或微妙级别处理,数据点会被写到很远以后或者很久以前,查询range(start: -1h)什么都查不出来。
另一个问题是时区。设备可能分布在不同的时区,如果报文里传的是本地时间而不是 Unix 时间戳,写入 InfluxDB 后做聚合统计就会错位。我现在的习惯是强制统一:设备端上报的时间一律用 Unix 毫秒时间戳,服务端写入时明确指定precision="ms",展示层再根据用户所在时区进行转换。这样数据层干净,界面层灵活。
5.3 字段类型冲突和数据膨胀
InfluxDB 的 field 类型是动态推断的,但这个灵活性也容易埋雷。一个 field 第一次写入的是整数 25,第二次写入的是浮点数 25.0,通常问题不大;但如果第一次写入的是字符串"25",第二次写入的是数字 25,InfluxDB 会抛 field type conflict,导致后面的写入失败。解决办法是上报协议里明确字段类型约定,数值一律用数字,不要用引号包裹。
数据膨胀的问题则和 Tag 设计有关。InfluxDB 的数据点如果能区分度高的 Tag 越多,series 基数就越大,存储和查询开销都会上升。比如把消息里的动态值或者随机值设成 Tag,比如request_id,每个数据点的 Tag 值都不同,series 基数会瞬间爆炸。Tag 只放设备号、区域、数据版本这类低基数字段,高基数的内容放到 Field 里去。
5.4 客户端掉线和消息丢没丢
设备端 MQTT 客户端经常出现连接频繁断开的情况,这时候优先查网络层面和 EMQX 的 keep alive 配置。EMQX 默认允许的心跳周期有限制,设备如果经常休眠或者网络抖动,可能在心跳超时后被 broker 判定为离线。解决方法是设备端根据自身上报频率合理设置 keepalive 间隔,比如每 10 秒发一次数据,可以把 keepalive 设置成 30 秒到 60 秒,给网络抖动留出余量。
真正发现丢消息时,先确认发布端使用的 QoS。QoS 0 在设备断线时会丢,这是预期的;如果是 QoS 1 还丢,大概率是客户端没有正确处理重连后未确认的 QoS 报文,或者持久会话配置有问题。设备端日志里要打印完整 puback 结果,方便定位到底是在连接阶段、订阅阶段还是发布阶段出的问题。
5.5 写入慢和 CPU 高怎么排查
InfluxDB 写入慢,先看磁盘是不是瓶颈,时序数据库对磁盘随机写入要求高,机械盘和小 IOPS 云盘都容易成为瓶颈。其次是检查批量写入配置,如果开发和测试时为了调试方便用了同步逐条写入,生产环境流量一大必然堵。可以把监控直接开起来,观察 InfluxDB 面板的 Write 吞吐量和排队数。
还有一类隐蔽问题来自字段类型冲突和 series 膨胀。字段类型冲突会导致写入重试,表面上 CPU 不高但延迟很大;series 膨胀会导致内存和索引文件快速增长。遇到这类问题,先用show field key和一段查询看字段类型历史,再用基数统计看 Tag 分布,基本能定位。系统性能的问题大多是设计阶段埋下的,运行阶段只是暴露。
6. 实际项目中的经验体会与扩展思路
6.1 从单机到集群的扩展方向
单机的 EMQX 加上单机的 InfluxDB,能覆盖大多数中小项目。但设备量上来之后,要么优化单机承载,要么考虑扩展。EMQX 本身就支持集群部署,多节点共享订阅和路由,连接数可以横向扩展。InfluxDB 2.x 也有专门的集群版本,但日常项目里多数场景靠降采样和分层存储就能扛住很长时间。
我自己的体会是,架构升级不要贪多。先做数据治理,比如合理设计 Tag、批量写入、保留策略,通常能把成本压下来一大截。真的到了需要扩容那一步,再考虑集群和数据迁移。很多项目还没到大流量阶段,就把 Kafka、流计算、分布式存储全堆上,结果运维复杂度反而把团队拖垮了。
6.2 我踩过坑之后留下的几条原则
做这个类型项目多了,我慢慢形成了几条固定原则。第一,主题名按设备维度分层,一定用sensor/{device_id}/data这种可读性强的结构。第二,设备上报统一用 JSON,并且字段名固定,新增字段向后兼容。第三,时间戳由设备客户端生成,全部使用 Unix 毫秒。第四,接入 InfluxDB 后,所有 tag、field、measurement 的命名和类型在第一天就定死,之后不改。
每次接新项目,我会先花半小时在 EMQX 的调试页面手动发布一条模拟消息,确认规则命中和 InfluxDB 查询都正常,再开始写正式的设备端代码。这一步看起来多余,但能避开后面百分之八十的联调问题。整套 InfluxDB 加 MQTT 的组合,真正跑顺之后会非常省心,设备、broker、数据库各司其职,你要做的就是持续观察数据曲线,等待问题自然暴露出来。