分类: 系统架构 > 端到端消息流

适用版本:TDengine v3.x(v3.3.x / v3.4.x) | 最后更新:2026-05-16
概述
本文追踪一条 SQL 语句从用户输入到结果返回的完整端到端路径,串联前 6 篇文章中讲解的各个组件(taosc、RPC、MNode、VNode、Raft、存储引擎),让读者建立全局视角。
我们分别追踪三类典型操作:
- 写入操作 :
INSERT INTO的完整链路 - 查询操作 :
SELECT的完整链路 - DDL 操作 :
CREATE DATABASE的完整链路
核心概念速查表
| 概念 | 说明 |
|---|---|
| taosc | 客户端驱动库,负责 SQL 解析、路由计算、请求分发 |
| Catalog | 客户端的元数据缓存(VGroup 路由、表 Schema 等) |
| Scheduler | 客户端的查询调度器,将物理计划拆分为子任务并分发 |
| VGroup 路由 | 根据表名 hash 确定数据所在 VGroup 及其 Leader 地址 |
| 消息类型 | 标识操作类型的枚举值(如 TDMT_VND_SUBMIT、TDMT_SCH_QUERY) |
| QNode | 可选的独立查询节点,分担 VNode 的查询计算负载 |
详细解析
1. 写入操作全链路
以 INSERT INTO d1001 VALUES (now, 10.3, 219, 0.31) 为例:
写入全链路(14 步):
┌─────────────────────────────────────────────────────────────────┐
│ 客户端 (taosc) │
│ │
│ ① 用户调用 API(如 taos_query) │
│ ② SQL 解析:识别为 INSERT 语句,提取数据库名、表名、列值 │
│ ③ Catalog 查询: │
│ - 表 d1001 的 Schema(列类型、列数)→ 用于数据编码 │
│ - 表 d1001 所属 VGroup 及其 Leader 地址 │
│ (如果缓存命中则跳过网络请求,否则向 MNode 拉取) │
│ ④ 数据编码:按列类型将值序列化为二进制提交格式 │
│ ⑤ 构建 SUBMIT 消息,通过 RPC 发往 VGroup Leader │
└───────────────────────────────────┬─────────────────────────────┘
│
▼ RPC(TCP)
┌─────────────────────────────────────────────────────────────────┐
│ VNode (VGroup Leader) │
│ │
│ ⑥ RPC 层接收消息,入写队列 │
│ ⑦ 写队列 Worker 取出消息 │
│ ⑧ Raft 提议: │
│ - 构建 Raft 日志条目 │
│ - 写入本地 WAL │
│ - 发送 AppendEntries 到 Follower │
│ ⑨ 等待多数派确认(Follower WAL 写入成功后回复) │
│ ⑩ Commit Index 推进 │
│ ⑪ 应用到状态机: │
│ - 解码提交数据 │
│ - 检查/自动创建子表(如果使用 USING 语法) │
│ - 写入 MemTable(跳表结构) │
│ - 触发流计算(如果有关联 Stream) │
│ - 触发 RSMA(如果数据库启用了 Rollup) │
│ ⑫ 构建响应(成功 + 影响行数) │
└───────────────────────────────────┬─────────────────────────────┘
│
▼ RPC 响应
┌─────────────────────────────────────────────────────────────────┐
│ 客户端 (taosc) │
│ │
│ ⑬ 收到响应,检查状态码 │
│ - 成功 → 返回影响行数给用户 │
│ - VGroup 迁移/Leader 切换 → 刷新 Catalog,自动重试 │
│ - 其他错误 → 返回错误码给用户 │
│ ⑭ 用户收到结果 │
└─────────────────────────────────────────────────────────────────┘
1.1 批量写入的优化路径
当一条 INSERT 包含多张表的数据时:
sql
INSERT INTO
d1001 VALUES (now, 10.3, 219, 0.31)
d1002 VALUES (now, 12.6, 220, 0.28)
d2001 VALUES (now, 9.8, 218, 0.35);
客户端会:
-
按表名 hash 将数据分组到不同 VGroup
-
为每个 VGroup 构建一条 SUBMIT 消息(包含该 VGroup 的所有表数据)
-
并行发送到各 VGroup 的 Leader
-
汇总所有响应后返回总影响行数
批量写入分发:
INSERT 包含 3 张表 → 按 VGroup 分组:
VGroup 2: [d1001, d1002] → 发往 dnode 1
VGroup 3: [d2001] → 发往 dnode 2两条 SUBMIT 消息并行发送,两个响应都回来后返回用户
1.2 自动建表写入
sql
INSERT INTO d1001 USING meters TAGS ('California', 1) VALUES (now, 10.3, 219, 0.31);
当表不存在时的额外步骤:
- VNode 收到 SUBMIT 后检查子表是否存在
- 不存在 → 根据 USING 子句的超级表名和 TAGS 自动创建子表
- 创建完成后继续写入数据
- 整个过程在一次 SUBMIT 请求内完成,无需客户端额外操作
2. 查询操作全链路
以 SELECT avg(voltage) FROM power.meters WHERE location = 'California' INTERVAL(1h) 为例:
查询全链路(18 步):
┌─────────────────────────────────────────────────────────────────┐
│ 客户端 (taosc) │
│ │
│ ① SQL 文本发送到 taosc │
│ ② 词法分析 + 语法分析 → 生成 AST(抽象语法树) │
│ ③ 语义分析: │
│ - 从 Catalog 获取超级表 Schema(验证列名、类型) │
│ - 权限检查 │
│ - 类型推导和隐式转换 │
│ ④ 生成逻辑计划(Scan → Filter → Interval → Aggregate) │
│ ⑤ 生成物理计划: │
│ - 按 VGroup 拆分为子计划 │
│ - VNode 层:每个 VGroup 执行 Scan + 本地聚合 │
│ - 合并层:汇总各 VGroup 的部分结果 │
│ ⑥ Scheduler 分发子计划: │
│ - 向每个 VGroup Leader 发送 QUERY 消息(携带子计划) │
│ - 在本地(或 QNode)准备合并任务 │
└───────────────────────────────────┬─────────────────────────────┘
│
┌───────────┼───────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ VGroup 2 │ │ VGroup 3 │ │ VGroup 4 │
│ │ │ │ │ │
│ ⑦ 收到子计划 │ │ ⑦ 收到子计划 │ │ ⑦ 收到子计划 │
│ ⑧ 反序列化 │ │ │ │ │
│ 物理计划 │ │ │ │ │
│ ⑨ 执行算子链:│ │ │ │ │
│ TableScan │ │ TableScan │ │ TableScan │
│ → Filter │ │ → Filter │ │ → Filter │
│ (location) │ │ (location) │ │ (location) │
│ → Interval │ │ → Interval │ │ → Interval │
│ → PartialAgg│ │ → PartialAgg│ │ → PartialAgg│
│ ⑩ 生成部分结果│ │ ⑩ │ │ ⑩ │
└───────┬───────┘ └───────┬───────┘ └───────┬───────┘
│ │ │
└──────────────────┼──────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ 客户端 Merge 节点 │
│ │
│ ⑪ 收到各 VGroup 的部分聚合结果 │
│ ⑫ 排序合并(按时间窗口对齐) │
│ ⑬ 最终聚合(将 partial avg 合并为 final avg) │
│ ⑭ 生成最终结果集 │
└───────────────────────────────────┬─────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ ⑮ 客户端通过 FETCH 逐批获取结果 │
│ ⑯ 每次 FETCH 返回一批行(默认 4096 行/批) │
│ ⑰ 直到所有结果返回完毕 │
│ ⑱ 用户收到完整结果集 │
└─────────────────────────────────────────────────────────────────┘
2.1 两阶段聚合
分布式查询采用两阶段聚合策略:
| 阶段 | 执行位置 | 操作 |
|---|---|---|
| Partial(部分聚合) | 各 VNode 本地 | 对本地数据执行初步聚合,输出中间结果 |
| Final(最终聚合) | 客户端或 QNode | 合并各 VNode 的中间结果,计算最终值 |
例如 AVG 的两阶段:
- Partial:计算本地的
SUM和COUNT - Final:
SUM(所有 partial_sum) / SUM(所有 partial_count)
2.2 查询与写入的隔离
查询请求走查询队列 ,写入请求走写队列,两者互不阻塞:
- 查询不持有写锁
- 使用 MVCC(多版本并发控制)读取------查询看到的是查询开始时刻的一致性快照
- 长时间查询不会阻塞写入
3. DDL 操作全链路
以 CREATE DATABASE power VGROUPS 4 REPLICA 3 为例:
DDL 全链路(12 步):
┌─────────────────────────────────────────────────────────────────┐
│ 客户端 (taosc) │
│ │
│ ① SQL 解析:识别为 DDL 语句(CREATE DATABASE) │
│ ② DDL 不在客户端执行计划,直接封装为消息发往 MNode │
│ ③ 从 Catalog 获取 MNode Leader 地址 │
│ ④ 发送 CREATE_DB 消息到 MNode Leader │
└───────────────────────────────────┬─────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ MNode (Leader) │
│ │
│ ⑤ 验证请求: │
│ - 数据库是否已存在 │
│ - 用户权限检查 │
│ - License/授权检查 │
│ - 参数合法性校验 │
│ ⑥ 资源分配: │
│ - 计算各 DNode 负载评分 │
│ - 为 4 个 VGroup 各选择 3 个最优 DNode │
│ - 分配 Hash 范围 │
│ ⑦ 创建事务(重试策略、DB 级冲突检测) │
│ ⑧ PREPARE 阶段:通过 Raft 持久化事务 + 中间状态元数据 │
└───────────────────────────────────┬─────────────────────────────┘
│
┌───────────┼───────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ DNode 1 │ │ DNode 2 │ │ DNode 3 │
│ │ │ │ │ │
│ ⑨ 收到创建 │ │ ⑨ 收到创建 │ │ ⑨ 收到创建 │
│ VNode 消息 │ │ VNode 消息 │ │ VNode 消息 │
│ 在本地创建 │ │ 在本地创建 │ │ 在本地创建 │
│ 数据目录 │ │ 数据目录 │ │ 数据目录 │
│ 初始化存储 │ │ 初始化存储 │ │ 初始化存储 │
│ 返回成功 │ │ 返回成功 │ │ 返回成功 │
└───────┬───────┘ └───────┬───────┘ └───────┬───────┘
│ │ │
└──────────────────┼──────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ MNode (Leader) │
│ │
│ ⑩ 所有 DNode 确认成功 │
│ ⑪ COMMIT 阶段:通过 Raft 将 DB 和 VGroup 状态更新为 READY │
│ 事务完成,从事务表中删除 │
└───────────────────────────────────┬─────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 客户端 (taosc) │
│ │
│ ⑫ 收到成功响应,返回给用户 │
│ 客户端刷新 Catalog 缓存(新数据库的 VGroup 信息) │
└─────────────────────────────────────────────────────────────────┘
4. Catalog 缓存机制
Catalog 是客户端性能的关键------避免每次操作都向 MNode 查询元数据:
Catalog 缓存层次:
客户端请求
│
▼
Catalog 本地缓存查找
│
├── 命中 → 直接使用(零网络开销)
│
└── 未命中 → 向 MNode 请求
│
▼
MNode 返回:
- VGroup 列表(vgId, hashRange, EP Set)
- 表 Schema(列定义、Tag 定义)
- 版本号(用于失效检测)
│
▼
写入本地缓存,设置版本号
缓存失效场景:
- 操作返回
TSDB_CODE_SYN_NOT_LEADER→ VGroup Leader 已切换,刷新 VGroup 路由 - 操作返回 Schema 不匹配错误 → 表结构已变更,刷新表 Schema
- 心跳检测到版本号变化 → 批量刷新过期缓存
5. 错误处理与自动重试
客户端对不同类型的错误采取不同的策略:
| 错误类型 | 行为 |
|---|---|
| Leader 切换 | 刷新 Catalog,重新路由到新 Leader,自动重试 |
| 网络超时 | RPC 层指数退避重试 |
| VGroup 迁移 | 刷新 Catalog,重新路由,自动重试 |
| Schema 不匹配 | 刷新表 Schema 缓存,重新编码数据,自动重试 |
| 权限不足 | 直接返回错误给用户 |
| 语法错误 | 直接返回错误给用户 |
| 磁盘满 | 直接返回错误给用户 |
6. 消息类型与路由总结
| 操作 | 消息类型 | 目标 | 路由方式 |
|---|---|---|---|
| INSERT | TDMT_VND_SUBMIT |
VNode Leader | 表名 hash → VGroup → Leader EP |
| SELECT(分发子计划) | TDMT_SCH_QUERY |
VNode/QNode | 每个相关 VGroup |
| SELECT(获取结果) | TDMT_SCH_FETCH |
VNode/QNode | 查询 ID 关联 |
| CREATE DATABASE | TDMT_MND_CREATE_DB |
MNode Leader | MNode EP Set |
| CREATE STABLE | TDMT_MND_CREATE_STB |
MNode Leader | MNode EP Set |
| DROP TABLE | TDMT_MND_DROP_TB |
MNode Leader | MNode EP Set |
| SHOW DNODES | TDMT_MND_SHOW |
MNode Leader | MNode EP Set |
| 心跳 | TDMT_MND_STATUS |
MNode Leader | MNode EP Set |
代码示例
观察查询执行计划
sql
-- 使用 EXPLAIN 查看查询的物理执行计划
EXPLAIN SELECT avg(voltage) FROM power.meters
WHERE location = 'California' INTERVAL(1h);
-- 输出示例(简化):
-- -> Merge (columns=2)
-- -> Partial Interval [1h] on meters (columns=2) on VGroup 2
-- -> Partial Interval [1h] on meters (columns=2) on VGroup 3
-- -> Partial Interval [1h] on meters (columns=2) on VGroup 4
-- 可以看到查询被拆分为 3 个子计划(3 个 VGroup)
-- 各 VGroup 执行 Partial Interval,最后 Merge 汇总
观察写入路由
sql
-- 查看表所属的 VGroup
SHOW power.VGROUPS;
-- 写入后通过 VGroup 统计确认数据分布
-- tables 列显示每个 VGroup 中的子表数量
连接后 Catalog 预热
sql
-- 首次查询某数据库时,Catalog 需要从 MNode 拉取元数据
-- 第一次查询可能稍慢(多一次 MNode 往返)
SELECT * FROM power.meters LIMIT 1; -- 触发 Catalog 缓存
-- 后续查询直接使用缓存,无额外延迟
SELECT avg(voltage) FROM power.meters INTERVAL(1h);
性能考量
各阶段耗时占比
| 阶段 | 典型耗时(同机房) | 瓶颈因素 |
|---|---|---|
| SQL 解析 | < 0.1ms | CPU |
| Catalog 查询(缓存命中) | 0 ms | --- |
| Catalog 查询(缓存未命中) | 1-5 ms | 网络 RTT |
| 数据编码 | < 0.1ms | CPU |
| RPC 传输 | 0.1-0.5 ms | 网络 |
| WAL 写入 | 0.05-0.5 ms | 磁盘 IOPS |
| Raft 复制确认 | 0.5-2 ms | 网络 RTT |
| MemTable 写入 | < 0.1ms | CPU/内存 |
优化建议
| 场景 | 优化方式 |
|---|---|
| 批量写入 | 单条 INSERT 包含多行多表,减少 RPC 次数 |
| 高频写入 | 使用 STMT2 参数绑定,避免重复 SQL 解析 |
| 跨 VGroup 查询慢 | 增加 VGROUPS 数量提高并行度 |
| DDL 慢 | 减少 VGROUPS 数量降低事务动作数 |
| 首次查询慢 | 连接后执行轻量查询预热 Catalog |
| Catalog 频繁失效 | 检查是否频繁执行 Schema 变更 |
FAQ
Q1: 写入时客户端需要知道数据发往哪个节点吗?
不需要用户关心。taosc 驱动自动完成路由:根据表名计算 hash 值 → 查找 Catalog 缓存确定 VGroup → 获取 VGroup Leader 的地址 → 直接发送。对用户完全透明。
Q2: 查询时数据从 Leader 还是 Follower 读取?
默认从 Leader 读取(保证强一致性)。这是因为查询需要获取最新的 commitIndex 数据。当前版本不支持 Follower 读。
Q3: 如果写入中途 Leader 切换了怎么办?
客户端会收到 TSDB_CODE_SYN_NOT_LEADER 错误,然后:
- 自动刷新 Catalog 获取新 Leader 地址
- 重新发送写入请求到新 Leader
- 对用户透明(重试在 RPC 层自动完成)
Q4: 一条 INSERT 包含 1000 张表的数据,是发 1000 次请求吗?
不是。客户端将 1000 张表按 VGroup 分组,每个 VGroup 只发 1 条 SUBMIT 消息。如果有 4 个 VGroup,则总共只发 4 条消息(并行发送)。这就是为什么批量 INSERT 比逐条 INSERT 效率高很多。
Q5: DDL 和 DML 走同一条路径吗?
不同:
- DML(INSERT/SELECT):客户端直接与 VNode 通信
- DDL(CREATE/ALTER/DROP):客户端与 MNode 通信,MNode 再通过事务机制协调各 DNode
DDL 操作经过 MNode 的事务引擎,有冲突检测和原子性保证。DML 操作通过 VNode 的 Raft 协议保证一致性。
Q6: 查询结果是一次性返回还是分批返回?
分批返回 。查询执行后,客户端通过 FETCH 消息逐批获取结果(每批默认 4096 行)。这避免了大结果集一次性占用过多内存。在 API 层面,taos_fetch_row / taos_fetch_block 封装了这个过程。
Q7: QNode 和 VNode 上的查询有什么区别?
- VNode 查询:在数据所在节点执行,适合带 WHERE 条件的扫描(数据本地化)
- QNode 查询:独立的计算节点,不存储数据,适合纯计算型的合并和聚合
默认情况下查询在 VNode 上执行。创建 QNode 后,合并层可以卸载到 QNode,减轻 VNode 的 CPU 负担。
Q8: 为什么有时候 INSERT 返回的行数比发送的少?
可能原因:
- 部分行的时间戳重复(同一子表同一时间戳视为更新,不增加行数计数)
- 部分表不存在且未使用
USING自动建表语法 - 数据类型不匹配导致部分行被拒绝
检查返回的错误码可以了解具体原因。
参考
系统构架篇
- 01-《TDengine 整体架构全景》
- 02-《集群拓扑深度解析》
- 03-《MNode 内部机制深度解析》
- 04-《RPC 通信层深度解析》
- 05-《VNode 生命周期》
- 06-《RAFT 共识协议》
关于 TDengine
TDengine 专为物联网IoT平台、工业大数据平台设计。其中,TDengine TSDB 是一款高性能、分布式的时序数据库(Time Series Database),同时它还带有内建的缓存、流式计算、数据订阅等系统功能;TDengine IDMP 是一款AI原生工业数据管理平台,它通过树状层次结构建立数据目录,对数据进行标准化、情景化,并通过 AI 提供实时分析、可视化、事件管理与报警等功能。