前 9 篇,系统是跑在单机上的:一个 TDengine 容器,REPLICA 1,数据只有一份。节点挂了,数据还在盘上,但服务没了。这一篇把部署换成三节点集群,REPLICA 3,然后真的停掉一个节点做故障演练------看看查询还能不能继续。这也是本系列最后一篇:从部署到高可用,一条遥测数据的旅程完整闭环。
第 1 篇,我们从三个容器起步:TDengine、PostgreSQL、Grafana。一路写到写入管线、查询 API、告警引擎,系统已经能跑通「设备 → 入库 → 告警」的完整链路。
单机上的数据只有一份------REPLICA 1。第 2 篇讲库参数时提过,REPLICA 就是副本数。节点挂了,数据还在盘上,服务却没了。
这一篇是系列收官:部署换成三节点集群,REPLICA 3,然后真的停掉一个节点做故障演练。文末还有 10 篇全景回顾。
三节点集群长什么样
先过三个概念,后面都靠它们说话。
dnode 是一个运行的 TDengine 实例,一个容器就是一个 dnode。mnode 是管理节点,管集群元数据------dnode 列表、库、账号,集群里可以有多个,互为冗余。vnode/vgroup 是数据分片:一个 vgroup 是一组 vnode 副本,REPLICA 3 就是每个 vgroup 有 3 份副本,副本间用 RAFT 同步,主副本挂了自动选新主。

三节点的 compose 长这样(deploy/cluster/docker-compose.yml):
yaml
x-tdengine-common: &tdengine-common
image: ${TDENGINE_IMAGE:-tdengine/tdengine:3.3.6.6}
restart: unless-stopped
environment: &tdengine-environment
TAOS_FIRST_EP: td1:6030
TZ: ${TZ:-Asia/Shanghai}
services:
td1:
<<: *tdengine-common
hostname: td1
environment:
<<: *tdengine-environment
TAOS_FQDN: td1
ports:
- "6030:6030"
- "6041:6041"
volumes:
- td1-data:/var/lib/taos
- td1-log:/var/log/taos
几个关键点逐个看。
TAOS_FIRST_EP: td1:6030------新节点启动时连这个地址加入集群。td2、td3 都继承这个变量,所以它们启动后会自动找 td1 报到。
TAOS_FQDN: tdN------容器内节点之间用 hostname 互相通信,必须显式声明,否则节点之间互相找不到。
端口:td1 暴露 6030/6041 给宿主机。td2、td3 分别映射 16030/16041、26030/26041,主机端口偏移 10000 避免冲突,容器内端口不变。
卷:每节点独立命名卷 tdN-data、tdN-log。三节点各自的盘,模拟真实环境里的独立磁盘。
depends_on: [td1]------td2、td3 等 td1 先起来。加入集群总得有个入口。
对比第 1 篇的单机:一个 dnode、REPLICA 1、端口直接映射。集群只是多写几个配置,但数据结构从「一份」变成了「三份」。
初始化:等节点、建 mnode、REPLICA 3 建库
集群启动后不能马上建库。scripts/cluster-init.sh 做了三件事,第一件是等 taosAdapter 就绪:
bash
wait_http "http://${TDENGINE_HOST}:${TDENGINE_PORT}/-/ping" 90 2 || fail "cluster not ready"
REST 入口 /-/ping 通了才继续,90 次 × 2 秒。
然后是等三个 dnode 全部加入:
bash
for _attempt in {1..60}; do
response="$(tdengine_sql 'SHOW DNODES')"
if grep -q 'td3:6030' <<<"${response}"; then
break
fi
sleep 2
done
grep -q 'td3:6030' <<<"${response}" || fail "not all dnodes joined"
SHOW DNODES 里出现 td3:6030 才往下走,最多等 60 次 × 2 秒。没等到就直接 fail------集群没齐就开始建库,副本会缺胳膊少腿。
最后一步是扩 mnode:
bash
tdengine_sql 'CREATE MNODE ON DNODE 2' >/dev/null || true
tdengine_sql 'CREATE MNODE ON DNODE 3' >/dev/null || true
mnode 也要冗余。mnode 是集群的「控制面」,只有一个的话,它挂了集群就没人管了------数据面可能还在转,但增删库、加节点、改账号全都不行。
然后建库:
bash
tdengine_sql "CREATE DATABASE iot REPLICA 3 VGROUPS 4 BUFFER 256 KEEP 3650d PRECISION 'ms'"
和第 2 篇单机库参数对比,只改了一个:REPLICA 1 → 3。其余沿用(BUFFER 256 / KEEP 3650d / PRECISION ms)。VGROUPS 4 表示 4 个虚拟组分布在 3 个 dnode 上,每组 3 副本。

这里有个警告必须说清楚:cluster-init.sh 会重建 iot 库为 REPLICA 3,勿对已有生产数据执行。
健康检查:四个命令看四个层次
脚本 scripts/cluster-health.sh 就四个命令:
bash
print_query "Data nodes" "SHOW DNODES"
print_query "Management nodes" "SHOW MNODES"
print_query "Database" "SHOW DATABASES"
print_query "Virtual groups" "SHOW iot.VGROUPS"
每个命令看一个层次:
- SHOW DNODES:三个数据节点在不在线
- SHOW MNODES:管理节点几个,期望 3
- SHOW DATABASES:iot 库的 replica 列,期望 3
- SHOW iot.VGROUPS:4 个 vgroup,每个的 vnode 分布在哪些 dnode、谁是 leader
docs/05-ha-operation.md 还补充了用 information_schema.ins_dnodes、ins_mnodes 表查元数据。
运维红线也写在里面:不要让两个已经分别初始化的数据节点再尝试合并;生产环境用稳定 FQDN、固定网络、独立磁盘和时间同步服务。说的都是同一件事------集群依赖节点互相寻址,地址变了、时间偏了,集群就散。
故障演练:真的停掉一个节点
健康检查正常,接下来动真格。scripts/failure-drill.sh 完整逻辑如下:
bash
TARGET_CONTAINER="${1:-tdengine-iot-cluster-td2-1}"
DOWNTIME_SECONDS="${DOWNTIME_SECONDS:-30}"
log "pre-flight cluster health"
"${SCRIPT_DIR}/cluster-health.sh"
log "stopping ${TARGET_CONTAINER} for ${DOWNTIME_SECONDS}s"
docker stop "${TARGET_CONTAINER}" >/dev/null
recover() {
log "ensuring ${TARGET_CONTAINER} is running"
docker start "${TARGET_CONTAINER}" >/dev/null 2>&1 || true
}
trap recover EXIT
deadline=$((SECONDS + DOWNTIME_SECONDS))
successes=0
failures=0
while ((SECONDS < deadline)); do
if response="$(tdengine_sql 'SELECT COUNT(*) FROM iot.telemetry' 2>/dev/null)" \
&& tdengine_response_ok "${response}"; then
successes=$((successes + 1))
else
failures=$((failures + 1))
fi
sleep 1
done
recover
trap - EXIT
log "waiting for node recovery"
sleep 10
"${SCRIPT_DIR}/cluster-health.sh"
log "drill complete: successful queries=${successes}, failed queries=${failures}"
五个设计点值得拆开讲。
默认停 td2。容器名 tdengine-iot-cluster-td2-1(compose 项目名 + 服务名 + 序号),可参数化。想停 td3,传参就行。
先是 pre-flight 健康检查:演练前先确认集群是健康的,否则结果没法解读。集群本来就病着,停一个节点再测,分不清是谁的锅。
然后是 trap recover EXIT。脚本无论怎么退出------正常结束、中途 Ctrl-C、报错------都会先把容器拉起来。演练不能以留下一个停掉的节点收场,这条是安全底线。
循环体是每 1 秒一次 COUNT(*) 查询:持续读路径探针。tdengine_sql 走 REST /rest/sql,tdengine_response_ok 判 "status":"succ",统计 successes 和 failures。
收尾也有讲究:恢复后 sleep 10 再做健康检查。容器起来了不等于副本同步完了,等 raft 重选和同步稳定下来,再看集群状态。
那么结果如何?诚实的结论是:REPLICA 3 下停掉一个节点,查询应该大部分成功。因为 vgroup 的主副本通常在别的节点上,查询会路由到可用副本。
失败可能出现在三种情况:停掉的节点恰好是某个 vgroup 的 leader、raft 重选窗口内请求超时、taosAdapter 连接池指向死节点。具体数字取决于当时的 vgroup leader 分布,脚本只输出 successful queries=N, failed queries=N 的统计格式。

docs 里还有一段严谨的提醒:演练应同时运行持续写入,记录业务序列号,恢复后检查缺口、重复、vgroup 同步时间和客户端重连行为。failure-drill.sh 只允许在明确的演练环境使用。
备份≠副本
看到这里,有人可能觉得:REPLICA 3 都有了,是不是不用备份了?
docs 里有句原话:高可用副本不能替代备份。
副本防的是节点故障。一个 dnode 挂了,数据在别的节点上还有,服务继续。
备份防的是逻辑错误。误删库、坏数据写入、配置漂移、勒索病毒------副本会忠实地把错误同步到每一份拷贝上。三个副本一起坏,和一份数据坏,结果一样。
备份清单包括五类:数据库数据、账号权限、配置、PostgreSQL 元数据、Grafana 配置。
验证备份的标准只有一个:定期在隔离环境恢复,核验行数、时间范围、业务抽样。不能只看备份任务返回成功------备份任务成功只代表「写完了」,不代表「能恢复」。
具体备份工具不展开,按你的 TDengine 版本选官方工具即可。

收官:10 篇全景回顾
从第 1 篇到第 10 篇,一条线走完了:部署、建模、SQL、写入管线、数据保底、连接方式、数据入口、查询 API、告警、高可用。
主线只有一句话:一条遥测数据,从设备出发,写入 TDengine,被查询 API 读取,被告警引擎盯住,最后在集群故障时依然可用------完整闭环。
这 10 篇实现的是一个「能跑」的系统。往生产化走,还有几个方向:监控与告警推送、容量规划(KEEP/数据量/磁盘)、备份恢复 SOP、跨机房冗余、故障演练常态化。
最后想听你说说:你所在团队怎么做高可用演练?多久一次?评论区聊聊。
觉得有用?点个关注,持续获取优质内容。