TDengine C++ 系列(1):全景与最小闭环——从时序数据到第一个 C++ 读写

核心目标:理解 TDengine 解决什么、不解决什么;看懂"客户端 → taosd(mnode/vnode)→ WAL → 内存表 → 落盘"的写入链路;用 C++ 完成一次可观察、可验证、可清理的最小读写闭环。

前置知识:C++11/17(RAII、异常、STL);基础 SQL;了解 TCP 客户端-服务器模型。

验证环境:TDengine 3.4.x(正文基线 3.4.1.6,文档 v3.4.2);taosc(taos.h);C++17;CMake ≥ 3.20;Docker(服务端)或 Linux/Windows 本机安装。


0. 本篇问题场景

你负责一套 EMS(能量管理系统)的数据存储:变电站里上千台设备,每台每秒上报一组电压、电流、功率,一年下来是千亿级的数据点。之前用 MySQL 一张大表硬扛,几个月后:

  • 写入越来越慢,采集端开始积压;
  • 查询"某台主变最近 1 小时负荷曲线"要扫全表,报表接口动不动超时;
  • 磁盘占用失控,压缩率惨不忍睹。

于是你想换一个"时序数据库"。但选择很多:InfluxDB、OpenTSDB、TimescaleDB、Prometheus、TDengine......每个都说自己快。

本篇不急着比较基准测试,先回答三个更基础的问题:

  1. 时序数据到底特殊在哪,为什么 MySQL 会天然吃亏;
  2. TDengine 的定位与组件是什么,它凭什么在"写多、按时间查、只追加"的场景里占优;
  3. 一条数据从 C++ 程序发出到落盘,中间发生了什么------这条链路将贯穿整个系列。

最后我们用 C++ 跑通第一个最小闭环:连接 → 建库 → 建超级表 → 写入 → 查询。


1. 心智模型与关键概念

1.1 时序数据三要素

任何一条时序数据都可以拆成三个部分:

要素 含义 EMS 示例
时间戳 timestamp 数据产生/采集的时刻 2026-08-01 00:00:00.123
指标 metric 连续或离散的测量值 电压 110.2kV、有功 4.32MW
标签 tag 描述"谁产生的"静态属性 设备 d_0001、站点 华东-站A、电压等级 110kV

三要素的角色完全不同:时间戳是主键和排序键,指标是被压缩和聚合的对象,标签是过滤和分组的维度。所有时序数据库的优化都建立在这个结构之上。

1.2 TDengine 定位:不是"更快一点的 MySQL"

把 TDengine 和几个常被拿来对比的系统放在一张表里看:

系统 核心定位 数据模型 强项 弱项
MySQL/PostgreSQL 关系型 OLTP 表 + 行 + 强约束 事务、复杂关联、灵活 DDL 海量时序写入/压缩/时间聚合吃力
Redis 内存键值/缓存 key-value 低延迟缓存、计数器 容量、持久化成本高
InfluxDB 时序数据库 measurement + tag + field 生态成熟、查询灵活 集群版闭源,高基数场景弱
OpenTSDB 时序数据库(HBase 之上) metric + tag 大规模扩展 运维复杂、查询延迟高
Prometheus 监控指标库 指标 + label,拉取模型 服务发现、告警、生态 存储/查询是监控场景专用,不适合业务数据
ClickHouse 分析型列存 表 + 分区 极快分析扫描、压缩 单行写入/更新弱,运维重
TDengine 时序数据库(写入优先) 库 + 超级表 + 子表 超高写入吞吐、时序压缩、库内订阅/流计算/缓存 生态仍在成长,不适合复杂事务

TDengine 的关键判断是:时序数据是"写多读少、只追加、按时间有序、按设备分片"的。它把每个采集点(设备)的数据按时间有序存放、单独压缩,从根上避免了 MySQL"随机写 + 全表扫描"的困境。

1.3 3.x 组件全景

TDengine 3.x 的安装包里不止一个数据库引擎:

复制代码
┌─────────────────────────────────────────────────────────┐
│ 应用(C++ / 其他语言客户端)                               │
│   native 连接(6030,taosc 直连 taosd)                   │
│   WebSocket / REST(6041,经 taosAdapter)               │
└─────────────────────────┬───────────────────────────────┘
                          │
┌─────────────────────────▼───────────────────────────────┐
│ taosd(核心服务,单进程)                                  │
│   mnode 元数据 │ vnode 数据 │ qnode 查询 │ snode 流/订阅   │
│   WAL → 内存表 → 落盘(时间分区 + 列存 + 压缩)             │
└──────────────┬──────────────────────────────┬───────────┘
               │                              │
        taosKeeper(监控采集)         taosAdapter(REST/WebSocket/兼容协议)
        taosExplorer(Web 管理,企业版)  taosX(数据管道,企业版)
        taos CLI(taos shell)            taosBenchmark / taosdump / taosgen(工具)
  • taosd:一切数据操作的核心守护进程。3.x 在一个进程里按角色拆分:mnode 管元数据(库/表/标签定义),vnode 管数据分片与落盘,qnode 管查询执行,snode 管流计算与订阅。后面 Part 9 会展开集群形态。
  • taosAdapter:应用与集群之间的"桥",提供 RESTful 与 WebSocket 接口(端口 6041),并兼容 InfluxDB/OpenTSDB 写入协议。
  • taosKeeper:采集 taosd 运行指标并写入监控库,配合 Grafana 面板 TDinsight 使用。
  • taos CLI(taos :官方命令行,本系列最重要的观察与验证工具(不是主线语言,但每个结论都要用它验证)。
  • taosBenchmark / taosdump / taosgen:压测、备份恢复、造数工具。

端口约定6030(taosd native)、6041(taosAdapter REST/WebSocket)。记住这两个端口,排查连接问题时最先看它们。

1.4 执行链路:一条数据的一生

写入路径(重点,后面每个性能结论都从这里推导):

复制代码
C++ 客户端
  │  taos_query(conn, "INSERT ...") / taos_stmt 批量绑定
  ▼
taosd(native 6030 或经 taosAdapter 6041)
  │  鉴权 → SQL 解析
  ▼
mnode 路由:子表属于哪个 vgroup
  ▼
vnode(数据分片)
  │  ① 写 WAL(预写日志,可配置 fsync 策略)
  │  ② 写内存表(memtable)
  ▼
异步合并:内存表达到阈值 → 落盘为时间分区内的列存文件 → 后台合并/压缩

查询路径:

复制代码
C++ 客户端 → taosd → qnode 生成执行计划
  → vnode 并行扫描(时间范围裁剪 → 列裁剪 → 预计算过滤)
  → 聚合/排序 → 结果回传客户端

从这条链路能立刻推导出三条铁律,后面会反复用到:

  1. 时间戳必须是首列主键------因为存储和扫描都按时间有序组织,没有它一切优化都不成立;
  2. 查询必须尽量带时间范围------没有时间裁剪的查询等于全量扫描;
  3. 写入是"顺序追加 + 合并"------所以乱序数据(迟到的时间戳)会打断有序性,付出额外代价(Part 6 详细量化)。

2. 最小可运行示例

2.1 用 Docker 起一个 TDengine

本系列服务端统一用官方 Docker 镜像(可复现、便于后续多节点编排):

bash 复制代码
docker run -d --name tdengine -p 6030:6030 -p 6041:6041 \
  tdengine/tdengine:3.4.1.6

# 进入 taos CLI 验证
docker exec -it tdengine taos

taos 里核对版本并试一条 SQL:

sql 复制代码
SELECT SERVER_VERSION();
CREATE DATABASE demo KEEP 90 PRECISION 'ms';
USE demo;
CREATE STABLE meters (ts TIMESTAMP, current FLOAT, voltage FLOAT)
  TAGS (location BINARY(24));
INSERT INTO d_1001 USING meters TAGS ('beijing') VALUES (NOW, 10.2, 219.9);
SELECT LAST_ROW(*) FROM meters;

看到结果后 QUIT 退出。注意:taos CLI 只是验证手段,系列主线始终是 C++。

Windows 用户:官方提供 Windows 安装包(含服务端与 taos.h 客户端库),可直接本机开发验证;行为差异见第 6 节。

2.2 第一个 C++ 程序

本系列代码都在 ems-lab 项目里(tdengine/ems-lab/),先看最小的闭环 src/examples/part1_minimal.cpp 的核心逻辑:

cpp 复制代码
#include "ems_lab/td_conn.h"
using namespace ems_lab;

int main() {
    try {
        TdConn conn(config_from_env());              // 1) 连接(环境变量注入配置)

        conn.exec("CREATE DATABASE IF NOT EXISTS ems KEEP 90 DAYS 30 BUFFER 256 "
                  "WAL_LEVEL 1 PRECISION 'ms' CACHEMODEL 'both' CACHESIZE 1 VGROUPS 4");
        conn.exec("USE ems");                         // 2) 建库

        conn.exec("CREATE STABLE IF NOT EXISTS telemetry (ts TIMESTAMP, "
                  "ua FLOAT, ub FLOAT, uc FLOAT, ia FLOAT, ib FLOAT, ic FLOAT, "
                  "power FLOAT, freq FLOAT) "
                  "TAGS (station BINARY(32), device_type BINARY(24), "
                  "voltage_level BINARY(16), vendor BINARY(32))");  // 3) 建遥测超级表

        conn.exec("INSERT INTO d_0001 USING telemetry TAGS "
                  "('华东-站A', 'main_transformer', '110kV', 'ACME') "
                  "VALUES (NOW, 110.2, 109.8, 110.0, 12.5, 12.6, 12.4, 4.32, 50.01)");  // 4) 写入

        TAOS_RES* res = conn.query("SELECT LAST_ROW(*) FROM telemetry");  // 5) 查询最新一条
        TAOS_ROW row;
        while ((row = taos_fetch_row(res)) != nullptr) { /* 按列类型打印 */ }
        taos_free_result(res);                          // 6) 释放结果集
    } catch (const std::exception& e) {
        std::fprintf(stderr, "错误: %s\n", e.what());
        return 1;
    }
    return 0;
}

编译与运行:

bash 复制代码
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j

export EMS_HOST=localhost EMS_PORT=6030 EMS_USER=root EMS_PASS=taosdata EMS_DB=ems
./build/part1_minimal

预期输出(列名 + 一行数据):

text 复制代码
ts	ua	ub	uc	ia	ib	ic	power	freq	station	device_type	voltage_level	vendor
1785542400000	110.200	109.800	110.000	12.500	12.600	12.400	4.320	50.010	华东-站A	main_transformer	110kV	ACME
最小闭环完成

对照看一遍这条链:taos_connect(TCP 连接 6030)→ CREATE DATABASE(经 mnode 建元数据)→ CREATE STABLE(建超级表模板)→ INSERT ... USING ... TAGS自动建子表 d_0001 并写入)→ SELECT LAST_ROW(*)(取每个设备最新一行)→ taos_free_result(释放)。

2.3 三个 API 事实(3.x 与 2.x 不同)

写代码前必须知道三件 3.x 特有的 API 事实(Part 5 会展开):

  1. taos_query 直接返回结果集 TAOS_RES*。2.x 里要先 taos_querytaos_use_result 取结果;3.x 没有 taos_use_result,一条 SQL 对应一个结果集指针。
  2. SQL 失败时 taos_query 不返回 NULL ,而是返回一个"带错误的结果集"------必须用 taos_errno(res) != 0 判断,用 taos_errstr(res) 取信息。
  3. 连接失败返回 NULL ,错误信息通过 taos_errno(NULL) / taos_errstr(NULL) 获取。

这正是 TdConn::query() 的封装逻辑:

cpp 复制代码
TAOS_RES* TdConn::query(const std::string& sql) const {
    TAOS_RES* res = taos_query(conn_, sql.c_str());
    const int code = taos_errno(res);
    if (code != 0) {
        const char* msg = taos_errstr(res);
        taos_free_result(res);
        throw TdException("SQL 执行失败: " + std::string(msg ? msg : "unknown") +
                              " | SQL: " + sql, code);
    }
    return res;
}

3. 机制拆解:链路、时间主键与资源代价

3.1 写入链路逐层看

一条 INSERT 经过的每一层都回答一个问题"这一步花了什么代价":

做什么 代价/可配置项
客户端 组 SQL / 参数绑定、网络发送 网络往返(RTT)------所以批量写入远快于逐条写
taosd 鉴权、SQL 解析、路由 CPU
vnode 写 WAL → 写内存表 WAL 落盘策略(wal_level/fsync)决定数据安全与吞吐的取舍
后台 内存表阈值触发 → 落盘 → 合并压缩 决定写入峰值后多少数据在内存、多久落盘

理解这个分层后,几个现象就说得通了:

  • 批量写入为什么快:10 万行分 100 批 vs 逐条 10 万次网络往返,后者的 RTT 开销是前者的上千倍(Part 6 给量化数据);
  • wal_level 调低为什么写入更快但更险:WAL 不落盘/延迟落盘时,进程崩溃可能丢最近数据;
  • 刚写入的数据为什么能马上查到:数据在内存表里就可读,落盘是后台异步行为。

3.2 时间戳主键:硬约束还是特性?

TDengine 里每张表的第一个列必须是 TIMESTAMP。这不是限制,而是整个存储设计的地基:

  • 数据按时间有序写入 → 追加友好,天然支持 LSM 式合并;
  • 按时间分区(days)→ 过期数据整块淘汰(keep),查询按时间裁剪;
  • 时间戳做索引 → 范围查询 O(log n + 结果集) 而非全表扫。

代价也要知道:同一张表内时间戳必须唯一,重复时间戳写入会覆盖旧值(Part 3 验证);乱序时间戳会触发排序合并,性能下降(Part 6 量化)。

3.3 建库参数:先理解再照抄

CREATE DATABASE ems KEEP 90 DAYS 30 BUFFER 256 WAL_LEVEL 1 PRECISION 'ms' CACHEMODEL 'both' CACHESIZE 1 VGROUPS 4 里每个参数都有明确含义:

参数 本例 含义
KEEP 90 90 天 数据保留期,过期数据被自动清理
DAYS 30 30 天 每个数据文件覆盖的时间跨度(时间分区)
BUFFER 256 256 MB 每个 vnode 内存表缓冲上限
WAL_LEVEL 1 1 WAL 落盘策略(0/1/2,Part 6/11 详解)
PRECISION 'ms' 毫秒 时间戳精度 ms/us/ns,建库后不可改
CACHEMODEL/CACHESIZE both/1 最新值缓存(Read Cache),Part 8 详解
VGROUPS 4 4 数据分片数,Part 9 集群篇展开

Part 2 会逐个讲清楚"何时该改、改了有什么代价"。


4. ems-lab 工程实战:项目骨架与第一个封装

4.1 项目结构

按写作计划的 4.2 约定,ems-lab 从本篇开始逐步长成完整形态,当前只有最小骨架:

text 复制代码
ems-lab/
├── CMakeLists.txt               # TAOS_HOME 定位 taos.h/libtaos;core 库 + 示例 + 测试目标
├── README.md                    # Docker 验证环境、编译运行说明
├── src/
│   ├── ems_lab/
│   │   ├── settings.h           # 环境变量名(EMS_HOST/PORT/USER/PASS/DB)
│   │   ├── td_conn.h            # ConnConfig / TdException / TdConn
│   │   └── td_conn.cpp
│   └── examples/
│       └── part1_minimal.cpp    # 本篇最小闭环
├── scripts/
│   └── seed.cpp                 # 四遥数据模拟器(Part 2 引入)
└── tests/
    └── test_conn.cpp            # 集成测试(需要真实服务)

4.2 TdConn 封装的三条原则

TdConn 是系列里所有代码的地基,封装时坚持三条原则(也是 Part 5 的主题):

  1. RAII 管生命周期 :构造即连接、析构即关闭,禁止拷贝、支持移动。忘记 taos_close 的后果是连接句柄泄漏(Part 5 有失败实验);
  2. 失败即异常 :连接失败抛 TdException(带错误码),SQL 失败同样抛------调用方不需要记住"先查 errno 再 free"的顺序,封装里处理干净;
  3. 配置走环境变量config_from_env()EMS_* 读取连接信息,代码与文档里不出现硬编码凭据。

4.3 用 taos CLI 做对照验证

C++ 程序跑完后,用 taos CLI 验证同一件事,建立"代码 ↔ 真实数据库"的信任:

sql 复制代码
USE ems;
SHOW STABLES;            -- 应看到 telemetry
SHOW TABLES;             -- 应看到自动建出的 d_0001
DESCRIBE telemetry;      -- 列定义与标签
SELECT COUNT(*) FROM telemetry;  -- 1

建议习惯:每篇的每个结论都用 CLI 输出留档,这是系列"可复验"要求的落地方式。


5. 失败实验与根因

5.1 连接不存在的端口

cpp 复制代码
ConnConfig bad = config_from_env();
bad.port = 6031;   // 故意连错
TdConn conn(bad);  // 抛 TdException

现象:TdException 携带错误码(网络不可达类)与信息 连接 TDengine 失败: ... (host=..., port=6031)

根因与排查:taos_connect 返回 NULL。依次检查------服务是否启动(docker ps)、端口映射是否生效(6030)、防火墙是否放行。这也是项目里 test_conn.cpp 的第一个用例:错误端口必须抛异常,而不是静默返回。

5.2 未建库直接查询

cpp 复制代码
TdConn conn(cfg);                       // 登录时 db 为空
conn.query("SELECT * FROM telemetry");  // 抛异常

现象:错误信息形如 Database not specified(未指定数据库)。

根因:telemetry 属于 ems 库,但当前会话没有 USE ems,解析器找不到该表。修法:连接时带 EMS_DB,或执行 USE ems,或在 SQL 里写全限定名 ems.telemetry

5.3 时间戳缺失 / 重复

sql 复制代码
-- 缺时间戳:VALUES 里不写 ts 但表要求首列为 ts → 语法/列数错误
INSERT INTO d_0001 USING telemetry TAGS (...) VALUES (110.2, 109.8, ...);
-- 重复时间戳:同一子表写两次相同 ts → 第二次覆盖第一次
INSERT INTO d_0001 USING telemetry TAGS (...) VALUES (NOW, ...);
INSERT INTO d_0001 USING telemetry TAGS (...) VALUES (NOW, ...);  -- 覆盖!

根因:时间戳是主键。第一例违反"首列必须为时间戳";第二例触发"同表同 ts 覆盖"语义。这个覆盖语义在生产里是把双刃剑:重放数据时能幂等,但也意味着业务侧必须保证"同一设备同一时刻只有一个值"(Part 3 详述)。


6. 版本与环境差异

维度 说明
3.4.x(主线) 正文基线 3.4.1.6;3.4.0 起社区版/企业版客户端驱动不兼容,需按发行版安装对应 taosc
3.3.6(LTS) 官方标注 LTS,生产可选;与 3.4.x 的差异(新函数、流引擎等)在对应篇目标注
2.x 已不在本系列主线;taos_use_resulttaos_field_count 等 2.x API 与 3.x 不同,迁移见 Part 12
Docker 推荐验证方式,tdengine/tdengine:3.4.1.6 镜像,多节点编排 Part 9
Linux 官方包安装:/usr/local/taos(include/ + driver/libtaos.so),systemd 管理 taosd
Windows 官方安装包含服务端与客户端;taos.h 位于安装目录 include/,链接 taos.lib;本机验证可用,生产建议 Linux

发布前核对 :文中版本号、API 与行为以官方文档与所装版本的 taos.h 为准;升级版本后先重跑 test_conn


7. 测试与验收

7.1 自动化测试(test_conn.cpp

集成测试需要真实服务(README 的 Docker 方式):

bash 复制代码
ctest --test-dir build --output-on-failure

测试覆盖两条路径:

  • 失败路径 :错误端口连接必须抛 TdException(且错误码非 0);
  • 成功路径 :建独立库 ems_test → 建超级表 → 写 2 行 → COUNT(*) 断言 = 2 → DROP DATABASE ems_test 清理。

7.2 本篇验收清单

  • Docker 启动 TDengine 3.4.x,taos CLI 能 SELECT SERVER_VERSION()
  • part1_minimal 编译运行,输出 LAST_ROW 结果与预期一致;
  • 用 taos CLI 复验:SHOW STABLES / SHOW TABLES / DESCRIBE telemetry / COUNT(*)
  • 3 个失败实验全部复现并能解释根因(连接失败 / 未建库 / 时间戳覆盖);
  • test_conn 全部 PASS(含错误端口用例);
  • 能画出并口头解释"写入链路"与"查询链路"两张图。

8. 常见误区

误区 事实
"TDengine 是更快一点的 MySQL,可以直接迁移表结构" 数据模型完全不同:必须"时间戳主键 + 一表一设备 + 标签",Part 2 专门讲
"taos_query 失败返回 NULL,判空就行" 3.x 返回带错误的结果集,必须 taos_errno(res) 判断
"连接用完不 close,进程退出会自动回收" 长连接服务必须显式关闭,否则句柄泄漏,最终连接数打满(Part 5 验证)
"建库参数照抄官方示例就行" KEEP/DAYS/PRECISION/VGROUPS/WAL_LEVEL 每个都影响成本与行为,Part 2/11 逐个讲
"时序库就是不停 INSERT,不用管顺序" 同表同 ts 会覆盖;乱序写入有性能代价
"taos CLI 能跑通,代码一定没问题" CLI 与代码走同一协议,但连接管理、错误处理、资源释放只有代码才体现

9. 本篇小结

回到开篇的三个问题:

  1. 时序数据特殊在"三要素":时间戳是主键、指标被压缩聚合、标签做过滤分组。MySQL 的随机写 + 行存 + 全表扫描在这个形态下天然吃亏;
  2. TDengine 的定位:写优先的时序数据库,靠"时间有序 + 列存 + 时序压缩 + 库内订阅/流计算/缓存"换取写入吞吐与存储效率;组件上记住 taosd(6030)与 taosAdapter(6041)两个端口;
  3. 写入链路:客户端 → taosd → mnode 路由 → vnode(WAL → 内存表)→ 异步落盘合并。从链路推导出"时间戳主键""查询带时间范围""批量写入"三条铁律,整个系列都会复用。

你已经在 C++ 里跑通了最小闭环,并掌握了 3.x 的 API 事实(taos_query 直接返回结果集、错误检查 taos_errno(res)、连接失败查 taos_errno(NULL))。下一步是系列最重要的一课:数据模型设计------四遥数据到底该建几张超级表、标签怎么放、精度怎么选。

10. 官方资料

相关推荐
生活皆是风景2 小时前
抢占AI采信,问答布局正当时
大数据·安全·搜索引擎
生态学者2 小时前
Journal of Applied Ecology | 华南植物园王法明研究员团队揭示加纳红树林蓝碳储量及其环境调控机制
大数据·r语言·微信公众平台
MC皮蛋侠客2 小时前
TDengine C++ 系列(5):C/C++ 连接器——连接管理与查询 API
c语言·c++·tdengine
晓天衡宇•评测社区2 小时前
大语言模型 8 月榜单更新:Claude Opus 5 登顶,Gemini 3.6-Flash 与 DeepSeek-V4 Flash 展现差异化优势
大数据·人工智能
MC皮蛋侠客2 小时前
TDengine C++ 系列(8):流式计算与最新值缓存——库内实时处理
c++·缓存·tdengine
aiqianzhan2 小时前
采购数字化选型:智慧采购平台五维对照与常见路线
大数据·人工智能
2603_965148112 小时前
eBay商品数据API:寻找海外仓与价格洼地
大数据·人工智能·windows·python·microsoft
*.✧屠苏隐遥(ノ◕ヮ◕)ノ*.✧10 小时前
C++学习:基础知识的掌握
c++·visualstudio
Web3_Daisy11 小时前
Pump.fun 与 FOMO 竞争背后的 Meme 市场变局
大数据·人工智能·金融·web3·区块链