做煤机装备数据平台这些年我见过不少项目一步步被海量时序数据压垮——采煤机、刮板输送机、液压支架上的测点越来越多采样频率一提再提一天下来就是 TB 级的数据量。也是在这个背景下天地奔牛的煤机数据平台引入了 TDengine把曾经要等几十秒的查询直接压到秒级。这个案例我关注了很久也反复在自己的项目里验证过里面的技术细节今天把整套链路从头到尾拆一遍聊聊为什么能这么快、落地时有哪些坑。1. 煤机数据一天 TB 级传统数据库先从哪个环节崩掉1.1 数据从哪来一个智能综采工作面到底有多少测点天地奔牛的主业是刮板输送机、转载机、破碎机这些煤矿综采设备。以前大家对煤机的认知是傻大黑粗现在完全不是了。一台刮板输送机上面电机有电流、电压、功率减速器有轴承温度、油温、油压链轮轴有转速、振动链条还有张紧力和链速再算上冷却水流量、液位这些辅助测点单台设备轻松到一两百个测点。一个综采工作面不是只有一台设备采煤机、刮板输送机、转载机、破碎机、乳化液泵站、移动变电站往那一摆几十台设备是常态。更麻烦的是采样频率工艺量温度、压力、液位秒级采集就够了振动信号为了做故障诊断采样率动不动就是 50Hz、100Hz甚至更高。算一笔账你就明白了假设一台设备挂 200 个测点其中 80 个高频点按 50Hz 采集每个点 4 字节一天的数据量就是 80 × 50 × 86400 × 4约 1.33GB加上低频点和设备信息一台设备一天能到 1.5GB 左右。一个工作面按 30 台设备算一天就是 45GB。赶上多个工作面同时上传、再叠加历史数据补传每天的写入量破 TB 真不是夸张。1.2 关系库和离线数仓为什么先扛不住我见过太多团队一开始用 MySQL 硬扛。前三个月没问题半年之后单表行数过亿问题就开始集中爆发。第一是索引失效。B 树索引在数据量上来之后维护成本急剧上升写入要更新索引查询要扫索引两边抢资源。煤机数据又是典型的只追加、按时间读MySQL 的通用索引设计根本不为这种访问模式服务。到了后期一个简单的查某台设备最近一小时电流曲线都能卡好几秒。第二是全表膨胀后的运维噩梦。单表十几亿行想清理一个月前的老数据一条 DELETE 下去直接把主从拉垮。于是开始分库分表按设备分表、按月份分表分完发现跨设备统计查询变成噩梦业务 SQL 全得重写。也有人说用 HDFS Hive 的离线数仓不就完了。问题是离线链路有天然延迟数据从设备端到 KafKa 再到 Hive最快也是分钟级落地。集控中心的工程师想看的不是昨天的报表是这会儿这台减速器温度是不是异常。一个查询任务扔到 YARN 上排队等 Spark 作业起来半分钟一分钟过去了操作工早就不耐烦了。2. 选型那阵子为什么是 TDengine 而不是 InfluxDB 或 OpenTSDB2.1 工业现场对时序数据库的真实要求我在帮企业做技术选型时最看重的不是某个大厂的背书而是这四条部署运维能不能压到最低、写入吞吐能不能跟上现场采集、查询交互能不能做到秒级、开发团队能不能快速上手。煤矿企业的 IT 团队通常规模不大现场还有严格的网内隔离。你给他们上一套 OpenTSDB得先部署 HBase、ZK、HDFS光协调这些组件的版本就够折腾一个月。InfluxDB 的 TICK 栈也是看似全家桶真到了高写入下还得调 shard、调 compaction没两个专职运维根本玩不转。2.2 几类候选方案的现场横向对比我把当时筛选的几个方案摆在一起看方案部署架构写入模型查询接口在煤机场景的体感MySQL 分库分表多实例 中间件随机写入、索引维护重SQL亿级行后性能跳水运维复杂InfluxDBTICK 栈多服务自带写入协议InfluxQL/Flux生态可以但集群版是商业项Flux 学习成本高OpenTSDB依赖 HBase/ZK/HDFS写 HBaseHTTP/JSON组件太多实时聚合能力弱TimescaleDBPostgreSQL 扩展普通行存 hypertableSQL混合负载好用但单节点高写入吞吐有瓶颈TDengine单进程或轻量集群原生高性能写入 参数绑定标准 SQL部署轻、压缩高、窗口聚合内置当时有几件事让我倾向 TDengine。第一是超级表这个设计。煤机场景天然就是一类设备、多台机械每台设备的数据结构一样只是设备编号和所属工作面不同。TDengine 用一张超级表管理同一类设备每台设备一张子表查询时按标签过滤直接命中一组子表这个模型和煤机设备的物理世界完全对应省掉了很多抽象建模的工作。第二是部署简单到不像大数据系统。一个 taosd 进程就是全部测试环境我用一台 4 核 8G 的机器几分钟就搭起来了。生产环境也就是按节点数规划几台服务器不需要单独配 Hadoop 生态运维压力比关系库方案还轻。第三是 SQL 兼容性好。团队里都是写惯 MySQL 的 Java 开发TDengine 提供标准 SQL 接口和 JDBC 驱动业务系统里可以直接把它当数据库用。后来我们还聊过在若依这类后台管理框架里把 MySQL 和 TDengine 混合接入JDBC 这一层就解决了开发不用换工具链。3. 落地建表与采集写入从传感器一路到超级表3.1 建模原则一张超级表对应一类设备TDengine 里有超级表和子表两个概念落地之前一定要先想清楚映射关系。我踩过建模的弯路这里直接给正确做法一类物理设备建一张超级表每一台具体设备建一张子表。比如刮板输送机建一张scraper_monitor超级表全局标签放所属矿井、工作面编号、设备编号、设备型号这些静态信息普通字段放所有测点值。这样整个矿的所有刮板机共用一张超级表要查某台设备直接定位子表要查某个工作面所有设备就按face_id标签过滤。千万别反过来设计——把设备编号当普通字段存进一张大表里。那样的话所有设备的数据混在一个表里写入热点集中查询时还要通过字段过滤去圈定设备效率会差一个量级。3.2 建表 SQL 和字段注释别偷懒建表 SQL 大概长这样CREATE STABLE IF NOT EXISTS scraper_monitor ( ts TIMESTAMP, current_a FLOAT COMMENT 电机A相电流 A, current_b FLOAT COMMENT 电机B相电流 A, gearbox_temp FLOAT COMMENT 减速器轴承温度 ℃, chain_speed FLOAT COMMENT 刮板链速 m/s, vib_rms FLOAT COMMENT 减速器振动有效值 mm/s ) TAGS ( mine_id INT, face_id INT, device_no NCHAR(32), device_model NCHAR(32) ) COMMENT 刮板输送机实时运行数据;有几个细节值得说。字段类型上工业测点用 FLOAT 还是 DOUBLE 要提前定好。振动、电流这类精度要求不高的用 FLOAT 就够能省一半存储压缩率也更好。标签类型尽量用 INT 或短 NCHAR别在标签里塞长字符串标签会被反复扫描和匹配太长会拖慢过滤。字段注释 COMMENT 一定要写。煤机设备的数据字典往往在厂家手里现场工程师换一拨之后注释是唯一的语义约定。我在项目里吃过亏接手一套建好的表字段叫val1、val2没人知道哪个是温度哪个是振动最后只能翻采集程序的源码去对。子表命名建议带上设备编号后缀比如scraper_monitor_mc101查询时一眼就知道是哪个设备。TDengine 创建子表不需要预先建好写入时用USING子句自动创建也可以但生产环境我更推荐提前批量建好避免运行期频繁建表带来的元数据操作。3.3 C 采集端用参数绑定写入的细节天地奔牛的数据采集端是典型的 C/C 网关程序PLC 数据到站之后先做解析、清洗再写入 TDengine。这块最有价值的经验是写入一定要用参数绑定不要拼 SQL 字符串。很多新手第一次写 TDengine 采集程序图省事直接循环执行INSERT INTO ... VALUES(...)每条数据拼一个字符串发一个请求。数据量小没事每小时几十万条的时候就顶不住了。我用 taos_stmt 接口改造之后写入吞吐翻了一倍不止。核心逻辑是准备一条带?占位符的 SQL然后反复绑定参数、攒批、批量执行taos_stmt* stmt taos_stmt_init(taos); const char* sql INSERT INTO ? USING scraper_monitor TAGS(?,?,?,?) VALUES(?,?,?,?,?,?,?); taos_stmt_prepare(stmt, sql, (int)strlen(sql)); // 按字段顺序绑定标签和值 taos_bind_t params[11]; // ... 设置类型、长度、指针 ... // 每来一条测点数据绑定一次参数并加入批次 taos_stmt_bind_param(stmt, params, 11); taos_stmt_add_batch(stmt); // 满一批比如 5000 条后统一执行 if (batch_count 5000) { taos_stmt_execute(stmt); taos_stmt_clear_batch(stmt); batch_count 0; }为什么参数绑定能快这么多道理很简单省掉了字符串拼接和重复解析。拼 SQL 意味着每条数据都要在客户端把数字转成字符串服务端还要重新解析一遍。批量执行则能把几千条请求压成一个网络请求极大地减少网络往返也就能充分利用 TDengine 的顺序写入能力。批量大小我实测下来 3000 到 5000 条一批比较合适。太小了网络往返还有浪费太大了单条失败重试范围也会变大恢复成本高。另外采集程序是很多台设备共用一个写入连接还是每台设备一个连接要看现场规划。我倾向每组设备用一个连接连接内部按设备维度做好了顺序写入避免同一张子表被多个线程并发写造成锁竞争。4. 查询提速到秒级的核心窗口聚合、标签裁剪与时序对齐4.1 最常跑的三种查询长什么样数据落地之后业务端查询其实就三类。第一类是单台设备的历史曲线最基础也最频繁。操作工想看 4 小时前这台刮板机的电流是否有尖峰SQL 很简单SELECT ts, current_a, gearbox_temp FROM scraper_monitor_mc101 WHERE ts NOW - 4h ORDER BY ts ASC;这类查询在 TDengine 里基本是毫秒级因为每台设备一张子表时间戳又是主键按时间切片直接命中很小的数据块不用全表扫。第二类是跨设备的聚合统计。比如整个 3 号工作面所有刮板机过去一小时的平均电流和最高温度SELECT AVG(current_a) AS avg_cur, MAX(gearbox_temp) AS max_temp FROM scraper_monitor WHERE face_id 3 AND ts NOW - 1h PARTITION BY device_no INTERVAL(1m);PARTITION BY device_no是先把结果按设备分组再对每组做INTERVAL(1m)的窗口聚合。这是 TDengine 里很核心的用法它直接在存储引擎内把每台设备的原始数据压成分钟级的均值/极值业务端拿到的已经是浓缩后的结果。第三类是跨设备之间的对比分析比如把几台减速器的温度放在同一张图上但每台设备的上报间隔、延迟都不一样直接拉出来画图会发现时间轴对不齐。这就是热搜里常说的多个表时序一致问题需要显式对齐和补齐。4.2 为什么 INTERVAL 窗口聚合能把分钟级拖回秒级很多人以为查询快只是靠索引。TDengine 能到秒级的真正原因是它的存储布局和查询执行是围绕时间维度设计的。数据按时间戳排序写入文件按时间分块查询的时间范围能精确落到某几个块上而不是全表扫描。列式存储让SELECT gearbox_temp只需要读这一列的数据块把电流、链速等其他列跳过I/O 量直接砍到几分之一。更关键的是像AVG(current_a)这种窗口聚合不是把原始数据导到应用层算的而是在存储引擎里一边扫数据一边算。比如一小时的原始数据有几百万个点聚合成分钟窗口后只有 60 行返回给应用端。传输的数据量差了四个数量级这就是从等半分钟到秒开的秘密所在。所以在设计查询语句时能聚合就聚合能降采样就降采样不要一上来就SELECT *把海量原始点拉回应用内存再算。我见过太多慢查询根本不是数据库慢是 SQL 写得太粗暴把不该传输的数据全传了。4.3 跨设备时间对齐FILL 和 ALIGN 的正确用法煤机设备的数据不会像数据库一样整整齐齐。有的设备上报延迟 2 秒有的设备中间断采了 10 秒直接放在同一个时间窗口里做对比结果就会错位。这时候要用插值和填充把时间轴对齐。比如要把 10 分钟内每台设备的链速都补成 10 秒一个点缺失值用前一个有效值补SELECT ts, device_no, AVG(chain_speed) FROM scraper_monitor WHERE face_id 3 AND ts NOW - 10m PARTITION BY device_no INTERVAL(10s) FILL(PREV);FILL(PREV)表示用前一个非空数据填充当前窗口适合链速这类缓变量。振动、电流这种快速变化的量用FILL(PREV)反而会画出错误的长时间平直线这时更要考虑FILL(NONE)或者用线性插值具体要看业务语义。还要注意时区问题。TDengine 里 TIMESTAMP 存的是 Unix 毫秒时间戳本身不带时区。查询时如果客户端和服务端时区不一致按天、按小时做窗口聚合会出现偏移一个小时的怪现象。统一在采集端和服务端都把时区设成Asia/Shanghai再在连接参数里指定时区能避免这类看起来没毛病、实际上对不齐的坑。5. 上线过程中的坑写入吞吐、慢查询与数据修正5.1 写入吞吐上不去先查批处理和绑定我们第一次压测时写入吞吐死活上不去服务器 CPU 没满采集程序 CPU 也没满数据就是在积压。排查链路是这样的先用taosCLI 手动插入几千条随机数据发现秒级完成说明服务端没瓶颈。接着看了采集程序的网络包发现一秒发出去一堆小请求每个包就带几条数据——问题就出在拼接 SQL、逐条执行上。换成taos_stmt_prepareadd_batch批量执行后同样一批数据积压马上消失。这个坑太典型了给同行的建议就是先看是不是逐条写入再优化数据库参数顺序搞反了会浪费很多时间。5.2 慢查询定位时间范围和标签裁剪上线后偶发出现某个查询要跑十几秒排查时打开 TDengine 的慢查询日志把阈值设成 3 秒很快就抓到几条异常 SQL。问题出在一个统计页面上它把高频振动原始数据全部拉出来用通用窗口函数算峰值和均值。高频点一个设备一天就是几百万行跨 30 台设备、查整整一天再好的存储引擎也扛不住。正确的做法是特征值前置。振动传感器的原始数据在采集端就做 RMS、峰值、峭度这些特征提取落库的是已经降维的特征值查询端只碰这些低频特征值表。需要追溯原始波形时才按极短时间窗口去查原始数据表。把查阅原始高频信号和日常统计分析分成两条链路查询速度自然就起来了。另外检查 SQL 里有没有在WHERE条件中对字段做运算。比如WHERE ts % 60 0这种写法等于逼数据库放弃时间索引做全量扫描属于用法问题。5.3 历史数据迁移与修改删除的正确姿势从旧平台迁历史数据最容易翻车的是时间戳格式和时区。旧的 MySQL 库里时间存在VARCHAR格式是YYYY-MM-DD HH:mm:ss还没带时区。直接导入 TDengine 必须先统一转成TIMESTAMP否则查询时会出现数据整体偏移。我用的是分批导出的程序每批 10 万条通过参数绑定导入同时在导出时就把时间统一转成对应时区的毫秒时间戳。千万不能图省事让 TDengine 在写入时做字符串解析性能差不说格式一乱后面查数据全是坑。至于改数据和删数据工业系统里偶尔是要做的比如某个传感器故障产生了一批脏值或者 PLC 时间跳变导致数据错乱。TDengine 支持UPDATE和DELETE但它们都有前提必须在 WHERE 里指定主键时间范围不能像 MySQL 那样全表更新。清理某个时段的历史数据直接删除对应时间分区比逐条 DELETE 高效得多运维脚本里多利用这个能力就行。6. 上线后的真实体感与给同行的建议6.1 性能数据和存储压缩的实测体感整个平台跑起来之后体感上的变化是非常直接的。单条设备曲线查询从原来 MySQL 时的几秒甚至十几秒降到一两百毫秒。整个工作面的分钟级聚合统计原来用离线数仓跑一个任务等好几分钟现在秒级出结果。集控中心的大屏滚动着一堆实时曲线操作工来回切换设备看趋势再也没有转圈的等待。存储侧感受最明显的是压缩率。煤机数据以浮点为主TDengine 的浮点压缩效果很好几十 TB 的原始数据真正落盘后不到十分之一的存储空间。这意味着原来只能保留一周原始数据的磁盘现在可以留一个月历史回溯能力大幅提升。维护的工作量也下来了。之前 MySQL 要操心分库分表脚本HBase 方案要担心 Region 分裂和集群熔断现在就是盯住几个 taosd 进程的磁盘、CPU、内存配合慢查询日志一张监控面板就够。6.2 几个值得提前规划的设计决策复盘整套落地过程有几个决策如果重新来一次我会在一开始就做第一高频信号一律特征化后再落库。原始振动数据表只保留最近几天用于故障复现和算法验证日常状态监测全部走特征值表。这个决策越早做后续查询模型越干净。第二标签只放静态维度。设备型号、矿井、工作面编号、投运日期这类稳定的属性适合做标签。电流、温度这些实时变化的值必须放字段。如果把动态值塞进标签每次更新标签都是一次元数据操作高频场景下会拖垮写入。第三时间同步要在采集链路的源头解决。设备端时间不同步后面用什么数据库都救不了。井下设备要保证和集控中心统一对时采样时间戳尽量在边缘网关里统一打点别依赖每台 PLC 各自的时钟。实测下来很多多表时序对不齐的问题根子就在源头时间戳不齐。6.3 和现有业务系统集成的一点经验天地奔牛的平台端是典型的 Java 技术栈业务管理系统用的还是 Spring Boot 体系。TDengine 提供标准 JDBC 驱动连接串形如jdbc:TAOS://ip:6030/dbname用起来和 MySQL 驱动很相似已有的数据访问层稍加改造就能接入。所以像若依这类后台框架里混用 MySQL 做业务数据、TDengine 做时序数据的架构落地难度并不高关键是把实体模型和 SQL 方言理清楚。我个人的习惯是业务单据、用户权限、组织架构这些变化但不海量的数据留在 MySQL凡是带时间戳、按设备维度持续追加的数据全部进 TDengine。这样两边各司其职不会出现一个库里什么都塞最后什么都慢的局面。做工业时序数据平台最踏实的成就感不是看到某个跑分数字有多高而是之前那个查个曲线要等半分钟的老系统被替换掉的时候现场操作工很自然地说了一句这个快多了。如果你现在还用关系库硬扛 TB 级煤机数据看到查询延迟一天天变长真的可以认真考虑走一遍这条路——先把建模想清楚再让采集端用参数绑定把数据送进去查询侧多用窗口聚合和标签裁剪你会发现所谓的秒级从原理到实现都不玄乎就是存储模型和查询模型想明白了。