TDengine 集群拓扑深度解析 — 节点发现、EP 机制与负载均衡

在这里插入图片描述

适用版本:TDengine v3.x(v3.3.x / v3.4.x) | 最后更新:2026-05-12

概述

TDengine 的分布式架构从设计之初就面向多节点集群,通过 dnode(数据节点)组成集群,在集群内部自动进行节点发现、心跳维护、故障检测与 VGroup 负载均衡。本文深入解析 TDengine 集群的四个核心机制:

  1. Endpoint(EP)机制:集群中每个节点的唯一标识与寻址方式
  2. 节点发现与加入:新节点如何发现并加入已有集群
  3. 心跳与状态上报:节点如何保持存活感知和信息同步
  4. VGroup 分配与负载均衡:数据如何分布在多个节点上

核心概念速查表

概念说明
EP(Endpoint)FQDN:Port 组成的网络端点地址,是集群节点间通信的基础寻址方式
FQDN完全限定域名(Fully Qualified Domain Name),TDengine 使用域名而非 IP 作为节点标识
firstEp新节点加入集群时的入口端点,通常指向第一个 mnode 所在的 dnode
dnodetaosd 进程实例,集群的物理部署单元
dnode.json每个 dnode 本地持久化的集群成员信息文件
statusIntervaldnode 向 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 地址作为节点标识,原因包括:

  1. 容器化友好:在 Docker/K8s 环境中,容器 IP 会频繁变化,但 FQDN 可以保持稳定
  2. 多网卡支持:一台机器可能有多个 IP,FQDN 避免了网卡绑定的歧义
  3. 集群迁移简化:节点迁移到新 IP 后,只需更新 DNS 解析即可,无需重配所有节点
  4. 统一的配置管理:所有节点使用同一套 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 分配
dnodeVerdnode 表版本号,用于增量同步
clusterId集群唯一标识,防止跨集群误连接
dropped是否已被集群移除
dnodes已知的所有 dnode 端点列表,包含 isMnode 标记
1.4 EP 的更新流程

EP 信息通过心跳机制保持同步:

  1. 启动时:从 dnode.json 加载已知的集群成员信息
  2. 心跳响应:mnode 在心跳响应中返回最新的全量 dnode 端点列表
  3. 本地更新:dnode 收到响应后更新内存中的 EP 列表
  4. 持久化:将更新后的 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 matchdnodeId 不匹配
clusterId not matchclusterId 不匹配(跨集群误连接)
statusInterval not matchstatusInterval 配置不匹配
timezone not match时区不匹配
locale not match语言环境不匹配
charset not match字符集不匹配
encryption key not match加密密钥不匹配
time unsync节点间系统时间偏差过大

运维提示:节点 offline 最常见的原因是心跳超时(网络问题)和配置不匹配(时区、字符集等)。集群中所有节点的 timezonelocalecharset 配置必须一致。

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 收到心跳后,执行以下操作:

  1. 校验 clusterId——不匹配则拒绝
  2. 查找 dnode——按 ID 查找(正常心跳)或按 EP 查找(首次加入)
  3. 时间同步校验——节点间时间偏差超过阈值(默认 300 秒)则拒绝
  4. 配置一致性检查——版本号、时区、字符集等必须匹配
  5. 更新 VNode 状态——根据上报的负载信息更新各 VGroup 的状态
  6. 构建响应——返回分配的 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 参数和硬件资源决定

分配过程

  1. 只考虑在线supportVnodes > 0 的 dnode
  2. 按评分升序排列,优先选择负载最低的节点
  3. 检查 vnode 数量上限和内存余量
  4. 由于 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)│
     └──────────┘           └──────────┘           └──────────┘

性能考量

心跳间隔调优

参数默认值说明
statusInterval1(秒)心跳间隔,减小可更快检测故障,但增加网络开销
离线判定超时~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 的心跳。常见原因:

  1. 网络不通——检查防火墙和网络连通性
  2. FQDN 无法解析——确认 /etc/hosts 或 DNS 配置
  3. 目标 dnode 进程已崩溃——检查 taosd 进程和日志
  4. 时区/版本/配置不匹配——查看 offline reason 详细信息

Q2: 新建的 dnode 一直显示 offline 是什么原因?

最常见的原因是先启动了新节点,后才执行 CREATE DNODE。正确流程是:

  1. 先在集群中执行 CREATE DNODE 'new_fqdn:port'
  2. 再启动新节点的 taosd

如果顺序反了,新节点发送的心跳中 dnodeId=0,但 mnode 找不到匹配的 EP(因为还没注册),会直接拒绝。

Q3: BALANCE VGROUP 执行后没有效果?

可能的原因:

  1. 只有 1 个 dnode——至少需要 2 个节点才能平衡
  2. 有 dnode 离线——平衡操作要求所有节点在线
  3. 已经达到均衡——算法判断迁移后不会改善负载分布
  4. 数据库启用了 Arbitrator——此类数据库的 VGroup 不参与平衡

Q4: 一个 dnode 最多能承载多少 vnode?

supportVnodes 参数控制,默认根据 CPU 核数自动计算(通常为核数 × 2)。可以通过 taos.cfg 中的 supportVnodes 参数手动设置。每个 vnode 大约需要 1GB 内存(取决于 BUFFERPAGES 参数),所以实际上限受内存约束。

Q5: 客户端如何知道数据应该发往哪个 dnode?

客户端通过以下步骤确定路由:

  1. 对表名计算 hash 值
  2. 在 Catalog 缓存中查找 hash 值对应的 VGroup
  3. 从 VGroup 的 EP Set 中获取 Leader 的 FQDN:Port
  4. 直接发送请求到 Leader

Catalog 缓存通过版本号机制保持与 mnode 同步。当 VGroup 发生迁移或 Leader 切换时,客户端会自动刷新缓存。

Q6: 集群中各节点的配置参数必须一致吗?

以下参数必须一致,否则节点会被判定为 offline:

  • timezone(时区)
  • locale(语言环境)
  • charset(字符集)
  • 软件版本号
  • ttlChangeOnWrite
  • enableWhiteList
  • 加密密钥(如果启用加密)

其他参数(如 supportVnodesBUFFER)可以根据各节点硬件不同而异。

Q7: clusterId 是什么,有什么用?

clusterId 是集群的唯一标识,在第一个 mnode 创建时自动生成。它的核心作用是防止跨集群误连接——当一个 dnode 的 clusterId 与 mnode 不匹配时,心跳会被拒绝。这在多套集群共用网络环境时尤为重要。

参考

  • TDengine 官方文档:集群部署与管理
  • 相关文章:《TDengine 整体架构全景 — 深度解析》(同系列第 1 篇)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TDengine (老段)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值