工业时序数据中台架构演进:从 IoTDB + MySQL 到去 DWD 层简化实践

在钢铁行业全流程数据采集项目中,我们经历了一次彻底的数据架构重构。本文分享从传统 ODS/DWD/D1 分层到去 DWD 简化为 ODS 直查 IoTDB 的实战经验。


一、项目背景:钢铁全流程数据采集的挑战

1.1 业务场景

轧钢生产线包含 7 大工艺:入炉、加热、锻坯、压延、锯切、收集、出坑。每支钢坯从入炉到收集需要经历数小时,期间产生大量设备采集数据:

  • 秒级传感器数据:温度(℃)、压力(bar)、速度(m/s)、辊缝(mm)
  • 工艺步骤数据:开始/结束时间、操作员、设备状态
  • 质量检测数据:坯料编号、炉号、支号、规格

1.2 技术栈选型

层级技术选型存储内容
时序数据源IoTDB 1.3.x秒级设备采集数据
关系型数据库MySQL 8.0业务数据、工艺步骤、质量记录
ETL 工具RestCloud / NiFi数据抽取、转换、加载
后端服务Java + Spring Boot业务接口、大屏展示
前端Vue 3 + Element Plus大屏、管理页面

二、初始架构:ODS/DWD 双层存储

2.1 架构图

IoTDB (时序原始数据)
    │
    │ ETL 30s 聚合
    ▼
MySQL ODS 层 (原始数据快照)
    │
    │ 清洗、关联、归一化
    ▼
MySQL DWD 层 (明细数据仓库)
    │
    │ 业务查询
    ▼
Java 后端 → 前端大屏

2.2 存在的问题

经过 6 个月的生产运行,我们发现这套架构存在 4 个致命问题

问题1:查询链路过长

  • 前端请求 → DWD → ODS → IoTDB(三层跳转)
  • 平均查询响应时间:1.2s → 3.8s
  • 大屏 5 秒刷新一次,频繁超时

问题2:数据一致性差

  • ODS 和 DWD 同步依赖定时调度,存在 30s~2min 延迟
  • 工艺状态查询时,ODS 已更新但 DWD 未同步,导致"状态显示异常"

问题3:维护成本高

  • ODS 保留 90 天,DWD 保留 2 年,双份存储成本高
  • 需求变更时,需同时修改 ODS 抽取 SQL 和 DWD 查询逻辑
  • 调度任务复杂,排查问题需跨 3 个数据层

问题4:DWD 价值低

  • DWD 层的清洗逻辑(去重、补全、归一化)在业务侧重复实现
  • 前端大屏需要原始 ODS 数据,DWD 层反而成为"中间商"

三、架构重构:去 DWD 直查 IoTDB

3.1 新架构设计

IoTDB (时序原始数据,永久存储)
    │
    │ ETL 30s 聚合(仅 ODS 需要)
    ▼
MySQL ODS 层 (原始数据快照,保留90天)
    │
    │ 直接查询 + IoTDB 兜底
    ▼
Java 后端 → 前端大屏

核心原则

  • ODS 是唯一关系型数据源,保留最近 90 天热数据
  • IoTDB 是长期存储,ODS 无数据时自动回退查询
  • DWD 层彻底下线,关闭相关调度任务

3.2 查询策略

// 伪代码:ODS + IoTDB 混合查询
public List<ProcessData> queryProcessData(Long startTime, Long endTime) {
    // 1. 先查 ODS 条数
    long odsCount = odsMapper.countByTime(startTime, endTime);
    
    // 2. 查询绑定关系条数(业务表)
    long bindCount = bindMapper.countByTime(startTime, endTime);
    
    // 3. 一致则返回 ODS,不一致回退 IoTDB
    if (odsCount == bindCount) {
        return odsMapper.queryByTime(startTime, endTime);
    } else {
        return iotdbClient.queryByTime(startTime, endTime);
    }
}

关键逻辑

  • ODS 查询速度:< 100ms
  • IoTDB 查询速度:200ms ~ 800ms(取决于时间范围)
  • 通过"条数对比"决定数据源,避免不一致

四、核心技术改造

4.1 ODS 分区表改造

-- 改造前:普通表
CREATE TABLE fp_step_process (
    id BIGINT PRIMARY KEY,
    stove_no VARCHAR(64),
    create_time DATETIME
);

-- 改造后:按 create_time 分区(预建 3 年分区)
CREATE TABLE fp_step_process (
    id BIGINT PRIMARY KEY,
    stove_no VARCHAR(64),
    create_time DATETIME NOT NULL,
    ...
) PARTITION BY RANGE (TO_DAYS(create_time)) (
    PARTITION p2026_q1 VALUES LESS THAN (TO_DAYS('2026-04-01')),
    PARTITION p2026_q2 VALUES LESS THAN (TO_DAYS('2026-07-01')),
    PARTITION p2026_q3 VALUES LESS THAN (TO_DAYS('2026-10-01')),
    ...
);

收益

  • 查询自动剪枝,90 天内数据查询速度提升 3 倍
  • 历史分区可单独删除(如删除 2024 年数据:DROP PARTITION p2024_*

4.2 IoTDB 查询兜底

// IoTDB 查询模板(按时间范围 + 设备编号)
String sql = "SELECT * FROM root.steel.rolling.* " +
             "WHERE time >= {startTime} AND time <= {endTime} " +
             "AND device = '{deviceNo}' " +
             "ORDER BY time DESC LIMIT 1000";

优化点

  • IoTDB 查询始终带时间范围,避免全表扫描
  • 大屏最新一条数据:ORDER BY time DESC LIMIT 1,耗时 < 50ms
  • 分页查询:避免 OFFSET,改用时间戳游标

4.3 ETL 抽取优化

# RestCloud ETL 配置优化
# 1. 增量抽取(基于 IoTDB 最新时间戳)
SELECT * FROM process_data 
WHERE time > '${last_sync_time}' 

# 2. 批量写入(JDBC Batch)
batch_size = 5000
jdbc_url = "jdbc:mysql://prod:3306/ods?rewriteBatchedStatements=true"

# 3. 异常重试
retry_count = 3
retry_delay = 5s

关键优化

  • 从全表同步改为 增量同步,ETL 耗时从 5 分钟降到 30 秒
  • 开启 MySQL 批量写入重写,写入性能提升 10 倍

五、上线效果对比

5.1 性能指标

指标重构前重构后提升
大屏查询响应3.8s800ms4.75x
ETL 同步耗时5min30s10x
调度任务数12 个4 个-67%
存储成本2TB800GB-60%

5.2 稳定性提升

  • 查询链路简化:从 3 层减少到 2 层,故障点减少
  • 数据一致性:通过"条数对比"策略,不一致率从 5% 降到 0.1%
  • 排障效率:不再需要跨 ODS/DWD 两层排查,定位时间从 2h 降到 20min

六、踩坑实录

坑1:IoTDB 时间戳对齐失败

现象:ODS 条数 = 100,但 IoTDB 查询返回 98 条。

根因:IoTDB 和 MySQL 服务器时间不同步(相差 2 秒),导致边缘数据落在不同分区。

解决方案

  • ETL 抽取时以 IoTDB 时间戳为准,MySQL 只做映射
  • 增加 2 秒缓冲窗口:WHERE time >= start - 2s AND time <= end + 2s

坑2:DWD 关闭后遗留任务干扰

现象:去 DWD 后,某些历史报表仍然报 DWD 表不存在。

根因:未彻底清理 Quartz 调度任务,凌晨仍触发 DWD 同步 Job。

解决方案

-- 查询遗留任务
SELECT * FROM qrtz_triggers WHERE job_group LIKE '%dwd%';

-- 暂停并删除
DELETE FROM qrtz_triggers WHERE job_name = 'dwd_sync_job';

坑3:大屏分页 vs 最新一条不一致

现象:大屏列表页第 1 条数据 ≠ 大屏"最新一条"卡片数据。

根因:列表页查 ODS(分页),最新一条查 IoTDB(兜底),两个数据源时间窗口不一致。

解决方案

  • 统一数据源:列表页和详情页都走 ODS + IoTDB 混合查询
  • 增加数据源一致性校验:odsCount == bindCount 才用 ODS

七、适用场景与边界

7.1 适合去 DWD 的场景

✅ 业务查询以时间范围为主(如:最近 7 天工艺数据)
✅ 原始数据粒度细(秒级),DWD 聚合价值低
✅ 团队运维能力强,能handle 混合查询复杂度

7.2 不建议去 DWD 的场景

❌ 业务查询以维度关联为主(如:按产品型号汇总)
❌ 数据需要多源关联清洗(如:IoTDB + ERP + MES)
❌ 团队规模小,希望查询逻辑尽量简单


八、总结

这次架构重构的核心收获:

  1. 分层不是越多越好:DWD 层在特定场景下确实多余,直查 ODS + IoTDB 更高效
  2. 混合查询策略是关键:通过"条数对比"实现数据源自动切换,兼顾性能和一致性
  3. 分区表是基础:ODS 按时间分区后,查询性能和可维护性大幅提升
  4. 渐进式重构:先保留 DWD 双跑,验证新查询无误后再下线,降低风险

如果你也在做工业数据中台或时序数据架构选型,欢迎交流讨论。

更多推荐