
适用版本:TDengine v3.x(v3.3.x / v3.4.x) | 最后更新:2026-05-12
概述
TDengine 的分布式架构从设计之初就面向多节点集群,通过 dnode(数据节点)组成集群,在集群内部自动进行节点发现、心跳维护、故障检测与 VGroup 负载均衡。本文深入解析 TDengine 集群的四个核心机制:
- Endpoint(EP)机制:集群中每个节点的唯一标识与寻址方式
- 节点发现与加入:新节点如何发现并加入已有集群
- 心跳与状态上报:节点如何保持存活感知和信息同步
- VGroup 分配与负载均衡:数据如何分布在多个节点上
核心概念速查表
| 概念 | 说明 |
|---|---|
| EP(Endpoint) | 由 FQDN:Port 组成的网络端点地址,是集群节点间通信的基础寻址方式 |
| FQDN | 完全限定域名(Fully Qualified Domain Name),TDengine 使用域名而非 IP 作为节点标识 |
| firstEp | 新节点加入集群时的入口端点,通常指向第一个 mnode 所在的 dnode |
| dnode | taosd 进程实例,集群的物理部署单元 |
| dnode.json | 每个 dnode 本地持久化的集群成员信息文件 |
| statusInterval | dnode 向 mnode 发送心跳的间隔,默认 1 秒 |
| VGroup | 虚拟节点组,由分布在不同 dnode 上的 vnode 副本组成,是数据分片的逻辑单元 |
详细解析
1. Endpoint(EP)机制
EP 机制是 TDengine 集群通信的基石。集群中每个节点都通过 FQDN:Port 来寻址。
1.1 EP 集合与 MNode 高可用
由于 mnode 最多有 3 个副本,系统维护一个**端点集合(EP Set)**来记录所有 mnode 的地址,并标记当前活跃的 Leader 节点。当某个 mnode 不可达时,客户端和 dnode 自动切换到下一个 EP 重试。
MNode EP Set 示例:
EP[0] = node1.example.com:6030 ← 当前 Leader(inUse=0)
EP[1] = node2.example.com:6030
EP[2] = node3.example.com:6030
当 node1 不可达时:
→ 自动切换到 EP[1](inUse=1)
→ 如果 node2 也不可达,继续切换到 EP[2]
1.2 为什么使用 FQDN 而非 IP?
TDengine 强制使用 FQDN 而非 IP 地址作为节点标识,原因包括:
- 容器化友好:在 Docker/K8s 环境中,容器 IP 会频繁变化,但 FQDN 可以保持稳定
- 多网卡支持:一台机器可能有多个 IP,FQDN 避免了网卡绑定的歧义
- 集群迁移简化:节点迁移到新 IP 后,只需更新 DNS 解析即可,无需重配所有节点
- 统一的配置管理:所有节点使用同一套 FQDN 配置,降低运维复杂度
常见误区:很多用户在部署时直接使用 IP 而未配置 FQDN 解析,导致连接失败。务必确保每个节点的 FQDN 可被集群中所有其他节点以及客户端正确解析。
1.3 EP 的本地存储 — dnode.json
每个 dnode 启动后会在数据目录下维护一个 dnode.json 文件(路径:$dataDir/dnode/dnode.json),记录该节点已知的集群成员信息:
{
"dnodeId": 1,
"dnodeVer": 1000,
"clusterId": 5791574349661265952,
"dropped": 0,
"dnodes": [
{
"id": 1,
"fqdn": "server1.example.com",
"port": 6030,
"isMnode": 1
},
{
"id": 2,
"fqdn": "server2.example.com",
"port": 6030,
"isMnode": 0
},
{
"id": 3,
"fqdn": "server3.example.com",
"port": 6030,
"isMnode": 0
}
]
}
| 字段 | 说明 |
|---|---|
dnodeId | 本节点的 ID,由 mnode 分配 |
dnodeVer | dnode 表版本号,用于增量同步 |
clusterId | 集群唯一标识,防止跨集群误连接 |
dropped | 是否已被集群移除 |
dnodes | 已知的所有 dnode 端点列表,包含 isMnode 标记 |
1.4 EP 的更新流程
EP 信息通过心跳机制保持同步:
- 启动时:从
dnode.json加载已知的集群成员信息 - 心跳响应:mnode 在心跳响应中返回最新的全量 dnode 端点列表
- 本地更新:dnode 收到响应后更新内存中的 EP 列表
- 持久化:将更新后的 EP 列表写入
dnode.json
EP 数据使用读写锁保护,支持高并发读取。内部使用以 dnodeId 为 key 的哈希表实现 O(1) 的端点查询。
2. 集群拓扑与节点发现
2.1 集群形成的起点 — firstEp
TDengine 集群的形成始于一个关键配置参数 firstEp:
# taos.cfg
firstEp server1.example.com:6030 # 集群入口端点
secondEp server2.example.com:6030 # 备用入口端点(可选)
firstEp 的作用:
- 第一个节点启动时,
firstEp指向自己,自动成为集群的创始节点 - 后续节点启动时,
firstEp指向已有集群中的任意节点(通常是第一个 mnode) - 客户端连接时,也通过
firstEp作为初始入口
2.2 第一个节点启动 — 集群创建
第一个节点 (firstEp 指向自己):
1. taosd 启动,读取 taos.cfg
2. 检查 dnode.json 不存在 → 首次部署
3. 创建 dnodeId=1 的默认节点
4. 自动创建 mnode,分配 clusterId
5. 写入 dnode.json 持久化
6. 集群创建完成,开始接受连接
2.3 新节点加入集群
新节点加入流程:
┌───────────┐ ┌───────────┐
│ 新 dnode │ │ mnode │
│ (dnodeId=0)│ │ (Leader) │
└─────┬─────┘ └─────┬─────┘
│ │
│ 1. 启动,读取 firstEp │
│ 2. dnodeId=0 → 发送 Status 请求 │
│─────────────────────────────────────→│
│ │ 3. 检查 dnodeId==0
│ │ 通过 EP 匹配预注册的 dnode
│ │ (需要先执行 CREATE DNODE)
│ │
│ 4. 返回分配的 dnodeId + 全部 EP 列表 │
│←─────────────────────────────────────│
│ │
│ 5. 保存 dnodeId 和 EP 列表到 │
│ dnode.json │
│ 6. 后续正常心跳 │
│─────────────────────────────────────→│
关键步骤:必须先注册再启动
-- 步骤 1:在现有集群中注册新节点(分配 ID)
CREATE DNODE 'server2.example.com:6030';
-- 步骤 2:然后再启动新节点的 taosd
mnode 收到 CREATE DNODE 后,为新节点分配一个自增的唯一 ID,并通过 Raft 将此信息持久化到 SDB。当新节点发送首次心跳(dnodeId=0)时,mnode 通过 EP 匹配找到预注册的记录,将分配的 ID 返回给新节点。
2.4 节点管理 SQL
-- 查看集群所有 dnode
SHOW DNODES;
-- 输出示例:
-- id | endpoint | vnodes | support_vnodes | status | offline reason
-- 1 | server1:6030 | 4 | 16 | ready |
-- 2 | server2:6030 | 3 | 16 | ready |
-- 3 | server3:6030 | 0 | 16 | offline | status msg timeout
-- 添加新节点
CREATE DNODE 'server4.example.com:6030';
-- 移除节点(会自动迁移该节点上的 vnode 到其他节点)
DROP DNODE 2;
-- 查看 mnode
SHOW MNODES;
-- 创建/删除 mnode
CREATE MNODE ON DNODE 2;
DROP MNODE ON DNODE 2;
2.5 离线原因
当 SHOW DNODES 显示某节点 offline 时,offline reason 列会给出具体原因:
| 离线原因 | 说明 |
|---|---|
status msg timeout | 心跳超时(最常见,通常是网络或进程问题) |
status not received | 从未收到过心跳 |
version not match | 软件版本不匹配 |
dnodeId not match | dnodeId 不匹配 |
clusterId not match | clusterId 不匹配(跨集群误连接) |
statusInterval not match | statusInterval 配置不匹配 |
timezone not match | 时区不匹配 |
locale not match | 语言环境不匹配 |
charset not match | 字符集不匹配 |
encryption key not match | 加密密钥不匹配 |
time unsync | 节点间系统时间偏差过大 |
运维提示:节点 offline 最常见的原因是心跳超时(网络问题)和配置不匹配(时区、字符集等)。集群中所有节点的
timezone、locale、charset配置必须一致。
3. 心跳与状态上报机制
心跳机制是集群保持一致性和感知节点存活状态的基础。
3.1 心跳定时器
每个 dnode 启动后会创建一个专门的状态上报线程,默认每 1 秒(statusInterval)向 mnode 发送一次心跳。线程内部每 50ms 轮询一次时间,而非使用精确定时器,这样可以优雅地处理系统时钟回拨。
3.2 Status 请求 — dnode 上报了什么?
每次心跳中,dnode 向 mnode 上报以下信息:
心跳请求包含的信息:
基本信息:
├── 软件版本号
├── dnodeId / clusterId
├── 本节点端点(FQDN:Port)
├── 上次重启时间
├── CPU 核数、总内存、可用内存
└── 心跳序列号
集群配置(用于一致性校验):
├── statusInterval / timezone
├── locale / charset
└── 加密配置
负载信息:
├── 每个 vnode 的负载
│ ├── vgId、Raft 状态(Leader/Follower)、Raft 任期
│ ├── 缓存使用量、表数量、时间线数量
│ └── 写入点数、存储空间
├── mnode 负载(如果本节点有 mnode)
└── qnode 负载(如果本节点有 qnode)
3.3 Status 响应 — mnode 返回了什么?
mnode 收到心跳后,执行以下操作:
- 校验 clusterId——不匹配则拒绝
- 查找 dnode——按 ID 查找(正常心跳)或按 EP 查找(首次加入)
- 时间同步校验——节点间时间偏差超过阈值(默认 300 秒)则拒绝
- 配置一致性检查——版本号、时区、字符集等必须匹配
- 更新 VNode 状态——根据上报的负载信息更新各 VGroup 的状态
- 构建响应——返回分配的 dnodeId、clusterId、最新的全量 dnode 端点列表
dnode 收到响应后,如果发现端点列表有变化(例如新节点加入),会更新本地 dnode.json。
3.4 在线/离线检测
mnode 通过心跳超时来判断 dnode 是否在线:
- 如果当前时间与上次心跳时间的差值超过超时阈值(通常是
statusInterval的数倍),则判定为离线 - 离线原因自动设置为
status msg timeout - 超时阈值的倍数设计是为了防止偶发网络波动导致误判
3.5 mnode EP 自动切换
当 dnode 向 mnode 发送心跳超时时,会自动轮转到 EP Set 中的下一个 mnode 地址重试。这保证了即使当前 mnode Leader 不可达,dnode 也能自动切换到其他 mnode 副本继续上报。
4. VGroup 分配与负载均衡
4.1 Hash 分片机制
创建数据库时,mnode 根据 VGROUPS 参数创建指定数量的 VGroup,并将整个 32 位哈希空间 [0, 0xFFFFFFFF] 均匀分割:
示例:4 个 VGroup 的哈希分布
数据库 "power" (VGROUPS 4, REPLICA 1):
VGroup 1: [0x00000000, 0x3FFFFFFF] → dnode 1
VGroup 2: [0x40000000, 0x7FFFFFFF] → dnode 2
VGroup 3: [0x80000000, 0xBFFFFFFF] → dnode 3
VGroup 4: [0xC0000000, 0xFFFFFFFF] → dnode 1
写入时:
表名 "d1001" → hash("d1001") = 0x2A3B... → 落入 VGroup 1 → 发往 dnode 1
表名 "d2005" → hash("d2005") = 0x8F1C... → 落入 VGroup 3 → 发往 dnode 3
最后一个 VGroup 的 hash 范围会包含所有剩余值(hashEnd = 0xFFFFFFFF),确保整个 hash 空间无遗漏。
4.2 DNode 选择算法 — 负载评分
当分配 VGroup 的副本到 dnode 时,mnode 使用评分排序算法选择最优 dnode。评分公式为:
Score = 已有 VNode 数 + 其他逻辑节点数 × 0.9 支持的最大 VNode 数 \text{Score} = \frac{\text{已有 VNode 数} + \text{其他逻辑节点数} \times 0.9}{\text{支持的最大 VNode 数}} Score=支持的最大 VNode 数已有 VNode 数+其他逻辑节点数×0.9
其中:
- 已有 VNode 数:当前已部署在该 dnode 上的 vnode 数量
- 其他逻辑节点数:该 dnode 上的 mnode/qnode/snode 数量,乘以 0.9 的权重(因为这些节点也占用资源,但影响小于 vnode)
- 支持的最大 VNode 数:由
supportVnodes参数和硬件资源决定
分配过程:
- 只考虑在线且
supportVnodes > 0的 dnode - 按评分升序排列,优先选择负载最低的节点
- 检查 vnode 数量上限和内存余量
- 由于 mnode 所在 dnode 的"其他逻辑节点数"+1,其评分略高,系统会倾向于将 vnode 分散到非 mnode 节点
4.3 手动 Balance VGroup
当集群扩容(添加新 dnode)或存在负载不均时,可以手动触发重新平衡:
-- 平衡所有 VGroup(迁移 VNode 使各节点负载均匀)
BALANCE VGROUP;
-- 只平衡 Leader 分布(不迁移数据,仅切换 Leader)
BALANCE VGROUP LEADER;
-- 将指定 VGroup 迁移到指定 DNode
REDISTRIBUTE VGROUP 10 DNODE 1 DNODE 2 DNODE 3;
BALANCE VGROUP 的算法:
负载均衡算法(贪心策略):
循环执行:
1. 按评分排序所有 dnode
2. 找到评分最高(负载最重)和最低(负载最轻)的 dnode
3. 模拟从最重节点迁移 1 个 VNode 到最轻节点
4. 如果迁移后仍然改善负载均衡 → 执行迁移,继续循环
5. 否则 → 停止,已达到均衡
算法特点:
- 贪心策略:每次从负载最重的节点迁移一个 VGroup 到负载最轻的节点
- 收敛判断:当迁移后源节点评分不再高于目标节点时停止
- 事务保护:所有迁移操作封装在事务中,保证原子性
- 前置检查:要求所有 dnode 都在线,否则拒绝平衡操作
4.4 VGroup 查看
-- 查看某个数据库的 VGroup 分布
SHOW power.VGROUPS;
-- 输出示例:
-- vgId | tables | status | onlineDnodes | v1_dnode | v1_status | v2_dnode | v2_status | v3_dnode | v3_status
-- 2 | 1000 | ready | 3/3 | 1 | leader | 2 | follower | 3 | follower
-- 3 | 800 | ready | 3/3 | 2 | leader | 3 | follower | 1 | follower
-- 4 | 1200 | ready | 3/3 | 3 | leader | 1 | follower | 2 | follower
5. 客户端的 EP 缓存与路由
客户端(taosc)也维护自己的 EP 缓存,避免每次操作都查询 mnode。
5.1 连接建立
客户端首次连接时,通过 firstEp 发送连接请求。mnode 返回的响应包含:
- 完整的 mnode EP Set(最多 3 个地址)
- 分配的连接 ID
- 数据库 VGroup 路由信息
客户端将 mnode 的 EP Set 缓存在本地,后续直接使用。
5.2 VGroup 路由缓存(Catalog)
客户端通过 Catalog 模块 缓存数据库的 VGroup 分布信息:
客户端写入/查询的路由流程:
1. 对表名计算 hash 值
2. 从 Catalog 缓存中查找 hash 值对应的 VGroup
3. 获取该 VGroup 的 EP Set(包含 Leader/Follower 地址)
4. 将请求直接发送到 Leader 所在的 dnode
当 Catalog 缓存过期或 VGroup 发生迁移时,客户端会收到错误码,自动从 mnode 重新拉取最新的 VGroup 路由信息。
5.3 心跳维护
客户端也有自己的心跳机制,定期:
- 检查 VGroup 缓存的版本号是否过期
- 刷新认证和白名单信息
- 更新连接状态
代码示例
部署 3 节点集群
节点 1(首节点):
# /etc/taos/taos.cfg
firstEp node1.example.com:6030
fqdn node1.example.com
serverPort 6030
systemctl start taosd
节点 2、3:
# /etc/taos/taos.cfg (node2)
firstEp node1.example.com:6030
fqdn node2.example.com
serverPort 6030
# /etc/taos/taos.cfg (node3)
firstEp node1.example.com:6030
fqdn node3.example.com
serverPort 6030
在节点 1 上注册新节点:
CREATE DNODE 'node2.example.com:6030';
CREATE DNODE 'node3.example.com:6030';
然后启动节点 2 和 3:
# 在 node2 上
systemctl start taosd
# 在 node3 上
systemctl start taosd
创建 mnode 副本(高可用):
CREATE MNODE ON DNODE 2;
CREATE MNODE ON DNODE 3;
验证集群状态:
SHOW DNODES;
-- 所有节点 status 应为 ready
SHOW MNODES;
-- 应显示 3 个 mnode,1 个 leader,2 个 follower
创建数据库并观察 VGroup 分布
-- 创建 3 副本数据库,4 个 VGroup
CREATE DATABASE power VGROUPS 4 REPLICA 3;
-- 查看 VGroup 分布
SHOW power.VGROUPS;
-- 每个 VGroup 的 3 个副本会被自动分散到 3 个 dnode 上
-- Leader 会均匀分布
扩容场景
-- 添加第 4 个节点
CREATE DNODE 'node4.example.com:6030';
-- 等待 node4 上线后,执行平衡
BALANCE VGROUP;
-- 部分 VGroup 会被自动迁移到 node4
-- 观察迁移结果
SHOW power.VGROUPS;
集群拓扑的完整数据流
┌─────────────────────────────────────────┐
│ MNode (Leader) │
│ │
│ DNode 注册表 ← SDB 持久化(Raft 复制) │
│ ├── id, fqdn, port │
│ ├── vnode 数, 支持的最大 vnode 数 │
│ ├── 最后心跳时间 │
│ └── 离线原因 │
│ │
│ VGroup 注册表 ← SDB 持久化 │
│ ├── vgId, hash 范围 │
│ └── 副本分布(哪些 dnode) │
└───────────┬─────────────────────────────┘
│
Status 请求 / 响应(每 1 秒)
│
┌──────────────────────┼──────────────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ dnode 1 │ │ dnode 2 │ │ dnode 3 │
│ │ │ │ │ │
│ dnode.json │ dnode.json │ dnode.json
│ ├ dnodeId=1 │ ├ dnodeId=2 │ ├ dnodeId=3
│ ├ clusterId │ ├ clusterId │ ├ clusterId
│ └ dnodes[1,2,3] │ └ dnodes[1,2,3] │ └ dnodes[1,2,3]
│ │ │ │ │ │
│ vnode:2 │ │ vnode:2 │ │ vnode:2 │
│ vnode:4 │ │ vnode:3 │ │ vnode:3 │
│ (Leader) │ │(Follower)│ │(Follower)│
└──────────┘ └──────────┘ └──────────┘
性能考量
心跳间隔调优
| 参数 | 默认值 | 说明 |
|---|---|---|
statusInterval | 1(秒) | 心跳间隔,减小可更快检测故障,但增加网络开销 |
| 离线判定超时 | ~3-5 秒 | 通常为 statusInterval 的数倍,防止偶发网络波动 |
| 时间偏差容忍值 | 300(秒) | 节点间系统时间偏差超过此值会被拒绝 |
VGroup 数量建议
| 集群规模 | 建议 VGROUPS 数 | 说明 |
|---|---|---|
| 单节点 | 2-4 | 过多 VGroup 增加内存和线程开销 |
| 3 节点 | 4-12 | 确保每节点有 1-4 个 VGroup |
| 10+ 节点 | 20-100 | 根据表数量和写入量调整 |
原则:VGroup 数量应该是 dnode 数量的 2-4 倍,以实现良好的负载均衡。过少则无法充分利用多节点,过多则增加管理开销和内存消耗。
副本数与性能的权衡
| 副本数 | 写入性能 | 查询性能 | 可用性 |
|---|---|---|---|
| 1 | 最高(无同步开销) | 基准 | 无冗余 |
| 3 | 约 70-80%(需等多数派确认) | 读可分散到 Follower | 可容忍 1 节点故障 |
FAQ
Q1: 为什么 SHOW DNODES 显示节点 offline,原因是 “status msg timeout”?
这通常表示 mnode 在超时时间内未收到该 dnode 的心跳。常见原因:
- 网络不通——检查防火墙和网络连通性
- FQDN 无法解析——确认
/etc/hosts或 DNS 配置 - 目标 dnode 进程已崩溃——检查 taosd 进程和日志
- 时区/版本/配置不匹配——查看 offline reason 详细信息
Q2: 新建的 dnode 一直显示 offline 是什么原因?
最常见的原因是先启动了新节点,后才执行 CREATE DNODE。正确流程是:
- 先在集群中执行
CREATE DNODE 'new_fqdn:port' - 再启动新节点的 taosd
如果顺序反了,新节点发送的心跳中 dnodeId=0,但 mnode 找不到匹配的 EP(因为还没注册),会直接拒绝。
Q3: BALANCE VGROUP 执行后没有效果?
可能的原因:
- 只有 1 个 dnode——至少需要 2 个节点才能平衡
- 有 dnode 离线——平衡操作要求所有节点在线
- 已经达到均衡——算法判断迁移后不会改善负载分布
- 数据库启用了 Arbitrator——此类数据库的 VGroup 不参与平衡
Q4: 一个 dnode 最多能承载多少 vnode?
由 supportVnodes 参数控制,默认根据 CPU 核数自动计算(通常为核数 × 2)。可以通过 taos.cfg 中的 supportVnodes 参数手动设置。每个 vnode 大约需要 1GB 内存(取决于 BUFFER 和 PAGES 参数),所以实际上限受内存约束。
Q5: 客户端如何知道数据应该发往哪个 dnode?
客户端通过以下步骤确定路由:
- 对表名计算 hash 值
- 在 Catalog 缓存中查找 hash 值对应的 VGroup
- 从 VGroup 的 EP Set 中获取 Leader 的
FQDN:Port - 直接发送请求到 Leader
Catalog 缓存通过版本号机制保持与 mnode 同步。当 VGroup 发生迁移或 Leader 切换时,客户端会自动刷新缓存。
Q6: 集群中各节点的配置参数必须一致吗?
以下参数必须一致,否则节点会被判定为 offline:
timezone(时区)locale(语言环境)charset(字符集)- 软件版本号
ttlChangeOnWriteenableWhiteList- 加密密钥(如果启用加密)
其他参数(如 supportVnodes、BUFFER)可以根据各节点硬件不同而异。
Q7: clusterId 是什么,有什么用?
clusterId 是集群的唯一标识,在第一个 mnode 创建时自动生成。它的核心作用是防止跨集群误连接——当一个 dnode 的 clusterId 与 mnode 不匹配时,心跳会被拒绝。这在多套集群共用网络环境时尤为重要。
参考
- TDengine 官方文档:集群部署与管理
- 相关文章:《TDengine 整体架构全景 — 深度解析》(同系列第 1 篇)

236

被折叠的 条评论
为什么被折叠?



