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

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

概述

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
SEpSet 端点集合,包含最多 3 个 EP,用于 mnode 的高可用寻址和自动故障切换
dnode taosd 进程实例,集群的物理部署单元
dnode.json 每个 dnode 本地持久化的集群成员信息文件
statusInterval dnode 向 mnode 发送心跳的间隔,默认 1 秒
VGroup 虚拟节点组,由分布在不同 dnode 上的 vnode 副本组成,是数据分片的逻辑单元

详细解析

1. Endpoint(EP)机制

EP 机制是 TDengine 集群通信的基石。集群中每个节点(dnode/mnode/vnode)都通过 EP 来寻址。

1.1 核心数据结构

SEp --- 单个端点

c 复制代码
// include/common/tmsg.h
typedef struct SEp {
  char     fqdn[TSDB_FQDN_LEN];    // 完全限定域名,最长 128 字节
  uint16_t port;                     // 网络端口号
} SEp;

SEpSet --- 端点集合

c 复制代码
// include/common/tmsg.h
typedef struct SEpSet {
  int8_t inUse;                      // 当前正在使用的端点索引 (0-2)
  int8_t numOfEps;                   // 端点数量 (1-3)
  SEp    eps[TSDB_MAX_REPLICA];      // 最多 3 个端点
} SEpSet;

SEpSet 主要用于 mnode 的高可用寻址 :由于 mnode 最多有 3 个副本,客户端和各 dnode 通过 SEpSet 记录所有 mnode 的地址,通过 inUse 字段标记当前活跃的 Leader 节点。当某个 mnode 不可达时,自动切换到下一个 EP 重试。

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),记录该节点已知的集群成员信息:

json 复制代码
{
  "dnodeId": 1,
  "dnodeVer": 1000,
  "engineVer": 50000,
  "clusterId": 5791574349661265952,
  "dropped": 0,
  "encryptAlgor": 0,
  "encryptScope": 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 管理的核心代码在 source/dnode/mgmt/node_util/src/dmEps.c

复制代码
dmReadEps()     ── 启动时从 dnode.json 加载集群成员信息
     │
     ▼
dmUpdateEps()   ── 收到 mnode 心跳响应后更新内存中的 EP 列表
     │
     ▼
dmWriteEps()    ── 将更新后的 EP 列表持久化到 dnode.json
     │
     ▼
dmResetEps()    ── 重建 mnodeEps(SEpSet)和 dnodeHash(哈希表)

EP 查询的线程安全性 :EP 数据使用读写锁(TdThreadRwlock)保护,查询操作加读锁,更新操作加写锁,支持高并发读取:

c 复制代码
// source/dnode/mgmt/node_util/src/dmEps.c
void dmGetDnodeEp(void *data, int32_t dnodeId, char *pEp, char *pFqdn, uint16_t *pPort) {
  SDnodeData *pData = data;
  taosThreadRwlockRdlock(&pData->lock);          // 读锁

  SDnodeEp *pDnodeEp = taosHashGet(pData->dnodeHash, &dnodeId, sizeof(int32_t));
  if (pDnodeEp != NULL) {
    if (pPort) *pPort = pDnodeEp->ep.port;
    if (pFqdn) tstrncpy(pFqdn, pDnodeEp->ep.fqdn, TSDB_FQDN_LEN);
    if (pEp)   snprintf(pEp, TSDB_EP_LEN, "%s:%u", pDnodeEp->ep.fqdn, pDnodeEp->ep.port);
  }

  taosThreadRwlockUnlock(&pData->lock);
}

内部使用 dnodeHash(以 dnodeId 为 key 的哈希表)实现 O(1) 的端点查询。

2. 集群拓扑与节点发现

2.1 集群形成的起点 --- firstEp

TDengine 集群的形成始于一个关键配置参数 firstEp

bash 复制代码
# 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. mndCreateDefaultDnode() → 创建 dnodeId=1 的默认节点
  4. 自动创建 mnode(ID=1),分配 clusterId
  5. 写入 dnode.json 持久化
  6. 集群创建完成,开始接受连接

mnode 侧管理的 dnode 对象结构:

c 复制代码
// source/dnode/mnode/impl/inc/mndDef.h
typedef struct {
  int32_t    id;                    // DNode 唯一 ID
  int64_t    createdTime;           // 创建时间
  int64_t    updateTime;            // 最后更新时间
  int64_t    rebootTime;            // 最后重启时间
  int64_t    lastAccessTime;        // 最后心跳时间
  int32_t    accessTimes;           // 累计心跳次数
  int32_t    numOfVnodes;           // 当前 vnode 数量
  int32_t    numOfOtherNodes;       // 其他逻辑节点数量(mnode/qnode/snode)
  int32_t    numOfSupportVnodes;    // 支持的最大 vnode 数
  float      numOfCores;            // CPU 核心数
  int64_t    memTotal;              // 总内存(字节)
  int64_t    memAvail;              // 可用内存
  int64_t    memUsed;               // 已使用内存
  EDndReason offlineReason;         // 离线原因
  uint16_t   port;                  // 端口号
  char       fqdn[TSDB_FQDN_LEN];  // 完全限定域名
  char       ep[TSDB_EP_LEN];      // "fqdn:port" 格式
  char       machineId[TSDB_MACHINE_ID_LEN + 1]; // 机器唯一标识
} SDnodeObj;
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:管理员先在集群中注册新节点

sql 复制代码
-- 在现有集群中执行
CREATE DNODE 'server2.example.com:6030';

该命令由 mndProcessCreateDnodeReq() 处理,核心逻辑:

c 复制代码
// source/dnode/mnode/impl/src/mndDnode.c
static int32_t mndCreateDnode(SMnode *pMnode, SRpcMsg *pReq, SCreateDnodeReq *pCreate) {
  SDnodeObj dnodeObj = {0};
  dnodeObj.id = sdbGetMaxId(pMnode->pSdb, SDB_DNODE);  // 自增分配 ID
  dnodeObj.createdTime = taosGetTimestampMs();
  dnodeObj.port = pCreate->port;
  tstrncpy(dnodeObj.fqdn, pCreate->fqdn, TSDB_FQDN_LEN);
  snprintf(dnodeObj.ep, TSDB_EP_LEN, "%s:%u", dnodeObj.fqdn, dnodeObj.port);
  // ... 通过事务写入 SDB 持久化
}

步骤 2:新节点发送首次 Status 请求

新节点启动时 dnodeId=0,发送 Status 消息到 firstEp。mnode 收到后通过 EP 匹配到预注册的 dnode,返回分配的 ID。

步骤 3:mnode 响应,返回集群全量信息

mnode 在 SStatusRsp 中返回:

c 复制代码
typedef struct {
  int32_t  dnodeId;       // 分配给新节点的 ID
  int64_t  dnodeVer;      // dnode 表版本号
  int64_t  clusterId;     // 集群 ID
  SArray  *pDnodeEps;     // 所有 dnode 的端点列表(用于节点发现)
  // ... 其他字段
} SStatusRsp;

新节点收到响应后,更新本地的 dnode.json,至此加入集群完成。

2.4 节点管理 SQL
sql 复制代码
-- 查看集群所有 dnode
SHOW DNODES;

-- 输出示例:
--   id   |        endpoint       | vnodes | support_vnodes | status | create_time         |       offline reason       |
--   1    | server1:6030          |      4 |             16 | ready  | 2024-01-01 00:00:00 |                            |
--   2    | server2:6030          |      3 |             16 | ready  | 2024-01-02 00:00:00 |                            |
--   3    | server3:6030          |      0 |             16 | offline| 2024-01-03 00:00:00 | 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 列会给出具体原因:

c 复制代码
// source/dnode/mnode/impl/inc/mndDef.h
enum {
  DND_REASON_ONLINE = 0,                         // 在线
  DND_REASON_STATUS_MSG_TIMEOUT,                  // 心跳超时
  DND_REASON_STATUS_NOT_RECEIVED,                 // 从未收到心跳
  DND_REASON_VERSION_NOT_MATCH,                   // 软件版本不匹配
  DND_REASON_DNODE_ID_NOT_MATCH,                  // dnodeId 不匹配
  DND_REASON_CLUSTER_ID_NOT_MATCH,                // clusterId 不匹配
  DND_REASON_STATUS_INTERVAL_NOT_MATCH,           // statusInterval 不匹配
  DND_REASON_TIME_ZONE_NOT_MATCH,                 // 时区不匹配
  DND_REASON_LOCALE_NOT_MATCH,                    // 语言环境不匹配
  DND_REASON_CHARSET_NOT_MATCH,                   // 字符集不匹配
  DND_REASON_TTL_CHANGE_ON_WRITE_NOT_MATCH,       // TTL 配置不匹配
  DND_REASON_ENABLE_WHITELIST_NOT_MATCH,           // 白名单配置不匹配
  DND_REASON_ENCRYPTION_KEY_NOT_MATCH,             // 加密密钥不匹配
  DND_REASON_TIME_UNSYNC,                          // 系统时间不同步
};

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

3. 心跳与状态上报机制

心跳机制是集群保持一致性和感知节点存活状态的基础。

3.1 心跳定时器

每个 dnode 启动后会创建一个专门的状态上报线程:

c 复制代码
// source/dnode/mgmt/mgmt_dnode/src/dmWorker.c
static void *dmStatusThreadFp(void *param) {
  SDnodeMgmt *pMgmt = param;
  int64_t     lastTime = taosGetTimestampMs();
  setThreadName("dnode-status");

  while (1) {
    taosMsleep(50);                              // 每 50ms 检查一次
    if (pMgmt->pData->dropped || pMgmt->pData->stopped) break;

    int64_t curTime = taosGetTimestampMs();
    if (curTime < lastTime) lastTime = curTime;  // 防止时钟回拨
    float interval = curTime - lastTime;
    if (interval >= tsStatusIntervalMs) {         // 默认 1000ms
      dmSendStatusReq(pMgmt);                    // 发送心跳
      lastTime = curTime;
    }
  }
  return NULL;
}

设计要点

  • 线程每 50ms 轮询一次,而非精确定时,这样可以优雅地处理时钟回拨
  • tsStatusIntervalMs 默认为 1000ms,可通过 statusInterval 配置参数调整(单位:秒)
3.2 Status 请求 --- dnode 上报了什么?

dmSendStatusReq() 函数(source/dnode/mgmt/mgmt_dnode/src/dmHandle.c)负责构建和发送 Status 请求。请求中包含以下信息:

复制代码
┌──────────────── SStatusReq 心跳请求 ────────────────┐
│                                                       │
│  基本信息:                                            │
│    ├── sver (软件版本号)                              │
│    ├── dnodeId / clusterId                           │
│    ├── dnodeEp (本节点端点)                           │
│    ├── rebootTime (上次重启时间)                      │
│    ├── numOfCores (CPU 核数)                          │
│    ├── numOfSupportVnodes (支持的 vnode 数)           │
│    ├── memTotal / memAvail (内存信息)                 │
│    └── statusSeq (心跳序列号)                         │
│                                                       │
│  集群配置 (SClusterCfg):                              │
│    ├── statusInterval / timezone                     │
│    ├── locale / charset                              │
│    ├── ttlChangeOnWrite                              │
│    ├── enableWhiteList                               │
│    └── encryptionKeyStat / encryptionKeyChksum       │
│                                                       │
│  负载信息:                                            │
│    ├── pVloads[] (每个 vnode 的负载)                   │
│    │     ├── vgId, syncState, syncTerm               │
│    │     ├── cacheUsage, numOfTables                 │
│    │     ├── numOfTimeSeries, totalStorage            │
│    │     └── pointsWritten, compStorage              │
│    ├── mload (mnode 负载,如果本节点有 mnode)          │
│    └── qload (qnode 负载,如果本节点有 qnode)          │
│                                                       │
└───────────────────────────────────────────────────────┘
3.3 Status 响应 --- mnode 返回了什么?

mnode 在 mndProcessStatusReq()source/dnode/mnode/impl/src/mndDnode.c)中处理心跳,完成以下检查和操作后返回响应:

复制代码
mndProcessStatusReq() 处理流程:

  1. 反序列化请求
         │
  2. 校验 clusterId ──→ 不匹配则拒绝 (DNODE_DIFF_CLUSTER)
         │
  3. 查找 dnode 对象
     ├── dnodeId > 0 → 按 ID 查找
     └── dnodeId == 0 → 按 EP 查找 (首次加入)
         │
  4. 检测变更
     ├── dnodeChanged: dnode 表版本变化
     ├── reboot: 节点重启
     ├── supportVnodesChanged: vnode 容量变化
     └── encryptKeyChanged / enableWhiteListChanged
         │
  5. 时间同步校验
     └── |curMs - statusReq.timestamp| >= tsTimestampDeltaLimit
         → 拒绝 (TIME_UNSYNC)
         │
  6. 更新 VNode 状态
     └── 遍历 pVloads[], 更新每个 vgroup 的
         syncState, cacheUsage, numOfTables 等
         │
  7. 更新 MNode / QNode 状态
         │
  8. 配置一致性检查 (needCheck 时)
     ├── 软件版本匹配
     ├── clusterId 匹配
     └── 更新 dnode 的硬件信息
         │
  9. 构建 SStatusRsp 响应
     ├── dnodeId / clusterId
     ├── dnodeVer (dnode 表版本)
     └── pDnodeEps[] (全量 dnode 端点列表)
         │
 10. 更新 lastAccessTime / offlineReason
3.4 在线/离线检测

mnode 通过心跳超时来判断 dnode 是否在线:

c 复制代码
// source/dnode/mnode/impl/src/mndDnode.c
bool mndIsDnodeOnline(SDnodeObj *pDnode, int64_t curMs) {
  int64_t interval = TABS(pDnode->lastAccessTime - curMs);
  if (interval > (int64_t)tsStatusTimeoutMs) {   // 默认约 3-5 秒
    if (pDnode->rebootTime > 0 && pDnode->offlineReason == DND_REASON_ONLINE) {
      pDnode->offlineReason = DND_REASON_STATUS_MSG_TIMEOUT;
    }
    return false;
  }
  return true;
}

超时判断逻辑

  • 如果当前时间与上次心跳时间的差值超过 tsStatusTimeoutMs,则判定为离线
  • 离线原因自动设置为 DND_REASON_STATUS_MSG_TIMEOUT
  • tsStatusTimeoutMs 默认值通常是 statusInterval 的数倍(防止偶发网络波动导致误判)
3.5 mnode EP 自动切换

当 dnode 向 mnode 发送心跳超时时,会自动轮转 mnode 端点:

c 复制代码
// source/dnode/mgmt/mgmt_dnode/src/dmHandle.c --- dmSendStatusReq()
code = rpcSendRecvWithTimeout(pMgmt->msgCb.statusRpc, &epSet, &rpcMsg, &rpcRsp, ...);
if (code == TSDB_CODE_TIMEOUT_ERROR) {
  dmRotateMnodeEpSet(pMgmt->pData);   // 切换到下一个 mnode EP
}

这保证了即使当前 mnode Leader 不可达,dnode 也能自动切换到其他 mnode 副本继续上报。

4. VGroup 分配与负载均衡

4.1 Hash 分片机制

创建数据库时,mnode 根据 vgroups 参数创建指定数量的 VGroup,并将整个 uint32 哈希空间均匀分割:

c 复制代码
// source/dnode/mnode/impl/src/mndVgroup.c --- mndAllocVgroup()
uint32_t hashMin = 0;
uint32_t hashMax = UINT32_MAX;
uint32_t hashInterval = (hashMax - hashMin) / pDb->cfg.numOfVgroups;

for (uint32_t v = 0; v < pDb->cfg.numOfVgroups; v++) {
    pVgroup->hashBegin = hashMin + hashInterval * v;
    if (v == pDb->cfg.numOfVgroups - 1) {
        pVgroup->hashEnd = hashMax;          // 最后一个 VGroup 包含剩余哈希值
    } else {
        pVgroup->hashEnd = hashMin + hashInterval * (v + 1) - 1;
    }
    // ...
}

示例: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
4.2 DNode 选择算法 --- 负载评分

当分配 VGroup 的副本到 dnode 时,mnode 使用评分排序算法选择最优 dnode:

c 复制代码
// source/dnode/mnode/impl/src/mndVgroup.c
static float mndGetDnodeScore(SDnodeObj *pDnode, int32_t additionDnodes, float ratio) {
  float totalDnodes = pDnode->numOfVnodes + (float)pDnode->numOfOtherNodes * ratio + additionDnodes;
  return totalDnodes / pDnode->numOfSupportVnodes;
}

评分公式

Score = numOfVnodes + numOfOtherNodes × 0.9 + additionDnodes numOfSupportVnodes \text{Score} = \frac{\text{numOfVnodes} + \text{numOfOtherNodes} \times 0.9 + \text{additionDnodes}}{\text{numOfSupportVnodes}} Score=numOfSupportVnodesnumOfVnodes+numOfOtherNodes×0.9+additionDnodes

其中:

  • numOfVnodes:当前已部署的 vnode 数量
  • numOfOtherNodes:其他逻辑节点数量(mnode/qnode/snode),乘以 0.9 的权重系数
  • numOfSupportVnodes:该 dnode 支持的最大 vnode 数(由硬件资源决定)
  • additionDnodes:模拟增减后的额外节点数(用于平衡预判)

分配过程

c 复制代码
// source/dnode/mnode/impl/src/mndVgroup.c --- mndGetAvailableDnode()
static int32_t mndGetAvailableDnode(SMnode *pMnode, SDbObj *pDb, SVgObj *pVgroup, SArray *pArray) {
  // 1. 按评分升序排列所有可用 dnode
  taosArraySort(pArray, mndCompareDnodeVnodes);

  // 2. 依次选取评分最低的 dnode 作为副本所在节点
  for (int32_t v = 0; v < pVgroup->replica; ++v) {
    SDnodeObj *pDnode = taosArrayGet(pArray, v);

    // 3. 检查容量限制
    if (pDnode->numOfVnodes >= pDnode->numOfSupportVnodes)
      return TSDB_CODE_MND_NO_ENOUGH_VNODES;

    // 4. 检查内存限制
    int64_t vgMem = mndGetVgroupMemory(pMnode, pDb, pVgroup);
    if (pDnode->memAvail - vgMem - pDnode->memUsed <= 0)
      return TSDB_CODE_MND_NO_ENOUGH_MEM_IN_DNODE;

    pDnode->memUsed += vgMem;
    pVgid->dnodeId = pDnode->id;
    pDnode->numOfVnodes++;
  }

  mndSortVnodeGid(pVgroup);  // 按 dnodeId 排序副本列表
  return 0;
}

选择策略总结

  1. 只考虑在线numOfSupportVnodes > 0 的 dnode
  2. 按评分(负载比率)升序排列,优先选择负载最低的节点
  3. 检查 vnode 数量上限和内存余量
  4. mnode 所在的 dnode 的 numOfOtherNodes 会 +1,使其评分略高,从而倾向于将 vnode 分散到非 mnode 节点
4.3 DNode 候选列表构建

mndBuildDnodesArray() 负责构建可用的 dnode 候选列表:

c 复制代码
// source/dnode/mnode/impl/src/mndVgroup.c --- mndBuildDnodesArrayFp()
// 过滤逻辑(遍历所有 dnode,符合条件的加入候选列表):
bool online = mndIsDnodeOnline(pDnode, curMs);     // 必须在线
bool isMnode = mndIsMnode(pMnode, pDnode->id);     // 是否是 mnode

pDnode->numOfVnodes = mndGetVnodesNum(pMnode, pDnode->id);   // 统计当前 vnode 数
pDnode->memUsed = mndGetVnodesMemory(pMnode, pDnode->id);    // 统计当前内存使用

if (isMnode) {
  pDnode->numOfOtherNodes++;   // mnode 占用一个"逻辑节点"名额
}

if (online && pDnode->numOfSupportVnodes > 0) {
  taosArrayPush(pArray, pDnode);   // 加入候选
}
4.4 手动 Balance VGroup

当集群扩容(添加新 dnode)或存在负载不均时,可以手动触发 VGroup 重新平衡:

sql 复制代码
-- 平衡所有 vgroup
BALANCE VGROUP;

-- 平衡 vgroup leader 分布
BALANCE VGROUP LEADER;

-- 将指定 vgroup 迁移到指定 dnode
REDISTRIBUTE VGROUP 10 DNODE 1 DNODE 2 DNODE 3;

BALANCE VGROUP 的核心算法在 mndBalanceVgroup() 中实现:

c 复制代码
// source/dnode/mnode/impl/src/mndVgroup.c --- mndBalanceVgroup()
while (1) {
  // 1. 按评分排序所有 dnode
  taosArraySort(pArray, mndCompareDnodeVnodes);

  // 2. 找到评分最高(负载最重)和最低(负载最轻)的 dnode
  SDnodeObj *pSrc = taosArrayGet(pArray, taosArrayGetSize(pArray) - 1);  // 负载最重
  SDnodeObj *pDst = taosArrayGet(pArray, 0);                              // 负载最轻

  // 3. 模拟迁移后的评分
  float srcScore = mndGetDnodeScore(pSrc, -1, 1);  // 源减 1
  float dstScore = mndGetDnodeScore(pDst, 1, 1);   // 目标加 1

  // 4. 如果迁移后仍然改善负载均衡,则执行迁移
  if (srcScore > dstScore - 0.000001) {
    mndBalanceVgroupBetweenDnode(pMnode, pTrans, pSrc, pDst, ...);
    pSrc->numOfVnodes--;
    pDst->numOfVnodes++;
    continue;
  } else {
    break;  // 已达到均衡
  }
}

算法特点

  • 贪心策略:每次从负载最重的节点迁移一个 vgroup 到负载最轻的节点
  • 收敛判断:当迁移后源节点评分不再高于目标节点评分时停止
  • 事务保护:所有迁移操作封装在一个串行事务中,保证原子性
  • 前置检查:要求所有 dnode 都在线,否则拒绝平衡操作
4.5 VGroup 查看
sql 复制代码
-- 查看某个数据库的 VGroup 分布
SHOW db_name.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 发送 CONNECT 请求。mnode 返回的 SConnectRsp 包含:

  • 完整的 mnode SEpSet(3 副本地址)
  • 分配的 connId
  • 数据库 VGroup 路由信息

客户端将 mnode 的 SEpSet 缓存在本地,后续直接使用。

5.2 VGroup 路由缓存(Catalog)

客户端通过 Catalog 模块 缓存数据库的 VGroup 分布信息:

复制代码
客户端写入/查询流程:
  1. 客户端计算表名的 hash 值
  2. 从 Catalog 缓存中查找对应的 VGroup
  3. 获取该 VGroup 的 SEpSet(包含 Leader/Follower 地址)
  4. 将请求直接发送到 Leader 所在的 dnode

当 Catalog 缓存过期或 VGroup 发生迁移时,客户端会收到错误码,自动从 mnode 重新拉取最新的 VGroup 路由信息。

5.3 心跳维护

客户端也有自己的心跳机制(clientHb.c),定期:

  • 检查 VGroup 缓存的版本号是否过期
  • 刷新认证和白名单信息
  • 更新连接状态

代码示例

部署 3 节点集群

节点 1(首节点)

bash 复制代码
# /etc/taos/taos.cfg
firstEp    node1.example.com:6030
fqdn       node1.example.com
serverPort 6030
bash 复制代码
systemctl start taosd

节点 2、3

bash 复制代码
# /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 上注册新节点

sql 复制代码
CREATE DNODE 'node2.example.com:6030';
CREATE DNODE 'node3.example.com:6030';

然后启动节点 2 和 3

bash 复制代码
# 在 node2 上
systemctl start taosd

# 在 node3 上
systemctl start taosd

创建 mnode 副本(高可用)

sql 复制代码
CREATE MNODE ON DNODE 2;
CREATE MNODE ON DNODE 3;

验证集群状态

sql 复制代码
SHOW DNODES;
-- 所有节点 status 应为 ready

SHOW MNODES;
-- 应显示 3 个 mnode,1 个 leader,2 个 follower

创建数据库并观察 VGroup 分布

sql 复制代码
-- 创建 3 副本数据库,4 个 vgroup
CREATE DATABASE power VGROUPS 4 REPLICA 3;

-- 查看 VGroup 分布
SHOW power.VGROUPS;

-- 每个 VGroup 的 3 个副本会被自动分散到 3 个 dnode 上
-- Leader 会均匀分布

扩容场景

sql 复制代码
-- 添加第 4 个节点
CREATE DNODE 'node4.example.com:6030';

-- 等待 node4 上线后,执行平衡
BALANCE VGROUP;

-- 部分 VGroup 会被自动迁移到 node4
-- 观察迁移结果
SHOW power.VGROUPS;

内部实现

集群拓扑的完整数据流

复制代码
                     ┌─────────────────────────────────────────┐
                     │              MNode (Leader)              │
                     │                                         │
                     │  SDnodeObj[] ← SDB 持久化 (Raft 复制)    │
                     │    ├── id, fqdn, port                   │
                     │    ├── numOfVnodes, numOfSupportVnodes  │
                     │    ├── lastAccessTime                   │
                     │    └── offlineReason                    │
                     │                                         │
                     │  SVgObj[] ← SDB 持久化                   │
                     │    ├── vgId, hashRange                  │
                     │    └── vnodeGid[replica]                │
                     └───────────┬─────────────────────────────┘
                                 │
                    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)│
     └──────────┘           └──────────┘           └──────────┘

关键源码文件索引

模块 源码路径 核心函数
EP 管理 source/dnode/mgmt/node_util/src/dmEps.c dmReadEps(), dmWriteEps(), dmUpdateEps(), dmGetDnodeEp()
状态心跳(发送) source/dnode/mgmt/mgmt_dnode/src/dmHandle.c dmSendStatusReq()
状态线程 source/dnode/mgmt/mgmt_dnode/src/dmWorker.c dmStatusThreadFp()
DNode 管理(mnode 侧) source/dnode/mnode/impl/src/mndDnode.c mndProcessStatusReq(), mndCreateDnode(), mndIsDnodeOnline()
VGroup 管理 source/dnode/mnode/impl/src/mndVgroup.c mndAllocVgroup(), mndGetAvailableDnode(), mndBalanceVgroup()
数据结构定义 source/dnode/mnode/impl/inc/mndDef.h SDnodeObj, SVgObj, SVnodeGid
消息定义 include/common/tmsg.h SEp, SEpSet, SStatusReq, SStatusRsp
DNode 本地数据 source/dnode/mgmt/node_util/inc/dmUtil.h SDnodeData, SDnodeEp

性能考量

心跳间隔调优

参数 默认值 说明
statusInterval 1(秒) 心跳间隔,减小可更快检测故障,但增加网络开销
tsStatusTimeoutMs ~3-5 秒 离线判定超时,通常为 statusInterval 的数倍
tsTimestampDeltaLimit 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"?

A1 : 这通常表示 mnode 在 tsStatusTimeoutMs 内未收到该 dnode 的心跳。常见原因:

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

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

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

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

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

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

A3: 可能的原因:

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

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

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

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

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

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

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

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

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

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

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

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

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

参考

  • TDengine 官方文档:集群部署与管理
  • 源码路径:source/dnode/mgmt/(dnode 管理)、source/dnode/mnode/impl/src/(mnode 实现)
  • 相关文章:《TDengine 整体架构全景 --- 深度解析》(同系列第 1 篇)

关于 TDengine

TDengine 专为物联网IoT平台、工业大数据平台设计。其中,TDengine TSDB 是一款高性能、分布式的时序数据库(Time Series Database),同时它还带有内建的缓存、流式计算、数据订阅等系统功能;TDengine IDMP 是一款AI原生工业数据管理平台,它通过树状层次结构建立数据目录,对数据进行标准化、情景化,并通过 AI 提供实时分析、可视化、事件管理与报警等功能。

相关推荐
Kiyra1 小时前
异步任务不用 Kafka 也行:用 Redis Stream 搭一套轻量级 Producer/Consumer 框架
数据库·人工智能·redis·分布式·后端·缓存·kafka
城事漫游Molly1 小时前
定量研究设计清单:问卷、实验与变量操作化怎么做?
大数据·人工智能·算法·ai写作·论文笔记
涤生大数据1 小时前
大数据凉了?速看4月的就业数据新鲜出炉!AI时代岗位不会原地消失,而是岗位的标准会被逐步抬高
大数据·人工智能
七夜zippoe1 小时前
基于 JiuwenClaw AgentTeam 集群模式的年会策划实战:从源码部署到多智能体协作落地
人工智能·agent·openjiuwen·jiuwenclaw·agentteam
Soari1 小时前
科研绘图新纪元:深度拆解 3DCellForge,AI 驱动的交互式 3D 细胞建模神器
人工智能·3d·科研绘图·3dcellforg
new【一个】对象1 小时前
Python 包管理器uv
人工智能·windows·python
m0_591364731 小时前
Python如何进行数据平滑处理_使用Pandas滚动中位数计算
jvm·数据库·python
振宇i1 小时前
MySQL数据库修改表结构语句
数据库·mysql
智塑未来1 小时前
高精度3D室内定位设备如何赋能机器人科研创新
人工智能·安全