多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

TB级煤机时序数据秒级查询:TDengine全链路优化实践

TB级煤机时序数据秒级查询:TDengine全链路优化实践 煤机设备的数据规模很多人是没有具体概念的。一台采煤机身上几百上千个测点温度、压力、振动、电流、油位这些传感器按秒甚至毫秒级回传矿上几十台设备跑一天集团所有矿井的数据汇总起来一晚上落几个 TB 是常有的事。天地奔牛这种煤机装备制造与运维的一线企业面对的就是这种每天都在涨的时序数据而他们最终选择了 TDengine硬生生把分钟级都出不来的查询压到了秒级。这篇文章我不打算写成产品宣传稿而是以一个参与过同类项目建设的技术人视角把这类“TB 级工业时序数据”场景下的选型逻辑、数据建模、写入链路、查询优化、部署运维这几个关键环节整个拆一遍。用到的方法和思路放到任何一个做工业物联网、设备状态监测、能耗监控的项目里都能直接参考。如果你现在正被 MySQL 大表慢查询折磨或者刚刚接触 TDengine 准备迁移这篇文章应该能让你少走很多弯路。1. 先搞清楚“每天 TB 级”的压力到底从哪来做技术选型之前最怕的就是把“数据量大”挂在嘴边却说不清楚量大在哪里、卡点是什么。所以我先带你把账算明白再看传统方案为什么顶不住。1.1 一个煤机场景的真实数据画像煤机设备和普通工业设备不一样它不是一个传感器排一个数据的简单模型。以采煤机为例牵引系统、截割系统、液压系统、冷却系统都有各自的传感器电机电流和功率这类电气量往往是几十赫兹采集温度、压力、流量这些缓变量1 秒一条就够了振动通道则是重头戏振动加速度信号要做到每秒几千个采样点一条通道的数据量能顶几百条温度数据。按一个集团下多矿井来估算假设有 3000 台在运行设备平均每台设备挂 400 个低频测点按 1Hz 采样每秒就是 120 万条数据。每条数据包括时间戳、设备编码、测点编号、数值、质量码算 40 字节一秒钟就是 48MB一天下来 4.1TB 左右。再加上部分重点设备的振动高频采集30 台设备每台 32 通道、每通道 2kHz每秒又增加约 192 万条一天再多 1.3TB。这个规模合计起来一天 5TB 级别是完全正常的。这里要注意我说的还是“原始数据”。实际生产里还要叠加设备启停记录、报警事件、PLC 脚本日志这些非规则数据。如果做了边缘端处理可能还要把预处理后的特征值一并入库。所以“TB 级”不是夸张而是这个行业的基本盘。1.2 老架构为什么在这个数据量下崩掉很多煤炭企业最初的数据库方案是 MySQL因为历史系统都是这么建的。设备数据进来之后按天建表、按月分库一开始几千台设备的时候确实能跑但等到测点一多、保留周期拉长问题就全暴露出来了。第一个问题是写入锁竞争。工业数据是持续的高并发写入InnoDB 的行锁和间隙锁在这种写密集场景下非常吃力主从延时会从几秒慢慢涨到几分钟从库查询的数据滞后到完全没有监控价值。第二个问题是查询性能断崖式下跌。一张几亿行的明细表就算建了索引做一次跨设备、跨时间段的 AVG、MAX、MIN 聚合MySQL 要全表扫或者全索引扫一个查询跑几十秒很常见BI 报表彻底变成“转圈大赛”。也有人转向 HBase、ES 这类分布式存储。它们确实能扛写入但查询建模很痛苦。HBase 要自己管理聚合逻辑ES 对时序数据的存储和压缩开销偏大而且都要求你会养一个分布式团队。对一个煤机装备企业来说使用成本高得离谱。1.3 为什么选 TDengine核心是模型匹配这个项目选择 TDengine最大的原因在于数据模型天然匹配。TDengine 的核心抽象是超级表、子表、标签翻译成工业语言就是“一类设备、一台具体设备、设备的静态属性”。煤机设备的数据本质上就是“每个测点按固定周期产生数值”这正是时序数据库的标准场景。TDengine 把存储、写入、查询、保留策略做成了一体化的东西不用像传统架构那样拿关系库和缓存拼拼凑凑。压缩效率也是实打实的优势。工业数据重复性极高电机电流在一段时间内变化很小TDengine 列式存储配合压缩算法在我实测过的项目里压缩比能做到 5:1 到 10:1。上面说的每天 5TB 原始数据落盘后可能只有 500GB 左右一年下来 100TB 以内的存储成本完全可控。至于网上经常有人吐槽“TDengine 太贵了”这里要说句公道话社区版是开源免费的单集群小规模部署完全够用天地奔牛这种体量社区版起步没有压力企业版是商业软件收费提供的多节点集群规模、高级权限管理、官方支持这些对比自研存储引擎的人力投入其实谈不上贵。贵的从来不是软件是自己折腾的隐性成本。2. 数据模型设计从 MySQL 宽表到超级表子表的关键一跳选型定了真正的工程量在设计数据模型。这是决定后续查询性能的基础也是网上“MySQL 表结构自动转 TDengine 超级表 子表”这类问题被反复讨论的原因。2.1 超级表、子表和标签到底怎么理解如果你刚接触 TDengine我建议用最朴素的方式理解超级表是一个“模板”它定义了这批数据有哪些列比如时间戳、电机电流、泵站压力、振动有效值子表是模板的一个具体实例每台设备建一张子表数据落在子表里标签则是贴在子表上的说明卡片记录设备编号、所属矿井、设备型号、生产厂家这些不变属性。为什么把静态属性拆出来当标签而不是当普通列两个好处。一是过滤效率高查询时先按标签把子表筛出来再在子表范围内扫描数据你查“某个矿所有采煤机最近 5 分钟的电流均值”数据库只需定位到十几张子表而不是扫描整张宽表二是存储冗余小标签不会跟着每条数据重复存单条记录只有时间戳和测点值体量立刻就小了。与之相关的“多个表时序一致”问题在 TDengine 里没有玄学。它不是分布式事务数据库跨子表写入没有多表事务保证也不需要那种保证。时序一致性靠的是采集端时钟同步、同一设备同一批次的数据一次性写入、以及查询时按 INTERVAL 时间窗口对齐这三板斧。只要采集网关的时间统一走 NTP并且一个设备的数据由同一个采集进程负责分发多子表在时间轴上的对齐是完全做得到的。2.2 建表实操字段注释和测点分组给一个我能直接用的建表样例。煤机设备按照“同一采样频率的测点放一张超级表”的原则拆分低频秒级数据放一张表高频振动数据单独放一张表避免一张表里既有 1Hz 列又有 kHz 列导致稀疏浪费。CREATE STABLE shearer_sec ( ts TIMESTAMP, motor_current FLOAT COMMENT 截割电机电流, motor_temp FLOAT COMMENT 截割电机温度, pump_pressure FLOAT COMMENT 泵站压力, flow_rate FLOAT COMMENT 泵站流量, vibration_rms FLOAT COMMENT 振动有效值 ) TAGS ( device_id NCHAR(32) COMMENT 设备编号, device_type NCHAR(16) COMMENT 设备类型, mine_id NCHAR(16) COMMENT 矿井编号, vendor NCHAR(64) COMMENT 设备厂商 );这条 SQL 里有几个细节值得注意。很多从 MySQL 转过来的人会忽略字段注释结果几十个测点全靠猜管维的人换一茬就没人知道哪个列代表什么。TDengine 支持 COMMENT 写列注释和标签注释这件事在建表之初就做掉后面能省一万个因为“这列是电流还是电压”而起的沟通成本。再有就是时间戳类型必须显式指定。TDengine 的主键就是时间戳它决定了数据分布和索引方式。工业场景统一用毫秒精度就够了秒级精度太粗微秒和纳秒又会吃得更多存储除非采集端确实有这个需求否则不要盲目上微秒。2.3 从 MySQL 迁移的步骤与自动转换思路MySQL 表要迁移到 TDengine先不要急着导数据先把表结构理清楚。我在实际项目中按这个顺序操作第一步找出所有同时含有“设备编码 时间”字段的表这些表才有迁移价值纯配置表继续留在 MySQL。第二步按采样频率分组把同频测点合成一张超级表这里可以通过列名前缀做初步判断比如vib_开头的归振动表temp_开头的归温度组。第三步生成建表语句顺序是建超级表、批量建子表、最后导数据。子表用“模板 标签”自动创建CREATE TABLE shearer_sec_001 USING shearer_sec TAGS (SH-001, shearer, M01, 奔牛智造);数据导出的思路也简单从 MySQL 直接查询生成 CSV按时间戳排好序再用 TDengine 的导入工具批量灌入。但这中间有三个容易踩的坑MySQL 如果存的 DATETIME 有时区必须先统一转换成 UTC自增 ID 不要带进 TDengine时序数据不需要它质量码 null 那些脏数据最好在导出阶段就清洗掉别把“修数”的工作拖到大表里做。2.4 单测点多行还是多测点宽列一锤定音的选型TDengine 常用的模型有两种一行一条测点的窄表模型ts, device_id, metric, value和一列一个测点的宽表模型ts, motor_current, motor_temp, ...。这是 MySQL 用户最容易纠结的地方。我的看法是工业设备监测数据只要测点集合相对固定就选宽表模型。原因有几点。宽表一次写入就是一台设备一个时间点的完整快照查询时算均值、峰值不用先按 metric 做行转列SQL 写起来优雅得多同一时刻的多个测点落在同一行列压缩率更高因为相邻列在物理存储上密集排列子表数量也少一张子表就是一台设备管理粒度清晰。窄表模型也不是一无是处它适合测点动态增减、不同设备测点差异大的场景。但代价是查询要频繁 group by metric性能损耗肉眼可见。所以我给出的原则是能用宽表就别用窄表。3. 写入链路设计每天 TB 级的数据怎么稳定落库模型设计好了接下来是写入链路。这是整个系统里最容易出幺蛾子的环节因为工业现场的网络抖动、设备重启、断电都会直接反映到写入数据上。3.1 完整的数据接入链路参考天地奔牛这类项目数据不会直接裸连数据库链路通常是现场设备 → PLC/传感器 → 边缘采集网关 → MQTT Broker / Kafka → 数据接入服务 → TDengine边缘采集网关负责把各种协议Modbus、OPC UA、CANopen统一转成标准 JSON再推到消息队列里做削峰。数据接入服务消费消息队列里的数据批量拼装成 SQL 参数写入 TDengine。消息队列在这条链路里不是可选项因为设备上报往往是突发性的比如换刀、启停瞬间会产生大量数据没有队列缓冲数据库会被瞬时流量打穿。TDengine 3.x 提供了 taosAdapter 这样的标准接入组件可以直接消费 Kafka 数据也可以走 REST 接口。实际项目中我建议技术栈这么分如果接入端是大数据生态Spark、Flink走 taosAdapter 消费 Kafka 最顺滑如果接入端就是自研的采集程序直接用原生连接器写入更高效。3.2 C API 与批量写入要点煤机设备边缘采集程序很多是用 C/C 写的因为要直接和 PLC 底层打交道。TDengine 的 C/C 连接器在这种场景下非常合适它走原生 TCP 协议性能好还支持参数绑定。一个典型的 C 代码片段长这样#include taos.h #include stdio.h int main() { taos_init(); TAOS* conn taos_connect(192.168.1.10, root, taosdata, NULL, 6030); if (!conn) { fprintf(stderr, connect failed: %s\n, taos_errstr(conn)); return -1; } TAOS_STMT* stmt taos_stmt_init(conn); // 绑定子表名 taos_stmt_set_tbname(stmt, shearer_sec_001); // 预编译插入语句 taos_stmt_prepare(stmt, INSERT INTO ? VALUES (?, ?, ?, ?, ?, ?), 0); // 绑定时间戳和字段值的参数数组循环赋值后执行 // taos_stmt_bind_param_batch(stmt, ...); // taos_stmt_execute(stmt); taos_stmt_close(stmt); taos_close(conn); taos_cleanup(); return 0; }这里核心的心法是批量和预编译。预编译语句编译一次、多次执行避免每条数据都走完整 SQL 解析批量绑定参数一次写几百行吞吐量比单条 INSERT 高一个数量级。我在现场调过一条经验单条 INSERT 写入大概每秒几千条就把应用压垮了改成批量 500 条一批之后单线程轻松每秒几十万条应用 CPU 反而降了。3.3 写入吞吐与存储容量的估算方法接需求的时候最怕销售或者业务方甩一句“我们数据特别大”然后什么参数都不给。你可以拿下面这个表当需求调研模板。参数项示例值设备数量3000 台平均低频测点400 点/台1Hz高频振动设备30 台32 通道/台2kHz单条数据大小低频约 40B高频约 8B日新增原始数据约 5.4TB压缩后日新增存储约 500GB ~ 1TB写入峰值速率120 ~ 300 万条/秒有了这个表设计保留策略就有依据。高频振动数据保留 30 天足够做故障分析低频数据保留一年到三年更老的数据用降采样后的小时级均值归档原始明细可以清理。TDengine 的保留策略是建表时用KEEP参数控制的比如高频表设置KEEP 30低频表设置KEEP 1095系统会自动删除过期数据不用人工半夜起来清库。3.4 写入调优与脏数据处理的实战心得这一个小节是拿来给真正要碰生产环境的人看的。调优重点有三个第一控制乱序率。TDengine 对乱序数据有合并机制偶尔乱序没问题但乱序比例高了会让写入性能明显下降。采集端必须保证同一子表的数据按时间戳递增堆送网关断电重启后要能缓存断点期间的数据恢复后按顺序补发。第二连接数管理。很多团队一上来就开一大堆写连接反而把 vnode 搞得很紧张。经验值是写线程数是 vnode 数量的 1 到 2 倍足够了多用批量参数方式压单连接吞吐少用多连接堆并发。第三数据质量码一定要入库。工业数据里“测得不准”和“设备故障”是两回事要在字段里留一个 quality 列写0表示正常、1表示传感器超限、2表示通讯中断补偿值。查询时可以忽略非正常质量的数据又不会丢失原始信息。这个设计早期不做后面数据清洗会让人哭都哭不出来。4. 查询提速到“秒级”的机制与实战 SQL 拆解标题里最让人兴奋的是“秒级”。但秒级不是变魔术而是由存储结构、索引方式、计算下推共同决定的。理解这套机制你才能在自己的项目里复现同样的效果。4.1 秒级查询背后的四个引擎能力第一是列式存储。TDengine 每列数据独立连续存储查询只读取需要的列比如算电流均值就只读motor_current那一列的数据块其他几百列完全不碰磁盘 IO 一下子少掉两三个数量级。第二是时间分区裁剪。TDengine 的数据按时间自动分片元数据里有每个数据块的起止时间范围。你查最近 5 分钟的数据引擎直接定位到对应时间片更早的分区连打开都不用。这就像查字典只看部首目录而不是从第一页翻起。第三是块内 min/max 索引。每个数据块在写入时就记录了该块内各列的最小值和最大值区间查询时如果查询条件与块的 min/max 无交集整块直接跳过。温度正常的设备大多数块的 min/max 范围远小于全表区间裁剪效率极高。第四是聚合计算下推。查询时用 AVG、SUM、MAX 这些函数不是把原始数据拉到应用层算而是在数据节点直接从压缩后的块里读取、计算只返回结果集。以前 MySQL 要 50 秒跑完的 1 小时均值聚合TDengine 是在存储层按时间窗口直接预聚合的秒回并不奇怪。4.2 高频查询的 SQL 现场演练我用三类最高频的查询做演示你在自己的环境里可以直接改着用。第一类是“这台设备现在是什么状态”配监控大屏或状态看板。这种查询要的是每个测点的最新值SQL 直接点子表并按 ts 倒序取一条SELECT last_row(motor_current), last_row(motor_temp), last_row(pump_pressure) FROM shearer_sec WHERE device_id SH-001;第二类是“某台设备某个时间段的运行曲线”。直接走子表点查时间范围用索引裁剪SELECT ts, motor_current, motor_temp FROM shearer_sec_001 WHERE ts 2025-06-01 08:00:00 AND ts 2025-06-01 09:00:00;第三类是跨设备统计比如按分钟统计一个矿井所有采煤机的电流均值用超级表加标签过滤加时间窗口SELECT _wstart AS win_start, avg(motor_current) AS avg_current FROM shearer_sec WHERE mine_id M01 AND ts 2025-06-01 00:00:00 AND ts 2025-06-01 01:00:00 INTERVAL(1m);这类查询在 TDengine 上的表现基本就是“数据量不影响查询速度只影响扫描范围”因为窗口聚合是逐块完成的压缩过的数据解压也算增量开销总体非常可控。4.3 写入后立即读取临时数据的可见性问题网上经常有人问“TDengine 保存临时数据马上读取会不会读不到”这个我专门实测过。TDengine 的写入流程是数据先写内存和 WAL 日志客户端收到成功返回即代表数据已经进入可查询状态不需要额外调 FLUSH 才能读到。查询引擎看到内存中有这段数据就会直接返回这在 3.x 版本上是很稳定的事。真正会让你“读不到”的反而是一些架构层面的坑。比如写入进程和查询进程如果共用同一个带本地缓存的连接某些早期版本的客户端可能因为连接内事务一致性处理导致查询端看不到最新写入换成服务器端查询或者重连基本就好了。另一个坑是时区不一致应用服务器和 TDengine 服务端时区不同写入时时间戳带偏移查最近 1 分钟的数据就会出现“刚刚写入的查不到”的错觉。统一用 UTC 或者统一设置中国标准时间问题就不存在了。如果你对“崩溃不出问题”有硬要求比如不能因为断电丢数据那就在建库时设置 WAL 同步级别为强同步wal_level 2这样每次写入都要等 WAL 落盘再返回吞吐会降一些但数据安全等级更高适合告警事件这类绝对不能丢的数据。4.4 查询慢的真正原因排查思路如果哪天你也遇到“TDengine 查询没有想象中快”的情况不要慌按顺序排查。最常犯的错是SELECT *一把梭几百列读了个遍其次是查询条件没有带时间范围导致全量分区扫描再就是聚合粒度太细比如对一年数据做 1 秒级窗口这种量级任何数据库都救不了必须先降采样再聚合。还有一点要提醒TDengine 的EXPLAIN命令可以看到执行计划判断有没有走正确的标签过滤和时间裁剪。生产问题 80% 出在建模和 SQL 习惯上而不是引擎本身。5. 部署安装、集群配置与常见坑点排雷最后说部署和运维。这部分的经验不只是从天地奔牛这类项目来的也是我自己在实际环境里一步步踩出来的。5.1 社区版安装与 Windows 环境的那点事TDengine 社区版的安装在 Linux 服务器上其实是非常省心的。官方提供 RPM 和 DEB 包CentOS / Ubuntu 都能直接装装完用systemctl start taosd启动服务然后执行taos进命令行验证即可。如果是源码编译或者 tar 包注意配置/etc/taos/taos.cfg里的firstEp和fqdn单机测试默认配置通常不需要改。Windows 环境我多说两句因为网上讨论“tdengine windows 集群”特别多。TDengine 的服务端主力支持 LinuxWindows 上装服务端用于开发和功能验证没问题但生产环境我强烈不建议在 Windows 上跑集群。Windows 集群在文件句柄管理、网络性能、服务守护这些方面都没有 Linux 成熟稳定性差距明显。很多团队都是在 Windows 开发机上装了社区版测试 SQL 和功能然后部署到 Linux 服务器跑生产这没问题。Windows 上装客户端taos shell、C/C 驱动倒是很常规连接 Linux 集群没有任何障碍。5.2 生产环境问题排查速查表我把这一路踩过的常见问题整理成了一张表基本能覆盖运维初期 90% 的困扰。现象可能原因解决思路写入报内存不足vnode 缓存配置过小或子表分布不均调大 cache检查表数量是否集中在少数 vnode刚刚写入的数据按时间查不到时区设置不一致或查询窗口不对统一服务端与客户端的时区配置核对查询时间范围大批量迁移后数据对不上时间戳精度不一致毫秒被截断或微秒被忽略迁移时统一时间戳字段为毫秒级 INT64高频振动数据写入变慢乱序比例过高触发合并采集端按时间戳排序后批量提交乱序数据先本地排序C 连接在 Windows 上报 DLL 错误编译器版本与连接器库不匹配确认使用官方对应 MSVC 版本的连接器并正确加载 taos.dll磁盘增长过快KEEP 保留策略未设置或未按频率分表高频表保留 30 天低频表设置 1~3 年查询执行计划里全表扫SQL 没带设备标签过滤或时间范围使用超级表标签过滤 明确的时间窗口5.3 迁移过程中最容易忽略的四个细节迁移项目大多不是死在技术难度上而是死在细节上。第一个细节是建表顺序。不少人习惯先导数据再建索引TDengine 恰恰相反必须先建好超级表和子表再导数据否则每条数据都要现判断目标表存不存在性能惨不忍睹。第二个细节是 CSV 文件里的时间格式导入前按时间戳排好序否则乱序处理会拖慢整个导入过程。第三个细节是账号权限社区版默认的 root/taosdata 不要拿来做应用账号应用账号按读写分离创建避免误删和权限泄漏。第四个细节是监控告警TDengine 的taosMonitor或外部 Prometheus 采集器最好从第一天就接上磁盘空间、写入延迟、vnode 数量这些指标都有对应的 SQL 视图别等出了问题才去翻日志。迁移完成后的验收也不是查几个数就完了。我会建议做三类验证写入性能压测用taosBenchmark模拟生产写入速度、查询性能抽样把业务方反馈最慢的三条查询单独跑一遍、数据完整性核对按设备号和时间为维度抽样比对 MySQL 和 TDengine 的 count 和 sum。这三步过了迁移基本就算稳了。最后分享一点我个人在这个项目里的体会工业时序数据的建设工具选型当然重要但真正的分水岭在于你有没有把数据模型想清楚。天地奔牛这个场景能跑到秒级不是 TDengine 单独施了什么魔法而是从采集端把测点分好了组、在建表时把标签和字段注释做规范了、在写入端把批量和乱序控制住了、在查询端把窗口聚合和过滤条件用对了这四件事叠在一起才换来了最终的效果。如果再往前看一步这类 TB 级数据平台的价值绝不只在查询变快。数据进了 TDengine等于把设备全生命周期的运行状态都沉淀下来了。后面接设备健康评分、故障预测、备件寿命分析都是顺理成章的事。我个人的建议是先跑通数据采集和秒级查询这个底座再逐步往上加分析和预测应用。地基打牢了上层建筑有的是发挥空间。
返回列表