封面:TDengine工业IoT全景,数据库与工厂元素,蓝色科技感

一条遥测数据的旅程:看懂 IoT 架构与 Docker 部署

导读: 物联网数据一天产生上亿行,MySQL 扛得住吗?本文从一个可运行的工业 IoT 参考项目出发,讲清楚时序数据库为什么适合物联网、系统分哪五层、怎么用 Docker Compose 五分钟拉起完整环境,再跟随一条遥测数据走完从模拟器到 Grafana 面板的完整旅程。

MySQL 扛不住 IoT 的一天

业务方说"数据量不大,就几万个设备",结果上线三个月,单表涨到几亿行,查询越来越慢,索引越建越多,磁盘越吃越满。这套剧本在物联网项目里反复上演。

物联网数据天生有三个特征,都踩在关系型数据库的痛点上:

时间有序。每行数据必带时间戳,写入基本是追加模式,几乎不做更新。

写多读少。设备持续上报,写入量是查询量的几百倍。

按时间窗口聚合。业务要的是"过去 5 分钟的平均温度"“最近一小时的最大压力”“每台设备的最新状态”。

把这三件事丢给 MySQL:千万行以上的单表,每次插入都要维护索引 B+ 树;没有自动按时间分区,查"昨天 14:00-15:00 的数据"要全表扫;窗口聚合得自己写子查询加 GROUP BY,性能随数据量直线下降。

而 IoT 场景一天就能产生上亿行。这不是 MySQL 的错,是工具选型的问题——你需要的不是一张更快的表,而是一个为时序数据设计的存储引擎。

氛围:数据库压力场景,数据洪流冲击传统表格

时序数据库的答案:TDengine 怎么解

TDengine 是国产开源时序数据库,它的核心设计几乎是冲着上面三个痛点去的。

超级表 + 子表。一张超级表定义 schema,每台设备自动建一张子表。查询按子表过滤,天然做了数据隔离,不用手动分表。

列存 + 压缩。时序数据同一列的类型高度一致,列式存储配合压缩算法,存储成本能降到关系型的十分之一以下。

预聚合LAST_ROW() 直接取最新值,INTERVAL 语法原生支持时间窗口聚合,不用写复杂的子查询。

vnode 分布式。数据按时间自动分片到多个 vnode,扩容时自动重分布,对应用透明。

打个比方:关系型数据库适合"先有结构再存数据"的业务,时序数据库天生为设备上报、监控曲线、趋势分析而生。两类引擎各有各的战场,关键是把数据放对地方。

项目五层组件全景

这个参考项目叫 tdengine-iot-practice,约 5000 行,纯 Java + Python,核心目的就是让你看懂"一套可运行的 IoT 系统长什么样"。

整个系统分五层:

组件 技术栈
部署 docker-compose 单机/三节点集群 TDengine 3.3.6.6 + PostgreSQL 16 + Grafana 11.1
建模 database/tdengine/001_schema.sql 3 张超级表:telemetry / vehicle_track / device_event
数据入口 python-services simulator + collector asyncio 模拟器(3 类场景)、MQTT/HTTP/SCADA 采集
写入 python-services writer WebSocket/REST/Native 三种、批量 + 背压 + 重试 + spool
查询展示 java-api + Grafana Spring Boot 双数据源、窗口聚合 API、Grafana 面板

注意一个关键设计:双数据库。PostgreSQL 管设备档案、告警规则这类元数据——生命周期长、变更少;TDengine 管传感器时序数据——持续追加、海量增长。两类数据生命周期不同,分开存是合理的架构决策。

信息图:五层架构图,部署/建模/数据入口/写入/查询展示,数据流箭头

五分钟拉起环境:docker-compose 逐段拆解

项目提供 deploy/single/docker-compose.yml,默认拉起三个容器:TDengine(6030 Native / 6041 REST-WS / 6060 监控)、PostgreSQL 16、Grafana 11.1。不用装任何本地依赖,Docker 就是全部环境。

命名卷tdengine-datatdengine-logpostgres-datagrafana-data 都是命名卷。普通 docker compose down 不会删数据,只有 down -v 才会清空。开发时想重置环境,记得用 -v

初始化挂载../../database/tdengine:/docker-entrypoint-initdb.d:ro——TDengine 容器首次启动时,会自动执行这个目录下的 SQL 脚本,建库建表一步到位。

healthcheck。TDengine 的健康检查用 curl -fsS -u root:taosdata -d 'select server_version()' http://localhost:6041/rest/sql,Grafana 通过 depends_on: tdengine: condition: service_healthy 等 TDengine 完全就绪后才启动。

profilesjava-apiappssimulatorsimulation。默认只起数据库三件套,用 --profile apps --profile simulation up 才把应用层带上。

版本用 ${TDENGINE_IMAGE:-tdengine/tdengine:3.3.6.6} 做环境变量覆盖,想升级版本改 .env 就行。

流程图:docker compose up 到容器就绪的步骤

初始化与冒烟验证:真实 SQL 长这样

环境起来后,跑 scripts/bootstrap.sh:先等 6041 就绪(wait_http /-/ping),然后逐个执行 database/tdengine/*.sql,再用 psql 执行 PostgreSQL 的初始化脚本。

验证用 scripts/smoke-test.sh,核心三步。

第一步,检查超级表是否建好:

SHOW iot.STABLES;

第二步,插入一条测试数据。注意这个 SQL 的写法,后面写代码时会反复见到——USING iot.telemetry TAGS (...) 是"按超级表模板建子表并写入"的语法:

INSERT INTO iot.d_smoke USING iot.telemetry TAGS
('smoke','smoke-product','factory-a','workshop-01','sensor','east')
VALUES (1750000000000, 25.0, 50.0, 220.0, 1.0, 220.0,
        NULL, NULL, NULL, 0.01, 1, 1750000000000);

第三步,查回来验证:

SELECT COUNT(*) FROM iot.telemetry WHERE device_id = 'smoke';

健康检查端点也备好了:curl http://localhost:6041/-/ping 看 TDengine,curl http://localhost:8080/actuator/health 看 Java 服务。环境起没起对,一条命令就能分辨。

实证:终端执行 smoke-test.sh 的输出截图

一条遥测数据的旅程

现在跟着一条温度数据走完整个系统。这条路径就是项目的骨架,理解了它,后面每一篇都是在某个节点上做深挖。

第 1 站:模拟器生成数据。 SensorDevice.generate() 生成温度值:24 + sin(日相位)×4 + 高斯噪声,每秒每设备一条。模拟器用 asyncio 跑三个场景(环境传感器、工业电机、车辆轨迹),模拟真实 IoT 设备的上报节奏。

第 2 站:进入有界队列。 pipeline.submit() 把数据放进队列,队列满时自动阻塞,这就是背压。下游写入慢了,上游自动减速,不会把内存打爆。

第 3 站:批量组装。 worker 攒够 1000 行或 0.2 秒超时(默认配置),就生成一条多表 INSERT SQL。批量写入是时序数据高性能的关键——一条 SQL 插 1000 行和插 1 行,网络开销差了两个数量级。

第 4 站:WebSocket 写入。 taosws.connect(dsn)cursor.execute(sql),走 6041 端口。项目默认用 WebSocket 而不是 Native 连接,客户端不用装 taosc 驱动,Docker 容器里直接跑,对部署极其友好。

第 5 站:TDengine 落库。 数据进超级表 telemetry,TAG 存 device_id 等 6 个属性,vnode 按时间分区存储。

第 6 站:Java API 查询。 GET /api/devices/{id}/aggregate?metric=temperature&interval=5m,Spring Boot 用 INTERVAL 做窗口聚合,返回 5 分钟粒度的温度曲线。

第 7 站:Grafana 面板。 数据源指向 http://tdengine:6041,可视化展示。

流程图:模拟器→队列→批量→TDengine→API→Grafana六节点

这条链路里藏着一个容易忽略的安全细节:子表名生成。项目用 d_{可读前缀}_{sha1摘要} 而不是直接把外部 device_id 当 SQL 标识符——防止设备 ID 里带特殊字符搞出 SQL 注入。这个点后面会专门展开。

写在最后:10 篇路线图

本篇是系列开篇,目的是建立整体认知。接下来 9 篇逐层深入:

  • 第 2 篇:超级表建模与子表命名策略
  • 第 3 篇:时间窗口聚合查询实战
  • 第 4 篇:批量写入与背压机制
  • 第 5 篇:数据保底:重试、落盘、回放
  • 第 6 篇:三种连接方式实测对比
  • 第 7 篇:模拟器与多源采集
  • 第 8 篇:Java API 与双数据源
  • 第 9 篇:告警规则引擎与工业案例
  • 第 10 篇:高可用集群与故障演练

读完这篇,你应该能回答三个问题:为什么 IoT 场景要用时序数据库?这个项目分哪五层?一条数据从生成到展示经过哪些节点?

你在用 MySQL 扛 IoT 数据时踩过什么坑?欢迎留言聊聊。

参考文献

  • TDengine IoT 实战仓库:deploy/single/docker-compose.ymlscripts/bootstrap.shscripts/smoke-test.sh(部署与验证脚本,均可直接运行)
  • TDengine IoT 实战仓库:README.mddocs/01-installation.md(安装配置与端口说明)
  • TDengine 官方文档:https://docs.tdengine.com/operation/deployment/docker/(Docker 部署参考)

更多推荐