matrixorigin / matrixorigin/matrixone
[Bug]: IVF Wiki 10M has sustained object-column read/decompression amplification under bounded cache
- Dominant language
- Go
- Stars
- 1.9k
- Forks
- 311
- Avg merge
- 1d 3h
- Merged PRs (30d)
- 768
Description
- [x] I have checked the existing issues.
## Branch Name
`main`、`4.2-dev`
## Commit ID
本问题在以下三个部署版本中观察到:
- Main 三 CN 基线:`39225f5fbc63b44a190cdd43f954ee07d78e6cdc`
- 4.2-dev Family:`4199b11c5c7bb24eccdb379cd6f68abcbd89e450`
- Main Family:`f0c31cd4b830be32442cf329e0a3fb08aa9c16c3`
当前证据可以确认实际运行瓶颈及热点代码路径,但还不能定位到单个引入 PR。三次运行不是只替换 image 的受控 A/B,因此暂不能把本问题定性为某个版本或 PR 的回归。
## Other Environment Information
- 负载:Wiki 10M IVF-FLAT benchmark
- 数据:10,000,000 条、768 维、`float32` 向量
- 查询:每轮 10,000 条,100 并发,`k=10`、`probe_limit=5`、`ivf_preload_entries=0`
- 索引参数:`lists=3162`、`vector_l2_ops`、`kmeans_train_percent=2`、`kmeans_max_iteration=20`
- benchmark commit:`0bae04528673d7756c4ee2478ac8e4e41f54df1b`
- Main 基线拓扑:3 个通用 CN,每个 CN 为 14 CPU / 55Gi memory / 12Gi Memory Cache / `GOMEMLIMIT=25000MiB`
- Family 拓扑:1 个 TP CN + 2 个 AP CN,每个 CN 为 14 CPU / 48Gi memory / 12Gi Memory Cache / `GOMEMLIMIT=25000MiB`;向量压测使用 `workload=ap`
- 几次运行中 IVF entries 隐藏表约为:10M 行、212-213 个 object、1272-1274 个 block、原始大小约 29.29GiB、压缩大小约 28.96GiB。
## Actual Behavior
### 总结
当 Wiki 10M IVF 查询由两个、各配置 12Gi Memory Cache 的 AP CN 提供服务时,性能出现明显断崖。差距不能只用“两个 CN 比三个 CN 少三分之一算力”解释:
- Main 三 CN 基线:两轮 `l2_only/pre` 共 23.88 分钟。
- 4.2-dev 两 AP:相同两轮共 55.41 分钟。
- Main 两 AP:相同两轮共 138.26 分钟。
Prometheus 和 Pyroscope 共同表明:额外墙钟时间对应 ObjectIO/FileService 列读取路径中显著增加的 CPU 工作。Memory Cache 未命中后,IVF entries 的压缩列从 DiskCache 读取,随后重新构造缓存列数据并进行 LZ4 解压,两个 AP CN 长时间接近 CPU 上限。采样中向量距离计算和 Go GC 占比都很低。
因此可以确认这是持续的“列读取/解压放大”性能问题。尚未证明 Memory Cache 容量是唯一上游原因,也尚未定位引入该行为的具体 PR。
### 三次运行总览
| Run / 部署 | 实际参与 IVF 的 CN | 第一轮 | 第二轮 | 两轮合计 |
|---|---:|---:|---:|---:|
| [Main Run 34136649854 / Job 101873583884](https://github.com/matrixorigin/mo-nightly-regression/actions/runs/34136649854/job/101873583884),`39225f5f` | 3 | 782.61s,12.78 QPS | 650.45s,15.37 QPS | 1433.06s / 23.88min |
| [4.2-dev Run 34367823695 / Job 102598577698](https://github.com/matrixorigin/mo-nightly-regression/actions/runs/34367823695/job/102598577698),`4199b11c` | 2 AP | 2459.40s,4.07 QPS | 865.23s,11.56 QPS | 3324.63s / 55.41min |
| [Main Run 34307530968 / Job 102404950664](https://github.com/matrixorigin/mo-nightly-regression/actions/runs/34307530968/job/102404950664),`f0c31cd4` | 2 AP | 4725.83s,2.12 QPS | 3569.67s,2.80 QPS | 8295.50s / 138.26min |
三次运行使用相同 benchmark commit 和查询/索引参数。但 MO SHA、拓扑、前序负载、索引物理布局和 recall 并非全部相同,因此上表可以证明实际现象,不能直接作为版本回归 A/B。
### Case 1:Main 三 CN 基线
Job:https://github.com/matrixorigin/mo-nightly-regression/actions/runs/34136649854/job/101873583884
- IVF setup 约在 `2026-09-07 22:49:20 UTC` 结束。
- 第一轮:`22:49:22-23:02:24 UTC`,782.61s,12.78 QPS,avg 7.677s,p50 0.07284s,p95 41.804s,max 553.081s,recall 75.61%。
- 第二轮:`23:02:25-23:13:15 UTC`,650.45s,15.37 QPS,avg 6.423s,p50 1.346s,p95 29.855s,max 147.743s,recall 75.61%。
- 第一轮三个 CN 合计约消耗 21,918 CPU-seconds,平均实际使用约 28.0 核。
- Memory Cache entry 命中率约 97.14%,约 742,827 个 entry 下沉到 DiskCache。
- 这组基线更快,但不是所有请求都稳定低延迟:第一轮前 8,114 条查询约 114 秒完成,剩余请求形成明显长尾。
### Case 2:4.2-dev Family,1 TP + 2 AP
Job:https://github.com/matrixorigin/mo-nightly-regression/actions/runs/34367823695/job/102598577698
- IVF setup 约在 `2026-09-09 19:32:11 UTC` 结束。
- 第一轮:`19:32:13-20:13:12 UTC`,2459.40s,4.07 QPS,avg 24.565s,p50 20.357s,p95 49.646s,max 221.07s,recall 36.82%。
- 第二轮:`20:13:13-20:27:38 UTC`,865.23s,11.56 QPS,avg 8.631s,p50 7.752s,p95 18.052s,max 36.381s,recall 34.97%。
- 第一轮两个 AP 合计约消耗 65,946 CPU-seconds,平均实际使用约 26.82 核;两个 AP 分别平均使用约 13.48 和 13.34 核。
- Memory Cache entry 命中率约 85.63%,约 233 万个 entry 下沉到 DiskCache;DiskCache entry 命中率约 99.43%。
- IVF entries 表提交时记录 10M 行、212 objects、1272 blocks、原始大小 29.29GiB、压缩大小 28.97GiB。
- 索引物理布局在测试期间仍在变化:第一轮记录到 74 条 `MERGE-START` 和 72 条 `MERGE-END`;其中一次 Merge 报告 `pointDepth 173 >= 3`,说明初建索引存在较高 object range 重叠。
- 两个 AP 后续在 `post` 阶段分别于 `20:30:52`、`20:31:08 UTC` 被 OOMKilled;两轮 `pre` 已在 `20:27:38` 结束。因此 OOM 不可能倒推造成已经结束的 55 分钟 `pre`,OOM 的内存责任栈应另行分析。
### Case 3:Main Family,1 TP + 2 AP
Job:https://github.com/matrixorigin/mo-nightly-regression/actions/runs/34307530968/job/102404950664
- IVF setup:`2026-09-09 09:06:12-10:05:28 UTC`。
- 第一轮:`10:05:29-11:24:15 UTC`,4725.83s,2.12 QPS,avg 47.131s,p50 45.881s,p95 53.182s,max 330.745s,recall 75.53%。
- 第二轮:`11:24:17-12:23:46 UTC`,3569.67s,2.80 QPS,p50 35.343s,p95 45.473s,recall 75.53%。
- 后续两轮 `post` 分别约为 1136.96s / 8.80 QPS、866.43s / 11.54 QPS,合计仍约 33.39 分钟。`pre` 和 `post` 是不同执行模式,不能把它们当成纯粹的冷/热缓存 A/B。
- 本次运行 CN/TN/Log restart 均为 0,没有 OOM 证据。
本次运行同期存在其他 Job,但时间线不能解释掉 IVF 自身瓶颈:
| 任务 | UTC 时间 |
|---|---|
| SSB + TPCH Family | 05:41:05-09:05:38 |
| IVF setup | 09:06:12-10:05:28 |
| MOTR | 09:44:54-10:33:34 |
| ANLI | 09:51:13-11:50:44 |
| IVF pre 第一轮 | 10:05:29-11:24:15 |
| IVF pre 第二轮 | 11:24:17-12:23:46 |
- TPCH 在 IVF setup 开始前已经结束。
- MOTR 只与前段重叠;ANLI 与第一轮和第二轮前约 26 分钟重叠。
- ANLI 结束后,第二轮剩余约 6,077 条查询仍耗时约 2,000 秒,约 3.04 QPS。
- `12:00 UTC`,已观测到的其他负载结束后,两个 AP 仍分别使用约 13.92 和 10.58 核,TP CN 仅使用约 0.72 核;该分钟 AP 的 ranges 日志均指向 IVF entries 表。
- 第一轮两个 AP 合计约消耗 126,664 CPU-seconds,是 Main 三 CN 基线的约 5.78 倍;两边平均实际总用核却接近(26.8 对 28.0)。
- Memory Cache entry 命中率约 85.49%,约 403 万个 entry 下沉到 DiskCache,是 Main 基线的约 5.42 倍;DiskCache entry 命中率为 99.66%。
- 第二轮仍记录约 326 万次 DiskCache entry 读取,Memory Cache entry 命中率约 85.02%。第一轮结束后,活跃 decoded-column working set 仍未完整驻留内存。
### 运行时热点与代码路径
在并发 Job 结束后采集的五分钟 CPU profile 中,两个 AP 的热点几乎一致:
| Symbol | AP 1 | AP 2 |
|---|---:|---:|
| `relationScanner.ScanRelation` cumulative | 94.99% | 95.26% |
| `LoadColumnDataByTopN` cumulative | 94.07% | 94.18% |
| `DiskCache.Read` cumulative | 89.81% | 90.77% |
| `compress.Decompress` cumulative | 57.46% | 55.57% |
| `runtime.memmove` flat | 57.01% | 55.10% |
| `L2DistanceSqFloat32` cumulative | 0.13% | 0.18% |
4.2-dev 第一轮 profile 也呈现相同形态:`LoadColumnDataByTopN` cumulative 约 94%,解压/memmove 约 58%,向量距离计算仅约 0.1%;Go GC 在这些样本中同样可以忽略。
实际观测到的执行路径为:
```text
IVF planReader
relationScanner.ScanRelation
BuildReaders / reader.Read
BlockDataReadInner
LoadColumnDataByTopN / ReadColumnTopN
ObjectIO column read
S3FS.Read -> Memory Cache miss -> DiskCache.Read
IOEntry.ReadFromOSFile
IOEntry.setCachedData
columnCacheConstructorFactory / constructorFactory
compress.Decompress -> LZ4 decode -> runtime.memmove
```
受影响 Main SHA 中的对应代码:
- [`relationScanner.ScanRelation`](https://github.com/matrixorigin/matrixone/blob/f0c31cd4b830be32442cf329e0a3fb08aa9c16c3/pkg/vectorindex/ivfflat/plan_reader.go#L576-L647):获取 ranges 并构建 IVF 隐藏表 reader。
- [`LoadColumnDataByTopN`](https://github.com/matrixorigin/matrixone/blob/f0c31cd4b830be32442cf329e0a3fb08aa9c16c3/pkg/objectio/ioutil/loadfuncs.go#L349-L364):进入 ObjectIO 的 Top-N 列读取路径。
- [`ReadColumnTopN`](https://github.com/matrixorigin/matrixone/blob/f0c31cd4b830be32442cf329e0a3fb08aa9c16c3/pkg/objectio/column_topn.go#L30-L87):支持 chunk 读取,但对非 chunked 或不满足条件的列仍走完整列读取 fallback。
- [`IOEntry.ReadFromOSFile`](https://github.com/matrixorigin/matrixone/blob/f0c31cd4b830be32442cf329e0a3fb08aa9c16c3/pkg/fileservice/io_entry.go#L55-L101):读取缓存字节后调用 `setCachedData`。
- [`constructorFactory`](https://github.com/matrixorigin/matrixone/blob/f0c31cd4b830be32442cf329e0a3fb08aa9c16c3/pkg/objectio/constructors.go#L151-L179):为 legacy LZ4 列分配解压后空间并执行解压。
因此 DiskCache 命中不等于已命中解压后的列数据:仍可能需要读取压缩字节、构造列表示并解压。SQL 最终只返回 `k=10`,并不保证存储层只读取或解压 10 行数据。
### 证据分级
已经确认:
- 24、55、138 分钟分别是两轮真实查询时间,不包含索引 setup,也不是 stdout 时间戳误判。
- 慢运行实际走 IVF entries 隐藏表,AP CN 长时间 CPU-bound。
- 主要采样开销位于 ObjectIO/FileService 列读取、缓存数据构造和 LZ4 解压,而不是向量距离计算或 Go GC。
- Main Family 慢运行在已观测并发任务结束后仍持续低 QPS。
- DiskCache 大部分命中,但 Memory Cache miss 与 decoded-column 重建仍然很高。
- 4.2 的 AP OOM 发生在 `pre` 结束后,不能解释此前的慢查询。
高可信推断,但尚未闭环为唯一根因:
- 约 29.29GiB 的 entries 表跨越两个彼此独立的 12Gi Memory Cache,相比三个独立的 12Gi Cache,可能跨过缓存容量/分布阈值。85% 对 97% 的 entry 命中率以及 5.4 倍 DiskCache entry 数量支持这一解释。
- Cache 并不是一个统一共享的 24Gi/36Gi 池;object 分布、重复读取、其他数据占用、cache admission 和持续 Merge 重写都会降低有效驻留率。
尚未证明:
- 每条查询都会读取或解压整个 29GiB entries 表。
- MOTR/ANLI 并发是主要原因。
- S3 网络等待是主要原因;profile 是 CPU-heavy,DiskCache 命中率也很高。
- 单纯增大 Memory Cache 可以安全解决;当前环境同时存在 OOM 风险。
- 某个具体 MatrixOne PR 引入该问题;尚未完成同布局、同拓扑、只替换 image 的 A/B。
- 几次运行之间 recall 差异就是 correctness bug。recall 差异首先意味着当前跨运行性能比较不完全等价,需要单独验证。
## Expected Behavior
当 decoded data 超过 Memory Cache 后,IVF-FLAT 查询性能应可预测、平滑地下降,而不是因为持续重建/解压 object column 耗尽大部分 CN CPU,并相对可比运行下降 3-6 倍。对于能够安全复用的 DiskCache 数据,应在对象格式、ownership 和内存记账允许的前提下避免不必要的重复解码。
本 Issue 不要求两个 AP CN 达到三个 CN 的性能,也不报告 wrong result 或数据损坏。后续修复/优化必须受 CN 内存预算约束,不能用 OOM 换取 QPS。
## Steps to Reproduce
当前是生产化回归环境中的多次观测,还不是最小确定性 UT:
1. 部署 1 个 TP CN + 2 个 AP CN;每个 CN 配置 14 CPU、48Gi memory limit、12Gi Memory Cache、`GOMEMLIMIT=25000MiB`。
2. 通过 `workload=ap` 将向量压测租户路由到 AP CN。
3. 加载 Wiki 10M、768 维、`float32` 数据。
4. 创建 IVF-FLAT 索引:`lists=3162`、`vector_l2_ops`、`kmeans_train_percent=2`、`kmeans_max_iteration=20`。
5. 使用 100 并发执行 10,000 条 `l2_only` 查询,参数为 `k=10`、`probe_limit=5`、`ivf_preload_entries=0`,连续执行两轮 `pre`。
6. 保存每轮 QPS/latency、每个 CN 的 CPU、Memory Cache entry hit/miss、DiskCache read、FileService 分阶段计数和 CPU profile。
7. 使用同一个 image、同一批物理索引 object 和同一组查询向量,对照两个与三个实际参与 IVF 的 CN;然后固定拓扑/布局,仅替换 image 做第二组 A/B。
上述三个公开 Job 均可作为现象复现记录。
## Additional information
### 在归因具体 PR 前建议完成的验证
1. 固定相同的物理索引 object 和查询向量顺序。
2. 测量时冻结 Merge,或单独统计 Merge 影响,避免把物理布局正在变化的两轮当成纯缓存 A/B。
3. 在同一 image 上比较两个与三个有效 IVF CN,并记录每个 CN 的 decoded bytes/cache occupancy,而不只记录 entry 次数。
4. 固定拓扑和物理布局后,再比较候选 MatrixOne SHA。
5. 确认 benchmark 执行的是真实 literal-vector SQL;使用子查询向量的诊断 `EXPLAIN` 会形成不同计划,不能据此声称每条压测 SQL 都 full scan 29GiB。
6. 将建索引后的首轮 warm-up 与稳定态查询分别报告,并始终同时保留 recall 和 QPS,避免把候选工作量明显不同的运行视为等价。
### Duplicate search
已搜索近期 open/closed Issue:`34307530968`、`IVF Wiki 10M`、`IVF slow`、`cache performance`,未发现同时覆盖这三次运行现象和该热点路径的重复 Issue。
已有 Issue 可能覆盖单点向量或内存优化;本 Issue 专门记录在有界 Memory Cache 下持续的 ObjectIO/FileService 列读取/解压放大及其对 CN 拓扑高度敏感的性能断崖。
Contributor guide
Assessment
This issue has not been assessed yet.