wanggx opened a new issue, #69816:
URL: https://github.com/apache/doris/issues/69816
# [Bug] ANN/HNSW 索引在多副本间不一致导致查询负载静默倾斜到单个 BE,且无 SQL 可观测索引一致性
## Summary
Doris 4.1.5,单 tablet 3 副本(1 bucket, replication_num=3)的 HNSW 向量表,BE 重启后 3
副本的**数据版本一致但 HNSW 索引状态不一致**(1 个副本索引最新可用,2 个副本索引停留在旧 compaction 版本)。查询时 FE
偶尔调度到索引异常的副本会 fragment 执行失败并 cancel 回退到索引正常的副本,最终**所有查询负载静默倾斜到单个 BE**,另外 2 个 BE
完全闲置,3 倍资源只用了 1/3。
核心问题不是负载倾斜本身(那是回退机制的合理结果),而是**索引层不一致在现有 SQL/接口里完全不可观测**——`ADMIN SHOW REPLICA
STATUS` 显示 3 副本 Version 一致 / NORMAL / OK,`SHOW INDEX`
显示索引定义存在,运维无法发现索引异常,只能通过磁盘文件 mtime 或间接的查询耗时推断。
## Environment
- Doris: `doris-4.1.5-release-917b20be838`
- 3 BE + 1 FE(FE 独立,BE 纯 BE)
- 表:1 bucket, replication_num=3, HNSW index (inner_product, dim=768,
max_degree=128, ef_construction=256)
```sql
CREATE TABLE `t` (
`id` int NULL,
`embedding` array<float> NOT NULL,
INDEX idx_embedding (`embedding`) USING ANN PROPERTIES(
"dim" = "768", "ef_construction" = "256",
"index_type" = "hnsw", "max_degree" = "128", "metric_type" =
"inner_product")
) ENGINE=OLAP
DUPLICATE KEY(`id`)
DISTRIBUTED BY HASH(`id`) BUCKETS 1
PROPERTIES ("replication_allocation" = "tag.location.default: 3");
```
## Reproduction
1. 建表 + 导入 10M × 768d 数据,3 副本均衡,`ADMIN SHOW REPLICA STATUS` 全 NORMAL/OK
2. **重启 3 个 BE**(模拟运维/升级)
3. 重启后,3 副本数据 Version 一致(=21),但 compaction 进度(VersionNum)分化:BE-A=21(持续
compaction + index rebuild),BE-B=14,BE-C=12(重启后 compaction 未跟上,无 index rebuild)
4. 从客户端持续跑向量查询(`ORDER BY inner_product_approximate(embedding, cast(...) as
array<float>) LIMIT 100`),3 BE 开 sar 监控
## Observed Behavior
**负载全压在 BE-A 一个节点**(sar 1s 采样 91 点,覆盖 40s 查询窗口):
| BE | avg %user | idle>90% 秒数 |
| --- | --- | --- |
| BE-A(索引新) | 37.7% | 0 |
| BE-B(索引旧) | 0.3% | 91/91s |
| BE-C(索引旧) | 0.5% | 91/91s |
BE-B/C 全程完全闲置。查询 QPS ≈ 单 BE,3 倍资源浪费 2/3。
## Root Cause Analysis(实测证据链)
### 1. 索引新旧不一致(磁盘文件 mtime 直接证据)
3 副本 tablet 目录下 `.idx` 索引文件 mtime:
| BE | 最新索引文件时间 | 索引文件数 |
| --- | --- | --- |
| BE-A | 重启后持续 build(最新 14:31) | 88 |
| BE-B | 停在 10:09(重启前) | 36 |
| BE-C | 停在 01:35(重启前) | 36 |
BE-A 重启后持续 `ann_index_writer` build 到 14:31,BE-B/C 重启后无任何 `ann_index_writer`
日志。3 副本**数据 Version=21 一致,HNSW 索引停留在不同 compaction 版本**。
### 2. BE-B/C 索引异常导致查询慢(排除冷加载)
停掉 BE-A,强制查询落 BE-B/C,连续 6 条查询耗时:
```
q00 sec=20.845 q01 sec=20.794 q02 sec=20.670
q03 sec=21.021 q04 sec=20.875 q05 sec=20.575
```
**恒定 ~20s,无递减**——排除 page cache 冷加载(若是缓存冷,后续应递减)。BE-B/C 查询恒定慢 200 倍(vs BE-A 的
0.1s),结合索引文件 mtime 停在重启前、重启后无 index
build,**强烈指向索引异常**(具体是全扫描还是索引重建开销,未做字节级验证,但"索引问题导致慢"由 mtime + 恒定耗时 + BUILD
INDEX CANCELLED 三重间接证据支持)。
### 3. FE 调度到索引异常副本时 fragment 失败回退(be 日志铁证)
3 BE 全在查询时段,BE-C be.WARNING 有 4 条:
```
W ... fragment_mgr.cpp:712] Query <qid> does not exists, failed to cancel it
```
这 4 个被 BE-C cancel 的 query_id **全部在 BE-A be.INFO 有对应执行日志**(`rowid_fetcher
id_file_map not found for query_id: <qid>`)——**被索引异常副本 cancel 的查询最终在索引正常副本执行**。
### 4. 停 BE 实验确证 BE-B/C 可查询可执行
- 停 BE-A → 查询落 BE-B + BE-C(成功,~20s/条)
- 停 BE-A + BE-C → 查询落 BE-B(成功,21s/条)
**BE-B/C 完全可查询可执行**,不是副本选择机制不选它们,是索引异常导致 fragment 失败回退到 BE-A。
## The Real Issue: 索引一致性无可观测
负载倾斜是回退机制的合理结果,**真正的缺陷是索引层不一致在现有接口里不可观测**:
| 接口 | 显示 | 能否发现索引异常 |
| --- | --- | --- |
| `ADMIN SHOW REPLICA STATUS` | 3 副本 Version=21 / NORMAL / OK | ❌ 只看数据版本 |
| `SHOW INDEX FROM tbl` | 索引定义存在 | ❌ 只看定义 |
| `SHOW BUILD INDEX FROM db` | 主动触发的 build 任务历史(FINISHED/CANCELLED) | ⚠️
只看任务历史,非持续状态;且只覆盖主动触发的 build |
| `SHOW TABLET` / PROC 下钻 | 副本 pathHash/Version | ❌ 无索引版本字段 |
| BE `/api/meta/header` | rowset 级信息 | ⚠️ 二进制/JSON,无索引健康汇总 |
**运维盲区**:3 副本看着完全均衡(Version 一致、NORMAL、OK),实际 2 个副本索引异常、查询 200 倍慢、负载全压 1
个节点——没有任何 SQL 能直接发现,只能磁盘 `ls -lt *.idx` 看文件 mtime 或推断查询耗时。
触发条件常见:**BE 重启后 compaction 未跟上**就会触发,非极端场景。
## Suggestion
1. **`ADMIN SHOW REPLICA STATUS` 增加索引状态列**:显示每个副本的索引 build
版本/是否最新/是否健康,让索引不一致在副本状态层可见。
2. **索引异常告警**:当副本数据 Version 一致但索引版本落后时,tablet scheduler 应识别并触发索引重建(类似
REPLICA_MISSING 的机制),而非静默依赖 compaction 顺带重建。
3. **`BUILD INDEX` 失败后自动重试**:当前 `OBTAIN_LOCK_FAILED` 失败 10 次达阈值就 CANCELLED
放弃,应退避重试而非放弃。
## Related
- 副本选择源码:`OlapScanNode.addScanRangeLocations` 默认
`Collections.shuffle(replicas)` 随机选,但 fragment 在索引异常副本执行失败后 FE cancel
回退到正常副本(`SimpleScheduler.getLocation` 回退逻辑)。
- `BUILD INDEX` 只 build 新 rowset 的索引,不重建已有旧索引(`_input_rowsets is empty` 即
0ms 完成),无法修复重启后 compaction 未跟上的旧索引。
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]