手头有一个项目要从 Node.js 服务不间断往 TDengine 写传感器数据,我第一反应是去翻官方文档里的“语言连接器”章节。结果发现 Node.js 这块内容更新得不算勤,社区里的帖子也经常是 Java、Python 刷屏,自己动手绕了些弯路才跑通。这篇就当作操作备忘整理出来,拿到的项目需求如果和我类似——技术栈是 Node.js,数据库侧是 TDengine 3.x——可以直接照着来。
我会尽量把翻车点写细。不是那种“官方文档复制一份”的教程,而是把连接器原理、API 选择、离线排错这些东西都摊开讲,至少让你在下班前能跑出第一批数据。
1. 动手写代码前,先想清楚连接器的定位和边界
很多人拿到 TDengine 就下意识想找一个大而全的 ORM,觉得数据库驱动就该像 Mongoose 一样把 model 层也做了,其实不是这么回事。TDengine 的定位是高性能时序数据库,语言连接器目前的主要任务就是:帮你把一条 SQL 或一堆参数发给服务端,再把返回结果翻译成你语言里方便处理的类型。它不负责业务建模,也不负责把某个对象自动映射成表结构。
从这个角度看,Node.js 连接器本质上就是个协议翻译层。你在 Node.js 进程里调 conn.query(...),连接器把 SQL 语句按 TDengine 的网络协议打包,发给服务端的 6030 端口或者 6041 端口,然后读回响应、拆包,再转成数组或对象。这个链路里更靠近“能不能连上”的,是网络端口和鉴权,而不是 Node 的版本。
我建议你先记住三个端口:原生连接一般走 6030,REST 接口走 6041,taosExplorer 的 Web 界面走 6060。如果你用 taosExplorer 3.3.7.1 连不上数据库,先别怀疑浏览器或插件,很可能就是 6030 这个端口根本没开或者被防火墙拦了。这个问题后面我专门有一节来说,因为在实际项目里它占了连接故障的一大半。
TDengine 和 MySQL、PostgreSQL 另一个明显差异是:建表通常要带时间戳列,而且实际写入经常是几十万、上百万条的高频时序数据。你拿 Node.js 连 TDengine,如果还用传统数据库的写法一条一条 insert,必然要挨打。第 5 节我会把批量写入和参数组织的逻辑一并讲,那才是实践里的核心点。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备清单:Node 版本、TDengine 实例与连通性检查
2.1 Node.js 版本怎么选才稳
先聊版本。Node.js 官方当前推荐偶数版本进入 LTS,比如 20、22,非特殊需求不建议用奇数开发版,更别提前沿版本。网上你可能会看到类似 error installing 24.20.0: node.js v24.20.0 is not yet released or is not available 的报错,这种场景多半是你用了 nvm、n 这类版本管理工具,手动指定了一个远端版本列表里还没同步的版本号。解决办法很简单:先执行 nvm ls-remote 确认版本真的存在,再安装,不要照着某篇旧博客手输一个未来版本号。
做 TDengine 的 Node.js 连接器开发,我实测用 Node.js 22 LTS 最省心。TDengine 官方 npm 包在某些版本对 Node 版本有下限要求,比如提示需要 Node.js 22.13 以上,但你如果选了个非 LTS 预览版,又可能碰到原生模块编译不兼容。建议统一用 LTS,装一次版本管理器,例如 Mac/Linux 下用 nvm,Windows 下用官方安装包或 nvm-windows。
如果你问 Windows 7 能不能装 Node.js 18,我可以直说:不太建议。Node.js 官方很早就停止支持 Windows 7,而 TDengine 3.x 的生态也尽量往现代系统上靠。开发机如果还是 Win7,系统层面的 TLS、网络库都可能出问题,不如直接在虚拟机里跑 Linux 环境,反而少很多坑。
2.2 把 TDengine 起起来并确认服务正常
假设你本地已经装好了 TDengine 3.3.7.1 或同期的 3.x 版本。启动服务后,第一件事不是开编辑器,而是先在命令行里确认数据库本体是活的。Linux/Mac 下直接执行:
bash复制taos
进入 taos shell 后执行:
sql复制SELECT server_version();
能返回 3.x.x.x 之类的版本号,说明服务端进程没问题。接着再创建一个最简单数据库,验证磁盘和权限没毛病:
sql复制CREATE DATABASE IF NOT EXISTS test_demo;
USE test_demo;
CREATE TABLE IF NOT EXISTS t1 (ts TIMESTAMP, val FLOAT);
INSERT INTO t1 VALUES (NOW, 1.23);
SELECT * FROM t1;
如果这几条语句都能跑通,说明 TDengine 本体安装成功。这一步别嫌多余,很多 Node.js 连接器调不通的案例,最后排查下来不是连接器问题,而是服务端压根没起来。
在 TDengine 3.x 里,你也可以用 taosExplorer 来做可视化管理。默认通过 http://localhost:6060 打开,登录后能看到监控、SQL 终端、数据查看等功能。它的作用有点像 MySQL 的 phpMyAdmin,排查连接问题、查看表结构时非常好用。
2.3 验证 REST 端口与原生端口是否可达
接下来需要验证 Node.js 进程要访问的网络端口。有两种连接模式,对应的端口不一样:
| 连接模式 | 端口 | 协议 | 典型使用场景 |
|---|---|---|---|
| 原生连接 | 6030 | TCP | 高性能写入、复杂 SQL、需要连接池的场景 |
| REST API | 6041 | HTTP/HTTPS | 跨网络、跨版本、快速验证、轻量运维 |
| taosExplorer | 6060 | HTTP | 可视化 Web 管理 |
先测 REST 端口。TDengine 默认鉴权用户是 root,默认密码是 taosdata,测试命令直接发一条 SQL:
bash复制curl -u root:taosdata -d "select server_version()" http://127.0.0.1:6041/rest/sql
能返回 JSON 说明 6041 这路没问题。如果你要测原生 6030,用 taos shell 能进去就算通,但注意 shell 本身可能走的也是客户端库而不是网络,所以更稳妥的做法是换一台机器或在 Node.js 代码里连一次。查端口占用你可以用:
bash复制# Linux / macOS
lsof -i:6030
ss -lntup | grep 6030
# Windows
netstat -ano | findstr :6030
看到 LISTEN 状态才说明服务端在监听。
如果你在 Node.js 服务里连 6030 返回 ETIMEDOUT 或 ECONNREFUSED,而用 taos shell 又能正常操作,那基本可以确定是监听地址或防火墙问题。这时去部署机检查 taos.cfg 里的 fqdn 和 serverPort 配置,把地址从 localhost 改成实际 IP,再放行对应端口。这一条排查路径在本地和服务器部署都适用。
3. REST 还是原生驱动:两条路线的取舍和代码骨架
3.1 官方原生连接器的真实状态
TDengine 官方提供的 Node.js 连接器包名是 taos,它是对 C 客户端库的一层封装。安装很简单:
bash复制npm install taos
但有几个点要留心。首先,它依赖本机存在 TDengine 客户端库或者能在安装阶段拉取到对应预编译产物,所以你在 Docker 或最小化 Linux 镜像里直接 npm install taos,很可能因为缺少系统依赖编译失败。其次,原生连接器对服务端版本相对敏感,比如 TDengine 2.x 和 3.x 在协议细节上就有差异,如果你用很新的 Node.js LTS 去连一个很老的 TDengine,连接阶段就容易报协议错误。
原生连接器的基本调用方式类似这样:
js复制const taos = require('taos');
const conn = taos.connect({
host: '127.0.0.1',
port: 6030,
user: 'root',
password: 'taosdata'
});
conn.query('SELECT server_version()', (err, res) => {
if (err) {
console.error('执行失败:', err.message);
return;
}
console.log('查询结果:', res);
});
这是偏文档化的示意写法,根据你安装的 taos 版本不同,回调里的 res 可能是一个结果集对象,也可能带 each、toArray 之类的方法。我的建议是:如果项目不是对单条 SQL 的延迟极其敏感,没必要在原生连接器上反复折腾 C 库编译问题,把精力放到 REST 上,收益率高得多。
3.2 RESTful API 才是团队落地最顺的一路
TDengine 的 RESTful API 是 POST /rest/sql,直接以 SQL 文本作为请求体。它最大的优点是去掉了本地原生库依赖,只要 Node.js 进程能访问 6041 端口,就能用。这意味着你的应用可以很轻松地部署在容器里,和数据库散落在不同主机上也没问题。连接器自身的标准库就能实现,不需要额外装 C 依赖。
一个最精简的封装大概是这样:
js复制const TD_BASE = 'http://127.0.0.1:6041';
const TD_AUTH = 'Basic ' + Buffer.from('root:taosdata').toString('base64');
async function tdquery(sql) {
const res = await fetch(TD_BASE + '/rest/sql', {
method: 'POST',
headers: {
'Authorization': TD_AUTH,
'Content-Type': 'text/plain'
},
body: sql
});
const json = await res.json();
if (json.code !== 0) {
throw new Error(`TDengine error: ${json.desc}`);
}
return json;
}
fetch 在 Node.js 18+ 里直接可用,不需要引 axios。如果你还在 Node 16,那要给 Node 升个级,别为了省事装十几个依赖包。
TDengine REST 返回的 JSON 我解释一下:
json复制{
"code": 0,
"column_meta": [
["ts", "TIMESTAMP", 8],
["val", "FLOAT", 4]
],
"data": [
[1640995200000, 1.23]
],
"rows": 1
}
code 非 0 就代表 SQL 执行失败;column_meta 每项分别是列名、类型和类型码;data 是具体行数据;rows 是总行数。查询类语句用这种结构很舒服。对于建库、建表这类 DDL,返回的 data 往往是空数组,判断 code === 0 即可。
3.3 选哪条路,主要看部署边界
我自己的选型判断可以给你参考:
- 如果 Node.js 进程和 TDengine 之间隔公网,或者运维要求应用层只暴露 HTTP 端口,那直接走 REST,省去原生驱动带来的安全面。
- 如果客户端要常驻、请求量很高,且你愿意处理原生连接的编译和版本兼容,那原生 6030 连接理论上是更短路径,延迟更低。
- 如果项目只是写写脚本、跑个批处理、做数据校验,无脑 REST。
另外提醒一点:RESTful 方式也有 /rest/sqlt 和 /rest/sqlutc 这两个变体,前者返回结果列头带数据类型,后者返回 UTC 时间戳字符串。默认 /rest/sql 返回的毫秒时间戳是数字,某些前端图表库接收没有问题,但如果做数据清洗和展示,用 /rest/sqlutc 拿 ISO 字符串往往更直观。
4. 第一个最小可运行 Demo:从建库到查询
4.1 先定数据库和表结构
说一千道一万,不如先跑通一个完整流程。我在下面用一个物联网场景做示范,把库名、表名、数据模型都约定清楚。定义入参是:每 10 秒采集一次设备的电压和温度,设备有唯一的设备 ID,并且设备会挂到不同分组。
此时就应该用“超级表 + 子表”的建模方式。超级表可以理解成一张模板表,定义了字段结构;子表是实际存放数据的表,每个子表通过标签关联某一台设备。TDengine 的行存和列存混合引擎处理这种模型非常合适。
先执行 DDL:
js复制await tdquery(`CREATE DATABASE IF NOT EXISTS iot
DURATION 180 KEEP 3650 BUFFER 256 WAL_LEVEL 2`);
await tdquery(`CREATE STABLE IF NOT EXISTS iot.sensor_data (
ts TIMESTAMP,
voltage FLOAT,
temperature FLOAT
) TAGS (
device_id BINARY(64),
group_id INT
)`);
这里几个参数解释一下:DURATION 180 表示每 180 天水会切分一个文件组;KEEP 3650 表示数据保留 3650 天,也就是大约 10 年;BUFFER 256 是写入缓冲的内存池大小单位;WAL_LEVEL 2 表示每次写入都强制刷 WAL,防止掉电丢数据。具体数值你可以根据业务改,但生产环境不建议把 KEEP 设成无限大,因为存储成本和查询效率要平衡。
4.2 写入:用子表承载设备数据
TDengine 的插入有个很强的时间序特性,它不希望你像 Mongo 那样把文档塞进一个大数组,而是希望一条 SQL 能直指某张子表。写入时如果子表还不存在,可以用 USING 语法按超级表模板自动建表:
sql复制INSERT INTO iot.d_1001 USING iot.sensor_data TAGS ('device_1001', 1)
(ts, voltage, temperature) VALUES
(NOW, 220.3, 25.1);
执行完可以立刻查询:
sql复制SELECT tbname, ts, voltage, temperature
FROM iot.sensor_data
WHERE device_id = 'device_1001'
AND ts >= NOW - 1h
ORDER BY ts DESC;
如果返回了你期待的数据,说明这一条链路已经通了:Node.js -> REST 端口 -> TDengine -> 子表 -> 查询。这是最核心的验证闭环。你后面的项目代码基本上就是把这个过程抽象成函数。
4.3 一段完整的脚本长什么样
把前面串起来写一个完整的 demo.mjs:
js复制const TD_BASE = 'http://127.0.0.1:6041';
const TD_AUTH = 'Basic ' + Buffer.from('root:taosdata').toString('base64');
async function tdquery(sql) {
const res = await fetch(TD_BASE + '/rest/sql', {
method: 'POST',
headers: {
'Authorization': TD_AUTH,
'Content-Type': 'text/plain'
},
body: sql
});
const json = await res.json();
if (json.code !== 0) {
throw new Error(`TDengine error: ${json.desc}`);
}
return json;
}
// 1. 建库和超级表
await tdquery(`CREATE DATABASE IF NOT EXISTS iot
DURATION 180 KEEP 3650 BUFFER 256 WAL_LEVEL 2`);
await tdquery(`CREATE STABLE IF NOT EXISTS iot.sensor_data (
ts TIMESTAMP,
voltage FLOAT,
temperature FLOAT
) TAGS (
device_id BINARY(64),
group_id INT
)`);
// 2. 写入一条数据
await tdquery(`INSERT INTO iot.d_1001 USING iot.sensor_data
TAGS ('device_1001', 1) VALUES (NOW, 220.3, 25.1)`);
// 3. 查询最近 10 分钟的数据
const result = await tdquery(`SELECT tbname, ts, voltage, temperature
FROM iot.sensor_data
WHERE device_id = 'device_1001'
AND ts >= NOW - 10m`);
console.log(result.data);
这段代码假定项目是用 .mjs 或者在 "type": "module" 模式下运行。如果 CommonJS 环境,把 import 换成 require 即可。
跑完你会看到类似输出:
text复制[ [ 'd_1001', 1711080000000, 220.3, 25.1 ] ]
ts 是毫秒时间戳,把它 new Date(1711080000000) 就得到可读时间。到这里,最小工程已经完成。
5. 写入性能优化:别再做“一条一条 insert”了
时序数据库场景,写入性能基本是第一优先级。我见过不少新手第一次用 TDengine 时,用循环把每条数据执行一次 SQL,比如:
js复制for (const point of points) {
await tdquery(`INSERT INTO ... VALUES (...)`);
}
如果只有几十条数据倒无所谓,但如果是每分钟几万条上报,这种写法会产生巨大网络开销和 SQL 解析开销,服务端连接也被无谓消息打满。
正确姿势是攒一批再发,一条 SQL 里带多个 value:
sql复制INSERT INTO iot.d_1001 USING iot.sensor_data TAGS ('device_1001', 1)
VALUES
(NOW, 220.1, 25.1),
(NOW + 1s, 220.2, 25.2),
(NOW + 2s, 220.4, 25.3);
如果涉及多个设备,也可以在同一条 SQL 里交替写入不同子表:
sql复制INSERT INTO iot.d_1001 USING iot.sensor_data TAGS ('device_1001', 1) VALUES (NOW, 220.1, 25.1)
iot.d_1002 USING iot.sensor_data TAGS ('device_1002', 2) VALUES (NOW, 229.1, 26.1);
这种方式可以显著降低 RPC 往返次数。我在Node.js 的网关服务里一般会维护一个待提交队列,例如每秒聚一次,达到 200 条或积压 1 秒就整批量提交。如果还想更快,可以试试官方提供的参数绑定功能,通过预处理语句方式传入值,减少服务端解析 SQL 的消耗。参数绑定在 Java 连接器里已经比较成熟,Node.js 连接器你可以在原生 taos 包里找对应接口,REST 模式目前仍以文本 SQL 为主。
还要注意时间戳的精度。TDengine 默认数据库精度是毫秒,你在 SQL 里写 NOW 服务端会按库精度解析。如果业务里用秒级或微秒级时间,建库时要显式声明 PRECISION 'us' 或 PRECISION 'ns'。坑点是不同连接器对时间戳字符串的格式处理不一致,尤其在 REST 模式下,2025-04-01 10:00:00 会被当成本地时区,如果你希望按 UTC 写入,可以考虑用 REST 的 UTC 接口或显式带时区偏移。
在这个环节我的建议是:基础业务验证用 REST 文本 SQL 完全够,但需要极致写入吞吐时,优先把数据切到原生连接、主动控制批量大小,然后对磁盘 I/O、网络缓冲区、WAL 写入策略分别打点观察,别一味怪连接器。TDengine 服务端 taosdump、taosBenchmark 这些工具也能帮助你测试出基准值,好判断瓶颈到底在客户端还是服务端。
6. 运行一段时间后必然踩到的坑与排错思路
6.1 经典报错查因表
有几个报错在群里几乎天天看到,我直接整理成表格,遇到的时候不用慌:
| 报错片段 | 常见原因 | 第一步处理方向 |
|---|---|---|
ETIMEDOUT / connect ECONNREFUSED |
6030/6041端口不通 | 检查服务状态、防火墙、监听 IP 和端口 |
401 Unauthorized |
REST 请求鉴权信息错误 | 检查 root:taosdata 账号密码,改成实际密码 |
Database does not exist |
SQL 里写了不存在的库,或没加库名前缀 | 用 SHOW DATABASES 确认,SQL 改成 dbname.表名 |
Syntax error near 'xxx' |
SQL 拼写问题 | REST 请求不要把分号 ; 带上 |
Client version is too old |
原生连接器版本和 server 版本差太多 | 升级 npm taos 包,或改用 REST |
Unable to resolve fqdn |
原生连接把 host 当成 FQDN解析 | 用 IP 连,或保证 hosts 里配置了 FQDN |
其中“REST 请求里带了分号”我单独说一下,这个是真高频。写本地 SQL 习惯性结尾加分号没问题,但通过 POST body 提交给 /rest/sql 时,某些版本会直接解析失败或行为异常。封装函数时最好把 body 做一次 sql.trim().replace(/;$/, ''),省掉一堆麻烦。
还有个坑发生在 .mjs 文件里使用 fetch:如果你的 Node.js 版本比较老且没有全局 fetch,会报 fetch is not defined。这不是连接器的问题,是运行时缺 API。解决方式就是换 Node 18+,不要在项目里乱引入 polyfill,避免与全局变量的冲突。
6.2 端口被占用后的自查思路
连接器报错看着像代码问题,但十次里有六次其实出在端口或网络。比如你本机起了另一个服务占了 6030,taos shell 还能连,因为 shell 可能走了 unix socket 或者连接写到了不同实例;Node.js 连接走 TCP 就被别的进程劫持了。遇到这种先执行:
bash复制# Linux / macOS
lsof -i:6030 -P -n
# Windows
netstat -ano | findstr :6030
如果发现 6030 被一个不相干 PID 占用,那你就要去检查机器上是否装了多个 TDengine 或者有残留的守护进程。最典型的场景是用户装了多个版本的 TDengine 服务,旧版本没停干净,新版本也起不来,结果监听端口被旧进程占据。这时候先停掉所有 taosd 相关服务,再用系统服务管理工具统一拉起你需要的那个版本。
另一类情况是 Node.js 应用自己的端口被占,比如你想让 Node 服务监听 3000,但提示端口被占用。这个不是连接器范畴,但新手很容易混淆:其实只要换 Node 服务监听端口即可,不必动 TDengine。
6.3 容器化部署时对连接器的影响
如果你把 Node.js 服务打包进 Docker,再去连一台独立部署的 TDengine,有几个特别容易踩的坑。容器里没有你本机的 C 客户端库,所以原生 taos 包基本不可用或需要额外把客户端库 COPY 进镜像。这时候 REST 模式就显示出绝对优势:基础镜像不用安装任何 TDengine 相关依赖,只要网络能通 6041 即可。
写容器网络时也注意别把连接地址写成 localhost,因为 localhost 在容器里是指容器自身。要连宿主机或另外的服务,得写成宿主机 IP 或同 Compose 网络里的服务名。这个我在生产环境不止一次碰到,排错了半天才发现是地址解析问题。
如果你需要在没有 Node.js 的电脑上运行最终应用,那可以把应用打成可执行文件,但尽量用 REST 连接器来避免原生库缺失。比如用 pkg 或 Node SEA 打包后,原生模块的 .node 文件往往很麻烦;而 REST 模式只依赖 Node 内置的 fetch,打包后运行稳定得多。
6.4 连接 TDengine 集群时的认知准备
如果你手里的 TDengine 已经做了集群部署,而不是单机版,那也要明白连接器本身并不会帮你做完整的 Service Discovery。原生和 REST 连接通常都是指定某一组地址。在集群场景,你可以把连接地址配置为任意一个可达的 data node 地址,实际数据会按 vnode 分布在多个节点上;具体响应延迟和节点路由通常由服务端和数据分片决定。
换句话说,Node.js 连接器更关心“请求发给谁”,而不是“请求的数据在哪”。部署上最好有一层内网负载均衡,把多个 HTTP 地址或原生地址放进一个统一的访问入口,避免单点故障。连接器里的连接池、超时重试也要由你在应用层自己写好,不建议把一个 SQL 请求无限重试,那很容易把集群拖垮。
7. 收尾:连接器只是第一步,数据模型和时间戳才是大头
回头来看,Node.js 连接 TDengine 真正的大头其实不是 API 调用,而是搞清楚三件事:连接模式选 REST 还是原生,时序表用超级表还是普通表,时间戳精度怎么统一。连接器在这三者里承担的只是一个“信道”作用,把 SQL 送过去、把结果取回来,仅此而已。
我个人踩过最深的一个坑,是写了一个看起来非常可靠的写入模块,却因为外部系统上报的时间戳偶尔为 0,导致 TDengine 直接插入了 1970-01-01 00:00:00 的时间点。这种脏数据会在后续聚合查询时让你整个数据区间都失真。后来所有写入链路都强制判断了时间戳范围,非法的直接丢弃,不再无脑传给 SQL。这个经验也算不上什么高超技巧,但能少丢不少头发。如果你的项目里也接了各种异构设备,建议在调连接器之前先把数据清洗规则定好,否则等数据进库再修,成本只会翻倍。
