From ba9eb513bf142855ec26524a802692d130166dab Mon Sep 17 00:00:00 2001 From: ZIHAN DAI <99155080+PDGGK@users.noreply.github.com> Date: Fri, 21 Aug 2026 19:37:28 +1000 Subject: [PATCH 1/5] docs: add the ThingsBoard table-model integration page Adds a user-guide page for the iotdb-thingsboard-table module in apache/iotdb-extras: stock ThingsBoard storing telemetry and attributes in IoTDB 2.x table model, by placing the module and its runtime dependencies on ThingsBoard's classpath and setting a few properties. The page lists the exact jar set, including the ones that must NOT be copied because ThingsBoard bundles newer copies of them. Four new pages -- Thingsboard.md in latest-Table and Master/Table, English and Chinese -- plus the two sidebar groups and the four Ecosystem-Overview index rows. Structure follows the existing Spark-IoTDB page. The attributes selector is documented as not yet usable on a stock ThingsBoard: ThingsBoard registers its JPA attributes bean unconditionally and exposes no switch for it, so setting the selector currently fails startup. The fix is apache/iotdb-extras#125; the page says so and will be updated when a build containing it is released. --- src/.vuepress/sidebar/V2.0.x/en-Table.ts | 7 + src/.vuepress/sidebar/V2.0.x/zh-Table.ts | 7 + .../Ecosystem-Overview_apache.md | 4 +- .../Ecosystem-Integration/Thingsboard.md | 203 ++++++++++++++++++ .../Ecosystem-Overview_apache.md | 4 +- .../Ecosystem-Integration/Thingsboard.md | 203 ++++++++++++++++++ .../Ecosystem-Overview_apache.md | 4 +- .../Ecosystem-Integration/Thingsboard.md | 171 +++++++++++++++ .../Ecosystem-Overview_apache.md | 4 +- .../Ecosystem-Integration/Thingsboard.md | 171 +++++++++++++++ 10 files changed, 774 insertions(+), 4 deletions(-) create mode 100644 src/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md create mode 100644 src/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md create mode 100644 src/zh/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md create mode 100644 src/zh/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md diff --git a/src/.vuepress/sidebar/V2.0.x/en-Table.ts b/src/.vuepress/sidebar/V2.0.x/en-Table.ts index fdbfc2969..c2763ced3 100644 --- a/src/.vuepress/sidebar/V2.0.x/en-Table.ts +++ b/src/.vuepress/sidebar/V2.0.x/en-Table.ts @@ -221,6 +221,13 @@ export const enSidebar = { { text: 'MyBatisPlus Generator', link: 'MyBatisPlus-Generator' }, ], }, + { + text: 'IoT Platform', + collapsible: true, + children: [ + { text: 'ThingsBoard', link: 'Thingsboard' }, + ], + }, ], }, { diff --git a/src/.vuepress/sidebar/V2.0.x/zh-Table.ts b/src/.vuepress/sidebar/V2.0.x/zh-Table.ts index ce2dc8f4f..f8377108a 100644 --- a/src/.vuepress/sidebar/V2.0.x/zh-Table.ts +++ b/src/.vuepress/sidebar/V2.0.x/zh-Table.ts @@ -221,6 +221,13 @@ export const zhSidebar = { { text: 'MyBatisPlus Generator', link: 'MyBatisPlus-Generator' }, ], }, + { + text: '物联网对接', + collapsible: true, + children: [ + { text: 'ThingsBoard', link: 'Thingsboard' }, + ], + }, ], }, { diff --git a/src/UserGuide/Master/Table/Ecosystem-Integration/Ecosystem-Overview_apache.md b/src/UserGuide/Master/Table/Ecosystem-Integration/Ecosystem-Overview_apache.md index 96d73ffc1..87d8850eb 100644 --- a/src/UserGuide/Master/Table/Ecosystem-Integration/Ecosystem-Overview_apache.md +++ b/src/UserGuide/Master/Table/Ecosystem-Integration/Ecosystem-Overview_apache.md @@ -41,4 +41,6 @@ The following documentation will help you quickly and comprehensively understand - Programming Framework - Spring Boot Starter [Spring Boot Starter](./Spring-Boot-Starter.md) - Mybatis Generator [Mybatis Generator](./Mybatis-Generator.md) - - MyBatisPlus Generator [MyBatisPlus Generator](./MyBatisPlus-Generator.md) \ No newline at end of file + - MyBatisPlus Generator [MyBatisPlus Generator](./MyBatisPlus-Generator.md) +- IoT Platform + - ThingsBoard [ThingsBoard](./Thingsboard.md) \ No newline at end of file diff --git a/src/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md b/src/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md new file mode 100644 index 000000000..10defefba --- /dev/null +++ b/src/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md @@ -0,0 +1,203 @@ + + +# ThingsBoard + +## 1. Overview + +ThingsBoard is an open-source IoT platform for device management, data collection +and visualisation. It stores device telemetry, latest-value telemetry and entity +attributes through three storage SPIs, which allows the storage layer to be +replaced without changing the platform itself. + +`iotdb-thingsboard-table` implements those three SPIs on top of the IoTDB Table +Model, so a ThingsBoard deployment can keep its telemetry in IoTDB instead of +Cassandra or a relational database: + +| ThingsBoard SPI | Implementation | Purpose | +| --- | --- | --- | +| `TimeseriesDao` | `IoTDBTableTimeseriesDao` | Historical telemetry: batched writes, raw and time-bucketed aggregation reads, deletes | +| `TimeseriesLatestDao` | `IoTDBTableLatestDao` | Latest value per telemetry key | +| `AttributesDao` | `IoTDBTableAttributesDao` | Entity attributes, scoped by `SERVER_SCOPE` / `SHARED_SCOPE` / `CLIENT_SCOPE` | + +This page covers the **Table Model** integration, which runs against a stock +ThingsBoard release and is enabled by putting the module and its runtime +dependencies on the classpath and setting a few properties. The deployment +section below lists the exact set; it is not a single jar. +There is a separate, earlier integration for the **Tree Model**, described in +[ThingsBoard (Tree Model)](../../Tree/Ecosystem-Integration/Thingsboard.md); +that one stores data under `root.thingsboard` and requires an IoTDB-adapted +ThingsBoard build rather than a stock one. The two are independent — pick the one +that matches the data model you are using. + +Writes are batched through a bounded asynchronous queue into IoTDB tablets. +Reads cover both the raw path and the aggregation path: fixed-width millisecond +buckets use IoTDB's native `date_bin`, while calendar buckets +(`WEEK` / `WEEK_ISO` / `MONTH` / `QUARTER`) are walked per bucket so that +boundaries match ThingsBoard's own semantics in the timezone carried by each +query. + +## 2. Usage Steps + +### 2.1 Version Requirements + +* `IoTDB: 2.0.8` (Table Model) — the version the integration tests are executed + against (`apache/iotdb:2.0.8-standalone`). Other 2.x releases are untested. +* `ThingsBoard: 4.3.1.2` +* `JDK: >= 17` + +The module is compiled against the ThingsBoard 4.3.1.2 SPI surface. Because +ThingsBoard's `common/data` and `dao` artifacts are not published to Maven +Central, the module builds against a compile-only surface of the types it uses; +those classes are excluded from the packaged jar, so at runtime the real +ThingsBoard classes are used. + +### 2.2 Obtain the jar + +Build it from the `iotdb-extras` repository. The module sits behind an explicit +opt-in profile, so a plain reactor build does not include it: + +```bash +# from the apache/iotdb-extras repository root, with JDK 17+ +# https://github.com/apache/iotdb-extras +mvn -pl iotdb-thingsboard-table -am -P with-thingsboard clean package +``` + +The jar is produced under `iotdb-thingsboard-table/target/`. + +### 2.3 Deploy into ThingsBoard + +ThingsBoard must be installed **before** the module is enabled: its installer +resolves a `TsDatabaseSchemaService` bean that only the built-in backends +provide, so starting with the selectors of section 3 already set makes the +install step fail. + +1. Install and start IoTDB, see [IoTDB QuickStart](../QuickStart/QuickStart.md). +2. Install ThingsBoard normally, with none of the properties in section 3 set. +3. Put the module **and the IoTDB client's runtime dependencies** on + ThingsBoard's classpath. The module jar alone is not enough — it fails at + first session creation with `NoClassDefFoundError` on `ITableSessionPool`. + Collect the set with `dependency:copy-dependencies -DincludeScope=runtime`, + then remove the artifacts ThingsBoard already bundles, or its newer copies + are shadowed by the module's older ones. Against ThingsBoard 4.3.1.2 that is + nine — `antlr4-runtime`, `commons-codec`, `commons-io`, `commons-lang3`, + `httpclient`, `httpcore`, `lz4-java`, `snappy-java`, `zstd-jni` — plus + `commons-logging`, which ThingsBoard asks to have removed because it uses + `spring-jcl`. `antlr4-runtime` is the one that bites hardest: ThingsBoard + 4.3.1.2 ships 4.13.0 and the module brings 4.9.3, after which Spring Data + JPA's `HqlLexer` cannot deserialise its own grammar and startup fails. + Eleven jars remain. Re-derive that set against the ThingsBoard release you + are deploying to, and prefer a deployment that boots over a dependency diff: + the diff answers whether ThingsBoard *ships* an artifact, not whether it + *tolerates* one, and `commons-logging` is exactly where those differ. +4. How the classpath is extended depends on the installation. ThingsBoard runs + as a Spring Boot application: the Docker images launch through + `PropertiesLauncher` and already honour a `LOADER_PATH` entry, so placing the + jars in `/usr/share/thingsboard/extensions` is enough; the deb/rpm packages + execute the distribution jar directly. Consult ThingsBoard's own deployment + documentation for the installation method you use. +5. Add the configuration below to ThingsBoard's `thingsboard.yml`, or supply the + equivalent environment variables. +6. Restart ThingsBoard. On first start the module creates its database and tables + in IoTDB, unless that bootstrap is disabled. + +The module is a Spring Boot auto-configuration, so no component scanning or code +change is required on the ThingsBoard side. + +## 3. Configuration + +### 3.1 Activation + +Historical telemetry needs the timeseries selector plus the explicit opt-in: + +```Properties +# select this backend for historical telemetry +database.ts.type=iotdb-table +# explicit opt-in; required together with the selector above +iotdb.ts.experimental-raw-only=true +``` + +Latest-value telemetry needs its **own** selector in addition to those two. If it +is omitted, historical telemetry is stored in IoTDB while latest values stay on +ThingsBoard's default backend, with no error at startup: + +```Properties +database.ts_latest.type=iotdb-table +# required when the latest DAO is active: sticky-routing | disabled +iotdb.ts_latest.cluster_mode=sticky-routing +``` + +Entity attributes are a separate opt-in and are inert unless enabled. Unlike the +two timeseries selectors it is not independent of the host: ThingsBoard has no +attributes-backend switch of its own, so enabling this one changes ThingsBoard's +bean graph rather than only this module's. + +> **Do not enable this on a stock ThingsBoard yet.** ThingsBoard has no +> configuration switch of its own for attributes storage — its JPA attributes +> bean registers unconditionally — so setting the selector below makes startup +> fail on the module's conflict check. A fix is under review upstream as +> apache/iotdb-extras#125. It withdraws exactly one bean — ThingsBoard's own +> `jpaAttributeDao`, matched on both the bean name and the fully-qualified class +> name — and fails startup on any other competing `AttributesDao` rather than +> removing it. This note applies until that PR is merged and a build containing +> it is released. + + +```Properties +database.attributes.type=iotdb-table +# required when the attribute DAO is active: sticky-routing | disabled +iotdb.attributes.cluster_mode=sticky-routing +``` + +### 3.2 Connection and schema + +| Property | Default | Meaning | +| --- | --- | --- | +| `iotdb.host` / `iotdb.port` | `127.0.0.1` / `6667` | IoTDB node address | +| `iotdb.username` / `iotdb.password` | `root` / `root` | IoTDB credentials | +| `iotdb.database` | `thingsboard` | Target IoTDB database | +| `iotdb.session-pool-size` | `8` | Table session pool size | +| `iotdb.schema.bootstrap` | `true` | Create the database and tables on first start; set to `false` to manage the schema out of band | + +### 3.3 Cluster mode + +`iotdb.attributes.cluster_mode` and `iotdb.ts_latest.cluster_mode` must be set +explicitly when the corresponding DAO is active. Accepted values: + +* `sticky-routing` — writes for one identity are pinned to a single node +* `disabled` — single-node deployment, or best-effort convergence accepted + +Any other value, including leaving it empty, fails at startup rather than +silently. These write paths converge within a single JVM, so a multi-writer +cluster needs one of the two acknowledgements above. + +## 4. Known Limitations + +* Attribute and latest-overlay writes converge within a single JVM. A clustered + deployment must either pin each identity to one node (`sticky-routing`) or + explicitly accept best-effort convergence (`disabled`) — which is why the + cluster mode has to be stated rather than defaulted. +* The latest-value path is derived from the telemetry table, with a small overlay + for the latest-only write and delete paths that a pure derivation cannot + express. +* Table-level TTL is used for retention; see the module's + [user guide](https://github.com/apache/iotdb-extras/blob/master/iotdb-thingsboard-table/docs/user-guide.md) + for how it maps onto ThingsBoard's own retention settings. diff --git a/src/UserGuide/latest-Table/Ecosystem-Integration/Ecosystem-Overview_apache.md b/src/UserGuide/latest-Table/Ecosystem-Integration/Ecosystem-Overview_apache.md index 96d73ffc1..87d8850eb 100644 --- a/src/UserGuide/latest-Table/Ecosystem-Integration/Ecosystem-Overview_apache.md +++ b/src/UserGuide/latest-Table/Ecosystem-Integration/Ecosystem-Overview_apache.md @@ -41,4 +41,6 @@ The following documentation will help you quickly and comprehensively understand - Programming Framework - Spring Boot Starter [Spring Boot Starter](./Spring-Boot-Starter.md) - Mybatis Generator [Mybatis Generator](./Mybatis-Generator.md) - - MyBatisPlus Generator [MyBatisPlus Generator](./MyBatisPlus-Generator.md) \ No newline at end of file + - MyBatisPlus Generator [MyBatisPlus Generator](./MyBatisPlus-Generator.md) +- IoT Platform + - ThingsBoard [ThingsBoard](./Thingsboard.md) \ No newline at end of file diff --git a/src/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md b/src/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md new file mode 100644 index 000000000..c479850ad --- /dev/null +++ b/src/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md @@ -0,0 +1,203 @@ + + +# ThingsBoard + +## 1. Overview + +ThingsBoard is an open-source IoT platform for device management, data collection +and visualisation. It stores device telemetry, latest-value telemetry and entity +attributes through three storage SPIs, which allows the storage layer to be +replaced without changing the platform itself. + +`iotdb-thingsboard-table` implements those three SPIs on top of the IoTDB Table +Model, so a ThingsBoard deployment can keep its telemetry in IoTDB instead of +Cassandra or a relational database: + +| ThingsBoard SPI | Implementation | Purpose | +| --- | --- | --- | +| `TimeseriesDao` | `IoTDBTableTimeseriesDao` | Historical telemetry: batched writes, raw and time-bucketed aggregation reads, deletes | +| `TimeseriesLatestDao` | `IoTDBTableLatestDao` | Latest value per telemetry key | +| `AttributesDao` | `IoTDBTableAttributesDao` | Entity attributes, scoped by `SERVER_SCOPE` / `SHARED_SCOPE` / `CLIENT_SCOPE` | + +This page covers the **Table Model** integration, which runs against a stock +ThingsBoard release and is enabled by putting the module and its runtime +dependencies on the classpath and setting a few properties. The deployment +section below lists the exact set; it is not a single jar. +There is a separate, earlier integration for the **Tree Model**, described in +[ThingsBoard (Tree Model)](../../latest/Ecosystem-Integration/Thingsboard.md); +that one stores data under `root.thingsboard` and requires an IoTDB-adapted +ThingsBoard build rather than a stock one. The two are independent — pick the one +that matches the data model you are using. + +Writes are batched through a bounded asynchronous queue into IoTDB tablets. +Reads cover both the raw path and the aggregation path: fixed-width millisecond +buckets use IoTDB's native `date_bin`, while calendar buckets +(`WEEK` / `WEEK_ISO` / `MONTH` / `QUARTER`) are walked per bucket so that +boundaries match ThingsBoard's own semantics in the timezone carried by each +query. + +## 2. Usage Steps + +### 2.1 Version Requirements + +* `IoTDB: 2.0.8` (Table Model) — the version the integration tests are executed + against (`apache/iotdb:2.0.8-standalone`). Other 2.x releases are untested. +* `ThingsBoard: 4.3.1.2` +* `JDK: >= 17` + +The module is compiled against the ThingsBoard 4.3.1.2 SPI surface. Because +ThingsBoard's `common/data` and `dao` artifacts are not published to Maven +Central, the module builds against a compile-only surface of the types it uses; +those classes are excluded from the packaged jar, so at runtime the real +ThingsBoard classes are used. + +### 2.2 Obtain the jar + +Build it from the `iotdb-extras` repository. The module sits behind an explicit +opt-in profile, so a plain reactor build does not include it: + +```bash +# from the apache/iotdb-extras repository root, with JDK 17+ +# https://github.com/apache/iotdb-extras +mvn -pl iotdb-thingsboard-table -am -P with-thingsboard clean package +``` + +The jar is produced under `iotdb-thingsboard-table/target/`. + +### 2.3 Deploy into ThingsBoard + +ThingsBoard must be installed **before** the module is enabled: its installer +resolves a `TsDatabaseSchemaService` bean that only the built-in backends +provide, so starting with the selectors of section 3 already set makes the +install step fail. + +1. Install and start IoTDB, see [IoTDB QuickStart](../QuickStart/QuickStart.md). +2. Install ThingsBoard normally, with none of the properties in section 3 set. +3. Put the module **and the IoTDB client's runtime dependencies** on + ThingsBoard's classpath. The module jar alone is not enough — it fails at + first session creation with `NoClassDefFoundError` on `ITableSessionPool`. + Collect the set with `dependency:copy-dependencies -DincludeScope=runtime`, + then remove the artifacts ThingsBoard already bundles, or its newer copies + are shadowed by the module's older ones. Against ThingsBoard 4.3.1.2 that is + nine — `antlr4-runtime`, `commons-codec`, `commons-io`, `commons-lang3`, + `httpclient`, `httpcore`, `lz4-java`, `snappy-java`, `zstd-jni` — plus + `commons-logging`, which ThingsBoard asks to have removed because it uses + `spring-jcl`. `antlr4-runtime` is the one that bites hardest: ThingsBoard + 4.3.1.2 ships 4.13.0 and the module brings 4.9.3, after which Spring Data + JPA's `HqlLexer` cannot deserialise its own grammar and startup fails. + Eleven jars remain. Re-derive that set against the ThingsBoard release you + are deploying to, and prefer a deployment that boots over a dependency diff: + the diff answers whether ThingsBoard *ships* an artifact, not whether it + *tolerates* one, and `commons-logging` is exactly where those differ. +4. How the classpath is extended depends on the installation. ThingsBoard runs + as a Spring Boot application: the Docker images launch through + `PropertiesLauncher` and already honour a `LOADER_PATH` entry, so placing the + jars in `/usr/share/thingsboard/extensions` is enough; the deb/rpm packages + execute the distribution jar directly. Consult ThingsBoard's own deployment + documentation for the installation method you use. +5. Add the configuration below to ThingsBoard's `thingsboard.yml`, or supply the + equivalent environment variables. +6. Restart ThingsBoard. On first start the module creates its database and tables + in IoTDB, unless that bootstrap is disabled. + +The module is a Spring Boot auto-configuration, so no component scanning or code +change is required on the ThingsBoard side. + +## 3. Configuration + +### 3.1 Activation + +Historical telemetry needs the timeseries selector plus the explicit opt-in: + +```Properties +# select this backend for historical telemetry +database.ts.type=iotdb-table +# explicit opt-in; required together with the selector above +iotdb.ts.experimental-raw-only=true +``` + +Latest-value telemetry needs its **own** selector in addition to those two. If it +is omitted, historical telemetry is stored in IoTDB while latest values stay on +ThingsBoard's default backend, with no error at startup: + +```Properties +database.ts_latest.type=iotdb-table +# required when the latest DAO is active: sticky-routing | disabled +iotdb.ts_latest.cluster_mode=sticky-routing +``` + +Entity attributes are a separate opt-in and are inert unless enabled. Unlike the +two timeseries selectors it is not independent of the host: ThingsBoard has no +attributes-backend switch of its own, so enabling this one changes ThingsBoard's +bean graph rather than only this module's. + +> **Do not enable this on a stock ThingsBoard yet.** ThingsBoard has no +> configuration switch of its own for attributes storage — its JPA attributes +> bean registers unconditionally — so setting the selector below makes startup +> fail on the module's conflict check. A fix is under review upstream as +> apache/iotdb-extras#125. It withdraws exactly one bean — ThingsBoard's own +> `jpaAttributeDao`, matched on both the bean name and the fully-qualified class +> name — and fails startup on any other competing `AttributesDao` rather than +> removing it. This note applies until that PR is merged and a build containing +> it is released. + + +```Properties +database.attributes.type=iotdb-table +# required when the attribute DAO is active: sticky-routing | disabled +iotdb.attributes.cluster_mode=sticky-routing +``` + +### 3.2 Connection and schema + +| Property | Default | Meaning | +| --- | --- | --- | +| `iotdb.host` / `iotdb.port` | `127.0.0.1` / `6667` | IoTDB node address | +| `iotdb.username` / `iotdb.password` | `root` / `root` | IoTDB credentials | +| `iotdb.database` | `thingsboard` | Target IoTDB database | +| `iotdb.session-pool-size` | `8` | Table session pool size | +| `iotdb.schema.bootstrap` | `true` | Create the database and tables on first start; set to `false` to manage the schema out of band | + +### 3.3 Cluster mode + +`iotdb.attributes.cluster_mode` and `iotdb.ts_latest.cluster_mode` must be set +explicitly when the corresponding DAO is active. Accepted values: + +* `sticky-routing` — writes for one identity are pinned to a single node +* `disabled` — single-node deployment, or best-effort convergence accepted + +Any other value, including leaving it empty, fails at startup rather than +silently. These write paths converge within a single JVM, so a multi-writer +cluster needs one of the two acknowledgements above. + +## 4. Known Limitations + +* Attribute and latest-overlay writes converge within a single JVM. A clustered + deployment must either pin each identity to one node (`sticky-routing`) or + explicitly accept best-effort convergence (`disabled`) — which is why the + cluster mode has to be stated rather than defaulted. +* The latest-value path is derived from the telemetry table, with a small overlay + for the latest-only write and delete paths that a pure derivation cannot + express. +* Table-level TTL is used for retention; see the module's + [user guide](https://github.com/apache/iotdb-extras/blob/master/iotdb-thingsboard-table/docs/user-guide.md) + for how it maps onto ThingsBoard's own retention settings. diff --git a/src/zh/UserGuide/Master/Table/Ecosystem-Integration/Ecosystem-Overview_apache.md b/src/zh/UserGuide/Master/Table/Ecosystem-Integration/Ecosystem-Overview_apache.md index a27cd20bd..7dd061f5a 100644 --- a/src/zh/UserGuide/Master/Table/Ecosystem-Integration/Ecosystem-Overview_apache.md +++ b/src/zh/UserGuide/Master/Table/Ecosystem-Integration/Ecosystem-Overview_apache.md @@ -35,4 +35,6 @@ IoTDB 生态集成打通时序数据全链路:通过数据采集实现设备 - 编程框架 - Spring Boot Starter [Spring Boot Starter](./Spring-Boot-Starter.md) - Mybatis Generator [Mybatis Generator](./Mybatis-Generator.md) - - MyBatisPlus Generator [MyBatisPlus Generator](./MyBatisPlus-Generator.md) \ No newline at end of file + - MyBatisPlus Generator [MyBatisPlus Generator](./MyBatisPlus-Generator.md) +- 物联网对接 + - ThingsBoard [ThingsBoard](./Thingsboard.md) \ No newline at end of file diff --git a/src/zh/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md b/src/zh/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md new file mode 100644 index 000000000..cbf372d0f --- /dev/null +++ b/src/zh/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md @@ -0,0 +1,171 @@ + + +# ThingsBoard + +## 1. 功能概述 + +ThingsBoard 是一个开源物联网平台,用于设备管理、数据采集与可视化。它通过三个存储 SPI +分别存放设备时序数据、最新值时序数据和实体属性,因此可以在不改动平台本身的前提下替换存储层。 + +`iotdb-thingsboard-table` 在 IoTDB 表模型之上实现了这三个 SPI,使 ThingsBoard 部署可以把 +时序数据存进 IoTDB,而不是 Cassandra 或关系型数据库: + +| ThingsBoard SPI | 实现 | 用途 | +| --- | --- | --- | +| `TimeseriesDao` | `IoTDBTableTimeseriesDao` | 历史时序:批量写入、原始读与时间分桶聚合读、删除 | +| `TimeseriesLatestDao` | `IoTDBTableLatestDao` | 每个 telemetry key 的最新值 | +| `AttributesDao` | `IoTDBTableAttributesDao` | 实体属性,按 `SERVER_SCOPE` / `SHARED_SCOPE` / `CLIENT_SCOPE` 分域 | + +本页介绍的是**表模型**集成:它对接的是原版 ThingsBoard 发行版,把模块及其运行期依赖放上 +classpath 并配置若干属性即可启用。下面的部署一节列出了确切的 jar 集合,不是单独一个 jar。 +**树模型**另有一套更早的集成,见 +[ThingsBoard(树模型)](../../Tree/Ecosystem-Integration/Thingsboard.md);那一套把数据写在 +`root.thingsboard` 下,并且需要 IoTDB 适配版的 ThingsBoard 安装包而非原版。两者互相独立, +按你使用的数据模型选择即可。 + +写入经由一个有界异步队列批量落入 IoTDB tablet。读取覆盖原始路径和聚合路径:固定宽度的毫秒 +分桶使用 IoTDB 原生的 `date_bin`;日历分桶(`WEEK` / `WEEK_ISO` / `MONTH` / `QUARTER`) +则逐桶推进,以保证边界与 ThingsBoard 自身语义一致,并落在每个查询各自携带的时区上。 + +## 2. 使用步骤 + +### 2.1 版本要求 + +* `IoTDB: 2.0.8`(表模型)—— 集成测试实际运行的版本(`apache/iotdb:2.0.8-standalone`), + 其他 2.x 版本未经测试。 +* `ThingsBoard: 4.3.1.2` +* `JDK: >= 17` + +该模块是对着 ThingsBoard 4.3.1.2 的 SPI 编译的。由于 ThingsBoard 的 `common/data` 与 `dao` +构件未发布到 Maven Central,模块编译时使用的是其所需类型的编译期替身,这些类不会被打进产物 +jar,因此运行时使用的是真实的 ThingsBoard 类。 + +### 2.2 获取 jar + +从 `iotdb-extras` 仓库构建。该模块位于一个需显式开启的 profile 之后,普通的 reactor 构建不会 +包含它: + +```bash +# 在 apache/iotdb-extras 仓库根目录,使用 JDK 17+ +# https://github.com/apache/iotdb-extras +mvn -pl iotdb-thingsboard-table -am -P with-thingsboard clean package +``` + +产物 jar 位于 `iotdb-thingsboard-table/target/` 下。 + +### 2.3 部署到 ThingsBoard + +必须**先装好 ThingsBoard,再启用模块**:它的安装器会解析一个只有内置后端才提供的 +`TsDatabaseSchemaService` bean,因此在第 3 节的选择器已经打开的情况下启动,安装会失败。 + +1. 安装并启动 IoTDB,参见 [IoTDB 快速上手](../QuickStart/QuickStart.md)。 +2. 按常规方式装好 ThingsBoard,此时不要设置第 3 节的任何属性。 +3. 把模块**以及 IoTDB 客户端的运行期依赖**一起放上 ThingsBoard 的 classpath。只放模块 jar + 是不够的——首次创建会话时会抛 `ITableSessionPool` 的 `NoClassDefFoundError`。用 + `dependency:copy-dependencies -DincludeScope=runtime` 收集,然后**去掉 ThingsBoard 已经 + 自带的那些**,否则它更新的版本会被模块带的旧版本盖住。对 ThingsBoard 4.3.1.2 而言是九个 + ——`antlr4-runtime`、`commons-codec`、`commons-io`、`commons-lang3`、`httpclient`、 + `httpcore`、`lz4-java`、`snappy-java`、`zstd-jni`——外加 `commons-logging`,那个是 + ThingsBoard 自己要求移除的(它使用 `spring-jcl`)。其中 `antlr4-runtime` 后果最严重: + ThingsBoard 4.3.1.2 自带 4.13.0,模块带的是 4.9.3,之后 Spring Data JPA 的 `HqlLexer` + 无法反序列化自己的语法,启动直接失败。剩下十一个 jar。请对着你要部署的那个 ThingsBoard + 版本重新推导这个集合,并且**以能启动的部署为准,而不是以依赖比对为准**:比对回答的是 + ThingsBoard 是否*自带*某个构件,而不是它是否*容得下*某个构件,`commons-logging` 正是 + 两者分道扬镳的地方。 +4. 如何扩展 classpath 取决于安装方式。ThingsBoard 以 Spring Boot 应用运行:Docker 镜像通过 + `PropertiesLauncher` 启动并已支持 `LOADER_PATH`,把这些 jar 放进 + `/usr/share/thingsboard/extensions` 即可;deb/rpm 包则直接执行发行版 jar。请按你所用的 + 安装方式查阅 ThingsBoard 自身的部署文档。 +5. 将下方配置写入 ThingsBoard 的 `thingsboard.yml`,或提供等价的环境变量。 +6. 重启 ThingsBoard。首次启动时,模块会在 IoTDB 中创建所需的数据库与表,除非关闭了该引导。 + +该模块是一个 Spring Boot 自动配置,因此 ThingsBoard 侧无需组件扫描或代码改动。 + +## 3. 配置 + +### 3.1 激活 + +历史时序需要时序选择器加上显式开关: + +```Properties +# 选择本后端作为历史时序存储 +database.ts.type=iotdb-table +# 显式开关,必须与上面的选择器同时设置 +iotdb.ts.experimental-raw-only=true +``` + +最新值时序**另需**它自己的选择器。若遗漏,历史时序会存入 IoTDB,而最新值仍留在 ThingsBoard +的默认后端,且启动时不会报错: + +```Properties +database.ts_latest.type=iotdb-table +# 最新值 DAO 激活时必填:sticky-routing | disabled +iotdb.ts_latest.cluster_mode=sticky-routing +``` + +实体属性是单独的一项开关,未启用时该 DAO 不生效。与两个时间序列选择器不同,它并非 +独立于宿主:ThingsBoard 自身没有属性存储的配置开关,因此启用这一项会改动 +ThingsBoard 的 bean 图,而不只是本模块的。 + +> **暂时不要在原版 ThingsBoard 上启用这一项。** ThingsBoard 自身没有为属性存储提供配置开关 +> ——它的 JPA 属性 bean 是无条件注册的——因此设置下面这个选择器会让模块的冲突检查导致启动失败。 +> 相应的修复正在上游 apache/iotdb-extras#125 中等待评审。它只撤下一个 bean —— +> ThingsBoard 自己的 `jpaAttributeDao`,按 bean 名与全限定类名同时匹配 —— 其余任何 +> 竞争的 `AttributesDao` 一律不动,而是让启动失败。在该 PR 合入且包含它的构建发布之前, +> 本提示有效。 + +```Properties +database.attributes.type=iotdb-table +# 属性 DAO 激活时必填:sticky-routing | disabled +iotdb.attributes.cluster_mode=sticky-routing +``` + +### 3.2 连接与表结构 + +| 属性 | 默认值 | 含义 | +| --- | --- | --- | +| `iotdb.host` / `iotdb.port` | `127.0.0.1` / `6667` | IoTDB 节点地址 | +| `iotdb.username` / `iotdb.password` | `root` / `root` | IoTDB 凭据 | +| `iotdb.database` | `thingsboard` | 目标 IoTDB 数据库 | +| `iotdb.session-pool-size` | `8` | 表会话池大小 | +| `iotdb.schema.bootstrap` | `true` | 首次启动时创建数据库与表;若自行管理表结构则设为 `false` | + +### 3.3 集群模式 + +当对应 DAO 激活时,`iotdb.attributes.cluster_mode` 与 `iotdb.ts_latest.cluster_mode` 必须显式 +设置。可选值: + +* `sticky-routing` —— 同一 identity 的写入固定路由到单个节点 +* `disabled` —— 单节点部署,或已接受尽力而为的收敛 + +其他取值(包括留空)会在启动时直接失败,而不是静默通过。这两条写入路径只在单个 JVM 内收敛, +因此多写入者的集群部署需要上述两种确认之一。 + +## 4. 已知限制 + +* 属性写入与最新值覆盖层的写入只在单个 JVM 内收敛。集群部署必须二选一:把每个 identity 固定 + 到单个节点(`sticky-routing`),或显式接受尽力而为的收敛(`disabled`)—— 这正是集群模式必须 + 显式声明而非取默认值的原因。 +* 最新值路径由时序表派生而来,另有一个很小的覆盖层,用于承接纯派生无法表达的"只写最新值"与 + "只删最新值"路径。 +* 保留策略使用表级 TTL,其与 ThingsBoard 自身保留设置的对应关系,参见模块的 + [用户指南](https://github.com/apache/iotdb-extras/blob/master/iotdb-thingsboard-table/docs/user-guide.md)。 diff --git a/src/zh/UserGuide/latest-Table/Ecosystem-Integration/Ecosystem-Overview_apache.md b/src/zh/UserGuide/latest-Table/Ecosystem-Integration/Ecosystem-Overview_apache.md index a27cd20bd..7dd061f5a 100644 --- a/src/zh/UserGuide/latest-Table/Ecosystem-Integration/Ecosystem-Overview_apache.md +++ b/src/zh/UserGuide/latest-Table/Ecosystem-Integration/Ecosystem-Overview_apache.md @@ -35,4 +35,6 @@ IoTDB 生态集成打通时序数据全链路:通过数据采集实现设备 - 编程框架 - Spring Boot Starter [Spring Boot Starter](./Spring-Boot-Starter.md) - Mybatis Generator [Mybatis Generator](./Mybatis-Generator.md) - - MyBatisPlus Generator [MyBatisPlus Generator](./MyBatisPlus-Generator.md) \ No newline at end of file + - MyBatisPlus Generator [MyBatisPlus Generator](./MyBatisPlus-Generator.md) +- 物联网对接 + - ThingsBoard [ThingsBoard](./Thingsboard.md) \ No newline at end of file diff --git a/src/zh/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md b/src/zh/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md new file mode 100644 index 000000000..c24a26020 --- /dev/null +++ b/src/zh/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md @@ -0,0 +1,171 @@ + + +# ThingsBoard + +## 1. 功能概述 + +ThingsBoard 是一个开源物联网平台,用于设备管理、数据采集与可视化。它通过三个存储 SPI +分别存放设备时序数据、最新值时序数据和实体属性,因此可以在不改动平台本身的前提下替换存储层。 + +`iotdb-thingsboard-table` 在 IoTDB 表模型之上实现了这三个 SPI,使 ThingsBoard 部署可以把 +时序数据存进 IoTDB,而不是 Cassandra 或关系型数据库: + +| ThingsBoard SPI | 实现 | 用途 | +| --- | --- | --- | +| `TimeseriesDao` | `IoTDBTableTimeseriesDao` | 历史时序:批量写入、原始读与时间分桶聚合读、删除 | +| `TimeseriesLatestDao` | `IoTDBTableLatestDao` | 每个 telemetry key 的最新值 | +| `AttributesDao` | `IoTDBTableAttributesDao` | 实体属性,按 `SERVER_SCOPE` / `SHARED_SCOPE` / `CLIENT_SCOPE` 分域 | + +本页介绍的是**表模型**集成:它对接的是原版 ThingsBoard 发行版,把模块及其运行期依赖放上 +classpath 并配置若干属性即可启用。下面的部署一节列出了确切的 jar 集合,不是单独一个 jar。 +**树模型**另有一套更早的集成,见 +[ThingsBoard(树模型)](../../latest/Ecosystem-Integration/Thingsboard.md);那一套把数据写在 +`root.thingsboard` 下,并且需要 IoTDB 适配版的 ThingsBoard 安装包而非原版。两者互相独立, +按你使用的数据模型选择即可。 + +写入经由一个有界异步队列批量落入 IoTDB tablet。读取覆盖原始路径和聚合路径:固定宽度的毫秒 +分桶使用 IoTDB 原生的 `date_bin`;日历分桶(`WEEK` / `WEEK_ISO` / `MONTH` / `QUARTER`) +则逐桶推进,以保证边界与 ThingsBoard 自身语义一致,并落在每个查询各自携带的时区上。 + +## 2. 使用步骤 + +### 2.1 版本要求 + +* `IoTDB: 2.0.8`(表模型)—— 集成测试实际运行的版本(`apache/iotdb:2.0.8-standalone`), + 其他 2.x 版本未经测试。 +* `ThingsBoard: 4.3.1.2` +* `JDK: >= 17` + +该模块是对着 ThingsBoard 4.3.1.2 的 SPI 编译的。由于 ThingsBoard 的 `common/data` 与 `dao` +构件未发布到 Maven Central,模块编译时使用的是其所需类型的编译期替身,这些类不会被打进产物 +jar,因此运行时使用的是真实的 ThingsBoard 类。 + +### 2.2 获取 jar + +从 `iotdb-extras` 仓库构建。该模块位于一个需显式开启的 profile 之后,普通的 reactor 构建不会 +包含它: + +```bash +# 在 apache/iotdb-extras 仓库根目录,使用 JDK 17+ +# https://github.com/apache/iotdb-extras +mvn -pl iotdb-thingsboard-table -am -P with-thingsboard clean package +``` + +产物 jar 位于 `iotdb-thingsboard-table/target/` 下。 + +### 2.3 部署到 ThingsBoard + +必须**先装好 ThingsBoard,再启用模块**:它的安装器会解析一个只有内置后端才提供的 +`TsDatabaseSchemaService` bean,因此在第 3 节的选择器已经打开的情况下启动,安装会失败。 + +1. 安装并启动 IoTDB,参见 [IoTDB 快速上手](../QuickStart/QuickStart.md)。 +2. 按常规方式装好 ThingsBoard,此时不要设置第 3 节的任何属性。 +3. 把模块**以及 IoTDB 客户端的运行期依赖**一起放上 ThingsBoard 的 classpath。只放模块 jar + 是不够的——首次创建会话时会抛 `ITableSessionPool` 的 `NoClassDefFoundError`。用 + `dependency:copy-dependencies -DincludeScope=runtime` 收集,然后**去掉 ThingsBoard 已经 + 自带的那些**,否则它更新的版本会被模块带的旧版本盖住。对 ThingsBoard 4.3.1.2 而言是九个 + ——`antlr4-runtime`、`commons-codec`、`commons-io`、`commons-lang3`、`httpclient`、 + `httpcore`、`lz4-java`、`snappy-java`、`zstd-jni`——外加 `commons-logging`,那个是 + ThingsBoard 自己要求移除的(它使用 `spring-jcl`)。其中 `antlr4-runtime` 后果最严重: + ThingsBoard 4.3.1.2 自带 4.13.0,模块带的是 4.9.3,之后 Spring Data JPA 的 `HqlLexer` + 无法反序列化自己的语法,启动直接失败。剩下十一个 jar。请对着你要部署的那个 ThingsBoard + 版本重新推导这个集合,并且**以能启动的部署为准,而不是以依赖比对为准**:比对回答的是 + ThingsBoard 是否*自带*某个构件,而不是它是否*容得下*某个构件,`commons-logging` 正是 + 两者分道扬镳的地方。 +4. 如何扩展 classpath 取决于安装方式。ThingsBoard 以 Spring Boot 应用运行:Docker 镜像通过 + `PropertiesLauncher` 启动并已支持 `LOADER_PATH`,把这些 jar 放进 + `/usr/share/thingsboard/extensions` 即可;deb/rpm 包则直接执行发行版 jar。请按你所用的 + 安装方式查阅 ThingsBoard 自身的部署文档。 +5. 将下方配置写入 ThingsBoard 的 `thingsboard.yml`,或提供等价的环境变量。 +6. 重启 ThingsBoard。首次启动时,模块会在 IoTDB 中创建所需的数据库与表,除非关闭了该引导。 + +该模块是一个 Spring Boot 自动配置,因此 ThingsBoard 侧无需组件扫描或代码改动。 + +## 3. 配置 + +### 3.1 激活 + +历史时序需要时序选择器加上显式开关: + +```Properties +# 选择本后端作为历史时序存储 +database.ts.type=iotdb-table +# 显式开关,必须与上面的选择器同时设置 +iotdb.ts.experimental-raw-only=true +``` + +最新值时序**另需**它自己的选择器。若遗漏,历史时序会存入 IoTDB,而最新值仍留在 ThingsBoard +的默认后端,且启动时不会报错: + +```Properties +database.ts_latest.type=iotdb-table +# 最新值 DAO 激活时必填:sticky-routing | disabled +iotdb.ts_latest.cluster_mode=sticky-routing +``` + +实体属性是单独的一项开关,未启用时该 DAO 不生效。与两个时间序列选择器不同,它并非 +独立于宿主:ThingsBoard 自身没有属性存储的配置开关,因此启用这一项会改动 +ThingsBoard 的 bean 图,而不只是本模块的。 + +> **暂时不要在原版 ThingsBoard 上启用这一项。** ThingsBoard 自身没有为属性存储提供配置开关 +> ——它的 JPA 属性 bean 是无条件注册的——因此设置下面这个选择器会让模块的冲突检查导致启动失败。 +> 相应的修复正在上游 apache/iotdb-extras#125 中等待评审。它只撤下一个 bean —— +> ThingsBoard 自己的 `jpaAttributeDao`,按 bean 名与全限定类名同时匹配 —— 其余任何 +> 竞争的 `AttributesDao` 一律不动,而是让启动失败。在该 PR 合入且包含它的构建发布之前, +> 本提示有效。 + +```Properties +database.attributes.type=iotdb-table +# 属性 DAO 激活时必填:sticky-routing | disabled +iotdb.attributes.cluster_mode=sticky-routing +``` + +### 3.2 连接与表结构 + +| 属性 | 默认值 | 含义 | +| --- | --- | --- | +| `iotdb.host` / `iotdb.port` | `127.0.0.1` / `6667` | IoTDB 节点地址 | +| `iotdb.username` / `iotdb.password` | `root` / `root` | IoTDB 凭据 | +| `iotdb.database` | `thingsboard` | 目标 IoTDB 数据库 | +| `iotdb.session-pool-size` | `8` | 表会话池大小 | +| `iotdb.schema.bootstrap` | `true` | 首次启动时创建数据库与表;若自行管理表结构则设为 `false` | + +### 3.3 集群模式 + +当对应 DAO 激活时,`iotdb.attributes.cluster_mode` 与 `iotdb.ts_latest.cluster_mode` 必须显式 +设置。可选值: + +* `sticky-routing` —— 同一 identity 的写入固定路由到单个节点 +* `disabled` —— 单节点部署,或已接受尽力而为的收敛 + +其他取值(包括留空)会在启动时直接失败,而不是静默通过。这两条写入路径只在单个 JVM 内收敛, +因此多写入者的集群部署需要上述两种确认之一。 + +## 4. 已知限制 + +* 属性写入与最新值覆盖层的写入只在单个 JVM 内收敛。集群部署必须二选一:把每个 identity 固定 + 到单个节点(`sticky-routing`),或显式接受尽力而为的收敛(`disabled`)—— 这正是集群模式必须 + 显式声明而非取默认值的原因。 +* 最新值路径由时序表派生而来,另有一个很小的覆盖层,用于承接纯派生无法表达的"只写最新值"与 + "只删最新值"路径。 +* 保留策略使用表级 TTL,其与 ThingsBoard 自身保留设置的对应关系,参见模块的 + [用户指南](https://github.com/apache/iotdb-extras/blob/master/iotdb-thingsboard-table/docs/user-guide.md)。 From 3bac99ee08a567d2fd4cbf94d8a1775c63a0f6c6 Mon Sep 17 00:00:00 2001 From: Zihan Dai <99155080+PDGGK@users.noreply.github.com> Date: Tue, 25 Aug 2026 15:03:17 +1000 Subject: [PATCH 2/5] docs: make the ThingsBoard guide operator-focused --- .../img/thingsboard-table-attributes.png | Bin 0 -> 32078 bytes .../img/thingsboard-table-telemetry.png | Bin 0 -> 28693 bytes .../Ecosystem-Integration/Thingsboard.md | 303 +++++++++--------- .../Ecosystem-Integration/Thingsboard.md | 303 +++++++++--------- .../Ecosystem-Integration/Thingsboard.md | 236 +++++++------- .../Ecosystem-Integration/Thingsboard.md | 236 +++++++------- 6 files changed, 538 insertions(+), 540 deletions(-) create mode 100644 src/.vuepress/public/img/thingsboard-table-attributes.png create mode 100644 src/.vuepress/public/img/thingsboard-table-telemetry.png diff --git a/src/.vuepress/public/img/thingsboard-table-attributes.png b/src/.vuepress/public/img/thingsboard-table-attributes.png new file mode 100644 index 0000000000000000000000000000000000000000..f9e8eef6a511a0272e7188a003f207ed382f4c9c GIT binary patch literal 32078 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 z

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 MWK^Z=B+Wwp4=Sl`(f|Me literal 0 HcmV?d00001 diff --git a/src/.vuepress/public/img/thingsboard-table-telemetry.png b/src/.vuepress/public/img/thingsboard-table-telemetry.png new file mode 100644 index 0000000000000000000000000000000000000000..ea3067ae2719ddd5a163d7428b55283fc06ae71d GIT binary patch literal 28693 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 LR+6fcFnRxfBG8&w literal 0 HcmV?d00001 diff --git a/src/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md b/src/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md index 10defefba..47c73f907 100644 --- a/src/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md +++ b/src/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md @@ -7,9 +7,9 @@ to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - + http://www.apache.org/licenses/LICENSE-2.0 - + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @@ -21,183 +21,176 @@ # ThingsBoard -## 1. Overview - -ThingsBoard is an open-source IoT platform for device management, data collection -and visualisation. It stores device telemetry, latest-value telemetry and entity -attributes through three storage SPIs, which allows the storage layer to be -replaced without changing the platform itself. - -`iotdb-thingsboard-table` implements those three SPIs on top of the IoTDB Table -Model, so a ThingsBoard deployment can keep its telemetry in IoTDB instead of -Cassandra or a relational database: - -| ThingsBoard SPI | Implementation | Purpose | -| --- | --- | --- | -| `TimeseriesDao` | `IoTDBTableTimeseriesDao` | Historical telemetry: batched writes, raw and time-bucketed aggregation reads, deletes | -| `TimeseriesLatestDao` | `IoTDBTableLatestDao` | Latest value per telemetry key | -| `AttributesDao` | `IoTDBTableAttributesDao` | Entity attributes, scoped by `SERVER_SCOPE` / `SHARED_SCOPE` / `CLIENT_SCOPE` | +The `iotdb-thingsboard-table` module lets a stock ThingsBoard deployment store +historical telemetry, latest telemetry, and entity attributes in the IoTDB Table +Model. This page follows the operator workflow: download, install, configure, +run, and verify. -This page covers the **Table Model** integration, which runs against a stock -ThingsBoard release and is enabled by putting the module and its runtime -dependencies on the classpath and setting a few properties. The deployment -section below lists the exact set; it is not a single jar. -There is a separate, earlier integration for the **Tree Model**, described in -[ThingsBoard (Tree Model)](../../Tree/Ecosystem-Integration/Thingsboard.md); -that one stores data under `root.thingsboard` and requires an IoTDB-adapted -ThingsBoard build rather than a stock one. The two are independent — pick the one -that matches the data model you are using. +This integration is independent of the earlier +[ThingsBoard Tree Model integration](../../Tree/Ecosystem-Integration/Thingsboard.md), +which requires an IoTDB-adapted ThingsBoard build. -Writes are batched through a bounded asynchronous queue into IoTDB tablets. -Reads cover both the raw path and the aggregation path: fixed-width millisecond -buckets use IoTDB's native `date_bin`, while calendar buckets -(`WEEK` / `WEEK_ISO` / `MONTH` / `QUARTER`) are walked per bucket so that -boundaries match ThingsBoard's own semantics in the timezone carried by each -query. +## 1. Download -## 2. Usage Steps +The tested combination is: -### 2.1 Version Requirements +| Component | Version | +| --- | --- | +| IoTDB | 2.0.8, Table Model | +| ThingsBoard | 4.3.1.2 | +| JDK | 17 or later | -* `IoTDB: 2.0.8` (Table Model) — the version the integration tests are executed - against (`apache/iotdb:2.0.8-standalone`). Other 2.x releases are untested. -* `ThingsBoard: 4.3.1.2` -* `JDK: >= 17` - -The module is compiled against the ThingsBoard 4.3.1.2 SPI surface. Because -ThingsBoard's `common/data` and `dao` artifacts are not published to Maven -Central, the module builds against a compile-only surface of the types it uses; -those classes are excluded from the packaged jar, so at runtime the real -ThingsBoard classes are used. - -### 2.2 Obtain the jar - -Build it from the `iotdb-extras` repository. The module sits behind an explicit -opt-in profile, so a plain reactor build does not include it: +This module is not yet included in an ASF release. Build it from the official +`apache/iotdb-extras` source at commit `275043ed` or later: ```bash -# from the apache/iotdb-extras repository root, with JDK 17+ -# https://github.com/apache/iotdb-extras -mvn -pl iotdb-thingsboard-table -am -P with-thingsboard clean package +git clone https://github.com/apache/iotdb-extras.git +cd iotdb-extras +git checkout 275043ed +mvn -P with-thingsboard -pl iotdb-thingsboard-table -am clean package +mvn -P with-thingsboard -pl iotdb-thingsboard-table -am \ + dependency:copy-dependencies -DincludeScope=runtime \ + -DoutputDirectory="$PWD/runtime-deps" ``` -The jar is produced under `iotdb-thingsboard-table/target/`. - -### 2.3 Deploy into ThingsBoard - -ThingsBoard must be installed **before** the module is enabled: its installer -resolves a `TsDatabaseSchemaService` bean that only the built-in backends -provide, so starting with the selectors of section 3 already set makes the -install step fail. - -1. Install and start IoTDB, see [IoTDB QuickStart](../QuickStart/QuickStart.md). -2. Install ThingsBoard normally, with none of the properties in section 3 set. -3. Put the module **and the IoTDB client's runtime dependencies** on - ThingsBoard's classpath. The module jar alone is not enough — it fails at - first session creation with `NoClassDefFoundError` on `ITableSessionPool`. - Collect the set with `dependency:copy-dependencies -DincludeScope=runtime`, - then remove the artifacts ThingsBoard already bundles, or its newer copies - are shadowed by the module's older ones. Against ThingsBoard 4.3.1.2 that is - nine — `antlr4-runtime`, `commons-codec`, `commons-io`, `commons-lang3`, - `httpclient`, `httpcore`, `lz4-java`, `snappy-java`, `zstd-jni` — plus - `commons-logging`, which ThingsBoard asks to have removed because it uses - `spring-jcl`. `antlr4-runtime` is the one that bites hardest: ThingsBoard - 4.3.1.2 ships 4.13.0 and the module brings 4.9.3, after which Spring Data - JPA's `HqlLexer` cannot deserialise its own grammar and startup fails. - Eleven jars remain. Re-derive that set against the ThingsBoard release you - are deploying to, and prefer a deployment that boots over a dependency diff: - the diff answers whether ThingsBoard *ships* an artifact, not whether it - *tolerates* one, and `commons-logging` is exactly where those differ. -4. How the classpath is extended depends on the installation. ThingsBoard runs - as a Spring Boot application: the Docker images launch through - `PropertiesLauncher` and already honour a `LOADER_PATH` entry, so placing the - jars in `/usr/share/thingsboard/extensions` is enough; the deb/rpm packages - execute the distribution jar directly. Consult ThingsBoard's own deployment - documentation for the installation method you use. -5. Add the configuration below to ThingsBoard's `thingsboard.yml`, or supply the - equivalent environment variables. -6. Restart ThingsBoard. On first start the module creates its database and tables - in IoTDB, unless that bootstrap is disabled. - -The module is a Spring Boot auto-configuration, so no component scanning or code -change is required on the ThingsBoard side. - -## 3. Configuration - -### 3.1 Activation - -Historical telemetry needs the timeseries selector plus the explicit opt-in: - -```Properties -# select this backend for historical telemetry +The second command intentionally produces an unfiltered dependency directory. +Section 2 lists the exact runtime jars to deploy; do not copy the directory as +a whole. + +## 2. Install + +ThingsBoard must be installed before the IoTDB selectors are enabled. Its +installer resolves a `TsDatabaseSchemaService` supplied by the built-in +backends, so enabling this module during installation makes the installation +fail. + +1. Install and start IoTDB. See [IoTDB QuickStart](../QuickStart/QuickStart.md). + + When IoTDB 2.0.8 and ThingsBoard run in separate containers, put them on the + same Docker network and make IoTDB advertise a reachable service name. The + `2.0.8-standalone` image otherwise binds these endpoints to `127.0.0.1`. + For an IoTDB service named `iotdb`, set: + + ```yaml + cn_seed_config_node: iotdb:10710 + dn_seed_config_node: iotdb:10710 + cn_internal_address: iotdb + dn_internal_address: iotdb + dn_rpc_address: iotdb + ``` + +2. Install ThingsBoard normally without any of the properties from section 3. +3. Create a deployment `lib/` directory containing the module's binary jar from + `iotdb-thingsboard-table/target/` and the ten runtime jars listed below from + `runtime-deps/`. Place all eleven jars on the ThingsBoard classpath. + +For the ThingsBoard Docker image, mount or copy the jars to +`/usr/share/thingsboard/extensions`; its `PropertiesLauncher` already includes +that directory through `LOADER_PATH`. Other installation formats may use a +different classpath extension mechanism. + +The deployment directory contains exactly eleven jars: the module, `iotdb-session`, +`isession`, `service-rpc`, `iotdb-thrift`, `iotdb-thrift-commons`, `libthrift`, +`pipe-api`, `tsfile`, `common`, and `xz`. Do not add the unfiltered Maven runtime +directory: it contains older copies of libraries already supplied by +ThingsBoard. In particular, `antlr4-runtime` 4.9.3 shadows ThingsBoard's 4.13.0 +copy and prevents Spring Data JPA from starting. + +## 3. Configure + +Add the properties for the storage paths you want to move to IoTDB. The +following example enables all three paths for a single-node deployment: + +```properties +# IoTDB connection +# Docker service name; use 127.0.0.1 only when both processes share a host +iotdb.host=iotdb +iotdb.port=6667 +iotdb.username=root +iotdb.password=root +iotdb.database=thingsboard +iotdb.session-pool-size=8 +iotdb.schema.bootstrap=true + +# Historical telemetry database.ts.type=iotdb-table -# explicit opt-in; required together with the selector above iotdb.ts.experimental-raw-only=true -``` - -Latest-value telemetry needs its **own** selector in addition to those two. If it -is omitted, historical telemetry is stored in IoTDB while latest values stay on -ThingsBoard's default backend, with no error at startup: -```Properties +# Latest telemetry database.ts_latest.type=iotdb-table -# required when the latest DAO is active: sticky-routing | disabled -iotdb.ts_latest.cluster_mode=sticky-routing +iotdb.ts_latest.cluster_mode=disabled + +# Entity attributes +database.attributes.type=iotdb-table +iotdb.attributes.cluster_mode=disabled ``` -Entity attributes are a separate opt-in and are inert unless enabled. Unlike the -two timeseries selectors it is not independent of the host: ThingsBoard has no -attributes-backend switch of its own, so enabling this one changes ThingsBoard's -bean graph rather than only this module's. +The historical and latest selectors are separate. Omitting +`database.ts_latest.type` leaves latest values on the original ThingsBoard +backend even when historical telemetry is stored in IoTDB. -> **Do not enable this on a stock ThingsBoard yet.** ThingsBoard has no -> configuration switch of its own for attributes storage — its JPA attributes -> bean registers unconditionally — so setting the selector below makes startup -> fail on the module's conflict check. A fix is under review upstream as -> apache/iotdb-extras#125. It withdraws exactly one bean — ThingsBoard's own -> `jpaAttributeDao`, matched on both the bean name and the fully-qualified class -> name — and fails startup on any other competing `AttributesDao` rather than -> removing it. This note applies until that PR is merged and a build containing -> it is released. +The example writes the connection defaults explicitly. Outside Docker, +`iotdb.host` defaults to `127.0.0.1`; the other defaults are port `6667`, +username/password `root`/`root`, database `thingsboard`, session-pool size `8`, +and schema bootstrap enabled. +Attributes require a build containing +[apache/iotdb-extras#125](https://github.com/apache/iotdb-extras/pull/125), +merged as commit `275043ed`. Earlier builds cannot enable the attributes +selector on stock ThingsBoard. -```Properties -database.attributes.type=iotdb-table -# required when the attribute DAO is active: sticky-routing | disabled -iotdb.attributes.cluster_mode=sticky-routing -``` +When latest telemetry or attributes are enabled, their `cluster_mode` must be +set explicitly: + +- `sticky-routing`: route writes for one identity to one node. +- `disabled`: single-node deployment, or explicitly accept best-effort + convergence. -### 3.2 Connection and schema +Any other value, including an empty value, stops startup with an explanatory +error. -| Property | Default | Meaning | -| --- | --- | --- | -| `iotdb.host` / `iotdb.port` | `127.0.0.1` / `6667` | IoTDB node address | -| `iotdb.username` / `iotdb.password` | `root` / `root` | IoTDB credentials | -| `iotdb.database` | `thingsboard` | Target IoTDB database | -| `iotdb.session-pool-size` | `8` | Table session pool size | -| `iotdb.schema.bootstrap` | `true` | Create the database and tables on first start; set to `false` to manage the schema out of band | +## 4. Run and verify -### 3.3 Cluster mode +1. Restart ThingsBoard after the bundle and configuration are in place. +2. Check the log for `IoTDB Table Mode session pool initialized`. With schema + bootstrap enabled, also check for `IoTDB Table Mode schema bootstrap + complete`. +3. Send test telemetry and attributes to a ThingsBoard device, then confirm + them on the device's **Latest telemetry** and **Attributes** pages. -`iotdb.attributes.cluster_mode` and `iotdb.ts_latest.cluster_mode` must be set -explicitly when the corresponding DAO is active. Accepted values: + ![ThingsBoard latest telemetry stored through the IoTDB Table Model backend](/img/thingsboard-table-telemetry.png) -* `sticky-routing` — writes for one identity are pinned to a single node -* `disabled` — single-node deployment, or best-effort convergence accepted + ![ThingsBoard client attributes stored through the IoTDB Table Model backend](/img/thingsboard-table-attributes.png) -Any other value, including leaving it empty, fails at startup rather than -silently. These write paths converge within a single JVM, so a multi-writer -cluster needs one of the two acknowledgements above. +4. Confirm the same values in IoTDB: -## 4. Known Limitations +```sql +USE thingsboard; -* Attribute and latest-overlay writes converge within a single JVM. A clustered - deployment must either pin each identity to one node (`sticky-routing`) or - explicitly accept best-effort convergence (`disabled`) — which is why the - cluster mode has to be stated rather than defaulted. -* The latest-value path is derived from the telemetry table, with a small overlay - for the latest-only write and delete paths that a pure derivation cannot - express. -* Table-level TTL is used for retention; see the module's +SELECT * +FROM telemetry +ORDER BY time DESC +LIMIT 10; + +SELECT * +FROM entity_attributes +ORDER BY time DESC +LIMIT 10; +``` + +## 5. Known limitations + +- IoTDB 2.0.8 and ThingsBoard 4.3.1.2 are the tested versions. Other 2.x or + ThingsBoard versions require separate verification. +- Attribute and latest-overlay writes converge within one JVM. A multi-writer + deployment must use sticky routing or explicitly accept best-effort + convergence. +- The latest-value path is derived from the telemetry table, with a small + overlay for latest-only writes and deletes. +- Retention uses IoTDB table-level TTL. See the module's [user guide](https://github.com/apache/iotdb-extras/blob/master/iotdb-thingsboard-table/docs/user-guide.md) - for how it maps onto ThingsBoard's own retention settings. + for its mapping to ThingsBoard retention settings. + +Design, source-build, migration, and compile-surface verification details remain +in the +[`iotdb-thingsboard-table` module](https://github.com/apache/iotdb-extras/tree/master/iotdb-thingsboard-table). diff --git a/src/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md b/src/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md index c479850ad..53f8cbaf2 100644 --- a/src/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md +++ b/src/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md @@ -7,9 +7,9 @@ to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - + http://www.apache.org/licenses/LICENSE-2.0 - + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @@ -21,183 +21,176 @@ # ThingsBoard -## 1. Overview - -ThingsBoard is an open-source IoT platform for device management, data collection -and visualisation. It stores device telemetry, latest-value telemetry and entity -attributes through three storage SPIs, which allows the storage layer to be -replaced without changing the platform itself. - -`iotdb-thingsboard-table` implements those three SPIs on top of the IoTDB Table -Model, so a ThingsBoard deployment can keep its telemetry in IoTDB instead of -Cassandra or a relational database: - -| ThingsBoard SPI | Implementation | Purpose | -| --- | --- | --- | -| `TimeseriesDao` | `IoTDBTableTimeseriesDao` | Historical telemetry: batched writes, raw and time-bucketed aggregation reads, deletes | -| `TimeseriesLatestDao` | `IoTDBTableLatestDao` | Latest value per telemetry key | -| `AttributesDao` | `IoTDBTableAttributesDao` | Entity attributes, scoped by `SERVER_SCOPE` / `SHARED_SCOPE` / `CLIENT_SCOPE` | +The `iotdb-thingsboard-table` module lets a stock ThingsBoard deployment store +historical telemetry, latest telemetry, and entity attributes in the IoTDB Table +Model. This page follows the operator workflow: download, install, configure, +run, and verify. -This page covers the **Table Model** integration, which runs against a stock -ThingsBoard release and is enabled by putting the module and its runtime -dependencies on the classpath and setting a few properties. The deployment -section below lists the exact set; it is not a single jar. -There is a separate, earlier integration for the **Tree Model**, described in -[ThingsBoard (Tree Model)](../../latest/Ecosystem-Integration/Thingsboard.md); -that one stores data under `root.thingsboard` and requires an IoTDB-adapted -ThingsBoard build rather than a stock one. The two are independent — pick the one -that matches the data model you are using. +This integration is independent of the earlier +[ThingsBoard Tree Model integration](../../latest/Ecosystem-Integration/Thingsboard.md), +which requires an IoTDB-adapted ThingsBoard build. -Writes are batched through a bounded asynchronous queue into IoTDB tablets. -Reads cover both the raw path and the aggregation path: fixed-width millisecond -buckets use IoTDB's native `date_bin`, while calendar buckets -(`WEEK` / `WEEK_ISO` / `MONTH` / `QUARTER`) are walked per bucket so that -boundaries match ThingsBoard's own semantics in the timezone carried by each -query. +## 1. Download -## 2. Usage Steps +The tested combination is: -### 2.1 Version Requirements +| Component | Version | +| --- | --- | +| IoTDB | 2.0.8, Table Model | +| ThingsBoard | 4.3.1.2 | +| JDK | 17 or later | -* `IoTDB: 2.0.8` (Table Model) — the version the integration tests are executed - against (`apache/iotdb:2.0.8-standalone`). Other 2.x releases are untested. -* `ThingsBoard: 4.3.1.2` -* `JDK: >= 17` - -The module is compiled against the ThingsBoard 4.3.1.2 SPI surface. Because -ThingsBoard's `common/data` and `dao` artifacts are not published to Maven -Central, the module builds against a compile-only surface of the types it uses; -those classes are excluded from the packaged jar, so at runtime the real -ThingsBoard classes are used. - -### 2.2 Obtain the jar - -Build it from the `iotdb-extras` repository. The module sits behind an explicit -opt-in profile, so a plain reactor build does not include it: +This module is not yet included in an ASF release. Build it from the official +`apache/iotdb-extras` source at commit `275043ed` or later: ```bash -# from the apache/iotdb-extras repository root, with JDK 17+ -# https://github.com/apache/iotdb-extras -mvn -pl iotdb-thingsboard-table -am -P with-thingsboard clean package +git clone https://github.com/apache/iotdb-extras.git +cd iotdb-extras +git checkout 275043ed +mvn -P with-thingsboard -pl iotdb-thingsboard-table -am clean package +mvn -P with-thingsboard -pl iotdb-thingsboard-table -am \ + dependency:copy-dependencies -DincludeScope=runtime \ + -DoutputDirectory="$PWD/runtime-deps" ``` -The jar is produced under `iotdb-thingsboard-table/target/`. - -### 2.3 Deploy into ThingsBoard - -ThingsBoard must be installed **before** the module is enabled: its installer -resolves a `TsDatabaseSchemaService` bean that only the built-in backends -provide, so starting with the selectors of section 3 already set makes the -install step fail. - -1. Install and start IoTDB, see [IoTDB QuickStart](../QuickStart/QuickStart.md). -2. Install ThingsBoard normally, with none of the properties in section 3 set. -3. Put the module **and the IoTDB client's runtime dependencies** on - ThingsBoard's classpath. The module jar alone is not enough — it fails at - first session creation with `NoClassDefFoundError` on `ITableSessionPool`. - Collect the set with `dependency:copy-dependencies -DincludeScope=runtime`, - then remove the artifacts ThingsBoard already bundles, or its newer copies - are shadowed by the module's older ones. Against ThingsBoard 4.3.1.2 that is - nine — `antlr4-runtime`, `commons-codec`, `commons-io`, `commons-lang3`, - `httpclient`, `httpcore`, `lz4-java`, `snappy-java`, `zstd-jni` — plus - `commons-logging`, which ThingsBoard asks to have removed because it uses - `spring-jcl`. `antlr4-runtime` is the one that bites hardest: ThingsBoard - 4.3.1.2 ships 4.13.0 and the module brings 4.9.3, after which Spring Data - JPA's `HqlLexer` cannot deserialise its own grammar and startup fails. - Eleven jars remain. Re-derive that set against the ThingsBoard release you - are deploying to, and prefer a deployment that boots over a dependency diff: - the diff answers whether ThingsBoard *ships* an artifact, not whether it - *tolerates* one, and `commons-logging` is exactly where those differ. -4. How the classpath is extended depends on the installation. ThingsBoard runs - as a Spring Boot application: the Docker images launch through - `PropertiesLauncher` and already honour a `LOADER_PATH` entry, so placing the - jars in `/usr/share/thingsboard/extensions` is enough; the deb/rpm packages - execute the distribution jar directly. Consult ThingsBoard's own deployment - documentation for the installation method you use. -5. Add the configuration below to ThingsBoard's `thingsboard.yml`, or supply the - equivalent environment variables. -6. Restart ThingsBoard. On first start the module creates its database and tables - in IoTDB, unless that bootstrap is disabled. - -The module is a Spring Boot auto-configuration, so no component scanning or code -change is required on the ThingsBoard side. - -## 3. Configuration - -### 3.1 Activation - -Historical telemetry needs the timeseries selector plus the explicit opt-in: - -```Properties -# select this backend for historical telemetry +The second command intentionally produces an unfiltered dependency directory. +Section 2 lists the exact runtime jars to deploy; do not copy the directory as +a whole. + +## 2. Install + +ThingsBoard must be installed before the IoTDB selectors are enabled. Its +installer resolves a `TsDatabaseSchemaService` supplied by the built-in +backends, so enabling this module during installation makes the installation +fail. + +1. Install and start IoTDB. See [IoTDB QuickStart](../QuickStart/QuickStart.md). + + When IoTDB 2.0.8 and ThingsBoard run in separate containers, put them on the + same Docker network and make IoTDB advertise a reachable service name. The + `2.0.8-standalone` image otherwise binds these endpoints to `127.0.0.1`. + For an IoTDB service named `iotdb`, set: + + ```yaml + cn_seed_config_node: iotdb:10710 + dn_seed_config_node: iotdb:10710 + cn_internal_address: iotdb + dn_internal_address: iotdb + dn_rpc_address: iotdb + ``` + +2. Install ThingsBoard normally without any of the properties from section 3. +3. Create a deployment `lib/` directory containing the module's binary jar from + `iotdb-thingsboard-table/target/` and the ten runtime jars listed below from + `runtime-deps/`. Place all eleven jars on the ThingsBoard classpath. + +For the ThingsBoard Docker image, mount or copy the jars to +`/usr/share/thingsboard/extensions`; its `PropertiesLauncher` already includes +that directory through `LOADER_PATH`. Other installation formats may use a +different classpath extension mechanism. + +The deployment directory contains exactly eleven jars: the module, `iotdb-session`, +`isession`, `service-rpc`, `iotdb-thrift`, `iotdb-thrift-commons`, `libthrift`, +`pipe-api`, `tsfile`, `common`, and `xz`. Do not add the unfiltered Maven runtime +directory: it contains older copies of libraries already supplied by +ThingsBoard. In particular, `antlr4-runtime` 4.9.3 shadows ThingsBoard's 4.13.0 +copy and prevents Spring Data JPA from starting. + +## 3. Configure + +Add the properties for the storage paths you want to move to IoTDB. The +following example enables all three paths for a single-node deployment: + +```properties +# IoTDB connection +# Docker service name; use 127.0.0.1 only when both processes share a host +iotdb.host=iotdb +iotdb.port=6667 +iotdb.username=root +iotdb.password=root +iotdb.database=thingsboard +iotdb.session-pool-size=8 +iotdb.schema.bootstrap=true + +# Historical telemetry database.ts.type=iotdb-table -# explicit opt-in; required together with the selector above iotdb.ts.experimental-raw-only=true -``` - -Latest-value telemetry needs its **own** selector in addition to those two. If it -is omitted, historical telemetry is stored in IoTDB while latest values stay on -ThingsBoard's default backend, with no error at startup: -```Properties +# Latest telemetry database.ts_latest.type=iotdb-table -# required when the latest DAO is active: sticky-routing | disabled -iotdb.ts_latest.cluster_mode=sticky-routing +iotdb.ts_latest.cluster_mode=disabled + +# Entity attributes +database.attributes.type=iotdb-table +iotdb.attributes.cluster_mode=disabled ``` -Entity attributes are a separate opt-in and are inert unless enabled. Unlike the -two timeseries selectors it is not independent of the host: ThingsBoard has no -attributes-backend switch of its own, so enabling this one changes ThingsBoard's -bean graph rather than only this module's. +The historical and latest selectors are separate. Omitting +`database.ts_latest.type` leaves latest values on the original ThingsBoard +backend even when historical telemetry is stored in IoTDB. -> **Do not enable this on a stock ThingsBoard yet.** ThingsBoard has no -> configuration switch of its own for attributes storage — its JPA attributes -> bean registers unconditionally — so setting the selector below makes startup -> fail on the module's conflict check. A fix is under review upstream as -> apache/iotdb-extras#125. It withdraws exactly one bean — ThingsBoard's own -> `jpaAttributeDao`, matched on both the bean name and the fully-qualified class -> name — and fails startup on any other competing `AttributesDao` rather than -> removing it. This note applies until that PR is merged and a build containing -> it is released. +The example writes the connection defaults explicitly. Outside Docker, +`iotdb.host` defaults to `127.0.0.1`; the other defaults are port `6667`, +username/password `root`/`root`, database `thingsboard`, session-pool size `8`, +and schema bootstrap enabled. +Attributes require a build containing +[apache/iotdb-extras#125](https://github.com/apache/iotdb-extras/pull/125), +merged as commit `275043ed`. Earlier builds cannot enable the attributes +selector on stock ThingsBoard. -```Properties -database.attributes.type=iotdb-table -# required when the attribute DAO is active: sticky-routing | disabled -iotdb.attributes.cluster_mode=sticky-routing -``` +When latest telemetry or attributes are enabled, their `cluster_mode` must be +set explicitly: + +- `sticky-routing`: route writes for one identity to one node. +- `disabled`: single-node deployment, or explicitly accept best-effort + convergence. -### 3.2 Connection and schema +Any other value, including an empty value, stops startup with an explanatory +error. -| Property | Default | Meaning | -| --- | --- | --- | -| `iotdb.host` / `iotdb.port` | `127.0.0.1` / `6667` | IoTDB node address | -| `iotdb.username` / `iotdb.password` | `root` / `root` | IoTDB credentials | -| `iotdb.database` | `thingsboard` | Target IoTDB database | -| `iotdb.session-pool-size` | `8` | Table session pool size | -| `iotdb.schema.bootstrap` | `true` | Create the database and tables on first start; set to `false` to manage the schema out of band | +## 4. Run and verify -### 3.3 Cluster mode +1. Restart ThingsBoard after the bundle and configuration are in place. +2. Check the log for `IoTDB Table Mode session pool initialized`. With schema + bootstrap enabled, also check for `IoTDB Table Mode schema bootstrap + complete`. +3. Send test telemetry and attributes to a ThingsBoard device, then confirm + them on the device's **Latest telemetry** and **Attributes** pages. -`iotdb.attributes.cluster_mode` and `iotdb.ts_latest.cluster_mode` must be set -explicitly when the corresponding DAO is active. Accepted values: + ![ThingsBoard latest telemetry stored through the IoTDB Table Model backend](/img/thingsboard-table-telemetry.png) -* `sticky-routing` — writes for one identity are pinned to a single node -* `disabled` — single-node deployment, or best-effort convergence accepted + ![ThingsBoard client attributes stored through the IoTDB Table Model backend](/img/thingsboard-table-attributes.png) -Any other value, including leaving it empty, fails at startup rather than -silently. These write paths converge within a single JVM, so a multi-writer -cluster needs one of the two acknowledgements above. +4. Confirm the same values in IoTDB: -## 4. Known Limitations +```sql +USE thingsboard; -* Attribute and latest-overlay writes converge within a single JVM. A clustered - deployment must either pin each identity to one node (`sticky-routing`) or - explicitly accept best-effort convergence (`disabled`) — which is why the - cluster mode has to be stated rather than defaulted. -* The latest-value path is derived from the telemetry table, with a small overlay - for the latest-only write and delete paths that a pure derivation cannot - express. -* Table-level TTL is used for retention; see the module's +SELECT * +FROM telemetry +ORDER BY time DESC +LIMIT 10; + +SELECT * +FROM entity_attributes +ORDER BY time DESC +LIMIT 10; +``` + +## 5. Known limitations + +- IoTDB 2.0.8 and ThingsBoard 4.3.1.2 are the tested versions. Other 2.x or + ThingsBoard versions require separate verification. +- Attribute and latest-overlay writes converge within one JVM. A multi-writer + deployment must use sticky routing or explicitly accept best-effort + convergence. +- The latest-value path is derived from the telemetry table, with a small + overlay for latest-only writes and deletes. +- Retention uses IoTDB table-level TTL. See the module's [user guide](https://github.com/apache/iotdb-extras/blob/master/iotdb-thingsboard-table/docs/user-guide.md) - for how it maps onto ThingsBoard's own retention settings. + for its mapping to ThingsBoard retention settings. + +Design, source-build, migration, and compile-surface verification details remain +in the +[`iotdb-thingsboard-table` module](https://github.com/apache/iotdb-extras/tree/master/iotdb-thingsboard-table). diff --git a/src/zh/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md b/src/zh/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md index cbf372d0f..2b4c23e7e 100644 --- a/src/zh/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md +++ b/src/zh/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md @@ -7,9 +7,9 @@ to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - + http://www.apache.org/licenses/LICENSE-2.0 - + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @@ -21,151 +21,157 @@ # ThingsBoard -## 1. 功能概述 +`iotdb-thingsboard-table` 模块让原版 ThingsBoard 可以把历史时序、最新值时序和实体属性 +存入 IoTDB 表模型。本页按操作者真正使用它的顺序说明:下载、安装、配置、运行与验证。 -ThingsBoard 是一个开源物联网平台,用于设备管理、数据采集与可视化。它通过三个存储 SPI -分别存放设备时序数据、最新值时序数据和实体属性,因此可以在不改动平台本身的前提下替换存储层。 +这套集成独立于较早的 +[ThingsBoard 树模型集成](../../Tree/Ecosystem-Integration/Thingsboard.md),后者需要使用 +IoTDB 适配版 ThingsBoard。 -`iotdb-thingsboard-table` 在 IoTDB 表模型之上实现了这三个 SPI,使 ThingsBoard 部署可以把 -时序数据存进 IoTDB,而不是 Cassandra 或关系型数据库: +## 1. 下载 -| ThingsBoard SPI | 实现 | 用途 | -| --- | --- | --- | -| `TimeseriesDao` | `IoTDBTableTimeseriesDao` | 历史时序:批量写入、原始读与时间分桶聚合读、删除 | -| `TimeseriesLatestDao` | `IoTDBTableLatestDao` | 每个 telemetry key 的最新值 | -| `AttributesDao` | `IoTDBTableAttributesDao` | 实体属性,按 `SERVER_SCOPE` / `SHARED_SCOPE` / `CLIENT_SCOPE` 分域 | +已经验证的版本组合如下: -本页介绍的是**表模型**集成:它对接的是原版 ThingsBoard 发行版,把模块及其运行期依赖放上 -classpath 并配置若干属性即可启用。下面的部署一节列出了确切的 jar 集合,不是单独一个 jar。 -**树模型**另有一套更早的集成,见 -[ThingsBoard(树模型)](../../Tree/Ecosystem-Integration/Thingsboard.md);那一套把数据写在 -`root.thingsboard` 下,并且需要 IoTDB 适配版的 ThingsBoard 安装包而非原版。两者互相独立, -按你使用的数据模型选择即可。 +| 组件 | 版本 | +| --- | --- | +| IoTDB | 2.0.8,表模型 | +| ThingsBoard | 4.3.1.2 | +| JDK | 17 或更高版本 | -写入经由一个有界异步队列批量落入 IoTDB tablet。读取覆盖原始路径和聚合路径:固定宽度的毫秒 -分桶使用 IoTDB 原生的 `date_bin`;日历分桶(`WEEK` / `WEEK_ISO` / `MONTH` / `QUARTER`) -则逐桶推进,以保证边界与 ThingsBoard 自身语义一致,并落在每个查询各自携带的时区上。 +本模块尚未进入 ASF Release。请从 `apache/iotdb-extras` 官方源码的 commit `275043ed` +或更新版本构建: -## 2. 使用步骤 +```bash +git clone https://github.com/apache/iotdb-extras.git +cd iotdb-extras +git checkout 275043ed +mvn -P with-thingsboard -pl iotdb-thingsboard-table -am clean package +mvn -P with-thingsboard -pl iotdb-thingsboard-table -am \ + dependency:copy-dependencies -DincludeScope=runtime \ + -DoutputDirectory="$PWD/runtime-deps" +``` -### 2.1 版本要求 +第二条命令会有意生成未经筛选的依赖目录。第 2 节列出了实际需要部署的运行期 jar;不要 +直接复制整个目录。 -* `IoTDB: 2.0.8`(表模型)—— 集成测试实际运行的版本(`apache/iotdb:2.0.8-standalone`), - 其他 2.x 版本未经测试。 -* `ThingsBoard: 4.3.1.2` -* `JDK: >= 17` +## 2. 安装 -该模块是对着 ThingsBoard 4.3.1.2 的 SPI 编译的。由于 ThingsBoard 的 `common/data` 与 `dao` -构件未发布到 Maven Central,模块编译时使用的是其所需类型的编译期替身,这些类不会被打进产物 -jar,因此运行时使用的是真实的 ThingsBoard 类。 +必须先安装 ThingsBoard,再启用 IoTDB 选择器。ThingsBoard 安装器会解析由内置后端提供的 +`TsDatabaseSchemaService`;若安装阶段已经启用本模块,安装会失败。 -### 2.2 获取 jar +1. 安装并启动 IoTDB,参见 [IoTDB 快速上手](../QuickStart/QuickStart.md)。 -从 `iotdb-extras` 仓库构建。该模块位于一个需显式开启的 profile 之后,普通的 reactor 构建不会 -包含它: + 若 IoTDB 2.0.8 与 ThingsBoard 分别运行在容器中,请把两者放入同一 Docker 网络,并让 + IoTDB 公布网络内可达的服务名。`2.0.8-standalone` 镜像默认把这些地址绑定到 + `127.0.0.1`。假设 IoTDB 服务名为 `iotdb`,请设置: -```bash -# 在 apache/iotdb-extras 仓库根目录,使用 JDK 17+ -# https://github.com/apache/iotdb-extras -mvn -pl iotdb-thingsboard-table -am -P with-thingsboard clean package -``` + ```yaml + cn_seed_config_node: iotdb:10710 + dn_seed_config_node: iotdb:10710 + cn_internal_address: iotdb + dn_internal_address: iotdb + dn_rpc_address: iotdb + ``` -产物 jar 位于 `iotdb-thingsboard-table/target/` 下。 - -### 2.3 部署到 ThingsBoard - -必须**先装好 ThingsBoard,再启用模块**:它的安装器会解析一个只有内置后端才提供的 -`TsDatabaseSchemaService` bean,因此在第 3 节的选择器已经打开的情况下启动,安装会失败。 - -1. 安装并启动 IoTDB,参见 [IoTDB 快速上手](../QuickStart/QuickStart.md)。 -2. 按常规方式装好 ThingsBoard,此时不要设置第 3 节的任何属性。 -3. 把模块**以及 IoTDB 客户端的运行期依赖**一起放上 ThingsBoard 的 classpath。只放模块 jar - 是不够的——首次创建会话时会抛 `ITableSessionPool` 的 `NoClassDefFoundError`。用 - `dependency:copy-dependencies -DincludeScope=runtime` 收集,然后**去掉 ThingsBoard 已经 - 自带的那些**,否则它更新的版本会被模块带的旧版本盖住。对 ThingsBoard 4.3.1.2 而言是九个 - ——`antlr4-runtime`、`commons-codec`、`commons-io`、`commons-lang3`、`httpclient`、 - `httpcore`、`lz4-java`、`snappy-java`、`zstd-jni`——外加 `commons-logging`,那个是 - ThingsBoard 自己要求移除的(它使用 `spring-jcl`)。其中 `antlr4-runtime` 后果最严重: - ThingsBoard 4.3.1.2 自带 4.13.0,模块带的是 4.9.3,之后 Spring Data JPA 的 `HqlLexer` - 无法反序列化自己的语法,启动直接失败。剩下十一个 jar。请对着你要部署的那个 ThingsBoard - 版本重新推导这个集合,并且**以能启动的部署为准,而不是以依赖比对为准**:比对回答的是 - ThingsBoard 是否*自带*某个构件,而不是它是否*容得下*某个构件,`commons-logging` 正是 - 两者分道扬镳的地方。 -4. 如何扩展 classpath 取决于安装方式。ThingsBoard 以 Spring Boot 应用运行:Docker 镜像通过 - `PropertiesLauncher` 启动并已支持 `LOADER_PATH`,把这些 jar 放进 - `/usr/share/thingsboard/extensions` 即可;deb/rpm 包则直接执行发行版 jar。请按你所用的 - 安装方式查阅 ThingsBoard 自身的部署文档。 -5. 将下方配置写入 ThingsBoard 的 `thingsboard.yml`,或提供等价的环境变量。 -6. 重启 ThingsBoard。首次启动时,模块会在 IoTDB 中创建所需的数据库与表,除非关闭了该引导。 - -该模块是一个 Spring Boot 自动配置,因此 ThingsBoard 侧无需组件扫描或代码改动。 +2. 按常规方式安装 ThingsBoard,此时不要设置第 3 节的任何属性。 +3. 创建部署用的 `lib/` 目录,从 `iotdb-thingsboard-table/target/` 复制模块的二进制 jar, + 再从 `runtime-deps/` 复制下方列出的 10 个运行期 jar。将这 11 个 jar 全部放到 + ThingsBoard classpath 上。 + +对于 ThingsBoard Docker 镜像,可将这些 jar 挂载或复制到 +`/usr/share/thingsboard/extensions`。镜像使用的 `PropertiesLauncher` 已通过 +`LOADER_PATH` 包含该目录。其他安装方式可能采用不同的 classpath 扩展机制。 + +部署目录必须正好包含 11 个 jar:模块本身,以及 `iotdb-session`、`isession`、 +`service-rpc`、`iotdb-thrift`、`iotdb-thrift-commons`、`libthrift`、`pipe-api`、 +`tsfile`、`common` 和 `xz`。不要直接放入 Maven 复制出的全部运行期依赖;其中包含 +ThingsBoard 已自带的旧版本冲突项。特别是 `antlr4-runtime` 4.9.3 会盖住 ThingsBoard 的 +4.13.0,导致 Spring Data JPA 无法启动。 ## 3. 配置 -### 3.1 激活 +按需要启用要迁移到 IoTDB 的存储路径。下面的单节点示例同时启用三条路径: -历史时序需要时序选择器加上显式开关: +```properties +# IoTDB 连接 +# Docker 服务名;仅当两个进程共享同一主机时使用 127.0.0.1 +iotdb.host=iotdb +iotdb.port=6667 +iotdb.username=root +iotdb.password=root +iotdb.database=thingsboard +iotdb.session-pool-size=8 +iotdb.schema.bootstrap=true -```Properties -# 选择本后端作为历史时序存储 +# 历史时序 database.ts.type=iotdb-table -# 显式开关,必须与上面的选择器同时设置 iotdb.ts.experimental-raw-only=true -``` -最新值时序**另需**它自己的选择器。若遗漏,历史时序会存入 IoTDB,而最新值仍留在 ThingsBoard -的默认后端,且启动时不会报错: - -```Properties +# 最新值时序 database.ts_latest.type=iotdb-table -# 最新值 DAO 激活时必填:sticky-routing | disabled -iotdb.ts_latest.cluster_mode=sticky-routing +iotdb.ts_latest.cluster_mode=disabled + +# 实体属性 +database.attributes.type=iotdb-table +iotdb.attributes.cluster_mode=disabled ``` -实体属性是单独的一项开关,未启用时该 DAO 不生效。与两个时间序列选择器不同,它并非 -独立于宿主:ThingsBoard 自身没有属性存储的配置开关,因此启用这一项会改动 -ThingsBoard 的 bean 图,而不只是本模块的。 +历史时序和最新值时序使用两个独立选择器。若遗漏 `database.ts_latest.type`,即使历史数据 +已经存入 IoTDB,最新值仍会留在 ThingsBoard 原来的后端中。 -> **暂时不要在原版 ThingsBoard 上启用这一项。** ThingsBoard 自身没有为属性存储提供配置开关 -> ——它的 JPA 属性 bean 是无条件注册的——因此设置下面这个选择器会让模块的冲突检查导致启动失败。 -> 相应的修复正在上游 apache/iotdb-extras#125 中等待评审。它只撤下一个 bean —— -> ThingsBoard 自己的 `jpaAttributeDao`,按 bean 名与全限定类名同时匹配 —— 其余任何 -> 竞争的 `AttributesDao` 一律不动,而是让启动失败。在该 PR 合入且包含它的构建发布之前, -> 本提示有效。 +示例显式写出了连接配置的默认值。在非 Docker 环境中,`iotdb.host` 默认为 +`127.0.0.1`;其他默认值为端口 `6667`、用户名/密码 `root`/`root`、数据库 +`thingsboard`、会话池大小 `8`,并默认启用表结构引导。 -```Properties -database.attributes.type=iotdb-table -# 属性 DAO 激活时必填:sticky-routing | disabled -iotdb.attributes.cluster_mode=sticky-routing -``` +属性路径要求构建中包含 +[apache/iotdb-extras#125](https://github.com/apache/iotdb-extras/pull/125),即上游 +commit `275043ed` 或更新版本。更早的构建无法在原版 ThingsBoard 上启用属性选择器。 + +启用最新值或属性时,必须显式设置对应的 `cluster_mode`: + +- `sticky-routing`:把同一 identity 的写入固定路由到一个节点。 +- `disabled`:单节点部署,或明确接受尽力而为的收敛。 -### 3.2 连接与表结构 +其他取值(包括空值)都会让启动停止并给出明确错误。 -| 属性 | 默认值 | 含义 | -| --- | --- | --- | -| `iotdb.host` / `iotdb.port` | `127.0.0.1` / `6667` | IoTDB 节点地址 | -| `iotdb.username` / `iotdb.password` | `root` / `root` | IoTDB 凭据 | -| `iotdb.database` | `thingsboard` | 目标 IoTDB 数据库 | -| `iotdb.session-pool-size` | `8` | 表会话池大小 | -| `iotdb.schema.bootstrap` | `true` | 首次启动时创建数据库与表;若自行管理表结构则设为 `false` | +## 4. 运行与验证 -### 3.3 集群模式 +1. 部署 jar 并完成配置后,重启 ThingsBoard。 +2. 在日志中确认出现 `IoTDB Table Mode session pool initialized`。若启用了表结构引导, + 还应出现 `IoTDB Table Mode schema bootstrap complete`。 +3. 向一台 ThingsBoard 测试设备发送时序和属性数据,然后在设备的“最新遥测”和“属性”页面 + 确认这些值。 -当对应 DAO 激活时,`iotdb.attributes.cluster_mode` 与 `iotdb.ts_latest.cluster_mode` 必须显式 -设置。可选值: + ![通过 IoTDB 表模型后端存储的 ThingsBoard 最新遥测](/img/thingsboard-table-telemetry.png) -* `sticky-routing` —— 同一 identity 的写入固定路由到单个节点 -* `disabled` —— 单节点部署,或已接受尽力而为的收敛 + ![通过 IoTDB 表模型后端存储的 ThingsBoard 客户端属性](/img/thingsboard-table-attributes.png) -其他取值(包括留空)会在启动时直接失败,而不是静默通过。这两条写入路径只在单个 JVM 内收敛, -因此多写入者的集群部署需要上述两种确认之一。 +4. 在 IoTDB 中查询同一批数据: -## 4. 已知限制 +```sql +USE thingsboard; -* 属性写入与最新值覆盖层的写入只在单个 JVM 内收敛。集群部署必须二选一:把每个 identity 固定 - 到单个节点(`sticky-routing`),或显式接受尽力而为的收敛(`disabled`)—— 这正是集群模式必须 - 显式声明而非取默认值的原因。 -* 最新值路径由时序表派生而来,另有一个很小的覆盖层,用于承接纯派生无法表达的"只写最新值"与 - "只删最新值"路径。 -* 保留策略使用表级 TTL,其与 ThingsBoard 自身保留设置的对应关系,参见模块的 +SELECT * +FROM telemetry +ORDER BY time DESC +LIMIT 10; + +SELECT * +FROM entity_attributes +ORDER BY time DESC +LIMIT 10; +``` + +## 5. 已知限制 + +- 已验证版本为 IoTDB 2.0.8 和 ThingsBoard 4.3.1.2。其他 IoTDB 2.x 或 ThingsBoard + 版本需要单独验证。 +- 属性写入和最新值覆盖层只在单个 JVM 内收敛。多写入者部署必须使用固定路由,或明确接受 + 尽力而为的收敛。 +- 最新值路径从时序表派生,并使用一个小型覆盖层承接仅写最新值和仅删最新值的路径。 +- 保留策略使用 IoTDB 表级 TTL;其与 ThingsBoard 保留设置的对应关系参见模块的 [用户指南](https://github.com/apache/iotdb-extras/blob/master/iotdb-thingsboard-table/docs/user-guide.md)。 + +设计、源码构建、迁移和编译接口验证细节保留在 +[`iotdb-thingsboard-table` 模块](https://github.com/apache/iotdb-extras/tree/master/iotdb-thingsboard-table) +中。 diff --git a/src/zh/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md b/src/zh/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md index c24a26020..ef51e6af0 100644 --- a/src/zh/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md +++ b/src/zh/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md @@ -7,9 +7,9 @@ to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - + http://www.apache.org/licenses/LICENSE-2.0 - + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @@ -21,151 +21,157 @@ # ThingsBoard -## 1. 功能概述 +`iotdb-thingsboard-table` 模块让原版 ThingsBoard 可以把历史时序、最新值时序和实体属性 +存入 IoTDB 表模型。本页按操作者真正使用它的顺序说明:下载、安装、配置、运行与验证。 -ThingsBoard 是一个开源物联网平台,用于设备管理、数据采集与可视化。它通过三个存储 SPI -分别存放设备时序数据、最新值时序数据和实体属性,因此可以在不改动平台本身的前提下替换存储层。 +这套集成独立于较早的 +[ThingsBoard 树模型集成](../../latest/Ecosystem-Integration/Thingsboard.md),后者需要使用 +IoTDB 适配版 ThingsBoard。 -`iotdb-thingsboard-table` 在 IoTDB 表模型之上实现了这三个 SPI,使 ThingsBoard 部署可以把 -时序数据存进 IoTDB,而不是 Cassandra 或关系型数据库: +## 1. 下载 -| ThingsBoard SPI | 实现 | 用途 | -| --- | --- | --- | -| `TimeseriesDao` | `IoTDBTableTimeseriesDao` | 历史时序:批量写入、原始读与时间分桶聚合读、删除 | -| `TimeseriesLatestDao` | `IoTDBTableLatestDao` | 每个 telemetry key 的最新值 | -| `AttributesDao` | `IoTDBTableAttributesDao` | 实体属性,按 `SERVER_SCOPE` / `SHARED_SCOPE` / `CLIENT_SCOPE` 分域 | +已经验证的版本组合如下: -本页介绍的是**表模型**集成:它对接的是原版 ThingsBoard 发行版,把模块及其运行期依赖放上 -classpath 并配置若干属性即可启用。下面的部署一节列出了确切的 jar 集合,不是单独一个 jar。 -**树模型**另有一套更早的集成,见 -[ThingsBoard(树模型)](../../latest/Ecosystem-Integration/Thingsboard.md);那一套把数据写在 -`root.thingsboard` 下,并且需要 IoTDB 适配版的 ThingsBoard 安装包而非原版。两者互相独立, -按你使用的数据模型选择即可。 +| 组件 | 版本 | +| --- | --- | +| IoTDB | 2.0.8,表模型 | +| ThingsBoard | 4.3.1.2 | +| JDK | 17 或更高版本 | -写入经由一个有界异步队列批量落入 IoTDB tablet。读取覆盖原始路径和聚合路径:固定宽度的毫秒 -分桶使用 IoTDB 原生的 `date_bin`;日历分桶(`WEEK` / `WEEK_ISO` / `MONTH` / `QUARTER`) -则逐桶推进,以保证边界与 ThingsBoard 自身语义一致,并落在每个查询各自携带的时区上。 +本模块尚未进入 ASF Release。请从 `apache/iotdb-extras` 官方源码的 commit `275043ed` +或更新版本构建: -## 2. 使用步骤 +```bash +git clone https://github.com/apache/iotdb-extras.git +cd iotdb-extras +git checkout 275043ed +mvn -P with-thingsboard -pl iotdb-thingsboard-table -am clean package +mvn -P with-thingsboard -pl iotdb-thingsboard-table -am \ + dependency:copy-dependencies -DincludeScope=runtime \ + -DoutputDirectory="$PWD/runtime-deps" +``` -### 2.1 版本要求 +第二条命令会有意生成未经筛选的依赖目录。第 2 节列出了实际需要部署的运行期 jar;不要 +直接复制整个目录。 -* `IoTDB: 2.0.8`(表模型)—— 集成测试实际运行的版本(`apache/iotdb:2.0.8-standalone`), - 其他 2.x 版本未经测试。 -* `ThingsBoard: 4.3.1.2` -* `JDK: >= 17` +## 2. 安装 -该模块是对着 ThingsBoard 4.3.1.2 的 SPI 编译的。由于 ThingsBoard 的 `common/data` 与 `dao` -构件未发布到 Maven Central,模块编译时使用的是其所需类型的编译期替身,这些类不会被打进产物 -jar,因此运行时使用的是真实的 ThingsBoard 类。 +必须先安装 ThingsBoard,再启用 IoTDB 选择器。ThingsBoard 安装器会解析由内置后端提供的 +`TsDatabaseSchemaService`;若安装阶段已经启用本模块,安装会失败。 -### 2.2 获取 jar +1. 安装并启动 IoTDB,参见 [IoTDB 快速上手](../QuickStart/QuickStart.md)。 -从 `iotdb-extras` 仓库构建。该模块位于一个需显式开启的 profile 之后,普通的 reactor 构建不会 -包含它: + 若 IoTDB 2.0.8 与 ThingsBoard 分别运行在容器中,请把两者放入同一 Docker 网络,并让 + IoTDB 公布网络内可达的服务名。`2.0.8-standalone` 镜像默认把这些地址绑定到 + `127.0.0.1`。假设 IoTDB 服务名为 `iotdb`,请设置: -```bash -# 在 apache/iotdb-extras 仓库根目录,使用 JDK 17+ -# https://github.com/apache/iotdb-extras -mvn -pl iotdb-thingsboard-table -am -P with-thingsboard clean package -``` + ```yaml + cn_seed_config_node: iotdb:10710 + dn_seed_config_node: iotdb:10710 + cn_internal_address: iotdb + dn_internal_address: iotdb + dn_rpc_address: iotdb + ``` -产物 jar 位于 `iotdb-thingsboard-table/target/` 下。 - -### 2.3 部署到 ThingsBoard - -必须**先装好 ThingsBoard,再启用模块**:它的安装器会解析一个只有内置后端才提供的 -`TsDatabaseSchemaService` bean,因此在第 3 节的选择器已经打开的情况下启动,安装会失败。 - -1. 安装并启动 IoTDB,参见 [IoTDB 快速上手](../QuickStart/QuickStart.md)。 -2. 按常规方式装好 ThingsBoard,此时不要设置第 3 节的任何属性。 -3. 把模块**以及 IoTDB 客户端的运行期依赖**一起放上 ThingsBoard 的 classpath。只放模块 jar - 是不够的——首次创建会话时会抛 `ITableSessionPool` 的 `NoClassDefFoundError`。用 - `dependency:copy-dependencies -DincludeScope=runtime` 收集,然后**去掉 ThingsBoard 已经 - 自带的那些**,否则它更新的版本会被模块带的旧版本盖住。对 ThingsBoard 4.3.1.2 而言是九个 - ——`antlr4-runtime`、`commons-codec`、`commons-io`、`commons-lang3`、`httpclient`、 - `httpcore`、`lz4-java`、`snappy-java`、`zstd-jni`——外加 `commons-logging`,那个是 - ThingsBoard 自己要求移除的(它使用 `spring-jcl`)。其中 `antlr4-runtime` 后果最严重: - ThingsBoard 4.3.1.2 自带 4.13.0,模块带的是 4.9.3,之后 Spring Data JPA 的 `HqlLexer` - 无法反序列化自己的语法,启动直接失败。剩下十一个 jar。请对着你要部署的那个 ThingsBoard - 版本重新推导这个集合,并且**以能启动的部署为准,而不是以依赖比对为准**:比对回答的是 - ThingsBoard 是否*自带*某个构件,而不是它是否*容得下*某个构件,`commons-logging` 正是 - 两者分道扬镳的地方。 -4. 如何扩展 classpath 取决于安装方式。ThingsBoard 以 Spring Boot 应用运行:Docker 镜像通过 - `PropertiesLauncher` 启动并已支持 `LOADER_PATH`,把这些 jar 放进 - `/usr/share/thingsboard/extensions` 即可;deb/rpm 包则直接执行发行版 jar。请按你所用的 - 安装方式查阅 ThingsBoard 自身的部署文档。 -5. 将下方配置写入 ThingsBoard 的 `thingsboard.yml`,或提供等价的环境变量。 -6. 重启 ThingsBoard。首次启动时,模块会在 IoTDB 中创建所需的数据库与表,除非关闭了该引导。 - -该模块是一个 Spring Boot 自动配置,因此 ThingsBoard 侧无需组件扫描或代码改动。 +2. 按常规方式安装 ThingsBoard,此时不要设置第 3 节的任何属性。 +3. 创建部署用的 `lib/` 目录,从 `iotdb-thingsboard-table/target/` 复制模块的二进制 jar, + 再从 `runtime-deps/` 复制下方列出的 10 个运行期 jar。将这 11 个 jar 全部放到 + ThingsBoard classpath 上。 + +对于 ThingsBoard Docker 镜像,可将这些 jar 挂载或复制到 +`/usr/share/thingsboard/extensions`。镜像使用的 `PropertiesLauncher` 已通过 +`LOADER_PATH` 包含该目录。其他安装方式可能采用不同的 classpath 扩展机制。 + +部署目录必须正好包含 11 个 jar:模块本身,以及 `iotdb-session`、`isession`、 +`service-rpc`、`iotdb-thrift`、`iotdb-thrift-commons`、`libthrift`、`pipe-api`、 +`tsfile`、`common` 和 `xz`。不要直接放入 Maven 复制出的全部运行期依赖;其中包含 +ThingsBoard 已自带的旧版本冲突项。特别是 `antlr4-runtime` 4.9.3 会盖住 ThingsBoard 的 +4.13.0,导致 Spring Data JPA 无法启动。 ## 3. 配置 -### 3.1 激活 +按需要启用要迁移到 IoTDB 的存储路径。下面的单节点示例同时启用三条路径: -历史时序需要时序选择器加上显式开关: +```properties +# IoTDB 连接 +# Docker 服务名;仅当两个进程共享同一主机时使用 127.0.0.1 +iotdb.host=iotdb +iotdb.port=6667 +iotdb.username=root +iotdb.password=root +iotdb.database=thingsboard +iotdb.session-pool-size=8 +iotdb.schema.bootstrap=true -```Properties -# 选择本后端作为历史时序存储 +# 历史时序 database.ts.type=iotdb-table -# 显式开关,必须与上面的选择器同时设置 iotdb.ts.experimental-raw-only=true -``` -最新值时序**另需**它自己的选择器。若遗漏,历史时序会存入 IoTDB,而最新值仍留在 ThingsBoard -的默认后端,且启动时不会报错: - -```Properties +# 最新值时序 database.ts_latest.type=iotdb-table -# 最新值 DAO 激活时必填:sticky-routing | disabled -iotdb.ts_latest.cluster_mode=sticky-routing +iotdb.ts_latest.cluster_mode=disabled + +# 实体属性 +database.attributes.type=iotdb-table +iotdb.attributes.cluster_mode=disabled ``` -实体属性是单独的一项开关,未启用时该 DAO 不生效。与两个时间序列选择器不同,它并非 -独立于宿主:ThingsBoard 自身没有属性存储的配置开关,因此启用这一项会改动 -ThingsBoard 的 bean 图,而不只是本模块的。 +历史时序和最新值时序使用两个独立选择器。若遗漏 `database.ts_latest.type`,即使历史数据 +已经存入 IoTDB,最新值仍会留在 ThingsBoard 原来的后端中。 -> **暂时不要在原版 ThingsBoard 上启用这一项。** ThingsBoard 自身没有为属性存储提供配置开关 -> ——它的 JPA 属性 bean 是无条件注册的——因此设置下面这个选择器会让模块的冲突检查导致启动失败。 -> 相应的修复正在上游 apache/iotdb-extras#125 中等待评审。它只撤下一个 bean —— -> ThingsBoard 自己的 `jpaAttributeDao`,按 bean 名与全限定类名同时匹配 —— 其余任何 -> 竞争的 `AttributesDao` 一律不动,而是让启动失败。在该 PR 合入且包含它的构建发布之前, -> 本提示有效。 +示例显式写出了连接配置的默认值。在非 Docker 环境中,`iotdb.host` 默认为 +`127.0.0.1`;其他默认值为端口 `6667`、用户名/密码 `root`/`root`、数据库 +`thingsboard`、会话池大小 `8`,并默认启用表结构引导。 -```Properties -database.attributes.type=iotdb-table -# 属性 DAO 激活时必填:sticky-routing | disabled -iotdb.attributes.cluster_mode=sticky-routing -``` +属性路径要求构建中包含 +[apache/iotdb-extras#125](https://github.com/apache/iotdb-extras/pull/125),即上游 +commit `275043ed` 或更新版本。更早的构建无法在原版 ThingsBoard 上启用属性选择器。 + +启用最新值或属性时,必须显式设置对应的 `cluster_mode`: + +- `sticky-routing`:把同一 identity 的写入固定路由到一个节点。 +- `disabled`:单节点部署,或明确接受尽力而为的收敛。 -### 3.2 连接与表结构 +其他取值(包括空值)都会让启动停止并给出明确错误。 -| 属性 | 默认值 | 含义 | -| --- | --- | --- | -| `iotdb.host` / `iotdb.port` | `127.0.0.1` / `6667` | IoTDB 节点地址 | -| `iotdb.username` / `iotdb.password` | `root` / `root` | IoTDB 凭据 | -| `iotdb.database` | `thingsboard` | 目标 IoTDB 数据库 | -| `iotdb.session-pool-size` | `8` | 表会话池大小 | -| `iotdb.schema.bootstrap` | `true` | 首次启动时创建数据库与表;若自行管理表结构则设为 `false` | +## 4. 运行与验证 -### 3.3 集群模式 +1. 部署 jar 并完成配置后,重启 ThingsBoard。 +2. 在日志中确认出现 `IoTDB Table Mode session pool initialized`。若启用了表结构引导, + 还应出现 `IoTDB Table Mode schema bootstrap complete`。 +3. 向一台 ThingsBoard 测试设备发送时序和属性数据,然后在设备的“最新遥测”和“属性”页面 + 确认这些值。 -当对应 DAO 激活时,`iotdb.attributes.cluster_mode` 与 `iotdb.ts_latest.cluster_mode` 必须显式 -设置。可选值: + ![通过 IoTDB 表模型后端存储的 ThingsBoard 最新遥测](/img/thingsboard-table-telemetry.png) -* `sticky-routing` —— 同一 identity 的写入固定路由到单个节点 -* `disabled` —— 单节点部署,或已接受尽力而为的收敛 + ![通过 IoTDB 表模型后端存储的 ThingsBoard 客户端属性](/img/thingsboard-table-attributes.png) -其他取值(包括留空)会在启动时直接失败,而不是静默通过。这两条写入路径只在单个 JVM 内收敛, -因此多写入者的集群部署需要上述两种确认之一。 +4. 在 IoTDB 中查询同一批数据: -## 4. 已知限制 +```sql +USE thingsboard; -* 属性写入与最新值覆盖层的写入只在单个 JVM 内收敛。集群部署必须二选一:把每个 identity 固定 - 到单个节点(`sticky-routing`),或显式接受尽力而为的收敛(`disabled`)—— 这正是集群模式必须 - 显式声明而非取默认值的原因。 -* 最新值路径由时序表派生而来,另有一个很小的覆盖层,用于承接纯派生无法表达的"只写最新值"与 - "只删最新值"路径。 -* 保留策略使用表级 TTL,其与 ThingsBoard 自身保留设置的对应关系,参见模块的 +SELECT * +FROM telemetry +ORDER BY time DESC +LIMIT 10; + +SELECT * +FROM entity_attributes +ORDER BY time DESC +LIMIT 10; +``` + +## 5. 已知限制 + +- 已验证版本为 IoTDB 2.0.8 和 ThingsBoard 4.3.1.2。其他 IoTDB 2.x 或 ThingsBoard + 版本需要单独验证。 +- 属性写入和最新值覆盖层只在单个 JVM 内收敛。多写入者部署必须使用固定路由,或明确接受 + 尽力而为的收敛。 +- 最新值路径从时序表派生,并使用一个小型覆盖层承接仅写最新值和仅删最新值的路径。 +- 保留策略使用 IoTDB 表级 TTL;其与 ThingsBoard 保留设置的对应关系参见模块的 [用户指南](https://github.com/apache/iotdb-extras/blob/master/iotdb-thingsboard-table/docs/user-guide.md)。 + +设计、源码构建、迁移和编译接口验证细节保留在 +[`iotdb-thingsboard-table` 模块](https://github.com/apache/iotdb-extras/tree/master/iotdb-thingsboard-table) +中。 From a46467302e7617223bdf9b8d1d2bf54e7119507b Mon Sep 17 00:00:00 2001 From: Zihan Dai <1436286758@qq.com> Date: Fri, 23 Oct 2026 15:07:01 +1100 Subject: [PATCH 3/5] docs: install the ThingsBoard table-model module from the 2.0.11 release The module is now released in Apache IoTDB Extras 2.0.11, with its jar attached to the GitHub release. Download it and its nine IoTDB client dependencies instead of building from a commit, and update the tested IoTDB version to 2.0.11. The jar list changes for the 2.0.11 client: pipe-api is no longer needed, libthrift is 0.23.0 and tsfile/common are 2.4.0. All ten jars are required even with no selector set, because ThingsBoard does not start with the module jar alone. Signed-off-by: Zihan Dai <1436286758@qq.com> --- .../Ecosystem-Integration/Thingsboard.md | 74 +++++++++++-------- .../Ecosystem-Integration/Thingsboard.md | 74 +++++++++++-------- .../Ecosystem-Integration/Thingsboard.md | 69 +++++++++-------- .../Ecosystem-Integration/Thingsboard.md | 69 +++++++++-------- 4 files changed, 166 insertions(+), 120 deletions(-) diff --git a/src/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md b/src/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md index 47c73f907..9753793f8 100644 --- a/src/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md +++ b/src/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md @@ -36,26 +36,43 @@ The tested combination is: | Component | Version | | --- | --- | -| IoTDB | 2.0.8, Table Model | +| IoTDB | 2.0.11, Table Model | | ThingsBoard | 4.3.1.2 | | JDK | 17 or later | -This module is not yet included in an ASF release. Build it from the official -`apache/iotdb-extras` source at commit `275043ed` or later: +The module is part of the Apache IoTDB Extras 2.0.11 release. Its jar is +attached to the +[2.0.11 release](https://github.com/apache/iotdb-extras/releases/tag/v2.0.11) +with a signature that can be checked against the +[IoTDB KEYS](https://dist.apache.org/repos/dist/release/iotdb/KEYS) file. The +jar does not contain the IoTDB client, so the nine client jars it needs are +downloaded from Maven Central into the same `lib/` directory: ```bash -git clone https://github.com/apache/iotdb-extras.git -cd iotdb-extras -git checkout 275043ed -mvn -P with-thingsboard -pl iotdb-thingsboard-table -am clean package -mvn -P with-thingsboard -pl iotdb-thingsboard-table -am \ - dependency:copy-dependencies -DincludeScope=runtime \ - -DoutputDirectory="$PWD/runtime-deps" +mkdir -p lib +R=https://github.com/apache/iotdb-extras/releases/download/v2.0.11 +curl -fL -o lib/iotdb-thingsboard-table-2.0.11.jar $R/iotdb-thingsboard-table-2.0.11.jar +curl -fLO $R/iotdb-thingsboard-table-2.0.11.jar.asc +curl -fL https://dist.apache.org/repos/dist/release/iotdb/KEYS | gpg --import +gpg --verify iotdb-thingsboard-table-2.0.11.jar.asc lib/iotdb-thingsboard-table-2.0.11.jar + +M=https://repo1.maven.org/maven2 +for path in \ + org/apache/iotdb/iotdb-session/2.0.11/iotdb-session-2.0.11.jar \ + org/apache/iotdb/isession/2.0.11/isession-2.0.11.jar \ + org/apache/iotdb/service-rpc/2.0.11/service-rpc-2.0.11.jar \ + org/apache/iotdb/iotdb-thrift/2.0.11/iotdb-thrift-2.0.11.jar \ + org/apache/iotdb/iotdb-thrift-commons/2.0.11/iotdb-thrift-commons-2.0.11.jar \ + org/apache/thrift/libthrift/0.23.0/libthrift-0.23.0.jar \ + org/apache/tsfile/tsfile/2.4.0/tsfile-2.4.0.jar \ + org/apache/tsfile/common/2.4.0/common-2.4.0.jar \ + org/tukaani/xz/1.10/xz-1.10.jar; do + curl -fL -o "lib/${path##*/}" "$M/$path" +done ``` -The second command intentionally produces an unfiltered dependency directory. -Section 2 lists the exact runtime jars to deploy; do not copy the directory as -a whole. +`lib/` then holds the ten jars to deploy. The module can also be built from the +`apache-iotdb-extras-2.0.11-source-release.zip` source package. ## 2. Install @@ -66,9 +83,9 @@ fail. 1. Install and start IoTDB. See [IoTDB QuickStart](../QuickStart/QuickStart.md). - When IoTDB 2.0.8 and ThingsBoard run in separate containers, put them on the + When IoTDB 2.0.11 and ThingsBoard run in separate containers, put them on the same Docker network and make IoTDB advertise a reachable service name. The - `2.0.8-standalone` image otherwise binds these endpoints to `127.0.0.1`. + `2.0.11-standalone` image otherwise binds these endpoints to `127.0.0.1`. For an IoTDB service named `iotdb`, set: ```yaml @@ -80,21 +97,21 @@ fail. ``` 2. Install ThingsBoard normally without any of the properties from section 3. -3. Create a deployment `lib/` directory containing the module's binary jar from - `iotdb-thingsboard-table/target/` and the ten runtime jars listed below from - `runtime-deps/`. Place all eleven jars on the ThingsBoard classpath. +3. Place the ten jars from `lib/` on the ThingsBoard classpath. All ten are + needed even while no selector is set: with the module jar alone, ThingsBoard + fails to start with + `NoClassDefFoundError: org/apache/iotdb/isession/pool/ITableSessionPool`. For the ThingsBoard Docker image, mount or copy the jars to `/usr/share/thingsboard/extensions`; its `PropertiesLauncher` already includes that directory through `LOADER_PATH`. Other installation formats may use a different classpath extension mechanism. -The deployment directory contains exactly eleven jars: the module, `iotdb-session`, +The deployment directory contains exactly ten jars: the module, `iotdb-session`, `isession`, `service-rpc`, `iotdb-thrift`, `iotdb-thrift-commons`, `libthrift`, -`pipe-api`, `tsfile`, `common`, and `xz`. Do not add the unfiltered Maven runtime -directory: it contains older copies of libraries already supplied by -ThingsBoard. In particular, `antlr4-runtime` 4.9.3 shadows ThingsBoard's 4.13.0 -copy and prevents Spring Data JPA from starting. +`tsfile`, `common`, and `xz`. ThingsBoard already ships the other libraries the +IoTDB client uses (`lz4-java`, `zstd-jni`, `snappy-java` and `antlr4-runtime`), +so do not add copies of them. ## 3. Configure @@ -134,11 +151,6 @@ The example writes the connection defaults explicitly. Outside Docker, username/password `root`/`root`, database `thingsboard`, session-pool size `8`, and schema bootstrap enabled. -Attributes require a build containing -[apache/iotdb-extras#125](https://github.com/apache/iotdb-extras/pull/125), -merged as commit `275043ed`. Earlier builds cannot enable the attributes -selector on stock ThingsBoard. - When latest telemetry or attributes are enabled, their `cluster_mode` must be set explicitly: @@ -180,7 +192,7 @@ LIMIT 10; ## 5. Known limitations -- IoTDB 2.0.8 and ThingsBoard 4.3.1.2 are the tested versions. Other 2.x or +- IoTDB 2.0.11 and ThingsBoard 4.3.1.2 are the tested versions. Other 2.x or ThingsBoard versions require separate verification. - Attribute and latest-overlay writes converge within one JVM. A multi-writer deployment must use sticky routing or explicitly accept best-effort @@ -188,9 +200,9 @@ LIMIT 10; - The latest-value path is derived from the telemetry table, with a small overlay for latest-only writes and deletes. - Retention uses IoTDB table-level TTL. See the module's - [user guide](https://github.com/apache/iotdb-extras/blob/master/iotdb-thingsboard-table/docs/user-guide.md) + [user guide](https://github.com/apache/iotdb-extras/blob/v2.0.11/iotdb-thingsboard-table/docs/user-guide.md) for its mapping to ThingsBoard retention settings. Design, source-build, migration, and compile-surface verification details remain in the -[`iotdb-thingsboard-table` module](https://github.com/apache/iotdb-extras/tree/master/iotdb-thingsboard-table). +[`iotdb-thingsboard-table` module](https://github.com/apache/iotdb-extras/tree/v2.0.11/iotdb-thingsboard-table). diff --git a/src/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md b/src/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md index 53f8cbaf2..8cbba71b3 100644 --- a/src/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md +++ b/src/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md @@ -36,26 +36,43 @@ The tested combination is: | Component | Version | | --- | --- | -| IoTDB | 2.0.8, Table Model | +| IoTDB | 2.0.11, Table Model | | ThingsBoard | 4.3.1.2 | | JDK | 17 or later | -This module is not yet included in an ASF release. Build it from the official -`apache/iotdb-extras` source at commit `275043ed` or later: +The module is part of the Apache IoTDB Extras 2.0.11 release. Its jar is +attached to the +[2.0.11 release](https://github.com/apache/iotdb-extras/releases/tag/v2.0.11) +with a signature that can be checked against the +[IoTDB KEYS](https://dist.apache.org/repos/dist/release/iotdb/KEYS) file. The +jar does not contain the IoTDB client, so the nine client jars it needs are +downloaded from Maven Central into the same `lib/` directory: ```bash -git clone https://github.com/apache/iotdb-extras.git -cd iotdb-extras -git checkout 275043ed -mvn -P with-thingsboard -pl iotdb-thingsboard-table -am clean package -mvn -P with-thingsboard -pl iotdb-thingsboard-table -am \ - dependency:copy-dependencies -DincludeScope=runtime \ - -DoutputDirectory="$PWD/runtime-deps" +mkdir -p lib +R=https://github.com/apache/iotdb-extras/releases/download/v2.0.11 +curl -fL -o lib/iotdb-thingsboard-table-2.0.11.jar $R/iotdb-thingsboard-table-2.0.11.jar +curl -fLO $R/iotdb-thingsboard-table-2.0.11.jar.asc +curl -fL https://dist.apache.org/repos/dist/release/iotdb/KEYS | gpg --import +gpg --verify iotdb-thingsboard-table-2.0.11.jar.asc lib/iotdb-thingsboard-table-2.0.11.jar + +M=https://repo1.maven.org/maven2 +for path in \ + org/apache/iotdb/iotdb-session/2.0.11/iotdb-session-2.0.11.jar \ + org/apache/iotdb/isession/2.0.11/isession-2.0.11.jar \ + org/apache/iotdb/service-rpc/2.0.11/service-rpc-2.0.11.jar \ + org/apache/iotdb/iotdb-thrift/2.0.11/iotdb-thrift-2.0.11.jar \ + org/apache/iotdb/iotdb-thrift-commons/2.0.11/iotdb-thrift-commons-2.0.11.jar \ + org/apache/thrift/libthrift/0.23.0/libthrift-0.23.0.jar \ + org/apache/tsfile/tsfile/2.4.0/tsfile-2.4.0.jar \ + org/apache/tsfile/common/2.4.0/common-2.4.0.jar \ + org/tukaani/xz/1.10/xz-1.10.jar; do + curl -fL -o "lib/${path##*/}" "$M/$path" +done ``` -The second command intentionally produces an unfiltered dependency directory. -Section 2 lists the exact runtime jars to deploy; do not copy the directory as -a whole. +`lib/` then holds the ten jars to deploy. The module can also be built from the +`apache-iotdb-extras-2.0.11-source-release.zip` source package. ## 2. Install @@ -66,9 +83,9 @@ fail. 1. Install and start IoTDB. See [IoTDB QuickStart](../QuickStart/QuickStart.md). - When IoTDB 2.0.8 and ThingsBoard run in separate containers, put them on the + When IoTDB 2.0.11 and ThingsBoard run in separate containers, put them on the same Docker network and make IoTDB advertise a reachable service name. The - `2.0.8-standalone` image otherwise binds these endpoints to `127.0.0.1`. + `2.0.11-standalone` image otherwise binds these endpoints to `127.0.0.1`. For an IoTDB service named `iotdb`, set: ```yaml @@ -80,21 +97,21 @@ fail. ``` 2. Install ThingsBoard normally without any of the properties from section 3. -3. Create a deployment `lib/` directory containing the module's binary jar from - `iotdb-thingsboard-table/target/` and the ten runtime jars listed below from - `runtime-deps/`. Place all eleven jars on the ThingsBoard classpath. +3. Place the ten jars from `lib/` on the ThingsBoard classpath. All ten are + needed even while no selector is set: with the module jar alone, ThingsBoard + fails to start with + `NoClassDefFoundError: org/apache/iotdb/isession/pool/ITableSessionPool`. For the ThingsBoard Docker image, mount or copy the jars to `/usr/share/thingsboard/extensions`; its `PropertiesLauncher` already includes that directory through `LOADER_PATH`. Other installation formats may use a different classpath extension mechanism. -The deployment directory contains exactly eleven jars: the module, `iotdb-session`, +The deployment directory contains exactly ten jars: the module, `iotdb-session`, `isession`, `service-rpc`, `iotdb-thrift`, `iotdb-thrift-commons`, `libthrift`, -`pipe-api`, `tsfile`, `common`, and `xz`. Do not add the unfiltered Maven runtime -directory: it contains older copies of libraries already supplied by -ThingsBoard. In particular, `antlr4-runtime` 4.9.3 shadows ThingsBoard's 4.13.0 -copy and prevents Spring Data JPA from starting. +`tsfile`, `common`, and `xz`. ThingsBoard already ships the other libraries the +IoTDB client uses (`lz4-java`, `zstd-jni`, `snappy-java` and `antlr4-runtime`), +so do not add copies of them. ## 3. Configure @@ -134,11 +151,6 @@ The example writes the connection defaults explicitly. Outside Docker, username/password `root`/`root`, database `thingsboard`, session-pool size `8`, and schema bootstrap enabled. -Attributes require a build containing -[apache/iotdb-extras#125](https://github.com/apache/iotdb-extras/pull/125), -merged as commit `275043ed`. Earlier builds cannot enable the attributes -selector on stock ThingsBoard. - When latest telemetry or attributes are enabled, their `cluster_mode` must be set explicitly: @@ -180,7 +192,7 @@ LIMIT 10; ## 5. Known limitations -- IoTDB 2.0.8 and ThingsBoard 4.3.1.2 are the tested versions. Other 2.x or +- IoTDB 2.0.11 and ThingsBoard 4.3.1.2 are the tested versions. Other 2.x or ThingsBoard versions require separate verification. - Attribute and latest-overlay writes converge within one JVM. A multi-writer deployment must use sticky routing or explicitly accept best-effort @@ -188,9 +200,9 @@ LIMIT 10; - The latest-value path is derived from the telemetry table, with a small overlay for latest-only writes and deletes. - Retention uses IoTDB table-level TTL. See the module's - [user guide](https://github.com/apache/iotdb-extras/blob/master/iotdb-thingsboard-table/docs/user-guide.md) + [user guide](https://github.com/apache/iotdb-extras/blob/v2.0.11/iotdb-thingsboard-table/docs/user-guide.md) for its mapping to ThingsBoard retention settings. Design, source-build, migration, and compile-surface verification details remain in the -[`iotdb-thingsboard-table` module](https://github.com/apache/iotdb-extras/tree/master/iotdb-thingsboard-table). +[`iotdb-thingsboard-table` module](https://github.com/apache/iotdb-extras/tree/v2.0.11/iotdb-thingsboard-table). diff --git a/src/zh/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md b/src/zh/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md index 2b4c23e7e..9849d0cc6 100644 --- a/src/zh/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md +++ b/src/zh/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md @@ -34,25 +34,41 @@ IoTDB 适配版 ThingsBoard。 | 组件 | 版本 | | --- | --- | -| IoTDB | 2.0.8,表模型 | +| IoTDB | 2.0.11,表模型 | | ThingsBoard | 4.3.1.2 | | JDK | 17 或更高版本 | -本模块尚未进入 ASF Release。请从 `apache/iotdb-extras` 官方源码的 commit `275043ed` -或更新版本构建: +本模块已随 Apache IoTDB Extras 2.0.11 正式发布。模块 jar 作为附件放在 +[2.0.11 Release](https://github.com/apache/iotdb-extras/releases/tag/v2.0.11) 中, +并附有签名,可用 [IoTDB KEYS](https://dist.apache.org/repos/dist/release/iotdb/KEYS) +校验。该 jar 不包含 IoTDB 客户端,因此还需从 Maven Central 下载它依赖的 9 个客户端 +jar,放入同一个 `lib/` 目录: ```bash -git clone https://github.com/apache/iotdb-extras.git -cd iotdb-extras -git checkout 275043ed -mvn -P with-thingsboard -pl iotdb-thingsboard-table -am clean package -mvn -P with-thingsboard -pl iotdb-thingsboard-table -am \ - dependency:copy-dependencies -DincludeScope=runtime \ - -DoutputDirectory="$PWD/runtime-deps" +mkdir -p lib +R=https://github.com/apache/iotdb-extras/releases/download/v2.0.11 +curl -fL -o lib/iotdb-thingsboard-table-2.0.11.jar $R/iotdb-thingsboard-table-2.0.11.jar +curl -fLO $R/iotdb-thingsboard-table-2.0.11.jar.asc +curl -fL https://dist.apache.org/repos/dist/release/iotdb/KEYS | gpg --import +gpg --verify iotdb-thingsboard-table-2.0.11.jar.asc lib/iotdb-thingsboard-table-2.0.11.jar + +M=https://repo1.maven.org/maven2 +for path in \ + org/apache/iotdb/iotdb-session/2.0.11/iotdb-session-2.0.11.jar \ + org/apache/iotdb/isession/2.0.11/isession-2.0.11.jar \ + org/apache/iotdb/service-rpc/2.0.11/service-rpc-2.0.11.jar \ + org/apache/iotdb/iotdb-thrift/2.0.11/iotdb-thrift-2.0.11.jar \ + org/apache/iotdb/iotdb-thrift-commons/2.0.11/iotdb-thrift-commons-2.0.11.jar \ + org/apache/thrift/libthrift/0.23.0/libthrift-0.23.0.jar \ + org/apache/tsfile/tsfile/2.4.0/tsfile-2.4.0.jar \ + org/apache/tsfile/common/2.4.0/common-2.4.0.jar \ + org/tukaani/xz/1.10/xz-1.10.jar; do + curl -fL -o "lib/${path##*/}" "$M/$path" +done ``` -第二条命令会有意生成未经筛选的依赖目录。第 2 节列出了实际需要部署的运行期 jar;不要 -直接复制整个目录。 +完成后 `lib/` 中即为需要部署的 10 个 jar。也可以用源码包 +`apache-iotdb-extras-2.0.11-source-release.zip` 自行构建本模块。 ## 2. 安装 @@ -61,8 +77,8 @@ mvn -P with-thingsboard -pl iotdb-thingsboard-table -am \ 1. 安装并启动 IoTDB,参见 [IoTDB 快速上手](../QuickStart/QuickStart.md)。 - 若 IoTDB 2.0.8 与 ThingsBoard 分别运行在容器中,请把两者放入同一 Docker 网络,并让 - IoTDB 公布网络内可达的服务名。`2.0.8-standalone` 镜像默认把这些地址绑定到 + 若 IoTDB 2.0.11 与 ThingsBoard 分别运行在容器中,请把两者放入同一 Docker 网络,并让 + IoTDB 公布网络内可达的服务名。`2.0.11-standalone` 镜像默认把这些地址绑定到 `127.0.0.1`。假设 IoTDB 服务名为 `iotdb`,请设置: ```yaml @@ -74,19 +90,18 @@ mvn -P with-thingsboard -pl iotdb-thingsboard-table -am \ ``` 2. 按常规方式安装 ThingsBoard,此时不要设置第 3 节的任何属性。 -3. 创建部署用的 `lib/` 目录,从 `iotdb-thingsboard-table/target/` 复制模块的二进制 jar, - 再从 `runtime-deps/` 复制下方列出的 10 个运行期 jar。将这 11 个 jar 全部放到 - ThingsBoard classpath 上。 +3. 将 `lib/` 中的 10 个 jar 放到 ThingsBoard classpath 上。即使尚未设置任何选择器, + 这 10 个 jar 也缺一不可:只放模块 jar 时,ThingsBoard 会启动失败,报 + `NoClassDefFoundError: org/apache/iotdb/isession/pool/ITableSessionPool`。 对于 ThingsBoard Docker 镜像,可将这些 jar 挂载或复制到 `/usr/share/thingsboard/extensions`。镜像使用的 `PropertiesLauncher` 已通过 `LOADER_PATH` 包含该目录。其他安装方式可能采用不同的 classpath 扩展机制。 -部署目录必须正好包含 11 个 jar:模块本身,以及 `iotdb-session`、`isession`、 -`service-rpc`、`iotdb-thrift`、`iotdb-thrift-commons`、`libthrift`、`pipe-api`、 -`tsfile`、`common` 和 `xz`。不要直接放入 Maven 复制出的全部运行期依赖;其中包含 -ThingsBoard 已自带的旧版本冲突项。特别是 `antlr4-runtime` 4.9.3 会盖住 ThingsBoard 的 -4.13.0,导致 Spring Data JPA 无法启动。 +部署目录必须正好包含 10 个 jar:模块本身,以及 `iotdb-session`、`isession`、 +`service-rpc`、`iotdb-thrift`、`iotdb-thrift-commons`、`libthrift`、`tsfile`、 +`common` 和 `xz`。IoTDB 客户端用到的其余库(`lz4-java`、`zstd-jni`、`snappy-java` +和 `antlr4-runtime`)ThingsBoard 已经自带,不要再放入其他副本。 ## 3. 配置 @@ -123,10 +138,6 @@ iotdb.attributes.cluster_mode=disabled `127.0.0.1`;其他默认值为端口 `6667`、用户名/密码 `root`/`root`、数据库 `thingsboard`、会话池大小 `8`,并默认启用表结构引导。 -属性路径要求构建中包含 -[apache/iotdb-extras#125](https://github.com/apache/iotdb-extras/pull/125),即上游 -commit `275043ed` 或更新版本。更早的构建无法在原版 ThingsBoard 上启用属性选择器。 - 启用最新值或属性时,必须显式设置对应的 `cluster_mode`: - `sticky-routing`:把同一 identity 的写入固定路由到一个节点。 @@ -164,14 +175,14 @@ LIMIT 10; ## 5. 已知限制 -- 已验证版本为 IoTDB 2.0.8 和 ThingsBoard 4.3.1.2。其他 IoTDB 2.x 或 ThingsBoard +- 已验证版本为 IoTDB 2.0.11 和 ThingsBoard 4.3.1.2。其他 IoTDB 2.x 或 ThingsBoard 版本需要单独验证。 - 属性写入和最新值覆盖层只在单个 JVM 内收敛。多写入者部署必须使用固定路由,或明确接受 尽力而为的收敛。 - 最新值路径从时序表派生,并使用一个小型覆盖层承接仅写最新值和仅删最新值的路径。 - 保留策略使用 IoTDB 表级 TTL;其与 ThingsBoard 保留设置的对应关系参见模块的 - [用户指南](https://github.com/apache/iotdb-extras/blob/master/iotdb-thingsboard-table/docs/user-guide.md)。 + [用户指南](https://github.com/apache/iotdb-extras/blob/v2.0.11/iotdb-thingsboard-table/docs/user-guide.md)。 设计、源码构建、迁移和编译接口验证细节保留在 -[`iotdb-thingsboard-table` 模块](https://github.com/apache/iotdb-extras/tree/master/iotdb-thingsboard-table) +[`iotdb-thingsboard-table` 模块](https://github.com/apache/iotdb-extras/tree/v2.0.11/iotdb-thingsboard-table) 中。 diff --git a/src/zh/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md b/src/zh/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md index ef51e6af0..a8fada336 100644 --- a/src/zh/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md +++ b/src/zh/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md @@ -34,25 +34,41 @@ IoTDB 适配版 ThingsBoard。 | 组件 | 版本 | | --- | --- | -| IoTDB | 2.0.8,表模型 | +| IoTDB | 2.0.11,表模型 | | ThingsBoard | 4.3.1.2 | | JDK | 17 或更高版本 | -本模块尚未进入 ASF Release。请从 `apache/iotdb-extras` 官方源码的 commit `275043ed` -或更新版本构建: +本模块已随 Apache IoTDB Extras 2.0.11 正式发布。模块 jar 作为附件放在 +[2.0.11 Release](https://github.com/apache/iotdb-extras/releases/tag/v2.0.11) 中, +并附有签名,可用 [IoTDB KEYS](https://dist.apache.org/repos/dist/release/iotdb/KEYS) +校验。该 jar 不包含 IoTDB 客户端,因此还需从 Maven Central 下载它依赖的 9 个客户端 +jar,放入同一个 `lib/` 目录: ```bash -git clone https://github.com/apache/iotdb-extras.git -cd iotdb-extras -git checkout 275043ed -mvn -P with-thingsboard -pl iotdb-thingsboard-table -am clean package -mvn -P with-thingsboard -pl iotdb-thingsboard-table -am \ - dependency:copy-dependencies -DincludeScope=runtime \ - -DoutputDirectory="$PWD/runtime-deps" +mkdir -p lib +R=https://github.com/apache/iotdb-extras/releases/download/v2.0.11 +curl -fL -o lib/iotdb-thingsboard-table-2.0.11.jar $R/iotdb-thingsboard-table-2.0.11.jar +curl -fLO $R/iotdb-thingsboard-table-2.0.11.jar.asc +curl -fL https://dist.apache.org/repos/dist/release/iotdb/KEYS | gpg --import +gpg --verify iotdb-thingsboard-table-2.0.11.jar.asc lib/iotdb-thingsboard-table-2.0.11.jar + +M=https://repo1.maven.org/maven2 +for path in \ + org/apache/iotdb/iotdb-session/2.0.11/iotdb-session-2.0.11.jar \ + org/apache/iotdb/isession/2.0.11/isession-2.0.11.jar \ + org/apache/iotdb/service-rpc/2.0.11/service-rpc-2.0.11.jar \ + org/apache/iotdb/iotdb-thrift/2.0.11/iotdb-thrift-2.0.11.jar \ + org/apache/iotdb/iotdb-thrift-commons/2.0.11/iotdb-thrift-commons-2.0.11.jar \ + org/apache/thrift/libthrift/0.23.0/libthrift-0.23.0.jar \ + org/apache/tsfile/tsfile/2.4.0/tsfile-2.4.0.jar \ + org/apache/tsfile/common/2.4.0/common-2.4.0.jar \ + org/tukaani/xz/1.10/xz-1.10.jar; do + curl -fL -o "lib/${path##*/}" "$M/$path" +done ``` -第二条命令会有意生成未经筛选的依赖目录。第 2 节列出了实际需要部署的运行期 jar;不要 -直接复制整个目录。 +完成后 `lib/` 中即为需要部署的 10 个 jar。也可以用源码包 +`apache-iotdb-extras-2.0.11-source-release.zip` 自行构建本模块。 ## 2. 安装 @@ -61,8 +77,8 @@ mvn -P with-thingsboard -pl iotdb-thingsboard-table -am \ 1. 安装并启动 IoTDB,参见 [IoTDB 快速上手](../QuickStart/QuickStart.md)。 - 若 IoTDB 2.0.8 与 ThingsBoard 分别运行在容器中,请把两者放入同一 Docker 网络,并让 - IoTDB 公布网络内可达的服务名。`2.0.8-standalone` 镜像默认把这些地址绑定到 + 若 IoTDB 2.0.11 与 ThingsBoard 分别运行在容器中,请把两者放入同一 Docker 网络,并让 + IoTDB 公布网络内可达的服务名。`2.0.11-standalone` 镜像默认把这些地址绑定到 `127.0.0.1`。假设 IoTDB 服务名为 `iotdb`,请设置: ```yaml @@ -74,19 +90,18 @@ mvn -P with-thingsboard -pl iotdb-thingsboard-table -am \ ``` 2. 按常规方式安装 ThingsBoard,此时不要设置第 3 节的任何属性。 -3. 创建部署用的 `lib/` 目录,从 `iotdb-thingsboard-table/target/` 复制模块的二进制 jar, - 再从 `runtime-deps/` 复制下方列出的 10 个运行期 jar。将这 11 个 jar 全部放到 - ThingsBoard classpath 上。 +3. 将 `lib/` 中的 10 个 jar 放到 ThingsBoard classpath 上。即使尚未设置任何选择器, + 这 10 个 jar 也缺一不可:只放模块 jar 时,ThingsBoard 会启动失败,报 + `NoClassDefFoundError: org/apache/iotdb/isession/pool/ITableSessionPool`。 对于 ThingsBoard Docker 镜像,可将这些 jar 挂载或复制到 `/usr/share/thingsboard/extensions`。镜像使用的 `PropertiesLauncher` 已通过 `LOADER_PATH` 包含该目录。其他安装方式可能采用不同的 classpath 扩展机制。 -部署目录必须正好包含 11 个 jar:模块本身,以及 `iotdb-session`、`isession`、 -`service-rpc`、`iotdb-thrift`、`iotdb-thrift-commons`、`libthrift`、`pipe-api`、 -`tsfile`、`common` 和 `xz`。不要直接放入 Maven 复制出的全部运行期依赖;其中包含 -ThingsBoard 已自带的旧版本冲突项。特别是 `antlr4-runtime` 4.9.3 会盖住 ThingsBoard 的 -4.13.0,导致 Spring Data JPA 无法启动。 +部署目录必须正好包含 10 个 jar:模块本身,以及 `iotdb-session`、`isession`、 +`service-rpc`、`iotdb-thrift`、`iotdb-thrift-commons`、`libthrift`、`tsfile`、 +`common` 和 `xz`。IoTDB 客户端用到的其余库(`lz4-java`、`zstd-jni`、`snappy-java` +和 `antlr4-runtime`)ThingsBoard 已经自带,不要再放入其他副本。 ## 3. 配置 @@ -123,10 +138,6 @@ iotdb.attributes.cluster_mode=disabled `127.0.0.1`;其他默认值为端口 `6667`、用户名/密码 `root`/`root`、数据库 `thingsboard`、会话池大小 `8`,并默认启用表结构引导。 -属性路径要求构建中包含 -[apache/iotdb-extras#125](https://github.com/apache/iotdb-extras/pull/125),即上游 -commit `275043ed` 或更新版本。更早的构建无法在原版 ThingsBoard 上启用属性选择器。 - 启用最新值或属性时,必须显式设置对应的 `cluster_mode`: - `sticky-routing`:把同一 identity 的写入固定路由到一个节点。 @@ -164,14 +175,14 @@ LIMIT 10; ## 5. 已知限制 -- 已验证版本为 IoTDB 2.0.8 和 ThingsBoard 4.3.1.2。其他 IoTDB 2.x 或 ThingsBoard +- 已验证版本为 IoTDB 2.0.11 和 ThingsBoard 4.3.1.2。其他 IoTDB 2.x 或 ThingsBoard 版本需要单独验证。 - 属性写入和最新值覆盖层只在单个 JVM 内收敛。多写入者部署必须使用固定路由,或明确接受 尽力而为的收敛。 - 最新值路径从时序表派生,并使用一个小型覆盖层承接仅写最新值和仅删最新值的路径。 - 保留策略使用 IoTDB 表级 TTL;其与 ThingsBoard 保留设置的对应关系参见模块的 - [用户指南](https://github.com/apache/iotdb-extras/blob/master/iotdb-thingsboard-table/docs/user-guide.md)。 + [用户指南](https://github.com/apache/iotdb-extras/blob/v2.0.11/iotdb-thingsboard-table/docs/user-guide.md)。 设计、源码构建、迁移和编译接口验证细节保留在 -[`iotdb-thingsboard-table` 模块](https://github.com/apache/iotdb-extras/tree/master/iotdb-thingsboard-table) +[`iotdb-thingsboard-table` 模块](https://github.com/apache/iotdb-extras/tree/v2.0.11/iotdb-thingsboard-table) 中。 From 0f3a5e326ff79e72bdd807a67942df0e0f126bcd Mon Sep 17 00:00:00 2001 From: Zihan Dai <1436286758@qq.com> Date: Fri, 23 Oct 2026 16:52:25 +1100 Subject: [PATCH 4/5] docs: make the ThingsBoard table-model guide easier to follow - Say where the settings go: a Docker Compose snippet and the Linux package's /etc/thingsboard/conf/thingsboard.conf, both passing them as SPRING_APPLICATION_JSON, and note that the .deb does not create /usr/share/thingsboard/extensions. - Explain each setting for operators: what iotdb.ts.experimental-raw-only actually switches on, how the three kinds of data depend on each other, and what cluster_mode requires with one or several ThingsBoard nodes. - List the jars once, keep other extension jars, and drop design terms the reader does not need. - Compare IoTDB rows against the device's own ID, keys and times. - Rewrite the Chinese page in plain Chinese rather than as a translation. Signed-off-by: Zihan Dai <1436286758@qq.com> --- .../Ecosystem-Integration/Thingsboard.md | 217 ++++++++++++------ .../Ecosystem-Integration/Thingsboard.md | 217 ++++++++++++------ .../Ecosystem-Integration/Thingsboard.md | 199 ++++++++++------ .../Ecosystem-Integration/Thingsboard.md | 199 ++++++++++------ 4 files changed, 562 insertions(+), 270 deletions(-) diff --git a/src/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md b/src/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md index 9753793f8..41ac8cd0a 100644 --- a/src/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md +++ b/src/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md @@ -21,10 +21,11 @@ # ThingsBoard -The `iotdb-thingsboard-table` module lets a stock ThingsBoard deployment store -historical telemetry, latest telemetry, and entity attributes in the IoTDB Table -Model. This page follows the operator workflow: download, install, configure, -run, and verify. +The `iotdb-thingsboard-table` module lets a standard ThingsBoard deployment +store its historical telemetry, latest telemetry values and entity attributes in +the IoTDB table model. Each of the three kinds of data can be moved to IoTDB on +its own. This page walks through download, installation, configuration and +verification. This integration is independent of the earlier [ThingsBoard Tree Model integration](../../Tree/Ecosystem-Integration/Thingsboard.md), @@ -43,10 +44,11 @@ The tested combination is: The module is part of the Apache IoTDB Extras 2.0.11 release. Its jar is attached to the [2.0.11 release](https://github.com/apache/iotdb-extras/releases/tag/v2.0.11) -with a signature that can be checked against the +together with a signature, which can be checked against the [IoTDB KEYS](https://dist.apache.org/repos/dist/release/iotdb/KEYS) file. The -jar does not contain the IoTDB client, so the nine client jars it needs are -downloaded from Maven Central into the same `lib/` directory: +module jar does not include the IoTDB client, so nine client jars are needed as +well. The following commands download all ten jars into a `lib/` directory and +verify the module's signature: ```bash mkdir -p lib @@ -71,22 +73,24 @@ for path in \ done ``` -`lib/` then holds the ten jars to deploy. The module can also be built from the +The other libraries the IoTDB client uses (`lz4-java`, `zstd-jni`, +`snappy-java` and `antlr4-runtime`) are already part of ThingsBoard; do not add +copies of them. The module can also be built from the `apache-iotdb-extras-2.0.11-source-release.zip` source package. ## 2. Install -ThingsBoard must be installed before the IoTDB selectors are enabled. Its -installer resolves a `TsDatabaseSchemaService` supplied by the built-in -backends, so enabling this module during installation makes the installation -fail. +Install ThingsBoard before you enable the module. The ThingsBoard installer +needs ThingsBoard's own storage backends; if the IoTDB settings from section 3 +are already present, the installer stops with +`No qualifying bean of type 'org.thingsboard.server.service.install.TsDatabaseSchemaService' available`. 1. Install and start IoTDB. See [IoTDB QuickStart](../QuickStart/QuickStart.md). - When IoTDB 2.0.11 and ThingsBoard run in separate containers, put them on the - same Docker network and make IoTDB advertise a reachable service name. The - `2.0.11-standalone` image otherwise binds these endpoints to `127.0.0.1`. - For an IoTDB service named `iotdb`, set: + When IoTDB and ThingsBoard run in separate containers, put them on the same + Docker network and make IoTDB announce its service name. Otherwise the + `2.0.11-standalone` image binds these endpoints to `127.0.0.1`, which the + ThingsBoard container cannot reach. For an IoTDB service named `iotdb`, set: ```yaml cn_seed_config_node: iotdb:10710 @@ -96,31 +100,26 @@ fail. dn_rpc_address: iotdb ``` -2. Install ThingsBoard normally without any of the properties from section 3. -3. Place the ten jars from `lib/` on the ThingsBoard classpath. All ten are - needed even while no selector is set: with the module jar alone, ThingsBoard - fails to start with - `NoClassDefFoundError: org/apache/iotdb/isession/pool/ITableSessionPool`. - -For the ThingsBoard Docker image, mount or copy the jars to -`/usr/share/thingsboard/extensions`; its `PropertiesLauncher` already includes -that directory through `LOADER_PATH`. Other installation formats may use a -different classpath extension mechanism. +2. Install ThingsBoard as usual, without any of the settings from section 3. +3. Put the ten jars from `lib/` into ThingsBoard's extensions directory, + `/usr/share/thingsboard/extensions`. Both the Docker image and the Linux + `.deb` package load every jar in that directory. Section 3 shows how to do this + for each kind of installation. Keep any other jars that are already there. -The deployment directory contains exactly ten jars: the module, `iotdb-session`, -`isession`, `service-rpc`, `iotdb-thrift`, `iotdb-thrift-commons`, `libthrift`, -`tsfile`, `common`, and `xz`. ThingsBoard already ships the other libraries the -IoTDB client uses (`lz4-java`, `zstd-jni`, `snappy-java` and `antlr4-runtime`), -so do not add copies of them. + All ten jars are required, even before the module is enabled. With the + module jar alone, ThingsBoard does not start and reports + `NoClassDefFoundError: org/apache/iotdb/isession/pool/ITableSessionPool`. + With all ten jars and none of the settings below, ThingsBoard runs on its + own storage and the module does nothing. ## 3. Configure -Add the properties for the storage paths you want to move to IoTDB. The -following example enables all three paths for a single-node deployment: +### Settings + +The following settings move all three kinds of data to a single IoTDB node: ```properties # IoTDB connection -# Docker service name; use 127.0.0.1 only when both processes share a host iotdb.host=iotdb iotdb.port=6667 iotdb.username=root @@ -133,7 +132,7 @@ iotdb.schema.bootstrap=true database.ts.type=iotdb-table iotdb.ts.experimental-raw-only=true -# Latest telemetry +# Latest telemetry values database.ts_latest.type=iotdb-table iotdb.ts_latest.cluster_mode=disabled @@ -142,67 +141,147 @@ database.attributes.type=iotdb-table iotdb.attributes.cluster_mode=disabled ``` -The historical and latest selectors are separate. Omitting -`database.ts_latest.type` leaves latest values on the original ThingsBoard -backend even when historical telemetry is stored in IoTDB. +What they mean: + +- **Connection.** `iotdb.host` is the address ThingsBoard uses to reach IoTDB: + the service name in Docker, or a host name or IP address otherwise. Use + `127.0.0.1` only when both run directly on the same host, outside containers, + or share one container network namespace. If a setting is left out, the defaults are host + `127.0.0.1`, port `6667`, user `root` with password `root`, database + `thingsboard`, and a pool of `8` sessions. With `iotdb.schema.bootstrap=true` + (the default), the module creates the database and its tables on first start. +- **Historical telemetry** moves to IoTDB when both `database.ts.type=iotdb-table` + and `iotdb.ts.experimental-raw-only=true` are set. The second setting is the + required switch that turns this storage on. Its name is kept for + compatibility with earlier builds; with it set, the module stores, reads, + deletes and aggregates historical telemetry. +- **Latest telemetry values** move to IoTDB with + `database.ts_latest.type=iotdb-table`. Latest values are read from the + historical telemetry kept in IoTDB, so this also needs the two historical + settings above. Without it, latest values stay in ThingsBoard's own storage + even when historical telemetry is in IoTDB. +- **Entity attributes** move to IoTDB with `database.attributes.type=iotdb-table`, + independently of the other two. ThingsBoard has no setting of its own for + this, so the module removes ThingsBoard's attribute storage bean at startup + and logs a `WARN` that names `jpaAttributeDao`. +- **`cluster_mode`** must be set when latest values or attributes are moved to + IoTDB; any other value, including an empty one, stops startup with an + error. It states how ThingsBoard nodes share writes. The module protects + concurrent changes to the same key only inside one ThingsBoard process; the + setting does not route requests or coordinate nodes. + - `disabled`: use this with a single ThingsBoard node. With several nodes, + use it only if you accept that concurrent updates to the same device or + entity from different nodes can leave inconsistent results. + - `sticky-routing`: use this with several nodes when your deployment makes + sure that all updates and deletes for one device or entity are handled by + the same node. Configure and verify that routing yourself. + +### Passing the settings to ThingsBoard + +The setting names contain dots, underscores and hyphens, so pass them as one +JSON object in the `SPRING_APPLICATION_JSON` environment variable. + +**Docker (Compose).** Add the environment variable and the jar directory to +the ThingsBoard service, keeping its other settings. The bind mount replaces the +image's extensions directory, so put any other extension jars you use into the +same `lib/` directory: + +```yaml +services: + thingsboard: + environment: + SPRING_APPLICATION_JSON: >- + {"iotdb.host": "iotdb", "iotdb.port": "6667", + "iotdb.username": "root", "iotdb.password": "root", + "iotdb.database": "thingsboard", "iotdb.session-pool-size": "8", + "iotdb.schema.bootstrap": "true", + "database.ts.type": "iotdb-table", + "iotdb.ts.experimental-raw-only": "true", + "database.ts_latest.type": "iotdb-table", + "iotdb.ts_latest.cluster_mode": "disabled", + "database.attributes.type": "iotdb-table", + "iotdb.attributes.cluster_mode": "disabled"} + volumes: + - ./lib:/usr/share/thingsboard/extensions:ro +``` -The example writes the connection defaults explicitly. Outside Docker, -`iotdb.host` defaults to `127.0.0.1`; the other defaults are port `6667`, -username/password `root`/`root`, database `thingsboard`, session-pool size `8`, -and schema bootstrap enabled. +Apply it with `docker compose up -d`, which recreates the container. Restarting +the old container does not pick up a changed environment. -When latest telemetry or attributes are enabled, their `cluster_mode` must be -set explicitly: +**Linux package (`.deb`).** The package does not create the extensions +directory. Create it and copy the jars: -- `sticky-routing`: route writes for one identity to one node. -- `disabled`: single-node deployment, or explicitly accept best-effort - convergence. +```bash +sudo mkdir -p /usr/share/thingsboard/extensions +sudo cp lib/*.jar /usr/share/thingsboard/extensions/ +``` + +Then add the settings to `/etc/thingsboard/conf/thingsboard.conf`, which the +ThingsBoard service reads at startup. The example assumes IoTDB on the same +host; change `iotdb.host` otherwise: + +```bash +export SPRING_APPLICATION_JSON='{ + "iotdb.host": "127.0.0.1", "iotdb.port": "6667", + "iotdb.username": "root", "iotdb.password": "root", + "iotdb.database": "thingsboard", "iotdb.session-pool-size": "8", + "iotdb.schema.bootstrap": "true", + "database.ts.type": "iotdb-table", + "iotdb.ts.experimental-raw-only": "true", + "database.ts_latest.type": "iotdb-table", + "iotdb.ts_latest.cluster_mode": "disabled", + "database.attributes.type": "iotdb-table", + "iotdb.attributes.cluster_mode": "disabled" +}' +``` -Any other value, including an empty value, stops startup with an explanatory -error. +Restart the service with `sudo systemctl restart thingsboard`. Setting the +variable in your own shell has no effect on the service. ## 4. Run and verify -1. Restart ThingsBoard after the bundle and configuration are in place. -2. Check the log for `IoTDB Table Mode session pool initialized`. With schema - bootstrap enabled, also check for `IoTDB Table Mode schema bootstrap - complete`. -3. Send test telemetry and attributes to a ThingsBoard device, then confirm - them on the device's **Latest telemetry** and **Attributes** pages. +1. Start ThingsBoard, or restart it if it is already running. +2. Check the ThingsBoard log for `IoTDB Table Mode session pool initialized`. + With schema bootstrap enabled, `IoTDB Table Mode schema bootstrap complete` + follows. When attributes are moved, the `WARN` about `jpaAttributeDao` is + expected. +3. Send test telemetry and attributes to a ThingsBoard device, and note the + device ID, the keys, the values and their times as shown on the device's + **Latest telemetry** and **Attributes** tabs. ![ThingsBoard latest telemetry stored through the IoTDB Table Model backend](/img/thingsboard-table-telemetry.png) ![ThingsBoard client attributes stored through the IoTDB Table Model backend](/img/thingsboard-table-attributes.png) -4. Confirm the same values in IoTDB: +4. Query the same device in IoTDB and compare keys, values and times. Replace + `` with the device ID: ```sql USE thingsboard; -SELECT * +SELECT time, key, bool_v, long_v, double_v, str_v, json_v FROM telemetry +WHERE entity_id = '' ORDER BY time DESC LIMIT 10; -SELECT * +SELECT time, attribute_scope, key, bool_v, long_v, double_v, str_v, json_v FROM entity_attributes +WHERE entity_id = '' ORDER BY time DESC LIMIT 10; ``` ## 5. Known limitations -- IoTDB 2.0.11 and ThingsBoard 4.3.1.2 are the tested versions. Other 2.x or - ThingsBoard versions require separate verification. -- Attribute and latest-overlay writes converge within one JVM. A multi-writer - deployment must use sticky routing or explicitly accept best-effort - convergence. -- The latest-value path is derived from the telemetry table, with a small - overlay for latest-only writes and deletes. -- Retention uses IoTDB table-level TTL. See the module's +- IoTDB 2.0.11 and ThingsBoard 4.3.1.2 are the tested versions. Other IoTDB + 2.x or ThingsBoard versions need their own verification. +- With several ThingsBoard nodes, consistent latest values and attributes depend + on the routing described under `cluster_mode` in section 3. +- Retention uses IoTDB table-level TTL. The module's [user guide](https://github.com/apache/iotdb-extras/blob/v2.0.11/iotdb-thingsboard-table/docs/user-guide.md) - for its mapping to ThingsBoard retention settings. + explains how it relates to ThingsBoard's retention settings. -Design, source-build, migration, and compile-surface verification details remain -in the +The design, building from source and migrating existing data are described in +the [`iotdb-thingsboard-table` module](https://github.com/apache/iotdb-extras/tree/v2.0.11/iotdb-thingsboard-table). diff --git a/src/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md b/src/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md index 8cbba71b3..c08f82b1a 100644 --- a/src/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md +++ b/src/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md @@ -21,10 +21,11 @@ # ThingsBoard -The `iotdb-thingsboard-table` module lets a stock ThingsBoard deployment store -historical telemetry, latest telemetry, and entity attributes in the IoTDB Table -Model. This page follows the operator workflow: download, install, configure, -run, and verify. +The `iotdb-thingsboard-table` module lets a standard ThingsBoard deployment +store its historical telemetry, latest telemetry values and entity attributes in +the IoTDB table model. Each of the three kinds of data can be moved to IoTDB on +its own. This page walks through download, installation, configuration and +verification. This integration is independent of the earlier [ThingsBoard Tree Model integration](../../latest/Ecosystem-Integration/Thingsboard.md), @@ -43,10 +44,11 @@ The tested combination is: The module is part of the Apache IoTDB Extras 2.0.11 release. Its jar is attached to the [2.0.11 release](https://github.com/apache/iotdb-extras/releases/tag/v2.0.11) -with a signature that can be checked against the +together with a signature, which can be checked against the [IoTDB KEYS](https://dist.apache.org/repos/dist/release/iotdb/KEYS) file. The -jar does not contain the IoTDB client, so the nine client jars it needs are -downloaded from Maven Central into the same `lib/` directory: +module jar does not include the IoTDB client, so nine client jars are needed as +well. The following commands download all ten jars into a `lib/` directory and +verify the module's signature: ```bash mkdir -p lib @@ -71,22 +73,24 @@ for path in \ done ``` -`lib/` then holds the ten jars to deploy. The module can also be built from the +The other libraries the IoTDB client uses (`lz4-java`, `zstd-jni`, +`snappy-java` and `antlr4-runtime`) are already part of ThingsBoard; do not add +copies of them. The module can also be built from the `apache-iotdb-extras-2.0.11-source-release.zip` source package. ## 2. Install -ThingsBoard must be installed before the IoTDB selectors are enabled. Its -installer resolves a `TsDatabaseSchemaService` supplied by the built-in -backends, so enabling this module during installation makes the installation -fail. +Install ThingsBoard before you enable the module. The ThingsBoard installer +needs ThingsBoard's own storage backends; if the IoTDB settings from section 3 +are already present, the installer stops with +`No qualifying bean of type 'org.thingsboard.server.service.install.TsDatabaseSchemaService' available`. 1. Install and start IoTDB. See [IoTDB QuickStart](../QuickStart/QuickStart.md). - When IoTDB 2.0.11 and ThingsBoard run in separate containers, put them on the - same Docker network and make IoTDB advertise a reachable service name. The - `2.0.11-standalone` image otherwise binds these endpoints to `127.0.0.1`. - For an IoTDB service named `iotdb`, set: + When IoTDB and ThingsBoard run in separate containers, put them on the same + Docker network and make IoTDB announce its service name. Otherwise the + `2.0.11-standalone` image binds these endpoints to `127.0.0.1`, which the + ThingsBoard container cannot reach. For an IoTDB service named `iotdb`, set: ```yaml cn_seed_config_node: iotdb:10710 @@ -96,31 +100,26 @@ fail. dn_rpc_address: iotdb ``` -2. Install ThingsBoard normally without any of the properties from section 3. -3. Place the ten jars from `lib/` on the ThingsBoard classpath. All ten are - needed even while no selector is set: with the module jar alone, ThingsBoard - fails to start with - `NoClassDefFoundError: org/apache/iotdb/isession/pool/ITableSessionPool`. - -For the ThingsBoard Docker image, mount or copy the jars to -`/usr/share/thingsboard/extensions`; its `PropertiesLauncher` already includes -that directory through `LOADER_PATH`. Other installation formats may use a -different classpath extension mechanism. +2. Install ThingsBoard as usual, without any of the settings from section 3. +3. Put the ten jars from `lib/` into ThingsBoard's extensions directory, + `/usr/share/thingsboard/extensions`. Both the Docker image and the Linux + `.deb` package load every jar in that directory. Section 3 shows how to do this + for each kind of installation. Keep any other jars that are already there. -The deployment directory contains exactly ten jars: the module, `iotdb-session`, -`isession`, `service-rpc`, `iotdb-thrift`, `iotdb-thrift-commons`, `libthrift`, -`tsfile`, `common`, and `xz`. ThingsBoard already ships the other libraries the -IoTDB client uses (`lz4-java`, `zstd-jni`, `snappy-java` and `antlr4-runtime`), -so do not add copies of them. + All ten jars are required, even before the module is enabled. With the + module jar alone, ThingsBoard does not start and reports + `NoClassDefFoundError: org/apache/iotdb/isession/pool/ITableSessionPool`. + With all ten jars and none of the settings below, ThingsBoard runs on its + own storage and the module does nothing. ## 3. Configure -Add the properties for the storage paths you want to move to IoTDB. The -following example enables all three paths for a single-node deployment: +### Settings + +The following settings move all three kinds of data to a single IoTDB node: ```properties # IoTDB connection -# Docker service name; use 127.0.0.1 only when both processes share a host iotdb.host=iotdb iotdb.port=6667 iotdb.username=root @@ -133,7 +132,7 @@ iotdb.schema.bootstrap=true database.ts.type=iotdb-table iotdb.ts.experimental-raw-only=true -# Latest telemetry +# Latest telemetry values database.ts_latest.type=iotdb-table iotdb.ts_latest.cluster_mode=disabled @@ -142,67 +141,147 @@ database.attributes.type=iotdb-table iotdb.attributes.cluster_mode=disabled ``` -The historical and latest selectors are separate. Omitting -`database.ts_latest.type` leaves latest values on the original ThingsBoard -backend even when historical telemetry is stored in IoTDB. +What they mean: + +- **Connection.** `iotdb.host` is the address ThingsBoard uses to reach IoTDB: + the service name in Docker, or a host name or IP address otherwise. Use + `127.0.0.1` only when both run directly on the same host, outside containers, + or share one container network namespace. If a setting is left out, the defaults are host + `127.0.0.1`, port `6667`, user `root` with password `root`, database + `thingsboard`, and a pool of `8` sessions. With `iotdb.schema.bootstrap=true` + (the default), the module creates the database and its tables on first start. +- **Historical telemetry** moves to IoTDB when both `database.ts.type=iotdb-table` + and `iotdb.ts.experimental-raw-only=true` are set. The second setting is the + required switch that turns this storage on. Its name is kept for + compatibility with earlier builds; with it set, the module stores, reads, + deletes and aggregates historical telemetry. +- **Latest telemetry values** move to IoTDB with + `database.ts_latest.type=iotdb-table`. Latest values are read from the + historical telemetry kept in IoTDB, so this also needs the two historical + settings above. Without it, latest values stay in ThingsBoard's own storage + even when historical telemetry is in IoTDB. +- **Entity attributes** move to IoTDB with `database.attributes.type=iotdb-table`, + independently of the other two. ThingsBoard has no setting of its own for + this, so the module removes ThingsBoard's attribute storage bean at startup + and logs a `WARN` that names `jpaAttributeDao`. +- **`cluster_mode`** must be set when latest values or attributes are moved to + IoTDB; any other value, including an empty one, stops startup with an + error. It states how ThingsBoard nodes share writes. The module protects + concurrent changes to the same key only inside one ThingsBoard process; the + setting does not route requests or coordinate nodes. + - `disabled`: use this with a single ThingsBoard node. With several nodes, + use it only if you accept that concurrent updates to the same device or + entity from different nodes can leave inconsistent results. + - `sticky-routing`: use this with several nodes when your deployment makes + sure that all updates and deletes for one device or entity are handled by + the same node. Configure and verify that routing yourself. + +### Passing the settings to ThingsBoard + +The setting names contain dots, underscores and hyphens, so pass them as one +JSON object in the `SPRING_APPLICATION_JSON` environment variable. + +**Docker (Compose).** Add the environment variable and the jar directory to +the ThingsBoard service, keeping its other settings. The bind mount replaces the +image's extensions directory, so put any other extension jars you use into the +same `lib/` directory: + +```yaml +services: + thingsboard: + environment: + SPRING_APPLICATION_JSON: >- + {"iotdb.host": "iotdb", "iotdb.port": "6667", + "iotdb.username": "root", "iotdb.password": "root", + "iotdb.database": "thingsboard", "iotdb.session-pool-size": "8", + "iotdb.schema.bootstrap": "true", + "database.ts.type": "iotdb-table", + "iotdb.ts.experimental-raw-only": "true", + "database.ts_latest.type": "iotdb-table", + "iotdb.ts_latest.cluster_mode": "disabled", + "database.attributes.type": "iotdb-table", + "iotdb.attributes.cluster_mode": "disabled"} + volumes: + - ./lib:/usr/share/thingsboard/extensions:ro +``` -The example writes the connection defaults explicitly. Outside Docker, -`iotdb.host` defaults to `127.0.0.1`; the other defaults are port `6667`, -username/password `root`/`root`, database `thingsboard`, session-pool size `8`, -and schema bootstrap enabled. +Apply it with `docker compose up -d`, which recreates the container. Restarting +the old container does not pick up a changed environment. -When latest telemetry or attributes are enabled, their `cluster_mode` must be -set explicitly: +**Linux package (`.deb`).** The package does not create the extensions +directory. Create it and copy the jars: -- `sticky-routing`: route writes for one identity to one node. -- `disabled`: single-node deployment, or explicitly accept best-effort - convergence. +```bash +sudo mkdir -p /usr/share/thingsboard/extensions +sudo cp lib/*.jar /usr/share/thingsboard/extensions/ +``` + +Then add the settings to `/etc/thingsboard/conf/thingsboard.conf`, which the +ThingsBoard service reads at startup. The example assumes IoTDB on the same +host; change `iotdb.host` otherwise: + +```bash +export SPRING_APPLICATION_JSON='{ + "iotdb.host": "127.0.0.1", "iotdb.port": "6667", + "iotdb.username": "root", "iotdb.password": "root", + "iotdb.database": "thingsboard", "iotdb.session-pool-size": "8", + "iotdb.schema.bootstrap": "true", + "database.ts.type": "iotdb-table", + "iotdb.ts.experimental-raw-only": "true", + "database.ts_latest.type": "iotdb-table", + "iotdb.ts_latest.cluster_mode": "disabled", + "database.attributes.type": "iotdb-table", + "iotdb.attributes.cluster_mode": "disabled" +}' +``` -Any other value, including an empty value, stops startup with an explanatory -error. +Restart the service with `sudo systemctl restart thingsboard`. Setting the +variable in your own shell has no effect on the service. ## 4. Run and verify -1. Restart ThingsBoard after the bundle and configuration are in place. -2. Check the log for `IoTDB Table Mode session pool initialized`. With schema - bootstrap enabled, also check for `IoTDB Table Mode schema bootstrap - complete`. -3. Send test telemetry and attributes to a ThingsBoard device, then confirm - them on the device's **Latest telemetry** and **Attributes** pages. +1. Start ThingsBoard, or restart it if it is already running. +2. Check the ThingsBoard log for `IoTDB Table Mode session pool initialized`. + With schema bootstrap enabled, `IoTDB Table Mode schema bootstrap complete` + follows. When attributes are moved, the `WARN` about `jpaAttributeDao` is + expected. +3. Send test telemetry and attributes to a ThingsBoard device, and note the + device ID, the keys, the values and their times as shown on the device's + **Latest telemetry** and **Attributes** tabs. ![ThingsBoard latest telemetry stored through the IoTDB Table Model backend](/img/thingsboard-table-telemetry.png) ![ThingsBoard client attributes stored through the IoTDB Table Model backend](/img/thingsboard-table-attributes.png) -4. Confirm the same values in IoTDB: +4. Query the same device in IoTDB and compare keys, values and times. Replace + `` with the device ID: ```sql USE thingsboard; -SELECT * +SELECT time, key, bool_v, long_v, double_v, str_v, json_v FROM telemetry +WHERE entity_id = '' ORDER BY time DESC LIMIT 10; -SELECT * +SELECT time, attribute_scope, key, bool_v, long_v, double_v, str_v, json_v FROM entity_attributes +WHERE entity_id = '' ORDER BY time DESC LIMIT 10; ``` ## 5. Known limitations -- IoTDB 2.0.11 and ThingsBoard 4.3.1.2 are the tested versions. Other 2.x or - ThingsBoard versions require separate verification. -- Attribute and latest-overlay writes converge within one JVM. A multi-writer - deployment must use sticky routing or explicitly accept best-effort - convergence. -- The latest-value path is derived from the telemetry table, with a small - overlay for latest-only writes and deletes. -- Retention uses IoTDB table-level TTL. See the module's +- IoTDB 2.0.11 and ThingsBoard 4.3.1.2 are the tested versions. Other IoTDB + 2.x or ThingsBoard versions need their own verification. +- With several ThingsBoard nodes, consistent latest values and attributes depend + on the routing described under `cluster_mode` in section 3. +- Retention uses IoTDB table-level TTL. The module's [user guide](https://github.com/apache/iotdb-extras/blob/v2.0.11/iotdb-thingsboard-table/docs/user-guide.md) - for its mapping to ThingsBoard retention settings. + explains how it relates to ThingsBoard's retention settings. -Design, source-build, migration, and compile-surface verification details remain -in the +The design, building from source and migrating existing data are described in +the [`iotdb-thingsboard-table` module](https://github.com/apache/iotdb-extras/tree/v2.0.11/iotdb-thingsboard-table). diff --git a/src/zh/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md b/src/zh/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md index 9849d0cc6..a9eb9ef3a 100644 --- a/src/zh/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md +++ b/src/zh/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md @@ -21,8 +21,9 @@ # ThingsBoard -`iotdb-thingsboard-table` 模块让原版 ThingsBoard 可以把历史时序、最新值时序和实体属性 -存入 IoTDB 表模型。本页按操作者真正使用它的顺序说明:下载、安装、配置、运行与验证。 +`iotdb-thingsboard-table` 模块可以让标准版 ThingsBoard 把历史遥测数据、最新遥测值和实体 +属性存入 IoTDB 表模型。三类数据可以分别迁移到 IoTDB。本页依次介绍下载、安装、配置和 +验证。 这套集成独立于较早的 [ThingsBoard 树模型集成](../../Tree/Ecosystem-Integration/Thingsboard.md),后者需要使用 @@ -30,7 +31,7 @@ IoTDB 适配版 ThingsBoard。 ## 1. 下载 -已经验证的版本组合如下: +经过验证的版本组合如下: | 组件 | 版本 | | --- | --- | @@ -38,11 +39,11 @@ IoTDB 适配版 ThingsBoard。 | ThingsBoard | 4.3.1.2 | | JDK | 17 或更高版本 | -本模块已随 Apache IoTDB Extras 2.0.11 正式发布。模块 jar 作为附件放在 -[2.0.11 Release](https://github.com/apache/iotdb-extras/releases/tag/v2.0.11) 中, -并附有签名,可用 [IoTDB KEYS](https://dist.apache.org/repos/dist/release/iotdb/KEYS) -校验。该 jar 不包含 IoTDB 客户端,因此还需从 Maven Central 下载它依赖的 9 个客户端 -jar,放入同一个 `lib/` 目录: +本模块随 Apache IoTDB Extras 2.0.11 正式发布。模块 jar 及其签名作为附件放在 +[2.0.11 Release](https://github.com/apache/iotdb-extras/releases/tag/v2.0.11) 中,签名可用 +[IoTDB KEYS](https://dist.apache.org/repos/dist/release/iotdb/KEYS) 校验。模块 jar 不包含 +IoTDB 客户端,因此还需要 9 个客户端 jar。下面的命令把这 10 个 jar 下载到 `lib/` 目录, +并校验模块 jar 的签名: ```bash mkdir -p lib @@ -67,49 +68,47 @@ for path in \ done ``` -完成后 `lib/` 中即为需要部署的 10 个 jar。也可以用源码包 +IoTDB 客户端用到的其他库(`lz4-java`、`zstd-jni`、`snappy-java` 和 `antlr4-runtime`) +ThingsBoard 已经自带,不要再另外放入。也可以用源码包 `apache-iotdb-extras-2.0.11-source-release.zip` 自行构建本模块。 ## 2. 安装 -必须先安装 ThingsBoard,再启用 IoTDB 选择器。ThingsBoard 安装器会解析由内置后端提供的 -`TsDatabaseSchemaService`;若安装阶段已经启用本模块,安装会失败。 +请先安装 ThingsBoard,再启用本模块。ThingsBoard 安装程序依赖它自带的存储后端;如果第 3 +节的 IoTDB 配置已经生效,安装会中止并报 +`No qualifying bean of type 'org.thingsboard.server.service.install.TsDatabaseSchemaService' available`。 1. 安装并启动 IoTDB,参见 [IoTDB 快速上手](../QuickStart/QuickStart.md)。 - 若 IoTDB 2.0.11 与 ThingsBoard 分别运行在容器中,请把两者放入同一 Docker 网络,并让 - IoTDB 公布网络内可达的服务名。`2.0.11-standalone` 镜像默认把这些地址绑定到 - `127.0.0.1`。假设 IoTDB 服务名为 `iotdb`,请设置: + 如果 IoTDB 和 ThingsBoard 分别运行在不同容器中,请把它们放在同一个 Docker 网络里,并让 + IoTDB 对外公布自己的服务名。否则 `2.0.11-standalone` 镜像会把这些地址绑定到 + `127.0.0.1`,ThingsBoard 容器无法访问。假设 IoTDB 的服务名为 `iotdb`,请设置: ```yaml - cn_seed_config_node: iotdb:10710 - dn_seed_config_node: iotdb:10710 - cn_internal_address: iotdb - dn_internal_address: iotdb - dn_rpc_address: iotdb - ``` - -2. 按常规方式安装 ThingsBoard,此时不要设置第 3 节的任何属性。 -3. 将 `lib/` 中的 10 个 jar 放到 ThingsBoard classpath 上。即使尚未设置任何选择器, - 这 10 个 jar 也缺一不可:只放模块 jar 时,ThingsBoard 会启动失败,报 - `NoClassDefFoundError: org/apache/iotdb/isession/pool/ITableSessionPool`。 - -对于 ThingsBoard Docker 镜像,可将这些 jar 挂载或复制到 -`/usr/share/thingsboard/extensions`。镜像使用的 `PropertiesLauncher` 已通过 -`LOADER_PATH` 包含该目录。其他安装方式可能采用不同的 classpath 扩展机制。 - -部署目录必须正好包含 10 个 jar:模块本身,以及 `iotdb-session`、`isession`、 -`service-rpc`、`iotdb-thrift`、`iotdb-thrift-commons`、`libthrift`、`tsfile`、 -`common` 和 `xz`。IoTDB 客户端用到的其余库(`lz4-java`、`zstd-jni`、`snappy-java` -和 `antlr4-runtime`)ThingsBoard 已经自带,不要再放入其他副本。 + cn_seed_config_node: iotdb:10710 + dn_seed_config_node: iotdb:10710 + cn_internal_address: iotdb + dn_internal_address: iotdb + dn_rpc_address: iotdb + ``` + +2. 按常规方式安装 ThingsBoard,此时不要加入第 3 节的任何配置。 +3. 把 `lib/` 中的 10 个 jar 放入 ThingsBoard 的扩展目录 `/usr/share/thingsboard/extensions`。 + Docker 镜像和 Linux `.deb` 安装包都会加载该目录中的所有 jar,具体做法见第 3 节。该目录中已有的 + 其他 jar 请保留。 + + 这 10 个 jar 缺一不可,即使还没有启用本模块也是如此。只放模块 jar 时,ThingsBoard 无法 + 启动,并报 `NoClassDefFoundError: org/apache/iotdb/isession/pool/ITableSessionPool`。 + 放齐 10 个 jar 但不加下面的配置时,ThingsBoard 照常使用自己的存储,本模块不起作用。 ## 3. 配置 -按需要启用要迁移到 IoTDB 的存储路径。下面的单节点示例同时启用三条路径: +### 配置项 + +下面这组配置把三类数据都存入单个 IoTDB 节点: ```properties -# IoTDB 连接 -# Docker 服务名;仅当两个进程共享同一主机时使用 127.0.0.1 +# IoTDB connection iotdb.host=iotdb iotdb.port=6667 iotdb.username=root @@ -118,71 +117,139 @@ iotdb.database=thingsboard iotdb.session-pool-size=8 iotdb.schema.bootstrap=true -# 历史时序 +# Historical telemetry database.ts.type=iotdb-table iotdb.ts.experimental-raw-only=true -# 最新值时序 +# Latest telemetry values database.ts_latest.type=iotdb-table iotdb.ts_latest.cluster_mode=disabled -# 实体属性 +# Entity attributes database.attributes.type=iotdb-table iotdb.attributes.cluster_mode=disabled ``` -历史时序和最新值时序使用两个独立选择器。若遗漏 `database.ts_latest.type`,即使历史数据 -已经存入 IoTDB,最新值仍会留在 ThingsBoard 原来的后端中。 +各配置项的含义: + +- **连接**:`iotdb.host` 是 ThingsBoard 访问 IoTDB 的地址。在 Docker 中填 IoTDB 的服务名, + 其他环境填主机名或 IP。只有两者直接运行在同一台主机上(不在容器中),或共用同一个容器 + 网络命名空间时,才使用 `127.0.0.1`。未设置的项使用默认值:地址 `127.0.0.1`、端口 `6667`、用户名和密码 + 均为 `root`、数据库 `thingsboard`、会话池大小 `8`。`iotdb.schema.bootstrap=true`(默认) + 时,模块会在首次启动时自动创建数据库和表。 +- **历史遥测数据**:同时设置 `database.ts.type=iotdb-table` 和 + `iotdb.ts.experimental-raw-only=true` 后存入 IoTDB。后者是启用这部分存储的必需开关,名字 + 为兼容早期版本而保留;开启后,历史遥测数据的写入、查询、删除和聚合都由本模块处理。 +- **最新遥测值**:设置 `database.ts_latest.type=iotdb-table` 后存入 IoTDB。最新值从 IoTDB + 中的历史遥测数据读取,所以还需要上面两项历史数据配置。不设置这一项时,即使历史数据已在 + IoTDB 中,最新值仍留在 ThingsBoard 原有的存储里。 +- **实体属性**:设置 `database.attributes.type=iotdb-table` 后存入 IoTDB,与前两类数据互不 + 依赖。ThingsBoard 本身没有切换属性存储的配置项,因此本模块会在启动时移除 ThingsBoard + 自带的属性存储组件,并输出一条提到 `jpaAttributeDao` 的 `WARN` 日志。 +- **`cluster_mode`**:把最新遥测值或实体属性存入 IoTDB 时必须设置;填其他值(包括留空) + 都会导致启动失败。它用于说明多个 ThingsBoard 节点之间如何分担写入。本模块只在单个 + ThingsBoard 进程内保护对同一个键的并发修改,这个配置不会转发请求,也不会协调各节点。 + - `disabled`:只有一个 ThingsBoard 节点时使用。有多个节点时,只有在能接受“不同节点同时 + 修改同一设备或实体可能导致结果不一致”的情况下才使用。 + - `sticky-routing`:有多个节点,并且你的部署能保证同一设备或实体的所有更新和删除都由同一 + 节点处理时使用。这种路由需要你自己配置并验证。 + +### 把配置传给 ThingsBoard + +配置项名称中含有点、下划线和连字符,因此请把它们写成一个 JSON 对象,通过环境变量 +`SPRING_APPLICATION_JSON` 传入。 + +**Docker(Compose)**:在 ThingsBoard 服务中加入该环境变量和 jar 目录,其他配置保持不变。 +挂载会替换镜像中的扩展目录,因此如果还用到其他扩展 jar,请一并放进同一个 `lib/` 目录: + +```yaml +services: + thingsboard: + environment: + SPRING_APPLICATION_JSON: >- + {"iotdb.host": "iotdb", "iotdb.port": "6667", + "iotdb.username": "root", "iotdb.password": "root", + "iotdb.database": "thingsboard", "iotdb.session-pool-size": "8", + "iotdb.schema.bootstrap": "true", + "database.ts.type": "iotdb-table", + "iotdb.ts.experimental-raw-only": "true", + "database.ts_latest.type": "iotdb-table", + "iotdb.ts_latest.cluster_mode": "disabled", + "database.attributes.type": "iotdb-table", + "iotdb.attributes.cluster_mode": "disabled"} + volumes: + - ./lib:/usr/share/thingsboard/extensions:ro +``` + +用 `docker compose up -d` 使配置生效,它会重新创建容器。只重启原有容器不会读取新的环境变量。 + +**Linux 安装包(`.deb`)**:安装包不会创建扩展目录。请先创建目录并复制 jar: -示例显式写出了连接配置的默认值。在非 Docker 环境中,`iotdb.host` 默认为 -`127.0.0.1`;其他默认值为端口 `6667`、用户名/密码 `root`/`root`、数据库 -`thingsboard`、会话池大小 `8`,并默认启用表结构引导。 +```bash +sudo mkdir -p /usr/share/thingsboard/extensions +sudo cp lib/*.jar /usr/share/thingsboard/extensions/ +``` -启用最新值或属性时,必须显式设置对应的 `cluster_mode`: +然后把配置加入 `/etc/thingsboard/conf/thingsboard.conf`,ThingsBoard 服务启动时会读取这个 +文件。示例假设 IoTDB 与 ThingsBoard 在同一台主机上,否则请修改 `iotdb.host`: -- `sticky-routing`:把同一 identity 的写入固定路由到一个节点。 -- `disabled`:单节点部署,或明确接受尽力而为的收敛。 +```bash +export SPRING_APPLICATION_JSON='{ + "iotdb.host": "127.0.0.1", "iotdb.port": "6667", + "iotdb.username": "root", "iotdb.password": "root", + "iotdb.database": "thingsboard", "iotdb.session-pool-size": "8", + "iotdb.schema.bootstrap": "true", + "database.ts.type": "iotdb-table", + "iotdb.ts.experimental-raw-only": "true", + "database.ts_latest.type": "iotdb-table", + "iotdb.ts_latest.cluster_mode": "disabled", + "database.attributes.type": "iotdb-table", + "iotdb.attributes.cluster_mode": "disabled" +}' +``` -其他取值(包括空值)都会让启动停止并给出明确错误。 +最后用 `sudo systemctl restart thingsboard` 重启服务。只在自己的终端里设置这个变量,对服务 +不起作用。 ## 4. 运行与验证 -1. 部署 jar 并完成配置后,重启 ThingsBoard。 -2. 在日志中确认出现 `IoTDB Table Mode session pool initialized`。若启用了表结构引导, - 还应出现 `IoTDB Table Mode schema bootstrap complete`。 -3. 向一台 ThingsBoard 测试设备发送时序和属性数据,然后在设备的“最新遥测”和“属性”页面 - 确认这些值。 +1. 启动 ThingsBoard;如果已在运行,则重启。 +2. 在 ThingsBoard 日志中确认出现 `IoTDB Table Mode session pool initialized`。启用表结构 + 自动创建时,随后还会出现 `IoTDB Table Mode schema bootstrap complete`。把实体属性存入 + IoTDB 时,出现提到 `jpaAttributeDao` 的 `WARN` 属于正常现象。 +3. 向一台 ThingsBoard 设备发送测试遥测数据和属性,在设备的“最新遥测”和“属性”页面记下 + 设备 ID、键、值和时间。 ![通过 IoTDB 表模型后端存储的 ThingsBoard 最新遥测](/img/thingsboard-table-telemetry.png) ![通过 IoTDB 表模型后端存储的 ThingsBoard 客户端属性](/img/thingsboard-table-attributes.png) -4. 在 IoTDB 中查询同一批数据: +4. 在 IoTDB 中查询同一台设备,核对键、值和时间。把 `` 换成设备 ID: ```sql USE thingsboard; -SELECT * +SELECT time, key, bool_v, long_v, double_v, str_v, json_v FROM telemetry +WHERE entity_id = '' ORDER BY time DESC LIMIT 10; -SELECT * +SELECT time, attribute_scope, key, bool_v, long_v, double_v, str_v, json_v FROM entity_attributes +WHERE entity_id = '' ORDER BY time DESC LIMIT 10; ``` ## 5. 已知限制 -- 已验证版本为 IoTDB 2.0.11 和 ThingsBoard 4.3.1.2。其他 IoTDB 2.x 或 ThingsBoard - 版本需要单独验证。 -- 属性写入和最新值覆盖层只在单个 JVM 内收敛。多写入者部署必须使用固定路由,或明确接受 - 尽力而为的收敛。 -- 最新值路径从时序表派生,并使用一个小型覆盖层承接仅写最新值和仅删最新值的路径。 -- 保留策略使用 IoTDB 表级 TTL;其与 ThingsBoard 保留设置的对应关系参见模块的 +- 已验证的版本为 IoTDB 2.0.11 和 ThingsBoard 4.3.1.2。其他 IoTDB 2.x 或 ThingsBoard 版本 + 需要另行验证。 +- 有多个 ThingsBoard 节点时,最新遥测值和实体属性能否保持一致,取决于第 3 节 `cluster_mode` + 中所说的路由。 +- 数据保留依靠 IoTDB 表级 TTL,它与 ThingsBoard 保留设置的对应关系参见模块的 [用户指南](https://github.com/apache/iotdb-extras/blob/v2.0.11/iotdb-thingsboard-table/docs/user-guide.md)。 -设计、源码构建、迁移和编译接口验证细节保留在 -[`iotdb-thingsboard-table` 模块](https://github.com/apache/iotdb-extras/tree/v2.0.11/iotdb-thingsboard-table) -中。 +设计说明、源码构建和已有数据迁移,参见 +[`iotdb-thingsboard-table` 模块](https://github.com/apache/iotdb-extras/tree/v2.0.11/iotdb-thingsboard-table)。 diff --git a/src/zh/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md b/src/zh/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md index a8fada336..b17aef0e8 100644 --- a/src/zh/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md +++ b/src/zh/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md @@ -21,8 +21,9 @@ # ThingsBoard -`iotdb-thingsboard-table` 模块让原版 ThingsBoard 可以把历史时序、最新值时序和实体属性 -存入 IoTDB 表模型。本页按操作者真正使用它的顺序说明:下载、安装、配置、运行与验证。 +`iotdb-thingsboard-table` 模块可以让标准版 ThingsBoard 把历史遥测数据、最新遥测值和实体 +属性存入 IoTDB 表模型。三类数据可以分别迁移到 IoTDB。本页依次介绍下载、安装、配置和 +验证。 这套集成独立于较早的 [ThingsBoard 树模型集成](../../latest/Ecosystem-Integration/Thingsboard.md),后者需要使用 @@ -30,7 +31,7 @@ IoTDB 适配版 ThingsBoard。 ## 1. 下载 -已经验证的版本组合如下: +经过验证的版本组合如下: | 组件 | 版本 | | --- | --- | @@ -38,11 +39,11 @@ IoTDB 适配版 ThingsBoard。 | ThingsBoard | 4.3.1.2 | | JDK | 17 或更高版本 | -本模块已随 Apache IoTDB Extras 2.0.11 正式发布。模块 jar 作为附件放在 -[2.0.11 Release](https://github.com/apache/iotdb-extras/releases/tag/v2.0.11) 中, -并附有签名,可用 [IoTDB KEYS](https://dist.apache.org/repos/dist/release/iotdb/KEYS) -校验。该 jar 不包含 IoTDB 客户端,因此还需从 Maven Central 下载它依赖的 9 个客户端 -jar,放入同一个 `lib/` 目录: +本模块随 Apache IoTDB Extras 2.0.11 正式发布。模块 jar 及其签名作为附件放在 +[2.0.11 Release](https://github.com/apache/iotdb-extras/releases/tag/v2.0.11) 中,签名可用 +[IoTDB KEYS](https://dist.apache.org/repos/dist/release/iotdb/KEYS) 校验。模块 jar 不包含 +IoTDB 客户端,因此还需要 9 个客户端 jar。下面的命令把这 10 个 jar 下载到 `lib/` 目录, +并校验模块 jar 的签名: ```bash mkdir -p lib @@ -67,49 +68,47 @@ for path in \ done ``` -完成后 `lib/` 中即为需要部署的 10 个 jar。也可以用源码包 +IoTDB 客户端用到的其他库(`lz4-java`、`zstd-jni`、`snappy-java` 和 `antlr4-runtime`) +ThingsBoard 已经自带,不要再另外放入。也可以用源码包 `apache-iotdb-extras-2.0.11-source-release.zip` 自行构建本模块。 ## 2. 安装 -必须先安装 ThingsBoard,再启用 IoTDB 选择器。ThingsBoard 安装器会解析由内置后端提供的 -`TsDatabaseSchemaService`;若安装阶段已经启用本模块,安装会失败。 +请先安装 ThingsBoard,再启用本模块。ThingsBoard 安装程序依赖它自带的存储后端;如果第 3 +节的 IoTDB 配置已经生效,安装会中止并报 +`No qualifying bean of type 'org.thingsboard.server.service.install.TsDatabaseSchemaService' available`。 1. 安装并启动 IoTDB,参见 [IoTDB 快速上手](../QuickStart/QuickStart.md)。 - 若 IoTDB 2.0.11 与 ThingsBoard 分别运行在容器中,请把两者放入同一 Docker 网络,并让 - IoTDB 公布网络内可达的服务名。`2.0.11-standalone` 镜像默认把这些地址绑定到 - `127.0.0.1`。假设 IoTDB 服务名为 `iotdb`,请设置: + 如果 IoTDB 和 ThingsBoard 分别运行在不同容器中,请把它们放在同一个 Docker 网络里,并让 + IoTDB 对外公布自己的服务名。否则 `2.0.11-standalone` 镜像会把这些地址绑定到 + `127.0.0.1`,ThingsBoard 容器无法访问。假设 IoTDB 的服务名为 `iotdb`,请设置: ```yaml - cn_seed_config_node: iotdb:10710 - dn_seed_config_node: iotdb:10710 - cn_internal_address: iotdb - dn_internal_address: iotdb - dn_rpc_address: iotdb - ``` - -2. 按常规方式安装 ThingsBoard,此时不要设置第 3 节的任何属性。 -3. 将 `lib/` 中的 10 个 jar 放到 ThingsBoard classpath 上。即使尚未设置任何选择器, - 这 10 个 jar 也缺一不可:只放模块 jar 时,ThingsBoard 会启动失败,报 - `NoClassDefFoundError: org/apache/iotdb/isession/pool/ITableSessionPool`。 - -对于 ThingsBoard Docker 镜像,可将这些 jar 挂载或复制到 -`/usr/share/thingsboard/extensions`。镜像使用的 `PropertiesLauncher` 已通过 -`LOADER_PATH` 包含该目录。其他安装方式可能采用不同的 classpath 扩展机制。 - -部署目录必须正好包含 10 个 jar:模块本身,以及 `iotdb-session`、`isession`、 -`service-rpc`、`iotdb-thrift`、`iotdb-thrift-commons`、`libthrift`、`tsfile`、 -`common` 和 `xz`。IoTDB 客户端用到的其余库(`lz4-java`、`zstd-jni`、`snappy-java` -和 `antlr4-runtime`)ThingsBoard 已经自带,不要再放入其他副本。 + cn_seed_config_node: iotdb:10710 + dn_seed_config_node: iotdb:10710 + cn_internal_address: iotdb + dn_internal_address: iotdb + dn_rpc_address: iotdb + ``` + +2. 按常规方式安装 ThingsBoard,此时不要加入第 3 节的任何配置。 +3. 把 `lib/` 中的 10 个 jar 放入 ThingsBoard 的扩展目录 `/usr/share/thingsboard/extensions`。 + Docker 镜像和 Linux `.deb` 安装包都会加载该目录中的所有 jar,具体做法见第 3 节。该目录中已有的 + 其他 jar 请保留。 + + 这 10 个 jar 缺一不可,即使还没有启用本模块也是如此。只放模块 jar 时,ThingsBoard 无法 + 启动,并报 `NoClassDefFoundError: org/apache/iotdb/isession/pool/ITableSessionPool`。 + 放齐 10 个 jar 但不加下面的配置时,ThingsBoard 照常使用自己的存储,本模块不起作用。 ## 3. 配置 -按需要启用要迁移到 IoTDB 的存储路径。下面的单节点示例同时启用三条路径: +### 配置项 + +下面这组配置把三类数据都存入单个 IoTDB 节点: ```properties -# IoTDB 连接 -# Docker 服务名;仅当两个进程共享同一主机时使用 127.0.0.1 +# IoTDB connection iotdb.host=iotdb iotdb.port=6667 iotdb.username=root @@ -118,71 +117,139 @@ iotdb.database=thingsboard iotdb.session-pool-size=8 iotdb.schema.bootstrap=true -# 历史时序 +# Historical telemetry database.ts.type=iotdb-table iotdb.ts.experimental-raw-only=true -# 最新值时序 +# Latest telemetry values database.ts_latest.type=iotdb-table iotdb.ts_latest.cluster_mode=disabled -# 实体属性 +# Entity attributes database.attributes.type=iotdb-table iotdb.attributes.cluster_mode=disabled ``` -历史时序和最新值时序使用两个独立选择器。若遗漏 `database.ts_latest.type`,即使历史数据 -已经存入 IoTDB,最新值仍会留在 ThingsBoard 原来的后端中。 +各配置项的含义: + +- **连接**:`iotdb.host` 是 ThingsBoard 访问 IoTDB 的地址。在 Docker 中填 IoTDB 的服务名, + 其他环境填主机名或 IP。只有两者直接运行在同一台主机上(不在容器中),或共用同一个容器 + 网络命名空间时,才使用 `127.0.0.1`。未设置的项使用默认值:地址 `127.0.0.1`、端口 `6667`、用户名和密码 + 均为 `root`、数据库 `thingsboard`、会话池大小 `8`。`iotdb.schema.bootstrap=true`(默认) + 时,模块会在首次启动时自动创建数据库和表。 +- **历史遥测数据**:同时设置 `database.ts.type=iotdb-table` 和 + `iotdb.ts.experimental-raw-only=true` 后存入 IoTDB。后者是启用这部分存储的必需开关,名字 + 为兼容早期版本而保留;开启后,历史遥测数据的写入、查询、删除和聚合都由本模块处理。 +- **最新遥测值**:设置 `database.ts_latest.type=iotdb-table` 后存入 IoTDB。最新值从 IoTDB + 中的历史遥测数据读取,所以还需要上面两项历史数据配置。不设置这一项时,即使历史数据已在 + IoTDB 中,最新值仍留在 ThingsBoard 原有的存储里。 +- **实体属性**:设置 `database.attributes.type=iotdb-table` 后存入 IoTDB,与前两类数据互不 + 依赖。ThingsBoard 本身没有切换属性存储的配置项,因此本模块会在启动时移除 ThingsBoard + 自带的属性存储组件,并输出一条提到 `jpaAttributeDao` 的 `WARN` 日志。 +- **`cluster_mode`**:把最新遥测值或实体属性存入 IoTDB 时必须设置;填其他值(包括留空) + 都会导致启动失败。它用于说明多个 ThingsBoard 节点之间如何分担写入。本模块只在单个 + ThingsBoard 进程内保护对同一个键的并发修改,这个配置不会转发请求,也不会协调各节点。 + - `disabled`:只有一个 ThingsBoard 节点时使用。有多个节点时,只有在能接受“不同节点同时 + 修改同一设备或实体可能导致结果不一致”的情况下才使用。 + - `sticky-routing`:有多个节点,并且你的部署能保证同一设备或实体的所有更新和删除都由同一 + 节点处理时使用。这种路由需要你自己配置并验证。 + +### 把配置传给 ThingsBoard + +配置项名称中含有点、下划线和连字符,因此请把它们写成一个 JSON 对象,通过环境变量 +`SPRING_APPLICATION_JSON` 传入。 + +**Docker(Compose)**:在 ThingsBoard 服务中加入该环境变量和 jar 目录,其他配置保持不变。 +挂载会替换镜像中的扩展目录,因此如果还用到其他扩展 jar,请一并放进同一个 `lib/` 目录: + +```yaml +services: + thingsboard: + environment: + SPRING_APPLICATION_JSON: >- + {"iotdb.host": "iotdb", "iotdb.port": "6667", + "iotdb.username": "root", "iotdb.password": "root", + "iotdb.database": "thingsboard", "iotdb.session-pool-size": "8", + "iotdb.schema.bootstrap": "true", + "database.ts.type": "iotdb-table", + "iotdb.ts.experimental-raw-only": "true", + "database.ts_latest.type": "iotdb-table", + "iotdb.ts_latest.cluster_mode": "disabled", + "database.attributes.type": "iotdb-table", + "iotdb.attributes.cluster_mode": "disabled"} + volumes: + - ./lib:/usr/share/thingsboard/extensions:ro +``` + +用 `docker compose up -d` 使配置生效,它会重新创建容器。只重启原有容器不会读取新的环境变量。 + +**Linux 安装包(`.deb`)**:安装包不会创建扩展目录。请先创建目录并复制 jar: -示例显式写出了连接配置的默认值。在非 Docker 环境中,`iotdb.host` 默认为 -`127.0.0.1`;其他默认值为端口 `6667`、用户名/密码 `root`/`root`、数据库 -`thingsboard`、会话池大小 `8`,并默认启用表结构引导。 +```bash +sudo mkdir -p /usr/share/thingsboard/extensions +sudo cp lib/*.jar /usr/share/thingsboard/extensions/ +``` -启用最新值或属性时,必须显式设置对应的 `cluster_mode`: +然后把配置加入 `/etc/thingsboard/conf/thingsboard.conf`,ThingsBoard 服务启动时会读取这个 +文件。示例假设 IoTDB 与 ThingsBoard 在同一台主机上,否则请修改 `iotdb.host`: -- `sticky-routing`:把同一 identity 的写入固定路由到一个节点。 -- `disabled`:单节点部署,或明确接受尽力而为的收敛。 +```bash +export SPRING_APPLICATION_JSON='{ + "iotdb.host": "127.0.0.1", "iotdb.port": "6667", + "iotdb.username": "root", "iotdb.password": "root", + "iotdb.database": "thingsboard", "iotdb.session-pool-size": "8", + "iotdb.schema.bootstrap": "true", + "database.ts.type": "iotdb-table", + "iotdb.ts.experimental-raw-only": "true", + "database.ts_latest.type": "iotdb-table", + "iotdb.ts_latest.cluster_mode": "disabled", + "database.attributes.type": "iotdb-table", + "iotdb.attributes.cluster_mode": "disabled" +}' +``` -其他取值(包括空值)都会让启动停止并给出明确错误。 +最后用 `sudo systemctl restart thingsboard` 重启服务。只在自己的终端里设置这个变量,对服务 +不起作用。 ## 4. 运行与验证 -1. 部署 jar 并完成配置后,重启 ThingsBoard。 -2. 在日志中确认出现 `IoTDB Table Mode session pool initialized`。若启用了表结构引导, - 还应出现 `IoTDB Table Mode schema bootstrap complete`。 -3. 向一台 ThingsBoard 测试设备发送时序和属性数据,然后在设备的“最新遥测”和“属性”页面 - 确认这些值。 +1. 启动 ThingsBoard;如果已在运行,则重启。 +2. 在 ThingsBoard 日志中确认出现 `IoTDB Table Mode session pool initialized`。启用表结构 + 自动创建时,随后还会出现 `IoTDB Table Mode schema bootstrap complete`。把实体属性存入 + IoTDB 时,出现提到 `jpaAttributeDao` 的 `WARN` 属于正常现象。 +3. 向一台 ThingsBoard 设备发送测试遥测数据和属性,在设备的“最新遥测”和“属性”页面记下 + 设备 ID、键、值和时间。 ![通过 IoTDB 表模型后端存储的 ThingsBoard 最新遥测](/img/thingsboard-table-telemetry.png) ![通过 IoTDB 表模型后端存储的 ThingsBoard 客户端属性](/img/thingsboard-table-attributes.png) -4. 在 IoTDB 中查询同一批数据: +4. 在 IoTDB 中查询同一台设备,核对键、值和时间。把 `` 换成设备 ID: ```sql USE thingsboard; -SELECT * +SELECT time, key, bool_v, long_v, double_v, str_v, json_v FROM telemetry +WHERE entity_id = '' ORDER BY time DESC LIMIT 10; -SELECT * +SELECT time, attribute_scope, key, bool_v, long_v, double_v, str_v, json_v FROM entity_attributes +WHERE entity_id = '' ORDER BY time DESC LIMIT 10; ``` ## 5. 已知限制 -- 已验证版本为 IoTDB 2.0.11 和 ThingsBoard 4.3.1.2。其他 IoTDB 2.x 或 ThingsBoard - 版本需要单独验证。 -- 属性写入和最新值覆盖层只在单个 JVM 内收敛。多写入者部署必须使用固定路由,或明确接受 - 尽力而为的收敛。 -- 最新值路径从时序表派生,并使用一个小型覆盖层承接仅写最新值和仅删最新值的路径。 -- 保留策略使用 IoTDB 表级 TTL;其与 ThingsBoard 保留设置的对应关系参见模块的 +- 已验证的版本为 IoTDB 2.0.11 和 ThingsBoard 4.3.1.2。其他 IoTDB 2.x 或 ThingsBoard 版本 + 需要另行验证。 +- 有多个 ThingsBoard 节点时,最新遥测值和实体属性能否保持一致,取决于第 3 节 `cluster_mode` + 中所说的路由。 +- 数据保留依靠 IoTDB 表级 TTL,它与 ThingsBoard 保留设置的对应关系参见模块的 [用户指南](https://github.com/apache/iotdb-extras/blob/v2.0.11/iotdb-thingsboard-table/docs/user-guide.md)。 -设计、源码构建、迁移和编译接口验证细节保留在 -[`iotdb-thingsboard-table` 模块](https://github.com/apache/iotdb-extras/tree/v2.0.11/iotdb-thingsboard-table) -中。 +设计说明、源码构建和已有数据迁移,参见 +[`iotdb-thingsboard-table` 模块](https://github.com/apache/iotdb-extras/tree/v2.0.11/iotdb-thingsboard-table)。 From f650b57f51eeac834ffcfd0c6940f64b0f7ca600 Mon Sep 17 00:00:00 2001 From: Zihan Dai <1436286758@qq.com> Date: Fri, 23 Oct 2026 18:17:52 +1100 Subject: [PATCH 5/5] docs: split the ThingsBoard configuration by installation method Give the configuration section one subsection each for Docker, the Linux package, choosing what IoTDB stores, and one or several ThingsBoard nodes, so the JSON appears only where it is used. Clarify that a node here is a ThingsBoard node, that sticky-routing does not set up routing and that disabled does not turn storage off, and that an existing SPRING_APPLICATION_JSON or extensions mount should be merged rather than duplicated. Install the jars on the .deb with install -m 0644. Signed-off-by: Zihan Dai <1436286758@qq.com> --- .../Ecosystem-Integration/Thingsboard.md | 338 ++++++++++-------- .../Ecosystem-Integration/Thingsboard.md | 338 ++++++++++-------- .../Ecosystem-Integration/Thingsboard.md | 318 +++++++++------- .../Ecosystem-Integration/Thingsboard.md | 318 +++++++++------- 4 files changed, 766 insertions(+), 546 deletions(-) diff --git a/src/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md b/src/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md index 41ac8cd0a..a153e244d 100644 --- a/src/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md +++ b/src/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md @@ -21,11 +21,10 @@ # ThingsBoard -The `iotdb-thingsboard-table` module lets a standard ThingsBoard deployment -store its historical telemetry, latest telemetry values and entity attributes in -the IoTDB table model. Each of the three kinds of data can be moved to IoTDB on -its own. This page walks through download, installation, configuration and -verification. +The `iotdb-thingsboard-table` module lets ThingsBoard store historical +telemetry, latest telemetry, and entity attributes in the IoTDB Table Model. +It runs inside an existing ThingsBoard installation and does not require a +custom ThingsBoard build. This integration is independent of the earlier [ThingsBoard Tree Model integration](../../Tree/Ecosystem-Integration/Thingsboard.md), @@ -41,14 +40,13 @@ The tested combination is: | ThingsBoard | 4.3.1.2 | | JDK | 17 or later | -The module is part of the Apache IoTDB Extras 2.0.11 release. Its jar is -attached to the +The module is part of Apache IoTDB Extras 2.0.11. Download its jar from the [2.0.11 release](https://github.com/apache/iotdb-extras/releases/tag/v2.0.11) -together with a signature, which can be checked against the -[IoTDB KEYS](https://dist.apache.org/repos/dist/release/iotdb/KEYS) file. The -module jar does not include the IoTDB client, so nine client jars are needed as -well. The following commands download all ten jars into a `lib/` directory and -verify the module's signature: +and verify its signature against the +[IoTDB KEYS](https://dist.apache.org/repos/dist/release/iotdb/KEYS) file. +The module jar does not include the IoTDB client. The commands below also +download the nine required client dependency jars from Maven Central into +the same `lib/` directory. ```bash mkdir -p lib @@ -73,24 +71,22 @@ for path in \ done ``` -The other libraries the IoTDB client uses (`lz4-java`, `zstd-jni`, -`snappy-java` and `antlr4-runtime`) are already part of ThingsBoard; do not add -copies of them. The module can also be built from the -`apache-iotdb-extras-2.0.11-source-release.zip` source package. +Deploy all ten jars downloaded to `lib/`. You can also build the module from +the `apache-iotdb-extras-2.0.11-source-release.zip` source package. ## 2. Install -Install ThingsBoard before you enable the module. The ThingsBoard installer -needs ThingsBoard's own storage backends; if the IoTDB settings from section 3 -are already present, the installer stops with -`No qualifying bean of type 'org.thingsboard.server.service.install.TsDatabaseSchemaService' available`. +Complete the normal ThingsBoard installation before enabling IoTDB storage. +Enabling this module while running the ThingsBoard installer makes the +installation fail with a missing `TsDatabaseSchemaService` error. 1. Install and start IoTDB. See [IoTDB QuickStart](../QuickStart/QuickStart.md). - When IoTDB and ThingsBoard run in separate containers, put them on the same - Docker network and make IoTDB announce its service name. Otherwise the - `2.0.11-standalone` image binds these endpoints to `127.0.0.1`, which the - ThingsBoard container cannot reach. For an IoTDB service named `iotdb`, set: + If IoTDB and ThingsBoard run in separate containers, connect them to the + same Docker network and configure IoTDB to use a service name that + ThingsBoard can reach. Otherwise the `apache/iotdb:2.0.11-standalone` image + uses `127.0.0.1` for these addresses. For an IoTDB service named `iotdb`, + set the following environment values on the IoTDB container: ```yaml cn_seed_config_node: iotdb:10710 @@ -100,131 +96,107 @@ are already present, the installer stops with dn_rpc_address: iotdb ``` -2. Install ThingsBoard as usual, without any of the settings from section 3. -3. Put the ten jars from `lib/` into ThingsBoard's extensions directory, - `/usr/share/thingsboard/extensions`. Both the Docker image and the Linux - `.deb` package load every jar in that directory. Section 3 shows how to do this - for each kind of installation. Keep any other jars that are already there. +2. Install ThingsBoard using its normal installation procedure and storage + settings. Do not apply the IoTDB configuration from section 3 to the + installer. - All ten jars are required, even before the module is enabled. With the - module jar alone, ThingsBoard does not start and reports - `NoClassDefFoundError: org/apache/iotdb/isession/pool/ITableSessionPool`. - With all ten jars and none of the settings below, ThingsBoard runs on its - own storage and the module does nothing. +3. Make all ten downloaded jars available to ThingsBoard in its extensions + directory, `/usr/share/thingsboard/extensions`. -## 3. Configure + For the Docker image, mount or copy them to that directory. Section 3.1 + shows a Compose mount for the `lib/` directory. + + For the Linux `.deb` package, the package does not create the directory. + After the installation has finished, run these commands from the directory + that contains `lib/`: -### Settings + ```bash + sudo install -d /usr/share/thingsboard/extensions + sudo install -m 0644 lib/*.jar /usr/share/thingsboard/extensions/ + ``` -The following settings move all three kinds of data to a single IoTDB node: +All ten jars are required even before IoTDB storage is enabled. If only the +module jar is present, ThingsBoard fails to start with: -```properties -# IoTDB connection -iotdb.host=iotdb -iotdb.port=6667 -iotdb.username=root -iotdb.password=root -iotdb.database=thingsboard -iotdb.session-pool-size=8 -iotdb.schema.bootstrap=true +```text +NoClassDefFoundError: org/apache/iotdb/isession/pool/ITableSessionPool +``` -# Historical telemetry -database.ts.type=iotdb-table -iotdb.ts.experimental-raw-only=true +With all ten jars present and no IoTDB storage settings enabled, ThingsBoard +starts normally and continues to use its existing storage backends. +ThingsBoard 4.3.1.2 already includes `lz4-java`, `zstd-jni`, `snappy-java`, +and `antlr4-runtime`; do not add another copy of these libraries. -# Latest telemetry values -database.ts_latest.type=iotdb-table -iotdb.ts_latest.cluster_mode=disabled +## 3. Configure -# Entity attributes -database.attributes.type=iotdb-table -iotdb.attributes.cluster_mode=disabled -``` +Pass the configuration to the ThingsBoard process through the +`SPRING_APPLICATION_JSON` environment variable. This keeps the property names +exactly as shown, including underscores and hyphens. Use the instructions for +your installation method below. Both examples enable all three storage paths +for one ThingsBoard node. -What they mean: - -- **Connection.** `iotdb.host` is the address ThingsBoard uses to reach IoTDB: - the service name in Docker, or a host name or IP address otherwise. Use - `127.0.0.1` only when both run directly on the same host, outside containers, - or share one container network namespace. If a setting is left out, the defaults are host - `127.0.0.1`, port `6667`, user `root` with password `root`, database - `thingsboard`, and a pool of `8` sessions. With `iotdb.schema.bootstrap=true` - (the default), the module creates the database and its tables on first start. -- **Historical telemetry** moves to IoTDB when both `database.ts.type=iotdb-table` - and `iotdb.ts.experimental-raw-only=true` are set. The second setting is the - required switch that turns this storage on. Its name is kept for - compatibility with earlier builds; with it set, the module stores, reads, - deletes and aggregates historical telemetry. -- **Latest telemetry values** move to IoTDB with - `database.ts_latest.type=iotdb-table`. Latest values are read from the - historical telemetry kept in IoTDB, so this also needs the two historical - settings above. Without it, latest values stay in ThingsBoard's own storage - even when historical telemetry is in IoTDB. -- **Entity attributes** move to IoTDB with `database.attributes.type=iotdb-table`, - independently of the other two. ThingsBoard has no setting of its own for - this, so the module removes ThingsBoard's attribute storage bean at startup - and logs a `WARN` that names `jpaAttributeDao`. -- **`cluster_mode`** must be set when latest values or attributes are moved to - IoTDB; any other value, including an empty one, stops startup with an - error. It states how ThingsBoard nodes share writes. The module protects - concurrent changes to the same key only inside one ThingsBoard process; the - setting does not route requests or coordinate nodes. - - `disabled`: use this with a single ThingsBoard node. With several nodes, - use it only if you accept that concurrent updates to the same device or - entity from different nodes can leave inconsistent results. - - `sticky-routing`: use this with several nodes when your deployment makes - sure that all updates and deletes for one device or entity are handled by - the same node. Configure and verify that routing yourself. - -### Passing the settings to ThingsBoard - -The setting names contain dots, underscores and hyphens, so pass them as one -JSON object in the `SPRING_APPLICATION_JSON` environment variable. - -**Docker (Compose).** Add the environment variable and the jar directory to -the ThingsBoard service, keeping its other settings. The bind mount replaces the -image's extensions directory, so put any other extension jars you use into the -same `lib/` directory: +Set `iotdb.host`, the credentials, and the database name for your deployment. +If `SPRING_APPLICATION_JSON` already contains other settings, merge these keys +into its existing JSON object. + +### 3.1 Docker + +In your existing Compose file, merge the following fragment into the +ThingsBoard service. The example names that service `thingsboard`; use your +existing service name if it differs. Keep its other environment settings, +volumes, ports, and network configuration. The `lib/` directory from section 1 +must be beside the Compose file, and the IoTDB service, named `iotdb`, must be +on the same Docker network as ThingsBoard. ```yaml services: thingsboard: - environment: - SPRING_APPLICATION_JSON: >- - {"iotdb.host": "iotdb", "iotdb.port": "6667", - "iotdb.username": "root", "iotdb.password": "root", - "iotdb.database": "thingsboard", "iotdb.session-pool-size": "8", - "iotdb.schema.bootstrap": "true", - "database.ts.type": "iotdb-table", - "iotdb.ts.experimental-raw-only": "true", - "database.ts_latest.type": "iotdb-table", - "iotdb.ts_latest.cluster_mode": "disabled", - "database.attributes.type": "iotdb-table", - "iotdb.attributes.cluster_mode": "disabled"} + image: thingsboard/tb-node:4.3.1.2 volumes: - ./lib:/usr/share/thingsboard/extensions:ro + environment: + SPRING_APPLICATION_JSON: >- + { + "iotdb.host": "iotdb", + "iotdb.port": "6667", + "iotdb.username": "root", + "iotdb.password": "root", + "iotdb.database": "thingsboard", + "iotdb.session-pool-size": "8", + "iotdb.schema.bootstrap": "true", + "database.ts.type": "iotdb-table", + "iotdb.ts.experimental-raw-only": "true", + "database.ts_latest.type": "iotdb-table", + "iotdb.ts_latest.cluster_mode": "disabled", + "database.attributes.type": "iotdb-table", + "iotdb.attributes.cluster_mode": "disabled" + } ``` -Apply it with `docker compose up -d`, which recreates the container. Restarting -the old container does not pick up a changed environment. +This is a fragment for an already installed ThingsBoard deployment, not a +complete Compose file. Apply it to the running service, not to the installer. +If you already mount the extensions directory, put the ten jars in that +directory instead of adding a second mount at the same location. -**Linux package (`.deb`).** The package does not create the extensions -directory. Create it and copy the jars: +### 3.2 Linux package -```bash -sudo mkdir -p /usr/share/thingsboard/extensions -sudo cp lib/*.jar /usr/share/thingsboard/extensions/ -``` +For the Linux `.deb` package, add the configuration below to +`/etc/thingsboard/conf/thingsboard.conf`, alongside its existing settings. The +ThingsBoard service reads this shell file at startup, so include the `export` +and the surrounding single quotes. Exporting the variable only in your +terminal does not configure the service. -Then add the settings to `/etc/thingsboard/conf/thingsboard.conf`, which the -ThingsBoard service reads at startup. The example assumes IoTDB on the same -host; change `iotdb.host` otherwise: +The example assumes IoTDB is reachable from the ThingsBoard service at +`127.0.0.1`. Change the host if your deployment uses another address. ```bash export SPRING_APPLICATION_JSON='{ - "iotdb.host": "127.0.0.1", "iotdb.port": "6667", - "iotdb.username": "root", "iotdb.password": "root", - "iotdb.database": "thingsboard", "iotdb.session-pool-size": "8", + "iotdb.host": "127.0.0.1", + "iotdb.port": "6667", + "iotdb.username": "root", + "iotdb.password": "root", + "iotdb.database": "thingsboard", + "iotdb.session-pool-size": "8", "iotdb.schema.bootstrap": "true", "database.ts.type": "iotdb-table", "iotdb.ts.experimental-raw-only": "true", @@ -235,26 +207,97 @@ export SPRING_APPLICATION_JSON='{ }' ``` -Restart the service with `sudo systemctl restart thingsboard`. Setting the -variable in your own shell has no effect on the service. +### 3.3 Choose what IoTDB stores + +Keep the settings for the storage paths you want to enable: + +- **Historical telemetry:** set both `database.ts.type=iotdb-table` and + `iotdb.ts.experimental-raw-only=true`. +- **Latest telemetry:** enable historical telemetry as above, then also set + `database.ts_latest.type=iotdb-table` and + `iotdb.ts_latest.cluster_mode`. Latest telemetry depends on the historical + telemetry backend because it reads from the IoTDB telemetry table. +- **Entity attributes:** set `database.attributes.type=iotdb-table` and + `iotdb.attributes.cluster_mode`. Attributes do not require either telemetry + backend to use IoTDB. + +Use the `cluster_mode` values described in section 3.4. Omit the settings for +storage paths you do not want to enable. If you enable historical telemetry +but omit `database.ts_latest.type`, latest telemetry continues to use the +original ThingsBoard backend. + +`iotdb.ts.experimental-raw-only` is the required opt-in switch for the +historical telemetry backend. Its name is retained for compatibility: enabling +it supports writes, raw reads, deletes, and time-bucketed aggregation. + +The default connection settings are host `127.0.0.1`, port `6667`, +username/password `root`/`root`, database `thingsboard`, and session-pool size +`8`. Schema initialization is enabled by default +(`iotdb.schema.bootstrap=true`) and creates the database and its tables on +first start. The Docker example overrides the host with `iotdb`; running in +Docker does not change the module's default host. + +### 3.4 One or several ThingsBoard nodes + +When latest telemetry or attributes use IoTDB, explicitly set the corresponding +`cluster_mode` property: + +- **One ThingsBoard node:** use `disabled`, as in the examples. +- **Several ThingsBoard nodes:** use `sticky-routing` only when your deployment + sends every operation that updates or deletes each device's or entity's + latest telemetry or attributes to the same ThingsBoard node. Configure and + verify that routing separately; setting this property does not establish it. +- **Several nodes without that routing guarantee:** use `disabled` only if + you accept that concurrent updates from different nodes to the same + latest-value key or attribute may leave inconsistent results. The module + does not coordinate these writes across nodes. + +Here, a node means a ThingsBoard node, not an IoTDB node. `disabled` does not +disable latest-telemetry or attribute storage. Any other value, including an +omitted or empty value, prevents startup when the corresponding backend is +enabled. ## 4. Run and verify -1. Start ThingsBoard, or restart it if it is already running. -2. Check the ThingsBoard log for `IoTDB Table Mode session pool initialized`. - With schema bootstrap enabled, `IoTDB Table Mode schema bootstrap complete` - follows. When attributes are moved, the `WARN` about `jpaAttributeDao` is - expected. -3. Send test telemetry and attributes to a ThingsBoard device, and note the - device ID, the keys, the values and their times as shown on the device's - **Latest telemetry** and **Attributes** tabs. +1. Start ThingsBoard with the ten jars and the new configuration. + + For the Compose example, apply the configuration from the directory + containing the Compose file. Replace `thingsboard` if your service has + another name: + + ```bash + docker compose up -d thingsboard + ``` + + This recreates the container when its configuration has changed. + Restarting an existing container alone does not apply changes to its + environment. + + For the Linux package, restart the service: + + ```bash + sudo systemctl restart thingsboard + ``` + +2. Check the log for `IoTDB Table Mode session pool initialized`. With schema + initialization enabled, also check for + `IoTDB Table Mode schema bootstrap complete`. + + When entity attributes use IoTDB, a warning that the module removed + ThingsBoard's `jpaAttributeDao` bean is expected. It means the module has + replaced the built-in attribute backend. + +3. Send test telemetry and a client attribute to a ThingsBoard device. Record + the device ID, keys, values, and timestamps that ThingsBoard shows on the + device's **Latest telemetry** and **Attributes** tabs. ![ThingsBoard latest telemetry stored through the IoTDB Table Model backend](/img/thingsboard-table-telemetry.png) ![ThingsBoard client attributes stored through the IoTDB Table Model backend](/img/thingsboard-table-attributes.png) -4. Query the same device in IoTDB and compare keys, values and times. Replace - `` with the device ID: +4. Query IoTDB for the same device and compare the keys, values, and + timestamps with step 3. Replace `` with the device ID, and + `thingsboard` if you changed the database name: ```sql USE thingsboard; @@ -274,14 +317,17 @@ LIMIT 10; ## 5. Known limitations -- IoTDB 2.0.11 and ThingsBoard 4.3.1.2 are the tested versions. Other IoTDB - 2.x or ThingsBoard versions need their own verification. -- With several ThingsBoard nodes, consistent latest values and attributes depend - on the routing described under `cluster_mode` in section 3. -- Retention uses IoTDB table-level TTL. The module's +- IoTDB 2.0.11 and ThingsBoard 4.3.1.2 are the tested versions. Other versions + require separate verification. +- Multi-node writes to latest telemetry and entity attributes require the + routing arrangement, or the acceptance of inconsistent results, described in + section 3.4. +- Latest telemetry depends on the IoTDB historical telemetry backend. The + module also handles writes and deletes that affect only the latest value; + see the module's user guide for those operations. +- Retention uses IoTDB table-level TTL. See the module's [user guide](https://github.com/apache/iotdb-extras/blob/v2.0.11/iotdb-thingsboard-table/docs/user-guide.md) - explains how it relates to ThingsBoard's retention settings. + for its mapping to ThingsBoard retention settings. -The design, building from source and migrating existing data are described in -the +For source builds, migration guidance, and implementation details, see the [`iotdb-thingsboard-table` module](https://github.com/apache/iotdb-extras/tree/v2.0.11/iotdb-thingsboard-table). diff --git a/src/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md b/src/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md index c08f82b1a..00f607319 100644 --- a/src/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md +++ b/src/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md @@ -21,11 +21,10 @@ # ThingsBoard -The `iotdb-thingsboard-table` module lets a standard ThingsBoard deployment -store its historical telemetry, latest telemetry values and entity attributes in -the IoTDB table model. Each of the three kinds of data can be moved to IoTDB on -its own. This page walks through download, installation, configuration and -verification. +The `iotdb-thingsboard-table` module lets ThingsBoard store historical +telemetry, latest telemetry, and entity attributes in the IoTDB Table Model. +It runs inside an existing ThingsBoard installation and does not require a +custom ThingsBoard build. This integration is independent of the earlier [ThingsBoard Tree Model integration](../../latest/Ecosystem-Integration/Thingsboard.md), @@ -41,14 +40,13 @@ The tested combination is: | ThingsBoard | 4.3.1.2 | | JDK | 17 or later | -The module is part of the Apache IoTDB Extras 2.0.11 release. Its jar is -attached to the +The module is part of Apache IoTDB Extras 2.0.11. Download its jar from the [2.0.11 release](https://github.com/apache/iotdb-extras/releases/tag/v2.0.11) -together with a signature, which can be checked against the -[IoTDB KEYS](https://dist.apache.org/repos/dist/release/iotdb/KEYS) file. The -module jar does not include the IoTDB client, so nine client jars are needed as -well. The following commands download all ten jars into a `lib/` directory and -verify the module's signature: +and verify its signature against the +[IoTDB KEYS](https://dist.apache.org/repos/dist/release/iotdb/KEYS) file. +The module jar does not include the IoTDB client. The commands below also +download the nine required client dependency jars from Maven Central into +the same `lib/` directory. ```bash mkdir -p lib @@ -73,24 +71,22 @@ for path in \ done ``` -The other libraries the IoTDB client uses (`lz4-java`, `zstd-jni`, -`snappy-java` and `antlr4-runtime`) are already part of ThingsBoard; do not add -copies of them. The module can also be built from the -`apache-iotdb-extras-2.0.11-source-release.zip` source package. +Deploy all ten jars downloaded to `lib/`. You can also build the module from +the `apache-iotdb-extras-2.0.11-source-release.zip` source package. ## 2. Install -Install ThingsBoard before you enable the module. The ThingsBoard installer -needs ThingsBoard's own storage backends; if the IoTDB settings from section 3 -are already present, the installer stops with -`No qualifying bean of type 'org.thingsboard.server.service.install.TsDatabaseSchemaService' available`. +Complete the normal ThingsBoard installation before enabling IoTDB storage. +Enabling this module while running the ThingsBoard installer makes the +installation fail with a missing `TsDatabaseSchemaService` error. 1. Install and start IoTDB. See [IoTDB QuickStart](../QuickStart/QuickStart.md). - When IoTDB and ThingsBoard run in separate containers, put them on the same - Docker network and make IoTDB announce its service name. Otherwise the - `2.0.11-standalone` image binds these endpoints to `127.0.0.1`, which the - ThingsBoard container cannot reach. For an IoTDB service named `iotdb`, set: + If IoTDB and ThingsBoard run in separate containers, connect them to the + same Docker network and configure IoTDB to use a service name that + ThingsBoard can reach. Otherwise the `apache/iotdb:2.0.11-standalone` image + uses `127.0.0.1` for these addresses. For an IoTDB service named `iotdb`, + set the following environment values on the IoTDB container: ```yaml cn_seed_config_node: iotdb:10710 @@ -100,131 +96,107 @@ are already present, the installer stops with dn_rpc_address: iotdb ``` -2. Install ThingsBoard as usual, without any of the settings from section 3. -3. Put the ten jars from `lib/` into ThingsBoard's extensions directory, - `/usr/share/thingsboard/extensions`. Both the Docker image and the Linux - `.deb` package load every jar in that directory. Section 3 shows how to do this - for each kind of installation. Keep any other jars that are already there. +2. Install ThingsBoard using its normal installation procedure and storage + settings. Do not apply the IoTDB configuration from section 3 to the + installer. - All ten jars are required, even before the module is enabled. With the - module jar alone, ThingsBoard does not start and reports - `NoClassDefFoundError: org/apache/iotdb/isession/pool/ITableSessionPool`. - With all ten jars and none of the settings below, ThingsBoard runs on its - own storage and the module does nothing. +3. Make all ten downloaded jars available to ThingsBoard in its extensions + directory, `/usr/share/thingsboard/extensions`. -## 3. Configure + For the Docker image, mount or copy them to that directory. Section 3.1 + shows a Compose mount for the `lib/` directory. + + For the Linux `.deb` package, the package does not create the directory. + After the installation has finished, run these commands from the directory + that contains `lib/`: -### Settings + ```bash + sudo install -d /usr/share/thingsboard/extensions + sudo install -m 0644 lib/*.jar /usr/share/thingsboard/extensions/ + ``` -The following settings move all three kinds of data to a single IoTDB node: +All ten jars are required even before IoTDB storage is enabled. If only the +module jar is present, ThingsBoard fails to start with: -```properties -# IoTDB connection -iotdb.host=iotdb -iotdb.port=6667 -iotdb.username=root -iotdb.password=root -iotdb.database=thingsboard -iotdb.session-pool-size=8 -iotdb.schema.bootstrap=true +```text +NoClassDefFoundError: org/apache/iotdb/isession/pool/ITableSessionPool +``` -# Historical telemetry -database.ts.type=iotdb-table -iotdb.ts.experimental-raw-only=true +With all ten jars present and no IoTDB storage settings enabled, ThingsBoard +starts normally and continues to use its existing storage backends. +ThingsBoard 4.3.1.2 already includes `lz4-java`, `zstd-jni`, `snappy-java`, +and `antlr4-runtime`; do not add another copy of these libraries. -# Latest telemetry values -database.ts_latest.type=iotdb-table -iotdb.ts_latest.cluster_mode=disabled +## 3. Configure -# Entity attributes -database.attributes.type=iotdb-table -iotdb.attributes.cluster_mode=disabled -``` +Pass the configuration to the ThingsBoard process through the +`SPRING_APPLICATION_JSON` environment variable. This keeps the property names +exactly as shown, including underscores and hyphens. Use the instructions for +your installation method below. Both examples enable all three storage paths +for one ThingsBoard node. -What they mean: - -- **Connection.** `iotdb.host` is the address ThingsBoard uses to reach IoTDB: - the service name in Docker, or a host name or IP address otherwise. Use - `127.0.0.1` only when both run directly on the same host, outside containers, - or share one container network namespace. If a setting is left out, the defaults are host - `127.0.0.1`, port `6667`, user `root` with password `root`, database - `thingsboard`, and a pool of `8` sessions. With `iotdb.schema.bootstrap=true` - (the default), the module creates the database and its tables on first start. -- **Historical telemetry** moves to IoTDB when both `database.ts.type=iotdb-table` - and `iotdb.ts.experimental-raw-only=true` are set. The second setting is the - required switch that turns this storage on. Its name is kept for - compatibility with earlier builds; with it set, the module stores, reads, - deletes and aggregates historical telemetry. -- **Latest telemetry values** move to IoTDB with - `database.ts_latest.type=iotdb-table`. Latest values are read from the - historical telemetry kept in IoTDB, so this also needs the two historical - settings above. Without it, latest values stay in ThingsBoard's own storage - even when historical telemetry is in IoTDB. -- **Entity attributes** move to IoTDB with `database.attributes.type=iotdb-table`, - independently of the other two. ThingsBoard has no setting of its own for - this, so the module removes ThingsBoard's attribute storage bean at startup - and logs a `WARN` that names `jpaAttributeDao`. -- **`cluster_mode`** must be set when latest values or attributes are moved to - IoTDB; any other value, including an empty one, stops startup with an - error. It states how ThingsBoard nodes share writes. The module protects - concurrent changes to the same key only inside one ThingsBoard process; the - setting does not route requests or coordinate nodes. - - `disabled`: use this with a single ThingsBoard node. With several nodes, - use it only if you accept that concurrent updates to the same device or - entity from different nodes can leave inconsistent results. - - `sticky-routing`: use this with several nodes when your deployment makes - sure that all updates and deletes for one device or entity are handled by - the same node. Configure and verify that routing yourself. - -### Passing the settings to ThingsBoard - -The setting names contain dots, underscores and hyphens, so pass them as one -JSON object in the `SPRING_APPLICATION_JSON` environment variable. - -**Docker (Compose).** Add the environment variable and the jar directory to -the ThingsBoard service, keeping its other settings. The bind mount replaces the -image's extensions directory, so put any other extension jars you use into the -same `lib/` directory: +Set `iotdb.host`, the credentials, and the database name for your deployment. +If `SPRING_APPLICATION_JSON` already contains other settings, merge these keys +into its existing JSON object. + +### 3.1 Docker + +In your existing Compose file, merge the following fragment into the +ThingsBoard service. The example names that service `thingsboard`; use your +existing service name if it differs. Keep its other environment settings, +volumes, ports, and network configuration. The `lib/` directory from section 1 +must be beside the Compose file, and the IoTDB service, named `iotdb`, must be +on the same Docker network as ThingsBoard. ```yaml services: thingsboard: - environment: - SPRING_APPLICATION_JSON: >- - {"iotdb.host": "iotdb", "iotdb.port": "6667", - "iotdb.username": "root", "iotdb.password": "root", - "iotdb.database": "thingsboard", "iotdb.session-pool-size": "8", - "iotdb.schema.bootstrap": "true", - "database.ts.type": "iotdb-table", - "iotdb.ts.experimental-raw-only": "true", - "database.ts_latest.type": "iotdb-table", - "iotdb.ts_latest.cluster_mode": "disabled", - "database.attributes.type": "iotdb-table", - "iotdb.attributes.cluster_mode": "disabled"} + image: thingsboard/tb-node:4.3.1.2 volumes: - ./lib:/usr/share/thingsboard/extensions:ro + environment: + SPRING_APPLICATION_JSON: >- + { + "iotdb.host": "iotdb", + "iotdb.port": "6667", + "iotdb.username": "root", + "iotdb.password": "root", + "iotdb.database": "thingsboard", + "iotdb.session-pool-size": "8", + "iotdb.schema.bootstrap": "true", + "database.ts.type": "iotdb-table", + "iotdb.ts.experimental-raw-only": "true", + "database.ts_latest.type": "iotdb-table", + "iotdb.ts_latest.cluster_mode": "disabled", + "database.attributes.type": "iotdb-table", + "iotdb.attributes.cluster_mode": "disabled" + } ``` -Apply it with `docker compose up -d`, which recreates the container. Restarting -the old container does not pick up a changed environment. +This is a fragment for an already installed ThingsBoard deployment, not a +complete Compose file. Apply it to the running service, not to the installer. +If you already mount the extensions directory, put the ten jars in that +directory instead of adding a second mount at the same location. -**Linux package (`.deb`).** The package does not create the extensions -directory. Create it and copy the jars: +### 3.2 Linux package -```bash -sudo mkdir -p /usr/share/thingsboard/extensions -sudo cp lib/*.jar /usr/share/thingsboard/extensions/ -``` +For the Linux `.deb` package, add the configuration below to +`/etc/thingsboard/conf/thingsboard.conf`, alongside its existing settings. The +ThingsBoard service reads this shell file at startup, so include the `export` +and the surrounding single quotes. Exporting the variable only in your +terminal does not configure the service. -Then add the settings to `/etc/thingsboard/conf/thingsboard.conf`, which the -ThingsBoard service reads at startup. The example assumes IoTDB on the same -host; change `iotdb.host` otherwise: +The example assumes IoTDB is reachable from the ThingsBoard service at +`127.0.0.1`. Change the host if your deployment uses another address. ```bash export SPRING_APPLICATION_JSON='{ - "iotdb.host": "127.0.0.1", "iotdb.port": "6667", - "iotdb.username": "root", "iotdb.password": "root", - "iotdb.database": "thingsboard", "iotdb.session-pool-size": "8", + "iotdb.host": "127.0.0.1", + "iotdb.port": "6667", + "iotdb.username": "root", + "iotdb.password": "root", + "iotdb.database": "thingsboard", + "iotdb.session-pool-size": "8", "iotdb.schema.bootstrap": "true", "database.ts.type": "iotdb-table", "iotdb.ts.experimental-raw-only": "true", @@ -235,26 +207,97 @@ export SPRING_APPLICATION_JSON='{ }' ``` -Restart the service with `sudo systemctl restart thingsboard`. Setting the -variable in your own shell has no effect on the service. +### 3.3 Choose what IoTDB stores + +Keep the settings for the storage paths you want to enable: + +- **Historical telemetry:** set both `database.ts.type=iotdb-table` and + `iotdb.ts.experimental-raw-only=true`. +- **Latest telemetry:** enable historical telemetry as above, then also set + `database.ts_latest.type=iotdb-table` and + `iotdb.ts_latest.cluster_mode`. Latest telemetry depends on the historical + telemetry backend because it reads from the IoTDB telemetry table. +- **Entity attributes:** set `database.attributes.type=iotdb-table` and + `iotdb.attributes.cluster_mode`. Attributes do not require either telemetry + backend to use IoTDB. + +Use the `cluster_mode` values described in section 3.4. Omit the settings for +storage paths you do not want to enable. If you enable historical telemetry +but omit `database.ts_latest.type`, latest telemetry continues to use the +original ThingsBoard backend. + +`iotdb.ts.experimental-raw-only` is the required opt-in switch for the +historical telemetry backend. Its name is retained for compatibility: enabling +it supports writes, raw reads, deletes, and time-bucketed aggregation. + +The default connection settings are host `127.0.0.1`, port `6667`, +username/password `root`/`root`, database `thingsboard`, and session-pool size +`8`. Schema initialization is enabled by default +(`iotdb.schema.bootstrap=true`) and creates the database and its tables on +first start. The Docker example overrides the host with `iotdb`; running in +Docker does not change the module's default host. + +### 3.4 One or several ThingsBoard nodes + +When latest telemetry or attributes use IoTDB, explicitly set the corresponding +`cluster_mode` property: + +- **One ThingsBoard node:** use `disabled`, as in the examples. +- **Several ThingsBoard nodes:** use `sticky-routing` only when your deployment + sends every operation that updates or deletes each device's or entity's + latest telemetry or attributes to the same ThingsBoard node. Configure and + verify that routing separately; setting this property does not establish it. +- **Several nodes without that routing guarantee:** use `disabled` only if + you accept that concurrent updates from different nodes to the same + latest-value key or attribute may leave inconsistent results. The module + does not coordinate these writes across nodes. + +Here, a node means a ThingsBoard node, not an IoTDB node. `disabled` does not +disable latest-telemetry or attribute storage. Any other value, including an +omitted or empty value, prevents startup when the corresponding backend is +enabled. ## 4. Run and verify -1. Start ThingsBoard, or restart it if it is already running. -2. Check the ThingsBoard log for `IoTDB Table Mode session pool initialized`. - With schema bootstrap enabled, `IoTDB Table Mode schema bootstrap complete` - follows. When attributes are moved, the `WARN` about `jpaAttributeDao` is - expected. -3. Send test telemetry and attributes to a ThingsBoard device, and note the - device ID, the keys, the values and their times as shown on the device's - **Latest telemetry** and **Attributes** tabs. +1. Start ThingsBoard with the ten jars and the new configuration. + + For the Compose example, apply the configuration from the directory + containing the Compose file. Replace `thingsboard` if your service has + another name: + + ```bash + docker compose up -d thingsboard + ``` + + This recreates the container when its configuration has changed. + Restarting an existing container alone does not apply changes to its + environment. + + For the Linux package, restart the service: + + ```bash + sudo systemctl restart thingsboard + ``` + +2. Check the log for `IoTDB Table Mode session pool initialized`. With schema + initialization enabled, also check for + `IoTDB Table Mode schema bootstrap complete`. + + When entity attributes use IoTDB, a warning that the module removed + ThingsBoard's `jpaAttributeDao` bean is expected. It means the module has + replaced the built-in attribute backend. + +3. Send test telemetry and a client attribute to a ThingsBoard device. Record + the device ID, keys, values, and timestamps that ThingsBoard shows on the + device's **Latest telemetry** and **Attributes** tabs. ![ThingsBoard latest telemetry stored through the IoTDB Table Model backend](/img/thingsboard-table-telemetry.png) ![ThingsBoard client attributes stored through the IoTDB Table Model backend](/img/thingsboard-table-attributes.png) -4. Query the same device in IoTDB and compare keys, values and times. Replace - `` with the device ID: +4. Query IoTDB for the same device and compare the keys, values, and + timestamps with step 3. Replace `` with the device ID, and + `thingsboard` if you changed the database name: ```sql USE thingsboard; @@ -274,14 +317,17 @@ LIMIT 10; ## 5. Known limitations -- IoTDB 2.0.11 and ThingsBoard 4.3.1.2 are the tested versions. Other IoTDB - 2.x or ThingsBoard versions need their own verification. -- With several ThingsBoard nodes, consistent latest values and attributes depend - on the routing described under `cluster_mode` in section 3. -- Retention uses IoTDB table-level TTL. The module's +- IoTDB 2.0.11 and ThingsBoard 4.3.1.2 are the tested versions. Other versions + require separate verification. +- Multi-node writes to latest telemetry and entity attributes require the + routing arrangement, or the acceptance of inconsistent results, described in + section 3.4. +- Latest telemetry depends on the IoTDB historical telemetry backend. The + module also handles writes and deletes that affect only the latest value; + see the module's user guide for those operations. +- Retention uses IoTDB table-level TTL. See the module's [user guide](https://github.com/apache/iotdb-extras/blob/v2.0.11/iotdb-thingsboard-table/docs/user-guide.md) - explains how it relates to ThingsBoard's retention settings. + for its mapping to ThingsBoard retention settings. -The design, building from source and migrating existing data are described in -the +For source builds, migration guidance, and implementation details, see the [`iotdb-thingsboard-table` module](https://github.com/apache/iotdb-extras/tree/v2.0.11/iotdb-thingsboard-table). diff --git a/src/zh/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md b/src/zh/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md index a9eb9ef3a..87acfbb87 100644 --- a/src/zh/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md +++ b/src/zh/UserGuide/Master/Table/Ecosystem-Integration/Thingsboard.md @@ -21,17 +21,17 @@ # ThingsBoard -`iotdb-thingsboard-table` 模块可以让标准版 ThingsBoard 把历史遥测数据、最新遥测值和实体 -属性存入 IoTDB 表模型。三类数据可以分别迁移到 IoTDB。本页依次介绍下载、安装、配置和 -验证。 +`iotdb-thingsboard-table` 模块可将 ThingsBoard 的历史遥测数据、 +最新遥测值和实体属性存入 IoTDB 表模型。模块直接加载到现有的 +ThingsBoard 中,无需使用定制版 ThingsBoard。 -这套集成独立于较早的 -[ThingsBoard 树模型集成](../../Tree/Ecosystem-Integration/Thingsboard.md),后者需要使用 -IoTDB 适配版 ThingsBoard。 +本模块与较早的 +[ThingsBoard 树模型集成](../../Tree/Ecosystem-Integration/Thingsboard.md) +是两套独立方案;树模型方案需要使用 IoTDB 适配版 ThingsBoard。 ## 1. 下载 -经过验证的版本组合如下: +已验证的版本组合如下: | 组件 | 版本 | | --- | --- | @@ -39,11 +39,12 @@ IoTDB 适配版 ThingsBoard。 | ThingsBoard | 4.3.1.2 | | JDK | 17 或更高版本 | -本模块随 Apache IoTDB Extras 2.0.11 正式发布。模块 jar 及其签名作为附件放在 -[2.0.11 Release](https://github.com/apache/iotdb-extras/releases/tag/v2.0.11) 中,签名可用 -[IoTDB KEYS](https://dist.apache.org/repos/dist/release/iotdb/KEYS) 校验。模块 jar 不包含 -IoTDB 客户端,因此还需要 9 个客户端 jar。下面的命令把这 10 个 jar 下载到 `lib/` 目录, -并校验模块 jar 的签名: +本模块随 Apache IoTDB Extras 2.0.11 发布。以下命令从 +[2.0.11 发布页面](https://github.com/apache/iotdb-extras/releases/tag/v2.0.11) +下载模块 jar,并使用 +[IoTDB KEYS](https://dist.apache.org/repos/dist/release/iotdb/KEYS) +校验签名。模块 jar 不包含 IoTDB 客户端,因此命令还会从 +Maven Central 下载所需的 9 个客户端依赖 jar,统一放入 `lib/`。 ```bash mkdir -p lib @@ -68,136 +69,129 @@ for path in \ done ``` -IoTDB 客户端用到的其他库(`lz4-java`、`zstd-jni`、`snappy-java` 和 `antlr4-runtime`) -ThingsBoard 已经自带,不要再另外放入。也可以用源码包 -`apache-iotdb-extras-2.0.11-source-release.zip` 自行构建本模块。 +部署时需要使用 `lib/` 中下载得到的全部 10 个 jar。也可以用源码包 +`apache-iotdb-extras-2.0.11-source-release.zip` 自行构建模块。 ## 2. 安装 -请先安装 ThingsBoard,再启用本模块。ThingsBoard 安装程序依赖它自带的存储后端;如果第 3 -节的 IoTDB 配置已经生效,安装会中止并报 -`No qualifying bean of type 'org.thingsboard.server.service.install.TsDatabaseSchemaService' available`。 +必须先完成 ThingsBoard 的正常安装,再配置 IoTDB 存储。 +如果在运行 ThingsBoard 安装程序时就启用本模块,安装会因缺少 +`TsDatabaseSchemaService` 而失败。 -1. 安装并启动 IoTDB,参见 [IoTDB 快速上手](../QuickStart/QuickStart.md)。 +1. 安装并启动 IoTDB,参见 + [IoTDB 快速上手](../QuickStart/QuickStart.md)。 - 如果 IoTDB 和 ThingsBoard 分别运行在不同容器中,请把它们放在同一个 Docker 网络里,并让 - IoTDB 对外公布自己的服务名。否则 `2.0.11-standalone` 镜像会把这些地址绑定到 - `127.0.0.1`,ThingsBoard 容器无法访问。假设 IoTDB 的服务名为 `iotdb`,请设置: + 如果 IoTDB 和 ThingsBoard 分别运行在容器中,请将两者接入同一 + Docker 网络,并让 IoTDB 使用 ThingsBoard 可以访问的服务名。 + `apache/iotdb:2.0.11-standalone` 镜像默认对以下地址使用 + `127.0.0.1`。假设 IoTDB 服务名为 `iotdb`,在 IoTDB 容器的 + 环境配置中设置: ```yaml - cn_seed_config_node: iotdb:10710 - dn_seed_config_node: iotdb:10710 - cn_internal_address: iotdb - dn_internal_address: iotdb - dn_rpc_address: iotdb - ``` - -2. 按常规方式安装 ThingsBoard,此时不要加入第 3 节的任何配置。 -3. 把 `lib/` 中的 10 个 jar 放入 ThingsBoard 的扩展目录 `/usr/share/thingsboard/extensions`。 - Docker 镜像和 Linux `.deb` 安装包都会加载该目录中的所有 jar,具体做法见第 3 节。该目录中已有的 - 其他 jar 请保留。 - - 这 10 个 jar 缺一不可,即使还没有启用本模块也是如此。只放模块 jar 时,ThingsBoard 无法 - 启动,并报 `NoClassDefFoundError: org/apache/iotdb/isession/pool/ITableSessionPool`。 - 放齐 10 个 jar 但不加下面的配置时,ThingsBoard 照常使用自己的存储,本模块不起作用。 + cn_seed_config_node: iotdb:10710 + dn_seed_config_node: iotdb:10710 + cn_internal_address: iotdb + dn_internal_address: iotdb + dn_rpc_address: iotdb + ``` -## 3. 配置 +2. 按 ThingsBoard 的正常流程和存储配置完成安装。 + 不要将第 3 节的 IoTDB 配置用于安装程序。 -### 配置项 +3. 将下载得到的全部 10 个 jar 放入 ThingsBoard 的扩展目录 + `/usr/share/thingsboard/extensions`。 -下面这组配置把三类数据都存入单个 IoTDB 节点: + 使用 Docker 镜像时,将这些 jar 挂载或复制到该目录。第 3.1 节 + 给出了挂载 `lib/` 目录的 Compose 示例。 -```properties -# IoTDB connection -iotdb.host=iotdb -iotdb.port=6667 -iotdb.username=root -iotdb.password=root -iotdb.database=thingsboard -iotdb.session-pool-size=8 -iotdb.schema.bootstrap=true + 使用 Linux `.deb` 安装包时,安装包不会创建该目录。请在安装 + 完成后,在包含 `lib/` 的目录中执行: -# Historical telemetry -database.ts.type=iotdb-table -iotdb.ts.experimental-raw-only=true + ```bash + sudo install -d /usr/share/thingsboard/extensions + sudo install -m 0644 lib/*.jar /usr/share/thingsboard/extensions/ + ``` -# Latest telemetry values -database.ts_latest.type=iotdb-table -iotdb.ts_latest.cluster_mode=disabled +即使还没有启用 IoTDB 存储,这 10 个 jar 也必须全部就位。 +如果只放入模块 jar,ThingsBoard 会启动失败并报错: -# Entity attributes -database.attributes.type=iotdb-table -iotdb.attributes.cluster_mode=disabled +```text +NoClassDefFoundError: org/apache/iotdb/isession/pool/ITableSessionPool ``` -各配置项的含义: - -- **连接**:`iotdb.host` 是 ThingsBoard 访问 IoTDB 的地址。在 Docker 中填 IoTDB 的服务名, - 其他环境填主机名或 IP。只有两者直接运行在同一台主机上(不在容器中),或共用同一个容器 - 网络命名空间时,才使用 `127.0.0.1`。未设置的项使用默认值:地址 `127.0.0.1`、端口 `6667`、用户名和密码 - 均为 `root`、数据库 `thingsboard`、会话池大小 `8`。`iotdb.schema.bootstrap=true`(默认) - 时,模块会在首次启动时自动创建数据库和表。 -- **历史遥测数据**:同时设置 `database.ts.type=iotdb-table` 和 - `iotdb.ts.experimental-raw-only=true` 后存入 IoTDB。后者是启用这部分存储的必需开关,名字 - 为兼容早期版本而保留;开启后,历史遥测数据的写入、查询、删除和聚合都由本模块处理。 -- **最新遥测值**:设置 `database.ts_latest.type=iotdb-table` 后存入 IoTDB。最新值从 IoTDB - 中的历史遥测数据读取,所以还需要上面两项历史数据配置。不设置这一项时,即使历史数据已在 - IoTDB 中,最新值仍留在 ThingsBoard 原有的存储里。 -- **实体属性**:设置 `database.attributes.type=iotdb-table` 后存入 IoTDB,与前两类数据互不 - 依赖。ThingsBoard 本身没有切换属性存储的配置项,因此本模块会在启动时移除 ThingsBoard - 自带的属性存储组件,并输出一条提到 `jpaAttributeDao` 的 `WARN` 日志。 -- **`cluster_mode`**:把最新遥测值或实体属性存入 IoTDB 时必须设置;填其他值(包括留空) - 都会导致启动失败。它用于说明多个 ThingsBoard 节点之间如何分担写入。本模块只在单个 - ThingsBoard 进程内保护对同一个键的并发修改,这个配置不会转发请求,也不会协调各节点。 - - `disabled`:只有一个 ThingsBoard 节点时使用。有多个节点时,只有在能接受“不同节点同时 - 修改同一设备或实体可能导致结果不一致”的情况下才使用。 - - `sticky-routing`:有多个节点,并且你的部署能保证同一设备或实体的所有更新和删除都由同一 - 节点处理时使用。这种路由需要你自己配置并验证。 - -### 把配置传给 ThingsBoard - -配置项名称中含有点、下划线和连字符,因此请把它们写成一个 JSON 对象,通过环境变量 -`SPRING_APPLICATION_JSON` 传入。 - -**Docker(Compose)**:在 ThingsBoard 服务中加入该环境变量和 jar 目录,其他配置保持不变。 -挂载会替换镜像中的扩展目录,因此如果还用到其他扩展 jar,请一并放进同一个 `lib/` 目录: +放齐 10 个 jar、但尚未启用任何 IoTDB 存储配置时,ThingsBoard +可以正常启动,继续使用原有存储后端。ThingsBoard 4.3.1.2 已自带 +`lz4-java`、`zstd-jni`、`snappy-java` 和 `antlr4-runtime`, +不要再添加这些库的其他副本。 + +## 3. 配置 + +通过 ThingsBoard 进程的 `SPRING_APPLICATION_JSON` 环境变量 +传入配置。JSON 中的属性名保留原样,包括下划线和连字符。 +请按部署方式使用下面的说明;两个示例均适用于只有一个 +ThingsBoard 节点、同时启用三类 IoTDB 存储的情况。 + +按实际部署修改 `iotdb.host`、用户名、密码和数据库名。 +如果已经设置了 `SPRING_APPLICATION_JSON`,将这些属性合并到 +现有 JSON 对象中,保留其中的其他配置。 + +### 3.1 Docker + +将以下片段合并到现有 Compose 文件的 ThingsBoard 服务配置中。 +示例将该服务命名为 `thingsboard`;如果已有名称不同,请使用 +实际名称。保留原有环境变量、数据卷、端口和网络配置。 +第 1 节生成的 `lib/` 应与 Compose 文件位于同一目录; +IoTDB 服务名为 `iotdb`,并与 ThingsBoard 使用同一 Docker 网络。 ```yaml services: thingsboard: - environment: - SPRING_APPLICATION_JSON: >- - {"iotdb.host": "iotdb", "iotdb.port": "6667", - "iotdb.username": "root", "iotdb.password": "root", - "iotdb.database": "thingsboard", "iotdb.session-pool-size": "8", - "iotdb.schema.bootstrap": "true", - "database.ts.type": "iotdb-table", - "iotdb.ts.experimental-raw-only": "true", - "database.ts_latest.type": "iotdb-table", - "iotdb.ts_latest.cluster_mode": "disabled", - "database.attributes.type": "iotdb-table", - "iotdb.attributes.cluster_mode": "disabled"} + image: thingsboard/tb-node:4.3.1.2 volumes: - ./lib:/usr/share/thingsboard/extensions:ro + environment: + SPRING_APPLICATION_JSON: >- + { + "iotdb.host": "iotdb", + "iotdb.port": "6667", + "iotdb.username": "root", + "iotdb.password": "root", + "iotdb.database": "thingsboard", + "iotdb.session-pool-size": "8", + "iotdb.schema.bootstrap": "true", + "database.ts.type": "iotdb-table", + "iotdb.ts.experimental-raw-only": "true", + "database.ts_latest.type": "iotdb-table", + "iotdb.ts_latest.cluster_mode": "disabled", + "database.attributes.type": "iotdb-table", + "iotdb.attributes.cluster_mode": "disabled" + } ``` -用 `docker compose up -d` 使配置生效,它会重新创建容器。只重启原有容器不会读取新的环境变量。 +这只是已有 ThingsBoard 部署的配置片段,不是完整的 Compose +文件。它用于安装完成后的运行服务,不用于安装程序。 +如果已经挂载了扩展目录,将 10 个 jar 放入原挂载目录即可, +不要再对同一容器路径添加第二个挂载。 -**Linux 安装包(`.deb`)**:安装包不会创建扩展目录。请先创建目录并复制 jar: +### 3.2 Linux 安装包 -```bash -sudo mkdir -p /usr/share/thingsboard/extensions -sudo cp lib/*.jar /usr/share/thingsboard/extensions/ -``` +使用 Linux `.deb` 安装包时,将下面的配置加入 +`/etc/thingsboard/conf/thingsboard.conf`,保留文件中的已有设置。 +ThingsBoard 服务启动时会读取这个 shell 文件,因此需要保留 +`export` 和 JSON 外侧的单引号。只在终端中导出环境变量, +并不能完成系统服务的配置。 -然后把配置加入 `/etc/thingsboard/conf/thingsboard.conf`,ThingsBoard 服务启动时会读取这个 -文件。示例假设 IoTDB 与 ThingsBoard 在同一台主机上,否则请修改 `iotdb.host`: +此例假设 ThingsBoard 服务可以通过 `127.0.0.1` 访问 IoTDB。 +如果实际地址不同,请修改 `iotdb.host`。 ```bash export SPRING_APPLICATION_JSON='{ - "iotdb.host": "127.0.0.1", "iotdb.port": "6667", - "iotdb.username": "root", "iotdb.password": "root", - "iotdb.database": "thingsboard", "iotdb.session-pool-size": "8", + "iotdb.host": "127.0.0.1", + "iotdb.port": "6667", + "iotdb.username": "root", + "iotdb.password": "root", + "iotdb.database": "thingsboard", + "iotdb.session-pool-size": "8", "iotdb.schema.bootstrap": "true", "database.ts.type": "iotdb-table", "iotdb.ts.experimental-raw-only": "true", @@ -208,23 +202,90 @@ export SPRING_APPLICATION_JSON='{ }' ``` -最后用 `sudo systemctl restart thingsboard` 重启服务。只在自己的终端里设置这个变量,对服务 -不起作用。 +### 3.3 选择使用 IoTDB 存储的数据 + +按需要保留相应配置: + +- **历史遥测数据:** 同时设置 `database.ts.type=iotdb-table` + 和 `iotdb.ts.experimental-raw-only=true`。 +- **最新遥测值:** 先按上一项启用历史遥测存储,再设置 + `database.ts_latest.type=iotdb-table` 和 + `iotdb.ts_latest.cluster_mode`。最新值需要读取 IoTDB + 遥测表,因此不能脱离历史遥测存储单独启用。 +- **实体属性:** 设置 `database.attributes.type=iotdb-table` + 和 `iotdb.attributes.cluster_mode`。属性存储不要求历史遥测 + 或最新遥测值也使用 IoTDB。 + +`cluster_mode` 的取值见第 3.4 节。不需要启用某类 IoTDB 存储时, +省略对应配置即可。如果启用了历史遥测存储,但未设置 +`database.ts_latest.type`,最新遥测值仍使用 ThingsBoard 原来的后端。 + +`iotdb.ts.experimental-raw-only` 是启用历史遥测后端时必须设置的 +开关。名称是为兼容旧配置而保留的,并不表示只支持原始数据: +启用后支持写入、原始数据查询、删除,以及按时间窗口聚合。 + +连接配置的默认值为:主机 `127.0.0.1`、端口 `6667`、 +用户名和密码 `root`/`root`、数据库 `thingsboard`、会话池大小 `8`。 +默认启用表结构初始化,即 `iotdb.schema.bootstrap=true`, +首次启动时会自动创建数据库和表。 +Docker 示例将主机显式改为 `iotdb`;模块的默认主机地址不会 +因为使用 Docker 而自动改变。 + +### 3.4 单节点与多节点 + +启用最新遥测值或实体属性的 IoTDB 存储后,必须明确设置对应的 +`cluster_mode`: + +- **只有一个 ThingsBoard 节点:** 按示例设为 `disabled`。 +- **有多个 ThingsBoard 节点:** 只有在已确保同一设备或实体的 + 最新遥测值和属性,无论更新还是删除,都由同一个 ThingsBoard + 节点处理时,才设为 `sticky-routing`。路由需要另行配置和验证; + 设置这个属性不会自动建立路由。 +- **有多个节点,但无法保证上述路由:** 只有在明确接受并发写入 + 风险时才使用 `disabled`。不同节点同时更新同一个最新值键 + 或属性,可能导致结果不一致;模块不会协调这些跨节点写入。 + +这里的“节点”指 ThingsBoard 节点,不是 IoTDB 节点。 +`disabled` 不会关闭最新遥测值或属性存储。对应存储功能启用后, +如果未设置该配置、将其留空或使用其他值,ThingsBoard 将无法启动。 ## 4. 运行与验证 -1. 启动 ThingsBoard;如果已在运行,则重启。 -2. 在 ThingsBoard 日志中确认出现 `IoTDB Table Mode session pool initialized`。启用表结构 - 自动创建时,随后还会出现 `IoTDB Table Mode schema bootstrap complete`。把实体属性存入 - IoTDB 时,出现提到 `jpaAttributeDao` 的 `WARN` 属于正常现象。 -3. 向一台 ThingsBoard 设备发送测试遥测数据和属性,在设备的“最新遥测”和“属性”页面记下 - 设备 ID、键、值和时间。 +1. 使用全部 10 个 jar 和新配置启动 ThingsBoard。 + + 使用 Compose 示例时,在 Compose 文件所在目录执行以下命令。 + 如果服务名不是 `thingsboard`,请替换为实际名称: + + ```bash + docker compose up -d thingsboard + ``` + + 配置有变化时,该命令会重建容器。仅重启已经创建的容器, + 不会应用对环境变量配置的修改。 + + 使用 Linux 安装包时,重启服务: + + ```bash + sudo systemctl restart thingsboard + ``` + +2. 确认日志中出现 `IoTDB Table Mode session pool initialized`。 + 如果启用了表结构初始化,还应看到 + `IoTDB Table Mode schema bootstrap complete`。 + + 启用 IoTDB 属性存储后,日志会警告模块移除了 ThingsBoard 的 + `jpaAttributeDao`。这是预期行为,表示内置属性后端已被替换。 + +3. 向一台测试设备发送遥测数据和一个客户端属性,在设备的 + “最新遥测”和“属性”页面记下设备 ID、键名、值和时间戳。 ![通过 IoTDB 表模型后端存储的 ThingsBoard 最新遥测](/img/thingsboard-table-telemetry.png) ![通过 IoTDB 表模型后端存储的 ThingsBoard 客户端属性](/img/thingsboard-table-attributes.png) -4. 在 IoTDB 中查询同一台设备,核对键、值和时间。把 `` 换成设备 ID: +4. 在 IoTDB 中查询同一台设备,核对键名、值和时间戳是否与第 3 步 + 记下的一致。把 `` 换成设备 ID;如果修改过数据库名, + 请同时替换 `thingsboard`: ```sql USE thingsboard; @@ -244,12 +305,15 @@ LIMIT 10; ## 5. 已知限制 -- 已验证的版本为 IoTDB 2.0.11 和 ThingsBoard 4.3.1.2。其他 IoTDB 2.x 或 ThingsBoard 版本 - 需要另行验证。 -- 有多个 ThingsBoard 节点时,最新遥测值和实体属性能否保持一致,取决于第 3 节 `cluster_mode` - 中所说的路由。 -- 数据保留依靠 IoTDB 表级 TTL,它与 ThingsBoard 保留设置的对应关系参见模块的 +- 已验证的版本为 IoTDB 2.0.11 和 ThingsBoard 4.3.1.2。 + 其他版本需要单独验证。 +- 多个 ThingsBoard 节点写入最新遥测值或实体属性时,需要满足 + 第 3.4 节的路由要求,或明确接受并发写入可能导致的不一致。 +- 最新遥测值依赖 IoTDB 历史遥测存储。模块也支持只写入或只删除 + 最新值的操作;具体处理方式参见模块的用户指南。 +- 数据保留期使用 IoTDB 表级 TTL。它与 ThingsBoard 数据保留设置 + 的对应关系,参见模块的 [用户指南](https://github.com/apache/iotdb-extras/blob/v2.0.11/iotdb-thingsboard-table/docs/user-guide.md)。 -设计说明、源码构建和已有数据迁移,参见 +源码构建、数据迁移及实现说明,参见 [`iotdb-thingsboard-table` 模块](https://github.com/apache/iotdb-extras/tree/v2.0.11/iotdb-thingsboard-table)。 diff --git a/src/zh/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md b/src/zh/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md index b17aef0e8..c3194ac85 100644 --- a/src/zh/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md +++ b/src/zh/UserGuide/latest-Table/Ecosystem-Integration/Thingsboard.md @@ -21,17 +21,17 @@ # ThingsBoard -`iotdb-thingsboard-table` 模块可以让标准版 ThingsBoard 把历史遥测数据、最新遥测值和实体 -属性存入 IoTDB 表模型。三类数据可以分别迁移到 IoTDB。本页依次介绍下载、安装、配置和 -验证。 +`iotdb-thingsboard-table` 模块可将 ThingsBoard 的历史遥测数据、 +最新遥测值和实体属性存入 IoTDB 表模型。模块直接加载到现有的 +ThingsBoard 中,无需使用定制版 ThingsBoard。 -这套集成独立于较早的 -[ThingsBoard 树模型集成](../../latest/Ecosystem-Integration/Thingsboard.md),后者需要使用 -IoTDB 适配版 ThingsBoard。 +本模块与较早的 +[ThingsBoard 树模型集成](../../latest/Ecosystem-Integration/Thingsboard.md) +是两套独立方案;树模型方案需要使用 IoTDB 适配版 ThingsBoard。 ## 1. 下载 -经过验证的版本组合如下: +已验证的版本组合如下: | 组件 | 版本 | | --- | --- | @@ -39,11 +39,12 @@ IoTDB 适配版 ThingsBoard。 | ThingsBoard | 4.3.1.2 | | JDK | 17 或更高版本 | -本模块随 Apache IoTDB Extras 2.0.11 正式发布。模块 jar 及其签名作为附件放在 -[2.0.11 Release](https://github.com/apache/iotdb-extras/releases/tag/v2.0.11) 中,签名可用 -[IoTDB KEYS](https://dist.apache.org/repos/dist/release/iotdb/KEYS) 校验。模块 jar 不包含 -IoTDB 客户端,因此还需要 9 个客户端 jar。下面的命令把这 10 个 jar 下载到 `lib/` 目录, -并校验模块 jar 的签名: +本模块随 Apache IoTDB Extras 2.0.11 发布。以下命令从 +[2.0.11 发布页面](https://github.com/apache/iotdb-extras/releases/tag/v2.0.11) +下载模块 jar,并使用 +[IoTDB KEYS](https://dist.apache.org/repos/dist/release/iotdb/KEYS) +校验签名。模块 jar 不包含 IoTDB 客户端,因此命令还会从 +Maven Central 下载所需的 9 个客户端依赖 jar,统一放入 `lib/`。 ```bash mkdir -p lib @@ -68,136 +69,129 @@ for path in \ done ``` -IoTDB 客户端用到的其他库(`lz4-java`、`zstd-jni`、`snappy-java` 和 `antlr4-runtime`) -ThingsBoard 已经自带,不要再另外放入。也可以用源码包 -`apache-iotdb-extras-2.0.11-source-release.zip` 自行构建本模块。 +部署时需要使用 `lib/` 中下载得到的全部 10 个 jar。也可以用源码包 +`apache-iotdb-extras-2.0.11-source-release.zip` 自行构建模块。 ## 2. 安装 -请先安装 ThingsBoard,再启用本模块。ThingsBoard 安装程序依赖它自带的存储后端;如果第 3 -节的 IoTDB 配置已经生效,安装会中止并报 -`No qualifying bean of type 'org.thingsboard.server.service.install.TsDatabaseSchemaService' available`。 +必须先完成 ThingsBoard 的正常安装,再配置 IoTDB 存储。 +如果在运行 ThingsBoard 安装程序时就启用本模块,安装会因缺少 +`TsDatabaseSchemaService` 而失败。 -1. 安装并启动 IoTDB,参见 [IoTDB 快速上手](../QuickStart/QuickStart.md)。 +1. 安装并启动 IoTDB,参见 + [IoTDB 快速上手](../QuickStart/QuickStart.md)。 - 如果 IoTDB 和 ThingsBoard 分别运行在不同容器中,请把它们放在同一个 Docker 网络里,并让 - IoTDB 对外公布自己的服务名。否则 `2.0.11-standalone` 镜像会把这些地址绑定到 - `127.0.0.1`,ThingsBoard 容器无法访问。假设 IoTDB 的服务名为 `iotdb`,请设置: + 如果 IoTDB 和 ThingsBoard 分别运行在容器中,请将两者接入同一 + Docker 网络,并让 IoTDB 使用 ThingsBoard 可以访问的服务名。 + `apache/iotdb:2.0.11-standalone` 镜像默认对以下地址使用 + `127.0.0.1`。假设 IoTDB 服务名为 `iotdb`,在 IoTDB 容器的 + 环境配置中设置: ```yaml - cn_seed_config_node: iotdb:10710 - dn_seed_config_node: iotdb:10710 - cn_internal_address: iotdb - dn_internal_address: iotdb - dn_rpc_address: iotdb - ``` - -2. 按常规方式安装 ThingsBoard,此时不要加入第 3 节的任何配置。 -3. 把 `lib/` 中的 10 个 jar 放入 ThingsBoard 的扩展目录 `/usr/share/thingsboard/extensions`。 - Docker 镜像和 Linux `.deb` 安装包都会加载该目录中的所有 jar,具体做法见第 3 节。该目录中已有的 - 其他 jar 请保留。 - - 这 10 个 jar 缺一不可,即使还没有启用本模块也是如此。只放模块 jar 时,ThingsBoard 无法 - 启动,并报 `NoClassDefFoundError: org/apache/iotdb/isession/pool/ITableSessionPool`。 - 放齐 10 个 jar 但不加下面的配置时,ThingsBoard 照常使用自己的存储,本模块不起作用。 + cn_seed_config_node: iotdb:10710 + dn_seed_config_node: iotdb:10710 + cn_internal_address: iotdb + dn_internal_address: iotdb + dn_rpc_address: iotdb + ``` -## 3. 配置 +2. 按 ThingsBoard 的正常流程和存储配置完成安装。 + 不要将第 3 节的 IoTDB 配置用于安装程序。 -### 配置项 +3. 将下载得到的全部 10 个 jar 放入 ThingsBoard 的扩展目录 + `/usr/share/thingsboard/extensions`。 -下面这组配置把三类数据都存入单个 IoTDB 节点: + 使用 Docker 镜像时,将这些 jar 挂载或复制到该目录。第 3.1 节 + 给出了挂载 `lib/` 目录的 Compose 示例。 -```properties -# IoTDB connection -iotdb.host=iotdb -iotdb.port=6667 -iotdb.username=root -iotdb.password=root -iotdb.database=thingsboard -iotdb.session-pool-size=8 -iotdb.schema.bootstrap=true + 使用 Linux `.deb` 安装包时,安装包不会创建该目录。请在安装 + 完成后,在包含 `lib/` 的目录中执行: -# Historical telemetry -database.ts.type=iotdb-table -iotdb.ts.experimental-raw-only=true + ```bash + sudo install -d /usr/share/thingsboard/extensions + sudo install -m 0644 lib/*.jar /usr/share/thingsboard/extensions/ + ``` -# Latest telemetry values -database.ts_latest.type=iotdb-table -iotdb.ts_latest.cluster_mode=disabled +即使还没有启用 IoTDB 存储,这 10 个 jar 也必须全部就位。 +如果只放入模块 jar,ThingsBoard 会启动失败并报错: -# Entity attributes -database.attributes.type=iotdb-table -iotdb.attributes.cluster_mode=disabled +```text +NoClassDefFoundError: org/apache/iotdb/isession/pool/ITableSessionPool ``` -各配置项的含义: - -- **连接**:`iotdb.host` 是 ThingsBoard 访问 IoTDB 的地址。在 Docker 中填 IoTDB 的服务名, - 其他环境填主机名或 IP。只有两者直接运行在同一台主机上(不在容器中),或共用同一个容器 - 网络命名空间时,才使用 `127.0.0.1`。未设置的项使用默认值:地址 `127.0.0.1`、端口 `6667`、用户名和密码 - 均为 `root`、数据库 `thingsboard`、会话池大小 `8`。`iotdb.schema.bootstrap=true`(默认) - 时,模块会在首次启动时自动创建数据库和表。 -- **历史遥测数据**:同时设置 `database.ts.type=iotdb-table` 和 - `iotdb.ts.experimental-raw-only=true` 后存入 IoTDB。后者是启用这部分存储的必需开关,名字 - 为兼容早期版本而保留;开启后,历史遥测数据的写入、查询、删除和聚合都由本模块处理。 -- **最新遥测值**:设置 `database.ts_latest.type=iotdb-table` 后存入 IoTDB。最新值从 IoTDB - 中的历史遥测数据读取,所以还需要上面两项历史数据配置。不设置这一项时,即使历史数据已在 - IoTDB 中,最新值仍留在 ThingsBoard 原有的存储里。 -- **实体属性**:设置 `database.attributes.type=iotdb-table` 后存入 IoTDB,与前两类数据互不 - 依赖。ThingsBoard 本身没有切换属性存储的配置项,因此本模块会在启动时移除 ThingsBoard - 自带的属性存储组件,并输出一条提到 `jpaAttributeDao` 的 `WARN` 日志。 -- **`cluster_mode`**:把最新遥测值或实体属性存入 IoTDB 时必须设置;填其他值(包括留空) - 都会导致启动失败。它用于说明多个 ThingsBoard 节点之间如何分担写入。本模块只在单个 - ThingsBoard 进程内保护对同一个键的并发修改,这个配置不会转发请求,也不会协调各节点。 - - `disabled`:只有一个 ThingsBoard 节点时使用。有多个节点时,只有在能接受“不同节点同时 - 修改同一设备或实体可能导致结果不一致”的情况下才使用。 - - `sticky-routing`:有多个节点,并且你的部署能保证同一设备或实体的所有更新和删除都由同一 - 节点处理时使用。这种路由需要你自己配置并验证。 - -### 把配置传给 ThingsBoard - -配置项名称中含有点、下划线和连字符,因此请把它们写成一个 JSON 对象,通过环境变量 -`SPRING_APPLICATION_JSON` 传入。 - -**Docker(Compose)**:在 ThingsBoard 服务中加入该环境变量和 jar 目录,其他配置保持不变。 -挂载会替换镜像中的扩展目录,因此如果还用到其他扩展 jar,请一并放进同一个 `lib/` 目录: +放齐 10 个 jar、但尚未启用任何 IoTDB 存储配置时,ThingsBoard +可以正常启动,继续使用原有存储后端。ThingsBoard 4.3.1.2 已自带 +`lz4-java`、`zstd-jni`、`snappy-java` 和 `antlr4-runtime`, +不要再添加这些库的其他副本。 + +## 3. 配置 + +通过 ThingsBoard 进程的 `SPRING_APPLICATION_JSON` 环境变量 +传入配置。JSON 中的属性名保留原样,包括下划线和连字符。 +请按部署方式使用下面的说明;两个示例均适用于只有一个 +ThingsBoard 节点、同时启用三类 IoTDB 存储的情况。 + +按实际部署修改 `iotdb.host`、用户名、密码和数据库名。 +如果已经设置了 `SPRING_APPLICATION_JSON`,将这些属性合并到 +现有 JSON 对象中,保留其中的其他配置。 + +### 3.1 Docker + +将以下片段合并到现有 Compose 文件的 ThingsBoard 服务配置中。 +示例将该服务命名为 `thingsboard`;如果已有名称不同,请使用 +实际名称。保留原有环境变量、数据卷、端口和网络配置。 +第 1 节生成的 `lib/` 应与 Compose 文件位于同一目录; +IoTDB 服务名为 `iotdb`,并与 ThingsBoard 使用同一 Docker 网络。 ```yaml services: thingsboard: - environment: - SPRING_APPLICATION_JSON: >- - {"iotdb.host": "iotdb", "iotdb.port": "6667", - "iotdb.username": "root", "iotdb.password": "root", - "iotdb.database": "thingsboard", "iotdb.session-pool-size": "8", - "iotdb.schema.bootstrap": "true", - "database.ts.type": "iotdb-table", - "iotdb.ts.experimental-raw-only": "true", - "database.ts_latest.type": "iotdb-table", - "iotdb.ts_latest.cluster_mode": "disabled", - "database.attributes.type": "iotdb-table", - "iotdb.attributes.cluster_mode": "disabled"} + image: thingsboard/tb-node:4.3.1.2 volumes: - ./lib:/usr/share/thingsboard/extensions:ro + environment: + SPRING_APPLICATION_JSON: >- + { + "iotdb.host": "iotdb", + "iotdb.port": "6667", + "iotdb.username": "root", + "iotdb.password": "root", + "iotdb.database": "thingsboard", + "iotdb.session-pool-size": "8", + "iotdb.schema.bootstrap": "true", + "database.ts.type": "iotdb-table", + "iotdb.ts.experimental-raw-only": "true", + "database.ts_latest.type": "iotdb-table", + "iotdb.ts_latest.cluster_mode": "disabled", + "database.attributes.type": "iotdb-table", + "iotdb.attributes.cluster_mode": "disabled" + } ``` -用 `docker compose up -d` 使配置生效,它会重新创建容器。只重启原有容器不会读取新的环境变量。 +这只是已有 ThingsBoard 部署的配置片段,不是完整的 Compose +文件。它用于安装完成后的运行服务,不用于安装程序。 +如果已经挂载了扩展目录,将 10 个 jar 放入原挂载目录即可, +不要再对同一容器路径添加第二个挂载。 -**Linux 安装包(`.deb`)**:安装包不会创建扩展目录。请先创建目录并复制 jar: +### 3.2 Linux 安装包 -```bash -sudo mkdir -p /usr/share/thingsboard/extensions -sudo cp lib/*.jar /usr/share/thingsboard/extensions/ -``` +使用 Linux `.deb` 安装包时,将下面的配置加入 +`/etc/thingsboard/conf/thingsboard.conf`,保留文件中的已有设置。 +ThingsBoard 服务启动时会读取这个 shell 文件,因此需要保留 +`export` 和 JSON 外侧的单引号。只在终端中导出环境变量, +并不能完成系统服务的配置。 -然后把配置加入 `/etc/thingsboard/conf/thingsboard.conf`,ThingsBoard 服务启动时会读取这个 -文件。示例假设 IoTDB 与 ThingsBoard 在同一台主机上,否则请修改 `iotdb.host`: +此例假设 ThingsBoard 服务可以通过 `127.0.0.1` 访问 IoTDB。 +如果实际地址不同,请修改 `iotdb.host`。 ```bash export SPRING_APPLICATION_JSON='{ - "iotdb.host": "127.0.0.1", "iotdb.port": "6667", - "iotdb.username": "root", "iotdb.password": "root", - "iotdb.database": "thingsboard", "iotdb.session-pool-size": "8", + "iotdb.host": "127.0.0.1", + "iotdb.port": "6667", + "iotdb.username": "root", + "iotdb.password": "root", + "iotdb.database": "thingsboard", + "iotdb.session-pool-size": "8", "iotdb.schema.bootstrap": "true", "database.ts.type": "iotdb-table", "iotdb.ts.experimental-raw-only": "true", @@ -208,23 +202,90 @@ export SPRING_APPLICATION_JSON='{ }' ``` -最后用 `sudo systemctl restart thingsboard` 重启服务。只在自己的终端里设置这个变量,对服务 -不起作用。 +### 3.3 选择使用 IoTDB 存储的数据 + +按需要保留相应配置: + +- **历史遥测数据:** 同时设置 `database.ts.type=iotdb-table` + 和 `iotdb.ts.experimental-raw-only=true`。 +- **最新遥测值:** 先按上一项启用历史遥测存储,再设置 + `database.ts_latest.type=iotdb-table` 和 + `iotdb.ts_latest.cluster_mode`。最新值需要读取 IoTDB + 遥测表,因此不能脱离历史遥测存储单独启用。 +- **实体属性:** 设置 `database.attributes.type=iotdb-table` + 和 `iotdb.attributes.cluster_mode`。属性存储不要求历史遥测 + 或最新遥测值也使用 IoTDB。 + +`cluster_mode` 的取值见第 3.4 节。不需要启用某类 IoTDB 存储时, +省略对应配置即可。如果启用了历史遥测存储,但未设置 +`database.ts_latest.type`,最新遥测值仍使用 ThingsBoard 原来的后端。 + +`iotdb.ts.experimental-raw-only` 是启用历史遥测后端时必须设置的 +开关。名称是为兼容旧配置而保留的,并不表示只支持原始数据: +启用后支持写入、原始数据查询、删除,以及按时间窗口聚合。 + +连接配置的默认值为:主机 `127.0.0.1`、端口 `6667`、 +用户名和密码 `root`/`root`、数据库 `thingsboard`、会话池大小 `8`。 +默认启用表结构初始化,即 `iotdb.schema.bootstrap=true`, +首次启动时会自动创建数据库和表。 +Docker 示例将主机显式改为 `iotdb`;模块的默认主机地址不会 +因为使用 Docker 而自动改变。 + +### 3.4 单节点与多节点 + +启用最新遥测值或实体属性的 IoTDB 存储后,必须明确设置对应的 +`cluster_mode`: + +- **只有一个 ThingsBoard 节点:** 按示例设为 `disabled`。 +- **有多个 ThingsBoard 节点:** 只有在已确保同一设备或实体的 + 最新遥测值和属性,无论更新还是删除,都由同一个 ThingsBoard + 节点处理时,才设为 `sticky-routing`。路由需要另行配置和验证; + 设置这个属性不会自动建立路由。 +- **有多个节点,但无法保证上述路由:** 只有在明确接受并发写入 + 风险时才使用 `disabled`。不同节点同时更新同一个最新值键 + 或属性,可能导致结果不一致;模块不会协调这些跨节点写入。 + +这里的“节点”指 ThingsBoard 节点,不是 IoTDB 节点。 +`disabled` 不会关闭最新遥测值或属性存储。对应存储功能启用后, +如果未设置该配置、将其留空或使用其他值,ThingsBoard 将无法启动。 ## 4. 运行与验证 -1. 启动 ThingsBoard;如果已在运行,则重启。 -2. 在 ThingsBoard 日志中确认出现 `IoTDB Table Mode session pool initialized`。启用表结构 - 自动创建时,随后还会出现 `IoTDB Table Mode schema bootstrap complete`。把实体属性存入 - IoTDB 时,出现提到 `jpaAttributeDao` 的 `WARN` 属于正常现象。 -3. 向一台 ThingsBoard 设备发送测试遥测数据和属性,在设备的“最新遥测”和“属性”页面记下 - 设备 ID、键、值和时间。 +1. 使用全部 10 个 jar 和新配置启动 ThingsBoard。 + + 使用 Compose 示例时,在 Compose 文件所在目录执行以下命令。 + 如果服务名不是 `thingsboard`,请替换为实际名称: + + ```bash + docker compose up -d thingsboard + ``` + + 配置有变化时,该命令会重建容器。仅重启已经创建的容器, + 不会应用对环境变量配置的修改。 + + 使用 Linux 安装包时,重启服务: + + ```bash + sudo systemctl restart thingsboard + ``` + +2. 确认日志中出现 `IoTDB Table Mode session pool initialized`。 + 如果启用了表结构初始化,还应看到 + `IoTDB Table Mode schema bootstrap complete`。 + + 启用 IoTDB 属性存储后,日志会警告模块移除了 ThingsBoard 的 + `jpaAttributeDao`。这是预期行为,表示内置属性后端已被替换。 + +3. 向一台测试设备发送遥测数据和一个客户端属性,在设备的 + “最新遥测”和“属性”页面记下设备 ID、键名、值和时间戳。 ![通过 IoTDB 表模型后端存储的 ThingsBoard 最新遥测](/img/thingsboard-table-telemetry.png) ![通过 IoTDB 表模型后端存储的 ThingsBoard 客户端属性](/img/thingsboard-table-attributes.png) -4. 在 IoTDB 中查询同一台设备,核对键、值和时间。把 `` 换成设备 ID: +4. 在 IoTDB 中查询同一台设备,核对键名、值和时间戳是否与第 3 步 + 记下的一致。把 `` 换成设备 ID;如果修改过数据库名, + 请同时替换 `thingsboard`: ```sql USE thingsboard; @@ -244,12 +305,15 @@ LIMIT 10; ## 5. 已知限制 -- 已验证的版本为 IoTDB 2.0.11 和 ThingsBoard 4.3.1.2。其他 IoTDB 2.x 或 ThingsBoard 版本 - 需要另行验证。 -- 有多个 ThingsBoard 节点时,最新遥测值和实体属性能否保持一致,取决于第 3 节 `cluster_mode` - 中所说的路由。 -- 数据保留依靠 IoTDB 表级 TTL,它与 ThingsBoard 保留设置的对应关系参见模块的 +- 已验证的版本为 IoTDB 2.0.11 和 ThingsBoard 4.3.1.2。 + 其他版本需要单独验证。 +- 多个 ThingsBoard 节点写入最新遥测值或实体属性时,需要满足 + 第 3.4 节的路由要求,或明确接受并发写入可能导致的不一致。 +- 最新遥测值依赖 IoTDB 历史遥测存储。模块也支持只写入或只删除 + 最新值的操作;具体处理方式参见模块的用户指南。 +- 数据保留期使用 IoTDB 表级 TTL。它与 ThingsBoard 数据保留设置 + 的对应关系,参见模块的 [用户指南](https://github.com/apache/iotdb-extras/blob/v2.0.11/iotdb-thingsboard-table/docs/user-guide.md)。 -设计说明、源码构建和已有数据迁移,参见 +源码构建、数据迁移及实现说明,参见 [`iotdb-thingsboard-table` 模块](https://github.com/apache/iotdb-extras/tree/v2.0.11/iotdb-thingsboard-table)。