apache / apache/iotdb

[Bug] 2.0.10 故障切换后 DataNode 保留过期 Region 路由,查询持续访问离线副本并超时

オープン
#18,630 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る
主要言語
Java
スター
6.4k
フォーク
1.2k
平均マージ
1日 23時間
マージ済み PR(30日)
115

説明

## 现象与环境

三节点集群断网测试后,两个在线 DataNode 均可登录、读取表结构,但同一条数据查询在 B 节点成功,在 C 节点持续超时。**已确认 C 缓存了过期的 Region 副本顺序;仅重发一次 ConfigNode 当前路由,查询立即恢复。**

- IoTDB 2.0.10(BuildInfo `3c26382`),Table model;openEuler 24.03 LTS-SP3,Temurin OpenJDK 25.0.3+9-LTS。
- 三台物理机,每台部署一个 ConfigNode 和一个 DataNode。A(DN3)断网,B(DN4)、C(DN5)在线,C 为 ConfigNode Leader。
- ConfigNode / SchemaRegion 使用 Ratis,Schema 副本数 3;DataRegion 使用 IoTConsensus,数据副本数 2。
- 默认参数:`read_consistency_level=strong`、`enable_topology_probing=false`;连接和查询超时均为 60000 ms。
- DataRegion 1 位于 A/C,Leader 为 C;DataRegion 2 位于 A/B,Leader 为 B。每个 Region 均有在线副本,B/C 之间相关内部端口可达。

主机名和库表名已脱敏,Node ID、Region ID 与耗时保留实测值。

## 现场复现与验证

1. 在已有三节点集群中进行网络中断/恢复测试,期间出现 ConfigNode Leader 切换;排查时保持 A 断网。
2. 等待集群显示 A 为 Unknown、B/C 为 Running,分别通过 B/C 执行:

```sql
SELECT * FROM test_db.test_table LIMIT 5;
SELECT COUNT(*) FROM test_db.test_table;
```

3. B 正常返回,C 多次约 60 秒后返回 `720: Current query is time out`。DBeaver 和安装包自带 CLI 均可复现;C 服务端日志显示仍尝试连接离线的 A:

```text
can't execute request on node TEndPoint(ip:node-A, port:10730),
error msg is java.net.SocketTimeoutException: Connect timed out
```

通过只读 Java Attach 诊断读取现有 JVM 对象,确认差异位于 `ClusterPartitionFetcher.partitionCache.groupIdToReplicaSetMap`:

| 路由来源 | DataRegion 2 副本顺序 |
| --- | --- |
| ConfigNode 当前路由 | [4, 3],在线 B 优先 |
| B 的 PartitionCache | [4, 3] |
| C 的 PartitionCache | **[3, 4],离线 A 优先** |

随后读取 ConfigNode 的 `getLatestRegionRouteMap()`,通过一次 `updateRegionCache(TRegionRouteReq)` 将其原始时间戳和路由发给 C,返回状态 200。C 的顺序变为 `[4, 3]`,相同查询立即恢复:`LIMIT 5` 为 0.058 秒,`COUNT(*)` 为 0.095 秒、返回 40 行。

整个验证期间 A 仍断网,C 的进程未重启,未修改配置或数据库数据。这证明过期路由与本次超时直接相关;重发路由只是临时恢复措施。

## 关键源码与疑似触发条件

以下链接固定到现场版本提交 `3c26382bebee73ec1c9593f836ef4cdcc0376062`。

**1. 路由广播排除 Unknown 节点,并且依赖优先级发生变化。**

[RouteBalancer.java,615–653 行](https://github.com/apache/iotdb/blob/3c26382bebee73ec1c9593f836ef4cdcc0376062/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/load/balancer/RouteBalancer.java#L615-L653) 中,仅当新旧优先级不同才设置 `needBroadcast`;广播目标过滤条件为:

```java
.filterDataNodeThroughStatus(
NodeStatus.Running, NodeStatus.Removing, NodeStatus.ReadOnly)
```

[节点状态变化回调,771–775 行](https://github.com/apache/iotdb/blob/3c26382bebee73ec1c9593f836ef4cdcc0376062/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/load/balancer/RouteBalancer.java#L771-L775) 调用 `handleBalanceAction(balanceAllEnabledRegionLeaders())`,没有直接向恢复的节点补发路由。若后续权威优先级不变,可能无法再次触发广播。

现场 ConfigNode 日志时间顺序(UTC+8):

```text
09:20:58.106 DN3/4/5:null -> Unknown
09:20:58.111 DataRegion 2:null -> [4,3]
09:20:58.115 DN4/5:Unknown -> Running
```

因此怀疑:Leader 切换初始化时,C 处于 Unknown 而错过路由,恢复 Running 后未获得补发。**尚未捕获当次广播的实际接收者或 RPC 报文,也未在全新集群中稳定复现首次漏发;这个具体触发时序仍是推断。**

**2. DataNode 命中旧缓存后,不会主动拉取最新路由。**

[PartitionCache.java,572–619 行](https://github.com/apache/iotdb/blob/3c26382bebee73ec1c9593f836ef4cdcc0376062/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/cache/partition/PartitionCache.java#L572-L619) 的 `getRegionReplicaSet()` 先查本地 Map,仅在 `result.isEmpty()` 时向 ConfigNode 获取路由。该 Map 没有 TTL,因此已有 Region 的旧副本顺序可持续命中。

**3. 连接离线副本可能耗尽整个查询时间。**

[FragmentInstanceDispatcherImpl.java,583–615 行](https://github.com/apache/iotdb/blob/3c26382bebee73ec1c9593f836ef4cdcc0376062/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/FragmentInstanceDispatcherImpl.java#L583-L615) 在连接异常后检查查询截止时间;若已超过,直接返回查询超时。现场连接与查询超时同为 60 秒,与日志中的连接超时后返回 720 一致。

## 希望确认

- 是否存在上述路由漏发且缺少恢复补发/缓存重新校验的问题?除了当前配置,还有哪些触发条件需要验证?
- 2.0.11 引入的元数据租约机制(相关 PR #18127、#18228)能否覆盖此场景,还是需要独立修复路由同步?目前未升级验证,也未找到明确对应本次完整现象的已修复 issue。

预期:集群状态稳定且每个 Region 仍有在线副本时,各在线 DataNode 应最终更新路由,避免持续将查询发往已离线的副本。

コントリビューションガイド

コントリビューションガイドを開く

調査の方向性

まず RouteBalancer.java の 615–653 行と 771–775 行から始め、DataNode のステータス変更に伴うルートブロードキャストを追跡し、次に PartitionCache.java の 572–619 行を調べて古いルートの再利用を確認します。報告された 3 ノードのフェイルオーバーシナリオを使い、ConfigNode のルートと各 DataNode のキャッシュを比較してください。FragmentInstanceDispatcherImpl.java の 583–615 行には、古いエンドポイントがどのようにタイムアウトになるかが示されています。オンラインの DataNode が現在のルートに収束し、クエリがオフラインのレプリカを対象にしなくなれば完了です。

索引モデルが issue の本文から書いたものです。

評価

技術スタック
java
領域
databases, distributed-systems
issue の種類
バグ
難易度
4/5
見積もり時間
3〜5日
活発さ
活発
明瞭さ
おおむね明確
初心者へのやさしさ
38/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。