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

概述
TDengine 的分布式架构从设计之初就面向多节点集群,通过 dnode(数据节点)组成集群,在集群内部自动进行节点发现、心跳维护、故障检测与 VGroup 负载均衡。本文从源码层面深入解析 TDengine 集群的四个核心机制:
- Endpoint(EP)机制:集群中每个节点的唯一标识与寻址方式
- 节点发现与加入:新节点如何发现并加入已有集群
- 心跳与状态上报:节点如何保持存活感知和信息同步
- 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 地址作为节点标识,原因包括:
- 容器化友好:在 Docker/K8s 环境中,容器 IP 会频繁变化,但 FQDN 可以保持稳定
- 多网卡支持:一台机器可能有多个 IP,FQDN 避免了网卡绑定的歧义
- 集群迁移简化:节点迁移到新 IP 后,只需更新 DNS 解析即可,无需重配所有节点
- 统一的配置管理:所有节点使用同一套 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;
}
选择策略总结:
- 只考虑在线 且
numOfSupportVnodes > 0的 dnode - 按评分(负载比率)升序排列,优先选择负载最低的节点
- 检查 vnode 数量上限和内存余量
- 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 的心跳。常见原因:
- 网络不通------检查防火墙和网络连通性
- FQDN 无法解析------确认
/etc/hosts或 DNS 配置 - 目标 dnode 进程已崩溃------检查 taosd 进程和日志
- 时区/版本/配置不匹配------查看 offline reason 详细信息
Q2: 新建的 dnode 一直显示 offline 是什么原因?
A2 : 最常见的原因是先启动了新节点,后才执行 CREATE DNODE。正确流程是:
- 先在集群中执行
CREATE DNODE 'new_fqdn:port' - 再启动新节点的 taosd
如果顺序反了,新节点发送的 Status 请求中 dnodeId=0,但 mnode 找不到匹配的 EP(因为还没注册),会直接拒绝。
Q3: BALANCE VGROUP 执行后没有效果?
A3: 可能的原因:
- 只有 1 个 dnode------至少需要 2 个节点才能平衡
- 有 dnode 离线------平衡操作要求所有节点在线
- 已经达到均衡------算法判断迁移后不会改善负载分布
- 数据库启用了 Arbitrator------此类数据库的 VGroup 不参与平衡
Q4: 一个 dnode 最多能承载多少 vnode?
A4 : 由 supportVnodes 参数控制,默认根据 CPU 核数自动计算(通常为 核数 * 2)。可以通过 taos.cfg 中的 supportVnodes 参数手动设置。每个 vnode 大约需要 1GB 内存(取决于 buffer 和 pages 参数),所以实际上限受内存约束。
Q5: 客户端如何知道数据应该发往哪个 dnode?
A5: 客户端通过以下步骤确定路由:
- 对表名计算 hash 值
- 在 Catalog 缓存中查找 hash 值对应的 VGroup
- 从 VGroup 的
SEpSet中获取 Leader 的FQDN:Port - 直接发送请求到 Leader
Catalog 缓存通过版本号机制保持与 mnode 同步。当 VGroup 发生迁移或 Leader 切换时,客户端会自动刷新缓存。
Q6: 集群中各节点的配置参数必须一致吗?
A6 : 以下参数必须一致,否则节点会被判定为 offline:
timezone(时区)locale(语言环境)charset(字符集)- 软件版本号(
sver) ttlChangeOnWriteenableWhiteList- 加密密钥(如果启用加密)
其他参数(如 supportVnodes、buffer)可以根据各节点硬件不同而异。
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 提供实时分析、可视化、事件管理与报警等功能。