[Bug] 2.0.10 故障切换后 DataNode 保留过期 Region 路由,查询持续访问离线副本并超时
- Lenguaje dominante
- Java
- Estrellas
- 6.4k
- Forks
- 1.2k
- Merge medio
- 1 d 23 h
- PR fusionados (30 d)
- 115
Descripción
## 现象与环境
三节点集群断网测试后,两个在线 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 应最终更新路由,避免持续将查询发往已离线的副本。
Guía de contribución
Línea de trabajo
Comienza con RouteBalancer.java, líneas 615–653 y 771–775, para rastrear las difusiones de rutas en torno a los cambios de estado de los DataNode; después, inspecciona PartitionCache.java, líneas 572–619, para detectar la reutilización de rutas obsoletas. Usa el escenario de failover de tres nodos informado y compara las rutas de ConfigNode con la caché de cada DataNode; FragmentInstanceDispatcherImpl.java, líneas 583–615, muestra cómo el endpoint obsoleto se convierte en un timeout. Se considera terminado cuando los DataNode en línea convergen en las rutas actuales y las consultas ya no apuntan a la réplica offline.
Escrito por el modelo de indexación a partir del texto del issue.
Evaluación
- Stack tecnológico
- java
- Área
- databases, distributed-systems
- Tipo de issue
- Error
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Estado de actividad
- Activo
- Claridad
- Bastante claro
- Aptitud para principiantes
- 38/100