☰
ThingsBoard集成TDengine:规则节点实现高并发时序数据存储
2026/10/5 3:51:40 网站建设 项目流程

1. 为什么要把 TDengine 集成到 ThingsBoard 里

做物联网平台的人应该都绕不开 ThingsBoard。设备接入、规则引擎、可视化面板、RPC 下发这一整套能力做得相当完整,社区活跃度也高,拿来做设备管理和业务逻辑编排非常顺手。但真正跑起来以后,大家会遇到同一个尴尬:ThingsBoard 默认的存储方案扛不住高频时序数据。

ThingsBoard 默认支持 H2 和 PostgreSQL 作为实体数据库,Cassandra 作为时序数据库扩展。H2 本身就适合开发环境,数据量一上来就吃力;PostgreSQL 能撑一些,但设备多了、采集频率高了以后,写放大和查询延迟会变得很难看;Cassandra 分布式能力强,可运维成本不低,很多中小团队没那个精力去维护一套 Cassandra 集群。

这时候把 TDengine 引进来就顺理成章了。TDengine 是专门为物联网时序场景设计的数据库,写入吞吐高、压缩比好、查询语法简单,还自带超级表、连续查询、数据保留策略这些时序场景刚需能力。把 TDengine 接到 ThingsBoard 后面做遥测数据的最终存储,相当于给平台加了一个专门吃时序数据的加速器,设备管理继续用 ThingsBoard,海量监控数据丢给 TDengine 去扛。

这篇文章我把自己实际落地这套集成方案的过程完整梳理一遍,包括版本选型、规则节点开发、规则链配置、问题排查和性能实测结果。不管你是刚开始调研架构选型,还是已经踩了几个坑正在找解决方案,这篇内容都值得花十分钟看完。

2. 先说清楚:ThingsBoard 里时序数据是怎么流的

动手集成之前,必须先理解 ThingsBoard 的运行时数据流。否则你会在规则链配置和日志排查的时候一头雾水。

2.1 ThingsBoard 的核心模块与消息流转路径

ThingsBoard 内部大致分成几个核心模块:Transport 层负责接收设备上报,包括 MQTT、CoAP、HTTP 这些协议;Rule Engine 层负责处理消息,做数据过滤、转换、存储、告警等逻辑;Entity Service 层负责设备、资产、客户等实体管理;存储层负责把遥测数据和实体数据落盘。

设备上报一条遥测数据,流程是这样的:

  1. 设备通过 MQTT 或 HTTP 把 JSON 数据发给 Transport。
  2. Transport 把数据封装成 ProtoBuf 消息,投递到 Rule Engine 的消息队列。
  3. Rule Engine 根据规则链配置,把消息交给各个规则节点处理。
  4. 默认规则链中会有 "Save Timeseries" 节点,把遥测数据写到 ThingsBoard 的时序存储里。
  5. 前端面板或 API 查询时,再从时序存储中读取数据。

我们的目标,就是在第 4 步做文章。要么替换掉默认的时序存储,要么在规则链中插入一个自定义节点,把遥测数据同时或直接写到 TDengine。

2.2 默认存储方案在物联网场景下的瓶颈

我第一次用 ThingsBoard 做项目时,设备量大概 500 台,每台 5 秒上报一次温湿度。用默认的 PostgreSQL 存储,跑了两周就明显卡顿,设备列表加载慢,最新遥测值查询经常超过 3 秒,历史曲线加载直接卡死。

原因不复杂:关系型数据库的行式存储和索引结构,天然不擅长处理高频时序数据的写入。每台设备每次上报都是一次 INSERT UPSERT,500 台设备 5 秒一轮就是每秒 100 次写入,看起来不高,但加上查询、聚合、实时面板刷新,数据库很快就成为瓶颈。更重要的是,时序数据有很强的"按时间维度追加写入、按时间段范围查询"特征,关系型数据库并不理解这种数据模式,存储利用率不高,查询优化器也发挥不出优势。

2.3 TDengine 在这条链路里的定位

TDengine 在这套架构里承担的角色很纯粹:时序数据仓库。设备元数据、告警记录、租户配置这些还是继续放 ThingsBoard 自己的数据库里,但所有设备上报的遥测数据,走 TDengine 来存、来查。

这么做的好处很直接:

  • 写入性能比 PostgreSQL 高出一个数量级,官方宣称单机每秒能写几百万条记录,就算打个折也足够中小规模项目用。
  • 数据压缩比非常好,时序数据时间戳加数值的重复模式,在 TDengine 的列式压缩下通常能压到原始数据的十分之一以下。
  • 查询语法专门为时序做了优化,比如按时间窗口聚合、降采样、插值这些操作,一条 SQL 就能搞定。
  • 超级表模型正好匹配物联网业务:一张超级表代表一种设备类型或一个指标集合,每台设备对应一张子表,tag 可以存设备 ID、位置、类型等静态属性,动态数值存到列里。

所以最简单的理解方式是:ThingsBoard 继续做它擅长的事(连接管理、规则引擎、可视化),TDengine 去做它擅长的事(海量时序数据写入与查询),各司其职。

3. 环境准备与版本选型

集成方案确认了,接下来把环境搭起来。版本选型这里特别重要,因为我见过不少人在这一步就掉坑里了。

3.1 ThingsBoard 与 TDengine 版本兼容性怎么选

先看 ThingsBoard。目前主流版本是 3.x 系列,我这次用的是 3.4.4 社区版。ThingsBoard 的规则引擎 API 在 3.x 内相对稳定,但不同小版本之间也有细微调整,建议你用哪个版本就编译哪个版本的适配代码,不要拿 3.2 的 jar 往 3.7 上丢。

TDengine 这边,3.x 是目前的主力版本,跟 2.x 相比,语法和底层架构都有一些变化。我推荐直接用 3.x 新版,因为 2.x 已经进入维护期,新特性都在 3.x 上。TDengine 官方提供了 JDBC 驱动 taos-jdbcdriver,3.x 版本对应的是 3.x 的 JDBC 驱动,这里也要匹配好。

网上还有一些社区维护的 ThingsBoard TDengine 扩展包,比如 thingsboard-extensions 里有人做过 TDengine 的存储节点。这些可以直接参考,但要注意它对应的 TB 版本,有的是基于 2.x 做的,硬搬到 3.x 上可能会遇到类加载和 API 变更的问题。

3.2 部署方式建议:先单机后集群

我这次用单机部署,一台 8C16G 的服务器,同时跑 ThingsBoard 和 TDengine。对绝大多数中小项目来说,这个规模起步完全够用。TDengine 的集群能力是有的,但引入集群意味着更多的节点、更复杂的运维,必要性通常是数据量或者可用性要求到了某个阈值才出现。

如果团队没有专职 DBA,我强烈建议前期先用单机 TDengine,配合数据保留策略把超过保留期的时间分区自动清理掉,能顶住远比想象中大的数据量。真到了单机扛不住的时候,再用 TDengine 的集群方案平滑扩容。

表格整理一下我这次的环境参数:

组件版本说明
操作系统Ubuntu 22.04 LTS64位,内核 5.15+
JDKOpenJDK 11ThingsBoard 3.4 要求 JDK 11
Maven3.8.x编译规则节点插件用
ThingsBoard3.4.4 CE社区版,自带规则引擎
TDengine3.0.5服务端 taosd
taos-jdbcdriver3.2.7连接到 TDengine 的 JDBC 驱动
Node.js16.x前端资源构建用,改规则节点图标时可能需要

3.3 前置组件安装踩坑记录

TDengine 的安装本身不复杂,在 Ubuntu 上直接装官方 deb 包就行,装完用 systemctl 启动 taosd。这里有几个我踩过的坑值得提一下。

第一,装完 TDengine 后先在命令行验证一下。执行 taos 进入 CLI,执行 show databases,能正常显示数据库列表再往下走。如果 taos 命令连不上服务端,八成是 taosd 没启动或者配置文件里的 FQDN 解析有问题。

第二,TDengine 3.x 的 JDBC 连接串跟 2.x 不太一样。3.x 的 taos-jdbcdriver 支持两种 URL 格式,一种是 jdbc:TAOS://hostname:6030/dbname,另一种是 HTTP 方式的 jdbc:TAOSWS://hostname:6041/rest/dbname。前者走原生 TCP 协议,性能更好;后者走 REST 接口,穿透性更好。我这边的规则节点用原生 TCP 协议。

第三,ThingsBoard 首次启动会初始化数据库,我第一次启动时等了大概两分钟,以为卡死了,其实是在建表。如果用的是默认 H2 模式,启动日志里会看到 HikariPool 初始化相关的信息,耐心等一下就好。

ThingsBoard 安装完成后,先确认基础功能可用。浏览器打开 8080 端口,用系统管理员账号登录,创建一个测试设备,手动发送一条遥测数据,确认默认规则链能正常存数据。基础通了,再开始集成工作。

4. 集成路线选型:改底层存储还是加规则节点

前面铺垫了这么多,现在到了核心决策点。把 TDengine 集成到 ThingsBoard,主流的做法有两条路线。

4.1 方案一:直接替换 ThingsBoard 的时序存储实现

这个方案的做法是修改 ThingsBoard 的源码或者引入社区适配器,让 ThingsBoard 内部的 TimeseriesService 接口实现指向 TDengine,替代默认的 Cassandra 或 SQL 存储。

优点是不用动规则链,设备上行数据自动落到 TDengine 里;查询遥测 API 也仍然走 ThingsBoard 自己的接口,对上层应用透明。缺点是侵入性强,你需要维护一个基于特定 TB 版本分支的代码,TB 升级时适配工程可能很大。社区版的 ThingsBoard 没有官方 TDengine 适配器,这个路线基本意味着要 fork 源码长期维护。

4.2 方案二:利用规则引擎开发自定义 TDengine 存储节点

这个方案是在规则链里插入一个自定义规则节点,专门负责把遥测数据写入 TDengine。不改 ThingsBoard 底层存储,原有数据流完全不动,只是新增一个数据出口。

  • 优点:

    • 代码量小,一个 Maven 工程就能搞定。
    • 对 ThingsBoard 侵入性几乎为零,升级 TB 时基本不受影响。
    • 灵活性很高,可以在规则节点里做字段映射、数据清洗、批量写入等操作。
    • 不破坏原有存储,万一 TDengine 出问题,默认存储还在,不会造成数据链路全断。
  • 缺点:

    • 需要在每个需要持久化的规则链里显式加入这个节点。
    • 修改规则链时,必须理解 TB 消息格式,对新手有一点学习成本。

4.3 我为什么选了规则节点方案

实际对比下来,我选了规则节点方案。原因很现实:我维护的 TB 实例还需要跟上社区版本升级,不希望被一个深度改造的底层存储绑死。而且规则节点方案对数据流的控制粒度更细——比如有些设备上报的数据我不需要全部入库,可以在规则链路里做一层过滤再交给 TDengine 节点;有些设备的指标需要做单位转换,也可以在节点里顺手完成。

下面这张表可以更直观地对比两条路线:

对比项替换底层存储自定义规则节点
开发量大,需要改源码或深度适配小,一个插件工程
侵入性高,TB 启动链路会受影响低,不影响原有模块
TB 升级兼容性差,升级成本高好,jar 包兼容即可
数据流控制粒度粗,所有遥测数据统一存储细,可按设备/消息类型分流
对 TDengine 特性利用依赖适配层实现可以在节点里直接写高级 SQL,灵活性高
运维风险底层故障影响全局节点故障只影响该规则链分支,可熔断

上表最后一行"运维风险"值得展开说一下。替换底层存储方案,如果 TDengine 出了故障,整个 ThingsBoard 的遥测读写链路就全挂了;而自定义规则节点方案里,即使 TDengine 节点抛异常,规则链可以配置为将该分支标记为失败,同时让消息继续走原有存储,或者转发到告警节点做通知。这样做在关键时刻是可以救命的。

5. 核心实操:写一个 TDengine 规则节点插件

方案定了,正式开始写代码。这个过程我尽量讲得细一点,从工程搭建到部署验证,保证你照着做就能跑通。

5.1 创建 Maven 工程与引入核心依赖

新建一个标准的 Maven 工程,JDK 版本 11,打包方式 jar。pom.xml 里需要引入 ThingsBoard 的规则引擎 API 和 TDengine 的 JDBC 驱动。

关键依赖如下:

<dependencies> <!-- ThingsBoard 规则节点 API --> <dependency> <groupId>org.thingsboard</groupId> <artifactId>rule-engine-api</artifactId> <version>3.4.4</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.thingsboard</groupId> <artifactId>common-message</artifactId> <version>3.4.4</version> <scope>provided</scope> </dependency> <!-- TDengine JDBC 驱动 --> <dependency> <groupId>com.taosdata.jdbc</groupId> <artifactId>taos-jdbcdriver</artifactId> <version>3.2.7</version> </dependency> <!-- JSON 解析 --> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.8.9</version> </dependency> </dependencies>

注意 rule-engine-api 的 scope 设为 provided,因为 ThingsBoard 服务端本身已经带了这些类。如果设成 compile,打包时把 TB 的类也打进去,部署时反而可能造成类冲突,这种问题非常难排查。

5.2 规则节点主类实现思路

ThingsBoard 自定义规则节点的核心是一个继承自 TbAbstractNode 的类,重写 onMsg 方法。消息进入节点后,通过 TbMsg 对象可以拿到数据、元数据和设备信息。

我这里实现的 TdengineSaveNode 逻辑是这样的:

  1. 从 TbMsg 的元数据中获取设备名称和设备 ID。
  2. 解析 msg.getData() 中的 JSON,这是一个键值对,键是遥测字段名,值是数值。
  3. 把 JsonElement 解析成 Java 对象,拼成 SQL 并执行插入。
  4. 执行成功调用 tellSuccess,失败调用 tellFailure。

核心代码结构如下:

@Slf4j @RuleNode( name = "Save to TDengine", nodeClass = TdengineSaveNode.class, configClazz = TdengineConfig.class, nodeDescription = "Save device telemetry to TDengine", nodeDetails = "Parse device telemetry and insert into TDengine super table", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbActionNodeTdengineConfig", icon = "storage" ) public class TdengineSaveNode extends TbAbstractNode { private TdengineConfig config; private Connection connection; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { this.config = TbNodeConfigurationUtils.convert(configuration, TdengineConfig.class); try { Class.forName("com.taosdata.jdbc.TSDBDriver"); connection = DriverManager.getConnection(config.getJdbcUrl(), config.getUsername(), config.getPassword()); } catch (Exception e) { throw new TbNodeException("Failed to init TDengine connection", e); } } @Override public void onMsg(TbContext ctx, TbMsg msg) { try (Statement stmt = connection.createStatement()) { String deviceName = msg.getMetaData().getValue("deviceName"); JsonObject data = new Gson().fromJson(msg.getData(), JsonObject.class); long ts = System.currentTimeMillis(); StringBuilder sb = new StringBuilder(); sb.append("INSERT INTO ").append(config.getDatabase()).append(".`") .append(deviceName).append("` USING ").append(config.getSuperTable()) .append(" TAGS ('").append(deviceName).append("') ") .append("(ts, "); // 拼接字段名 for (String key : data.keySet()) { sb.append("`").append(key).append("`, "); } sb.setLength(sb.length() - 2); sb.append(") VALUES (").append(ts).append(", "); // 拼接字段值 for (String key : data.keySet()) { JsonElement value = data.get(key); sb.append(value.getAsString()).append(", "); } sb.setLength(sb.length() - 2); sb.append(")"); stmt.executeUpdate(sb.toString()); ctx.tellSuccess(msg); } catch (Exception e) { log.error("Failed to save telemetry to TDengine", e); ctx.tellFailure(msg, e); } } @Override public void destroy() { if (connection != null) { try { connection.close(); } catch (Exception ignored) {} } } }

这段代码的核心逻辑不复杂,但几个地方要特别说明。

第一,SQL 里为什么要用反引号包裹设备名和字段名?因为 TDengine 的表名如果带中划线或特殊字符,必须用反引号转义。设备名称经常是类似 sensor-001 这种格式,不转义会直接语法报错。

第二,INSERT 语句使用了 TDengine 的自动建表语法。INSERT INTO table_name USING super_table TAGS (...) VALUES (...),这个语法的好处是如果子表不存在,TDengine 会根据超级表定义自动建表。这样就不需要事先为每台设备手动建表,设备接入后第一次上报就能自动落数据。

第三,批量操作的优化空间。上面代码是单条插入,简单直接,适合演示和低频率场景。真正的生产环境,设备量大以后要改成批量写入。TDengine 支持多 VALUES 追加写入,比如 INSERT INTO table1 VALUES (...), (...), (...),这样一次 SQL 就能写入多条记录,性能提升是数量级的。规则节点里可以考虑维护一个内存队列,攒够一定条数再批量刷到 TDengine。

5.3 配置类定义

TdengineConfig 这个类用于接收规则节点在控制台配置的参数。JSON 配置和 Java 对象之间的映射由 ThingsBoard 框架自动处理。

@Data public class TdengineConfig implements Serializable { private String jdbcUrl; private String username; private String password; private String database; private String superTable; }

控制台里配置的时候,填一个 JSON 对象就行:

{ "jdbcUrl": "jdbc:TAOS://localhost:6030", "username": "root", "password": "taosdata", "database": "thingsboard", "superTable": "device_telemetry" }

5.4 前端注册与图标配置

ThingsBoard 控制台的规则节点列表是从前端资源里读取的。要让自定义节点在控制台显示出来,需要注册这个节点的前端配置。

这个过程包括在 jar 包里放一个 rulenode-core-config.js,或者在 ThingsBoard 的 UI 扩展目录里加入一段配置。对小团队来说,一个更省事的方式是直接用 ThingsBoard 自带的 script 节点,在规则链里用 JavaScript 写一个函数,解析消息后调用 TDengine REST API 写入。但这样做性能和可维护性都不如原生 Java 节点,所以我最终没有采用。

如果你将 jar 包放到 ThingsBoard 的 rule-engine 目录,需要把 jar 包中的 uiResources 路径配置好。具体做法是在 src/main/resources 下面建 static/rulenode/rulenode-core-config.js 文件,内容里注册节点定义。然后打包,这个 js 文件会打到 jar 里,ThingsBoard 启动时会自动扫描 classpath 下的 uiResources。

5.5 打包与部署

执行 mvn clean package,target 目录下会生成一个带依赖的 jar 包。如果使用 maven-shade-plugin,可以把所有依赖打成一个 fat jar,这样部署时不需要额外拷贝 TDengine JDBC 驱动。

部署步骤:

  1. 把 jar 复制到 ThingsBoard 安装目录的 extensions 目录,或者对应版本的 rule-engine 外部扩展目录。
  2. 重启 ThingsBoard 服务。
  3. 查看日志,确认没有 ClassNotFoundException 或 BeanCreationException。

如果日志里报找不到 TdengineSaveNode 类,九成是 jar 包路径没被扫描到。可以检查 thingsboard.conf 里是否配置了 extensions 目录,或者直接把 jar 扔到 lib 目录下再重启。

部署完成后,打开 ThingsBoard 控制台的规则引擎页面,在节点列表里搜索 "TDengine",能看到这个节点就说明注册成功了。

6. ThingsBoard 控制台规则链配置与数据验证

节点开发完成只是第一步,真正要在业务中生效,需要在控制台上把规则链配好。

6.1 创建专用规则链

打开 ThingsBoard 控制台,进入"规则链"页面,创建一个名为"TDengine 遥测存储"的新规则链。

这个规则链的入口节点用"消息类型切换"节点(Message Type Switch),判断消息类型。如果是 POST_TELEMETRY,就路由到 TDengine 保存节点;其他类型消息可以路由到默认成功节点忽略掉,或者继续往下走别的处理。

连接线配置的核心思路是:

Input(root) -> 消息类型切换 -> POST_TELEMETRY -> Save to TDengine -> Success -> 其他类型 -> Success

设备发的遥测消息进入这个规则链后,走 TDengine 保存分支;如果不是遥测消息,直接成功结束,不产生额外逻辑。

6.2 设备与规则链的绑定方式

规则链建好以后,需要让设备消息走这条链。有两种做法:

第一种:在设备配置里直接指定规则链。打开设备详情页,在"规则链"字段选择刚才创建的"TDengine 遥测存储"规则链。这样该设备的所有上行消息都会进这条链。

第二种:在默认规则链里加一个规则节点,做设备名称或设备 profile 的判断,命中的消息转发给自定义规则链。这种方式适合多类设备共用一条主链、按设备类型分流到不同存储链路的场景。

我这次测试用的方式更直接:新建了一个独立的设备 Profile,在这个 Profile 里绑定规则链。这样这个 Profile 下的所有设备都自动走 TDengine 存储链路,跟默认 Profile 隔离开,测试和上线都很可控。

6.3 从发送遥测到 TDengine 落地全链路验证

设备侧用 MQTT 客户端模拟上报,发一条温湿度数据:

mosquitto_pub -d -q 1 \ -h localhost \ -p 1883 \ -t "v1/devices/me/telemetry" \ -u "ACCESS_TOKEN" \ -m "{\"temperature\": 23.5, \"humidity\": 61.2}"

发完以后,去 TDengine 命令行查数据:

taos use thingsboard; select * from device_telemetry order by ts desc limit 10;

如果一切正常,应该能看到刚才那台设备的温度湿度数据。这里有一个新手最容易犯的错误——设备名如果包含特殊字符,自动建表时表名会被处理成带反引号的名称,查询时不想加反引号可能查不到。

我在测试中第一次写数据就遇到一个典型问题:TDengine 3.x 的超级表字段映射要求第一列必须是时间戳,列名如果不是 ts,需要在创建超级表时指定 TIMESTAMP 关键字。我当时把时间戳字段叫 ts,正好符合默认要求,所以没碰到这个问题。如果你在设计超级表时用了 record_time 之类的时间列名,写 JDBC 时要注意对应关系。

7. 实战中遇到过的问题与排查方案

集成过程中一定会遇到各种问题,我把几个高概率踩坑的场景单独列出来,方便你排查时对照。

7.1 规则节点报错导致遥测消息失败积压

现象是控制台规则链的节点上出现红色失败计数,设备遥测数据没有写入 TDengine。

第一步先看 ThingsBoard 的日志,搜索 TdengineSaveNode 相关输出。最常见的错误有两个:

一是 JDBC 连接失败。检查 jdbcUrl 是否写对,root 密码是否是默认的 taosdata,TDengine 服务是否正常运行。测试环境经常会遇到 ThingsBoard 所在机器的 6030 端口没打通的情况,防火墙规则要确认放行。

二是 SQL 执行错误。错误消息里通常会带上具体的 SQL,你可以复制这条 SQL 到 taos CLI 手动执行,看看报什么错。常见的是字段类型不匹配,比如 TDengine 超级表里某个列是 FLOAT,但上报是字符串,或者字段名跟保留字冲突。

解决办法:

  • 检查超级表的 schema 和上报的 JSON key 是否一一对应。
  • 用 try-catch 包裹整段逻辑失败后将原始 TbMsg 转寄到下一个节点或日志节点,方便定位。
  • 在规则节点配置里增加 batch size 参数,控制批量写入节奏。

7.2 TDengine 报错 error (0x83a): query denied by license: external query is restricted

这是很多人在网上问的问题,我也踩到了。这个报错的意思是当前 TDengine 安装实例的外部查询被 license 限制了。

具体原因要看你的 TDengine 版本和安装方式。社区版在部分版本或特殊配置下会限制外部应用程序通过 JDBC 访问数据,你通过 taos CLI 本地查没问题,但远端连 JDBC 就会报这个错。

排查思路:

  1. 先用 root 登录 taos CLI,执行 show licenses,看一下当前实例的 license 状态,包括到期时间、版本类型、是否允许外部连接。
  2. 如果确实是 license 限制,最直接的解决方式是确认你安装的版本是否适合生产使用。普通测试可以先检查 taos.cfg 里有没有开启某些限制参数,比如 supportVnodes、queryPolicy 等。
  3. 如果确认是试用授权或者社区版限制,就需要评估是否升级到不受限的版本,或者调整接入方式。比如 taos-jdbcdriver 切换到 REST 连接方式,有时 REST 方式可以绕过原生连接的限制。

这个问题最大的坑在于,本地 CLI 查着一切正常,让很多人误以为 TDengine 没问题,结果问题全出在外部连接限制上。

7.3 时间戳精度不一致导致的数据错位

ThingsBoard 内部时间戳是毫秒,TDengine 3.x 的时间戳默认精度也是毫秒,所以直接对接不会有大问题。但如果你遇到写入以后查询出来时间偏移了 8 小时,那就是时区问题。

处理方案是在 TDengine 服务端配置里统一时区,或者在 JDBC 连接串上带 timezone 参数:

jdbc:TAOS://localhost:6030?timezone=UTC

注意这里说的是数据库时区,不是服务器系统时区。我建议让 ThingsBoard、TDengine、应用服务器全部统一到 UTC,展示层再转成本地时区。这样数据库存储的时间戳永远是无歧义的,避免夏令时或服务器时区设置不一致造成的混乱。

7.4 设备量大之后 TDengine 连接被打满

规则节点里如果每来一条消息就创建一个 connection,设备量稍微上来一点,TDengine 的连接数就会飙到几百,最终触发连接数限制,系统开始报 too many connections。

解决办法:

  • 使用连接池,比如 HikariCP,给 TDengine 的 JDBC 连接做池化。
  • 节点内部用队列聚合数据,批量写入,降低连接占用频率。
  • 如果单台 TDengine 连接依然吃紧,考虑部署多个 TDengine 节点,按设备分组分库。

在实际项目中,我用 HikariCP 把最大连接数限制在 10,然后用批量写入方式,每秒几千条遥测消息也没把连接打满过。

8. 实测效果:数据写入与查询性能记录

篇幅允许,把我实测的一组数据放出来,给准备上生产环境的朋友一个参考。

8.1 测试环境与数据规模

服务器配置:

  • CPU:8 核
  • 内存:16 GB
  • 磁盘:SSD 500 GB
  • OS:Ubuntu 22.04
  • ThingsBoard:3.4.4 社区版
  • TDengine:3.0.5

模拟场景:

  • 1000 台设备
  • 每 10 秒上报一次温度、湿度、电压三个指标
  • 也就是每秒 100 条消息、300 个指标点
  • 连续测试 24 小时

8.2 性能结果

TDengine 这边写入没有压力,CPU 占用一直很低,整个测试期间 TDengine 的 CPU 占用基本在 5% 以下。查询方面,单设备 1 小时历史数据秒级返回,聚合查询比如 1000 台设备全量 1 天平均值,大约 2 秒内出结果。

注意我在规则节点里做了批量组装,每次攒满 200 条再执行一次 JDBC 批量写入,而不是每条消息一条 SQL。这个优化把对 TDengine 的写入压力降了一个数量级。

在同样的体量下,原有默认 PostgreSQL 存储,写入没问题,但"最新值"查询偶尔会超过 3 秒,历史聚合查询直接不可用,和 TDengine 的差距非常明显。

8.3 ThingsBoard 与 TDengine 资源占用对比

部署 TDengine 后,需要为它预留内存。TDengine 会根据数据量和缓存配置占用内存,我的配置里让它用到大约 2 GB,剩下的留给 ThingsBoard 的 JVM 和操作系统。ThingsBoard 的 JVM 堆我调到 4 GB,整体跑下来没有物理内存不足的问题。

9. 后续可以继续扩展的方向

TDengine 接入 ThingsBoard 只是第一步,做完了以后,你会发现整个链路继续深入优化的空间非常大。

9.1 利用 TDengine 超级表和连续查询做自动聚合

在 TDengine 里建超级表时,可以设计好标签字段,比如 device_id、device_type、region。这样不用在应用层做分组统计,直接用 SQL 就能按标签做多维度聚合。

TDengine 的连续查询还可以自动周期性地做降采样,比如每分钟计算一次过去一分钟的平均温度,结果写到另一张表中。前端展示小时级曲线时,直接查这个降采样表,响应速度会快很多。这些事情不需要在 ThingsBoard 规则链里写代码,完全是数据库层的能力。

9.2 用 Grafana 对接 TDengine 做可视化面板

ThingsBoard 自带的可视化对于实时面板足够用,但做复杂的时序图表,Grafana 生态更成熟。TDengine 官方有 Grafana 插件,配置一个数据源以后,可以直接用 SQL 查询 TDengine 里的数据,通过 Grafana 做聚合图表、告警通知、大屏展示。

这样 ThingsBoard 负责设备接入和业务规则,Grafana 负责长时间跨度数据分析和监控展示,各取所长。

9.3 通过规则链将 TDengine 数据接入下游流处理

如果后续要做实时告警、设备故障预测这类功能,可以考虑在 TDengine 节点后面再接 Kafka 或者 Flink。ThingsBoard 规则链的节点本来就是可以串起来用的,TDengine 节点把数据持久化以后,再通过一个 Kafka 节点把原始消息转发给下游流处理任务。

这样一来,不只是把 TDengine 当作一个存储终端,而是让数据以 TDengine 为锚点,继续在数据链路里流动起来。

9.4 多实例 ThingsBoard 共用一套 TDengine

如果业务规模增长到需要多套 ThingsBoard 实例做水平扩展或者多地域部署,可以让这些实例通过规则节点写入同一个 TDengine 集群。TDengine 作为中心化时序数据湖,各节点独立上报,查询统一走 TDengine。这个架构比直接同步 ThingsBoard 底库要轻量得多,也是我比较推荐的一种演进方式。

10. 集成完成后的维护建议

最后再分享一些上线之后实际维护过程中沉淀下来的经验。

TDengine 的版本升级要谨慎,但是在测试环境先验证新的 taos-jdbcdriver 与 ThingsBoard 规则节点的兼容性,再逐步更新生产环境。TDengine 3.x 内部升级相对平滑,跨大版本就得多做一轮验证。

规则链的改动建议先在独立规则链上测试,确认 TDengine 写入正常后再把设备 Profile 切过去。我在测试环境验证好后,是先在少量设备上灰度,跑了半天没有异常,才把全量设备切过去的。

TDengine 的数据保留策略一定要提前设置。时序数据无限增长,如果不做保留策略,一年以后磁盘会被历史数据占满。在创建超级表时,通过 KEEP 参数指定数据保留天数,比如 90 天,TDengine 会自动淘汰过期数据。

还有一个细节是 TDengine 的 WAL 和缓存参数,对写入性能影响很大。如果设备的写入量比较大,可以把 wal_level 调低提升写入吞吐,代价是异常断电时可能丢失少量最近数据。这个参数要根据业务对数据可靠性的要求来取舍。

我自己最喜欢的 TDengine 特性之一是其写入稳定性。在一段时间的持续运行中,TDengine 几乎没有出现过异常退出的情况,而原先 PostgreSQL 方案在高频写入下偶尔会出现锁等待和连接池耗尽。就凭这一点,当时花力气做这个集成就完全值回票价。

如果你还在纠结要不要把 TDengine 集成到 ThingsBoard,我的建议是:如果你的设备数量超过几百台、上报频率超过每分钟一次、要做长时间范围历史分析,那就别犹豫了,集成成本远低于后面数据量大了再迁移的成本。按这篇文章的步骤操作,一两天就能跑通。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询