ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从MySQL到TDengine:智慧水务时序数据建模与迁移实战

从MySQL到TDengine:智慧水务时序数据建模与迁移实战 1. 为什么水务数据非得上时序数据库做了八年多的水利水务信息化项目说实话数据量从来不是一开始就吓人的那种大而是温水煮青蛙式地涨上来的。前年我们给嘉环科技做智慧水务平台的底层数据层改造压力点、流量计、水质监测站、泵站设备一张张表叠进来MySQL 从刚上线时的轻轻松松到半年后慢查询日志里全是SELECT AVG(pressure) ... GROUP BY DATE_FORMAT(create_time, %Y-%m-%d %H:)我就知道这架构差不多到头了。1.1 先看一组把我们压垮的真实数据量一个区县级水务项目规模不算夸张大概是这样压力监测点 300 个流量计 150 个水质在线监测站 40 个泵站和阀门状态点约 200 个。采集频率分别是压力点 1 分钟一次、流量计 15 分钟一次累计值、水质 1 小时一次、设备状态秒级心跳。算下来一天多少条压力点300 点位 × 1440 条/天 43.2 万条流量计150 × 96 1.44 万条水质站40 × 24 0.1 万条设备状态200 × 86400秒级 1728 万条这个量级我们后来直接做了预处理只在状态变化时落一条降到每天约 2 万条加总大约每天 45 万条一年 1.6 亿条MySQL 放进 1.6 亿行以后单条按时间范围查询还能忍但一旦涉及全点位聚合、同比环比、小时级降采样查询延迟直接奔着十几秒去。更痛苦的是存储同样的数据量MySQL 要占将近 2TB因为每行还得存主键索引、create_time 索引、device_id 索引。而时序数据库天生按列存储、按时间分区同样的量在 TDengine 里做完压缩连十分之一都不到。1.2 时序数据难搞的三个特点很多人觉得水务数据不复杂无非是压力、流量、水位、水质。但这类数据有三个共性特点正好是传统关系库最不擅长的地方第一写入模式极其单一。全是按时间顺序追加极少更新。你基本不会改一条昨天的压力记录只会查它、聚合它。MySQL 的 B 树索引在这种场景下是负资产每次写入都要随机维护多棵索引树而 TDengine 的写入模型是顺序追加写入吞吐量天然高一个量级。第二查询全是范围扫描加聚合。业务方要的从来不是“查某一条”而是“这个测点过去 24 小时的平均压力”“这条管线所有点位的最高水位”“过去一个月每天的流量累计”。这类查询在 MySQL 里全靠扫索引回表再在应用层写循环TDengine 里一句INTERVAL(1h)就完成了降采样。第三数据生命周期明显。一周前的原始数据就没人再看了但日报月报还得能出所以需要自动降采样和过期清理。TTL 保留策略在 MySQL 里要自己写定时任务 DELETE在 TDengine 里是数据库参数自带的能力。1.3 时序数据库与传统关系库的核心差异拿我们自己的对比测试来说同样 1 亿条数据同样查“最近 7 天每个监测点的小时均值”结果差距不是一点半点。对比项MySQLInnoDBTDengine写入吞吐约 5000 条/秒单机约 20 万 条/秒单机1 亿条占用空间约 180GB约 10GB压缩后7 天小时均值查询8~15 秒毫秒到秒级自动过期无需任务删除KEEP 参数控制降采样应用层实现连续查询/流式计算所以当嘉环科技那边提出来要“平台扛住五年数据、查询秒级返回”时我几乎没有犹豫就把 TDengine 列进了方案。不是因为它最流行而是这个场景它就是最顺手的工具。后面的事实也证明整个数据层替换下来最耗时的不是写入和查询反而是模型的重新设计。2. 嘉环智慧水务的整体链路TDengine 到底负责哪一段很多同行容易把时序数据库理解成“一个大容量 MySQL 替换品”其实时序库在智慧水务项目里承担的是一个明确的职责边界理解这个边界比学会建表重要得多。2.1 从采集到应用的完整链路嘉环科技这个智慧水务平台数据链路分成五层终端感知层压力变送器、电磁流量计、超声波液位计、在线水质仪、泵站工控 PLC。这些设备通过 4G/NB-IoT/有线网络把数据传到前置机。接入汇聚层前置采集服务我们自己写的采集网关负责协议解析。水务行业协议五花八门MODBUS-RTU、DL/T 645、HJ 212 都有这一层统一转成 JSON 格式打上时间戳和设备编号标签。数据处理层Kafka 做缓冲削峰数据清洗服务做异常值剔除负压力、超量程数据直接丢弃或者标记然后把有效数据落到 TDengine。存储计算层TDengine 负责所有时序数据的存储、降采样、连续查询。我们后来还把设备状态量的实时判断逻辑也挪到了 TDengine 的流计算里不再单独部署一套流处理引擎。应用呈现层Web 端的大屏、移动端的巡检工单、数据报表系统全部通过 REST API 从 TDengine 或者其他业务库里取数。TDengine 的位置非常清晰只要是带时间戳的、点位维度产生的测量值一律进 TDengine业务属性数据比如用户档案、工单、设备台账仍然留在 MySQL/PostgreSQL。这个分工一开始就定死后面才不会乱。2.2 时序库在其中的职责边界有的团队会把设备的配置信息也塞进时序库的标签里我建议不要做过头。TDengine 的标签适合存查询维度的属性比如测点编号、所属水厂、管径、安装位置不适合存频繁变化的属性比如设备维修状态、责任人、保养记录。标签在 TDengine 里是按值存储的频繁修改会导致元数据重复写而且历史数据的标签取值会被覆盖成最新值这点我们后面踩过坑会在第四节细讲。另外时序库不应该承担复杂关联查询。水务业务经常要查“某个片区的压力均值与这个片区同时段供水量的关系”这种关联如果硬在 TDengine 里做跨库 JOIN 很难受。我们的做法是TDengine 只输出各点位的时间序列聚合结果业务层拿到结果后在应用内存里做二次加工或者干脆把聚合结果再同步到 MySQL 的业务宽表里。分工明确以后TDengine 的性能优势才能完全发挥出来。2.3 超级表 STable 解决建模痛点TDengine 里最有价值也最需要理解的概念就是超级表STable。简单说它就是一类设备的模板每个具体物理点位是一张子表点位固化的属性是标签TAG采集的数据是普通列。同样的压力监测场景如果建一张 MySQL 大表要靠WHERE device_id xxx AND create_time BETWEEN ...来过滤。而 TDengine 里每个压力点是一张独立的子表查询单点就是查单表查询一组同类点位就是按超级表过滤标签。物理上表按设备隔离查询时又按标签批量圈选这个模型一上来就是贴合 IoT 场景的。CREATE STABLE TABLE pressure_1min ( ts TIMESTAMP, pressure FLOAT, status INT ) TAGS ( area VARCHAR(20), pipe_dn INT, station VARCHAR(50) );对应某台具体设备上线时我们一般不会手动逐张建子表而是直接在写入时让 TDengine 自动建表INSERT INTO p_014_001 USING pressure_1min TAGS (城东片区, 300, 城南加压泵站) VALUES (now, 0.42, 0);写入即建表运维上省了很大一块工作量。新增点位再也不用先跑一遍建表 DDL 了。这也是后面我们能把 300 多个压力点顺利接进来的原因之一。3. 数据建模与容量估算开工前不算清账会被数据追着跑接手嘉环项目的时候我发现他们原来的时序数据表存在一个共性问题不管什么类型的数据全都堆在一张表里压力、流量、水质、设备状态全混着靠type字段区分。这种做法在 TDengine 里非常吃亏因为不同采集频率的数据混在同一张子表里会导致时间线错乱、压缩率下降。所以第一件事就是分域建模。3.1 建表规范与字段设计我们的建表规范按“采集频率 数据类型”拆成多张超级表而不是一个大宽表超级表名适用对象主字段采集频率pressure_1min管网压力点pressure, status1 分钟flow_15min流量计total_flow, instant_flow15 分钟water_quality_1h水质站ph, turbidity, residual_chlorine, temperature1 小时pump_status泵站设备状态current, vibration, temperature状态变化/心跳每个超级表里测量值列尽量用 FLOAT 或 INT不要用 VARCHAR。时序数据大多数是数值型TDengine 对数值类型的压缩算法最成熟。像开关状态这种只有 0/1 的字段直接用 BOOL 或者 INT不要学别人用 VARCHAR 存 ON/OFF纯属浪费空间。另外每张超级表我都会加一列quality_flag INT用于标记数据质量。原始采集数据经常有毛刺比如压力突然从 0.4 跳到 1.2这可能是传感器瞬态干扰也可能是真的爆管。我们不能随手删掉异常数据但可以在写入时打上质量标签后续分析时排除掉quality_flag ! 0的测点。这个字段在真实项目中帮了大忙尤其是做爆管定位模型的时候异常值本身就是信号。3.2 容量估算的实操算法这里给一套可以直接抄作业的估算公式适用于绝大多数智慧水务场景总行数 点位数量 × 每天采集条数 × 保留天数存储空间 总行数 × 平均单行压缩后字节数不同数据类型的压缩后单行字节数不太一样。我们在嘉环项目实测的数据压力点FLOAT 压力值 INT 状态 时间戳压缩后约 12 字节流量计两个 FLOAT 时间戳压缩后约 18 字节水质站5 个 FLOAT 时间戳压缩后约 36 字节按 5 年保留期算300 个压力点300 × 1440 × 365 × 5 × 12 字节 ≈ 9.5GB150 个流量计150 × 96 × 365 × 5 × 18 字节 ≈ 0.47GB40 个水质站40 × 24 × 365 × 5 × 36 字节 ≈ 0.63GB全部加起来也就 10GB 出头这个数字当时让项目组的人挺惊讶的因为 MySQL 时代同样数据量要占 300GB 左右。TDengine 的列式存储 压缩算法在这个量级确实优势巨大。当然如果采集频率提高到秒级或者加了振动传感器这类高频数据估算公式不变但结果会指数级上升要预留足够的磁盘余量。3.3 保留策略与降采样设计时序库不能无限制囤原始数据成本和查询效率都受不了。我们的策略是两级热数据原始数据保留 30 天满足实时监控和近期的异常追溯。 温数据用连续查询Continuous Query自动每小时把原始数据聚合成 1 小时均值保留 5 年。CREATE DATABASE IF NOT EXISTS db_water KEEP 30 DURATION 10 BUFFER 64 WAL_LEVEL 2;CREATE TABLE IF NOT EXISTS avg_pressure_1h AS SELECT _wstart AS ws, tbname, AVG(pressure) AS avg_pressure FROM pressure_1min INTERVAL(1h);然后给连续查询设好调度周期每小时跑一次把结果写入avg_pressure_1h这张历史聚合表。这样大屏上展示“过去一年每小时压力趋势”时不用去扫 30 天前的原始数据查询速度依然非常快。4. 从 MySQL 迁到 TDengine 的四个坑时间、窗口、填值、标签写这部分之前我要先如实说一句任何数据库迁移真正的坑都不在“导入数据”这一步而在应用层那些你以为写对了的 SQL。从 MySQL 切换到 TDengine我们踩了四个比较有代表性的坑每个都花了不少时间排查。4.1 第一个坑时间戳到底该用字符串还是 BIGINTMySQL 里大家习惯用DATETIME存时间迁到 TDengine 时想当然地也想继续用字符串。测试阶段看不出来等数据量上来就出问题了字符串时间戳无法直接参与时间运算INTERVAL聚合完全失效界面上的趋势曲线全乱了。TDengine 的时间戳主键必须是 TIMESTAMP 类型底层用 BIGINT 毫秒存储。所以迁移时第一步把原来的DATETIME统一转换成毫秒时间戳。这里有个小坑要特别小心时区。MySQL 的DATETIME不带时区如果原始数据存的是北京时间UTC8转换时得显式加上时区偏移否则从 TDengine 里读出来的时间会整体少 8 小时。我们当时的转换 SQL 大致长这样-- MySQL 侧 SELECT device_id, UNIX_TIMESTAMP(create_time) * 1000 AS ts, pressure FROM raw_pressure WHERE create_time 2025-01-01 00:00:00;-- TDengine 侧接收时用毫秒时间戳直接写入这个操作本身不复杂但要命的是如果原库里混着不同时区写入的数据采集网关的服务器时区不一致转出来的时间戳会参差不齐。后来我们加了一步校验取每个点位数据的时间戳算相邻两条的间隔超过采集周期 3 倍的点位单独列出来排查。这一步能筛出大部分时间错乱问题。4.2 第二个坑INTERVAL 聚合的窗口边界TDengine 里最常用的降采样查询是INTERVAL(1h)但这个窗口边界跟 MySQL 的GROUP BY DATE_FORMAT(create_time, %Y-%m-%d %H:)有一个隐蔽的差别不知道的人很容易被带到沟里。TDengine 的INTERVAL是按数据库的时间分区边界对齐窗口的。如果你的数据保留了 10 天DURATION 10库内部是每 10 天一个分区文件默认窗口从分区起始时刻对齐。如果采集端的时间戳不是整点对齐的比如压力点采集周期是 1 分钟但第一台设备的启动时间是 09:00:30那么一个小时窗口可能是从 09:00:30 到 10:00:30而不是你直觉上的 09:00:00 到 10:00:00。排查这个问题时我们一度以为是聚合结果错乱后来发现窗口边界整体偏移了采集的起始相位。解决方法不复杂应用层统计时统一用_wstart作为时间标签而不是假设窗口一定对齐到自然小时。如果业务非要按自然时间对齐处理办法是洗数据时先把秒级偏移抹掉或者用时间截断函数把采集时间规整到整分钟/整点后再写入。4.3 第三个坑FILL 填值并不总是你想要的做报表的时候经常遇到某个时段没数据。比如压力点断线 20 分钟那这段时间的小时均值就没有。MySQL 的做法通常是应用层判断查到 None 就填 0 或者填上一个值。TDengine 提供了FILL语法用起来方便但容易产生误导。SELECT _wstart, AVG(pressure) AS avg_pressure FROM pressure_1min WHERE tbname p_014_001 AND ts 2025-06-01 00:00:00 AND ts 2025-06-02 00:00:00 INTERVAL(1h) FILL(PREV);FILL(PREV)会用前一个有效窗口的值填充当前空窗口。看起来挺好用但如果你采集终端本身就存在长期离线、前一天有值当天没值的情况这个“ PREV”会把 24 小时前的老旧数据填充进去报表上看起来一切正常实际上全是假数据。我们的做法是核心指标不用 PREV 填充直接用 NULL 表示缺失由业务层决定要不要插值就算要插值也只允许线性插值并打上“估算值”标记不允许静默填充。4.4 第四个坑标签值覆盖与设备台账不同步最后这个坑最隐蔽而且很容易被忽略。之前说过修改标签会覆盖历史数据上的标签取值。我们的业务场景里一个片区泵站被合并到另一个管理所时维护人员直接执行了ALTER TABLE p_014_001 SET TAG area 新片区。结果所有历史压力数据的 area 都变成了‘新片区’统计 2023 年各个片区压力分布的时候数据整体偏了。这类问题的根因是我们把“设备现在的属性”和“设备当时的属性”混为一谈了。时序数据记录的是历史事实标签如果表示的是设备当前位置物理事实改一改无可厚非但如果你要用标签做历史分域统计那就要么把归属变化单独建一张映射表要么在数据里新增一列area_snapshot在写入时固化当时的归属信息。最终我们的方案是业务映射表存动态属性时序标签只存稳定属性设备编号、型号、安装位置行政区划/管理归属全部外置到 MySQL 的业务表里查询时按时间区间关联。这样既保住了 TDengine 的标签过滤性能又不会污染历史数据。5. 部署调优与版本选择社区版能撑起多大场面项目进入部署阶段后我们其实做了一轮比较充足的选型和部署验证。这一节把结果和细节都贴出来给准备接手类似项目的团队一些可以直接参考的配置。5.1 安装与部署形态官方网站有详细的安装包支持 Linux RPM/DEB、Windows 安装包、Docker 等。像“TDengine 安装 window 教程”这种搜索热度一直很高说明很多团队习惯先在 Windows 上做验证。我们的经验是Windows 装个社区版做功能验证完全没问题装完启动 taosd、执行taos命令行即可但生产环境一定要放 Linux性能释放更充分运维手段也更成熟。一套集群的常规部署建议配置项开发测试环境生产环境节点数131 主 2 从或 3 数据节点CPU4 核16 核以上内存8GB64GB磁盘100GB HDD2TB NVMe SSDOSCentOS 7 / Ubuntu 22.04 均可同左建议统一启动以后先别急着写数据先建库设参数。其中DURATION数据落盘文件的时间跨度、WAL_LEVEL预写日志级别、CACHE内存块大小这几个参数建库之后有些很难在线调整最好一开始就规划好。我们的库建库 SQL 大概是这样CREATE DATABASE db_water KEEP 30 DURATION 10 BUFFER 64 WAL_LEVEL 2 WAL_FSYNC_PERIOD 3000 VGROUPS 8;VGROUPS决定了数据分多少个虚拟存储组。我们当时 300 多个压力点8 个组就够了。如果点位有几千个可以适当调大但不要创建后频繁调整迁移数据很麻烦。5.2 实测有效的参数调优固定调优清单如下不同的场景按需取用压力点这种高频小数据量点位多的情况把BUFFER调到 64MB 以上能有效降低磁盘 IOPS。WAL_LEVEL 0性能最高但有丢数据风险我们不允许丢数据用的WAL_LEVEL 2。历史聚合查询较多的建议打开查询缓存或者直接把常用连续查询结果物化。如果要把 TDengine 数据接入 Grafana 大屏建议用官方的 plug-in版本要跟服务端匹配早期踩过因为插件版本不对导致查询报错的坑。5.3 社区版和商业版的现实考量经常有人问“TDengine 太贵了有没有替代方案”。这里我说下我们的认知TDengine 社区版功能上覆盖了时序写入、超级表、连续查询、流式计算、多级存储这些核心能力我们在嘉环项目中用到的全部功能社区版都提供了没有功能上的瓶颈。商业版更多解决的是运维管控、多集群统一管理、企业级技术支持这类问题。对于区县级智慧水务平台这种规模团队自己有能力维护一套集群的话社区版完全够用。真正要花钱的地方其实是你的时间成本和技术兜底能力。预算充足、项目 SLA 要求高、手上没有专职 DBA 的话商业版本给你带来的运维提效是真实的但如果说“功能不够所以要买商业版”那是误解绝大多数场景下社区版撑起一个中等规模项目绰绰有余。部署完成以后我们还做了一轮压测写入稳定在每秒 10 万条数据不丢不堵查询 P99 延迟在 200ms 以内。这个性能表现当时的 MySQL 架构是完全不可能达到的。最后一个忠告在嘉环科技这个智慧水务项目结束以后我的体会是换时序数据库解决的是“数据存储和查询”这一层问题但不等于整套系统就变“智能”了。水务数据真正的难关是那些 0.1% 的脏数据、断线补传、时钟漂移和口径不一致。TDengine 给了我们一个极其趁手的时序底座但上层的数据治理和业务模型设计才是决定智慧水务平台最终能走多远的关键。时序库是骨架数据质量才是血液。这套话虽然不新鲜但亲身做一遍真能体会到什么叫“工欲善其事必先利其器”。
返回列表