Lance 查询最佳实践
本文帮助你为 Lance 查询选择每个 BE 的缓存预算和 Index Segment 布局。从工作负载出发,估算需要保留的缓存内容,再在目标并发下验证。示例是容量规划的起点,不是经过压测的最优值或 BE 内存上限。
Lance Catalog 是实验性功能,从 Apache Doris 4.2 开始支持。请先通过 Lance Catalog 配置访问,并检查其 Reader 兼容性。Doris 读取已有的 Lance 索引;构建和维护索引需要使用兼容的 Lance 工具。
标量过滤与向量或全文检索组合时,请参阅 Lance 混合检索性能最佳实践,了解过滤条件位置、索引对齐构建和 Profile 诊断。
1. 根据场景选择调优路径
| 查询场景 | 从哪里开始 | 验证重点 |
|---|---|---|
| 单查询低延迟的向量搜索 | 保留经常探测的向量分区;按粒度建议,相对于可用 BE 数对比均衡的 Segment 数量 | 相近召回率下的 P95 延迟、最慢扫描器、峰值内存 |
| 高并发向量搜索 | 为不同查询向量访问的热点分区并集做预算;为并发搜索保留内存和 CPU,而不是让每条查询都使用最多 Split | 目标并发下的吞吐、P95 延迟、缓存重新加载和各 BE 内存 |
| BTree 范围/点查或 Bitmap/LabelList 过滤 | 估算热点页面或位图条目,包含查找结构;确认谓词和索引实际使用情况 | 页面加载、选择率、临时 row ID 集合及输出列读取 |
| 全文或短语检索 | 估算词典、文档元信息、倒排,以及启用时的位置数据;预热时覆盖不同词项 | 高频词查询、短语查询内存、候选归并 |
| 向量、标量和全文查询共享 BE | 将各自的共享工作集相加,并混合测试 | 一类查询是否淘汰了另一类查询的缓存 |
| 输出列回表或扫描占主要耗时 | 检查数据文件读取及独立的磁盘数据缓存 | 有效读取量与读放大,解码或回表是否占主要耗时 |
标量过滤也可能是向量查询 Prefilter 的一部分,因此一条查询可以同时需要标量和向量索引内容。应选择所有适用的场景,不必将它们视为互斥选项。
2. 收集输入并确定内存预算
为每个 BE 收集以下信息,或在真实调度条件下选择有代表性的 BE:
- 工作负载: 目标 QPS/并发、延迟目标、召回率要求、
top_k、过滤条件、短语查询和输出列。 - 资源: 本次查询可用的 BE、CPU 能力,以及扣除其他 Doris 工作负载和进程/操作系统需求后可用的内存。
- 索引: 类型、版本、物理 Segment 数、覆盖情况、向量维度/编码、IVF 分区数,以及标量值宽度或字符串长度。
- 复用情况: 有代表性的查询时间窗口内访问的不同分区、页面、词项或位图条目。应使用跨查询的工作集,而不是一条查询返回的行数。
- 查询峰值: 目标并发下正在加载的分区、Prefilter/结果 row ID 集合、候选堆、解码、精排和二阶段回表内存。
使用索引公式估算载荷,再检查总预算是否容得下:
available_Lance_cache_budget_on_BE
= BE memory budget
- other BE memory needs
- peak concurrent query working memory
- safety headroom
index_cache_target + metadata_cache_target
<= available_Lance_cache_budget_on_BE
即从 BE 内存预算中扣除其他 BE 需求、并发查询峰值和安全余量,剩余部分才用于索引与元数据缓存。这是规划检查,不是强制的内存预留;缓存容量不限制查询内存或进程 RSS。不要将可复用的缓存工作集直接乘以并发查询数,但要为这些查询额外的工作内存预留空间;并发查询访问不同数据时,还应计入缓存内容并集的增长。
完整参数定义和默认值保留在 BE 配置中。将 lance_* BE 参数写入各 BE 的 be.conf,并重启该 BE。它们不是 Catalog 属性或 SQL 会话变量。Index Segment 数量和 IVF 构建参数需要通过 Lance 索引构建工具设置,不由这些缓存参数控制。
3. 估算索引工作集
应根据每个 BE 需要复用的索引分区,为 Index Cache 配置容量。存储中的完整索引大小、缓存工作集和查询峰值内存是三个不同的量。以下估算覆盖 Doris 支持的六种向量索引类型,用于容量规划,不代表精确的缓存计量或内存上限。
各索引类型的向量载荷
使用以下符号,所有公式的单位均为字节;1 GiB = 1073741824 bytes。
| 符号 | 含义 |
|---|---|
N | 待估算分区中的索引向量数。普通单向量列对应已索引行数;多向量列应统计已索引的子向量数量,并单独预留行映射和精排内存。 |
D | 向量维度。 |
w | 未量化向量中每个存储元素的字节数:Float16 为 2,Float32 为 4,Float64 为 8。对于二进制 uint8 向量,以实际字节长度作为 D,并取 w=1。 |
m、b | PQ 子向量数量和每个子向量编码的位数。这是索引构建参数,不是查询参数。 |
G | 加载后的 HNSW 图内存,包括所有层级及辅助结构。 |
需要累加所有适用组件,不能只计算向量编码。 第一张表拆分逐向量载荷,后续表格列出共享结构和 HNSW 图组件。公式估算加载后的缓冲区;标为 需测量 的项目同样要预留预算,不代表零开销。共享分配只计算一次。
| 索引类型 | 向量值 / 编码 | UInt64 row ID | HNSW 图 | 还需计入的预算 |
|---|---|---|---|---|
IVF_FLAT | N * D * w | 8 * N | 无 | 下表中的 IVF 结构及通用开销 |
IVF_SQ | SQ8 为 N * D | 8 * N | 无 | IVF 结构、SQ 元信息及通用开销 |
IVF_PQ | N * ceil(m * b / 8) | 8 * N | 无 | IVF 结构、PQ 码本及通用开销 |
IVF_HNSW_FLAT | N * D * w | 8 * N | 加上 G | IVF 结构及通用开销 |
IVF_HNSW_SQ | SQ8 为 N * D | 8 * N | 加上 G | IVF 结构、SQ 元信息及通用开销 |
IVF_HNSW_PQ | N * ceil(m * b / 8) | 8 * N | 加上 G | IVF 结构、PQ 码本及通用开销 |
PQ 的 b=8 时,每个向量的编码占 m 字节;b=4 时,在支持的偶数 m 配置下占 m/2 字节。例如,100 万个向量、m=96、b=8,编码占 96000000 字节,row ID 占 8000000 字节,合计 104000000 字节,约 99.2 MiB。压缩程度较高的索引尤其不能忽略 row ID 的开销。
| 额外组件 | 适用类型 | 加载后载荷 / 预算方法 |
|---|---|---|
| IVF 聚类中心 | 全部六种类型 | 每套保留的 FP32 聚类中心为 num_partitions * D * 4;其他布局按实际元素宽度计算 |
| IVF 分区目录 / 子索引元信息 | 全部六种类型 | 需测量:保留的分区偏移、长度及 Reader 结构;独立于向量值和聚类中心 |
| PQ 码本 | IVF_PQ、IVF_HNSW_PQ | 每份保留的标准 FP32 码本为 2^b * D * 4 |
| SQ 量化器元信息 | IVF_SQ、IVF_HNSW_SQ | 需测量:保留的量化边界/参数;不能假设各版本采用固定布局 |
| 范数、行覆盖结构及额外映射 | Reader 实际保留时 | 需测量:实际数组/集合;多向量的行映射需单独预留预算 |
| 索引对象、Arrow 有效性缓冲区、闲置容量及副本 | 全部六种类型 | 需测量:逻辑载荷以外的分配;共享缓冲区只计算一次 |
独立的 Index Segment 可能分别保留聚类中心和码本。不要将共享分配按每个分区重复计算,也不要假设所有副本都能共享。额外结构取决于嵌入的 Lance 版本。
HNSW 图的载荷拆分
图内存需要在向量编码和 row ID 之外额外计算。 对于使用 UInt32 节点/邻居编号、可能保留 Float32 边距离的 Arrow 布局,设 E 为已加载的所有层级中有向邻居条目总数,V 为这些层级的节点出现次数之和,A 为独立保留的图批次数。一个节点出现在多个层级时,需要在 V 中计入多次。
| 图组件 | 主要载荷,单位为字节 | 统计口径 |
|---|---|---|
| 邻居编号 | 4 * E | 每个有向邻居条目一个 UInt32,统计所有层级 |
| 保留的边距离 | 4 * E | 每个邻居条目对应一个 Float32;Reader 跳过距离列时为零 |
| 图节点编号 | 4 * V | 每次节点出现对应一个 UInt32;与数据集的 UInt64 row ID 不同 |
| 邻居列表偏移 | 4 * (V + A) | Int32 列表偏移,每个批次包含一个末尾偏移 |
| 距离列表偏移 | 4 * (V + A) | 第二组 Int32 列表偏移数组;Reader 跳过距离列时为零 |
| 保留距离时的小计 | 8 * E + 12 * V + 8 * A | 大批次可近似为 8 * E + 12 * V,尚不是完整的 G |
| 不保留距离时的小计 | 4 * E + 8 * V + 4 * A | 仅保留节点编号和邻居列表时使用;两种小计选择其一,不累加 |
| 层级偏移、入口点及节点查找结构 | 需测量 | 计入每个已加载图保留的元信息和查找结构分配 |
| 有效性缓冲区、图对象及闲置分配容量 | 需测量 | 计入上述缓冲区之外的实际分配,不重复计算共享缓冲区 |
完整图预算 G | 主要小计 + 测得的图额外开销 | 与向量/row ID 载荷及其他索引结构相加 |
这是特定布局的估算,不是每个节点固定占用的保证。HNSW 的 m 控制图连接数,与 PQ 的 m 无关。不要用 N * m * 4 代替 G:实际度数、上层节点、保留的距离以及 Reader 版本都会影响结果。应加载有代表性的分区,测量其保留的分配以校准估算。
例如,假设缓存的 IVF_HNSW_PQ 分区共包含 100 万个向量,PQ m=96、b=8,E=32000000、V=1100000、A=1。假设此 Reader 保留距离列。这里的图规模是示例输入,不是由 HNSW 的 m 推导出的数值:
| 组件 | 示例载荷 |
|---|---|
| PQ 编码 | 96000000 字节 |
| 数据集 row ID | 8000000 字节 |
| 主要图缓冲区 | 8 * 32000000 + 12 * 1100000 + 8 = 269200008 字节,约 256.7 MiB |
| 编码 + row ID + 主要图缓冲区 | 373200008 字节,约 355.9 MiB |
| 备选:跳过距离列时的主要图缓冲区 | 4 * 32000000 + 8 * 1100000 + 4 = 136800004 字节,约 130.5 MiB |
| 备选合计:编码 + row ID + 不保留距离的图缓冲区 | 240800004 字节,约 229.6 MiB |
| 两种情况均需加上 | 上表中的图额外开销、IVF 聚类中心/目录、PQ 码本及通用开销 |
按 Reader 实际保留的列选择对应示例。两种小计都不是完整的缓存容量建议。
上游 Lance Performance Guide 提供向量载荷和索引构建内存的估算,Lance 向量索引格式 说明 IVF 分区、子索引和量化存储的组成。上游最新文档可能包含 Doris 嵌入版本之后的功能,兼容性应以 Lance Catalog 页面的支持范围为准。
标量与全文索引的载荷
标量索引也可以估算,但不同索引类型需要的输入不同。应统计单个 BE 实际保留的条目,而不只是查询返回的行数。以下方法描述 Lance 的存储布局,不扩展 Doris 的谓词下推或索引兼容性范围。应确认嵌入的 Lance 版本支持该索引,并确认查询实际使用了它。
BTree。 加载后的叶子页包含字段值和 UInt64 row ID。设缓存叶子页共包含 R 条记录、H 个页面,定长值宽度为 w,这些页面中字符串的平均字节长度为 L:
| 叶子页组件 / 表示方式 | 值缓冲区 | 偏移数组 | UInt64 row ID |
|---|---|---|---|
| Int32 / Float32 | 4 * R | 无 | 8 * R |
| Int64 / Float64 / 64 位时间戳 | 8 * R | 无 | 8 * R |
| 其他定长值 | w * R;按位存储的 Boolean 使用实际位图缓冲区 | 无 | 8 * R |
| Arrow Utf8 / Binary | R * L | Int32 偏移为 4 * (R + H) | 8 * R |
| Arrow LargeUtf8 / LargeBinary | R * L | Int64 偏移为 8 * (R + H) | 8 * R |
叶子页小计为后三列载荷之和。 每个已打开的 Segment 还需计入下列组件;这里的 P 是该 Segment 的全部叶子页数,不只是缓存页数:
| BTree 额外组件 | 主要载荷 / 预算方法 |
|---|---|
| Lookup 最小值 | 定长值为 P * w;变长值使用实际值/偏移缓冲区 |
| Lookup 最大值 | 定长值为 P * w;变长值使用实际值/偏移缓冲区 |
| Lookup NULL 计数 | UInt32 布局为 4 * P |
| Lookup 页号 | UInt32 布局为 4 * P |
| 定长 Lookup 缓冲区小计 | P * (2 * w + 8),不含下面的额外结构 |
| NULL 页面列表及查找对象 | 需测量:保留的列表、对象及实现中的副本 |
| 叶子页/Lookup 有效性缓冲区、Arrow 对象及闲置容量 | 需测量:逻辑值/偏移/row ID 以外的分配 |
字符串公式假设使用表中的 Arrow 布局;字典或 view 表示需要使用实际缓冲区。磁盘压缩后的大小不能代表解码后的页面大小。BTree 默认每页 4096 条记录,不是字节;应使用索引的实际页大小。Lookup 属于索引工作集,不能因为它描述页面就划入 BE Metadata Cache 预算。
例如,1 亿个 Int64 值的叶子页值和 row ID 载荷为 100000000 * 16 = 1600000000 字节,约 1.49 GiB。如果缓存页面覆盖 1000 万条记录,则该载荷约为 152.6 MiB。按每页 4096 条记录计算,完整索引约有 24415 页;lookup batch 的 min/max/计数/页号载荷约 572 KiB,尚未包含其他结构。返回十条匹配记录不代表只缓存十条记录:Reader 加载的是页面。
Bitmap、LabelList 与 NGram。 累加下表中适用的组件。成员关系位图不能直接按每个匹配行 8 字节计算。LabelList 按不同标签统计成员关系,同一行可以出现在多个标签位图中。NGram 按不同 gram/行组合统计成员关系,同一行内重复出现同一个 gram 不增加成员关系。这里的 NGram 索引与使用 n-gram 分词器的 FTS 索引不同。
| 索引类型 | 组件 | 加载后载荷 / 预算方法 |
|---|---|---|
| Bitmap | 键查找结构 | 需测量:保留的键值、条目引用及查找对象 |
| LabelList | 标签查找结构 / 包装结构 | 需测量:不同标签键、条目引用及包装对象 |
| NGram | Gram 查找结构 | 需测量:保留的 gram、条目引用及查找对象 |
| 全部三种类型 | 成员关系位图:数组容器 | 每个覆盖 65536 个取值范围、含 c 个 UInt16 条目的常规 Roaring 数组容器约为 2 * c |
| 全部三种类型 | 成员关系位图:稠密容器 | 每个覆盖 65536 个取值范围的常规稠密容器为 8192 字节 |
| 全部三种类型 | 成员关系位图:run / 完整 Fragment 表示 | 需测量:实际表示;每个容器按实际选用的表示计算,不将各备选表示累加 |
| 全部三种类型 | 位图目录 / 容量 | 需测量:Fragment/高位目录、容器对象及闲置容量 |
| 实际存在时 | NULL / NULL 列表跟踪 | 需测量:单独计入保留的 NULL 成员关系结构 |
| 每种类型的合计 | 查找结构 + 已加载的成员关系表示 + 目录 + NULL 跟踪 | 共享分配只计算一次;序列化位图大小不能保证驻留内存大小 |
例如,256 个已加载的稠密容器有 2 MiB 的 bitset 载荷,尚未包含键、目录、NULL 跟踪和对象开销。
其他标量与全文索引。 每行列出一个独立组件;小计行汇总之前的组件,不能再次累加。只统计该 BE 保留的内容。需测量表示需要使用 Reader 的实际分配,不表示该组件可以忽略。
| 索引类型 | 组件 | 加载后载荷 / 预算方法 |
|---|---|---|
| FTS / Inverted | 词典 | 需测量:加载的词项、词典结构及查找分配 |
| FTS / Inverted | 文档元信息 | 需测量:保留的文档映射、长度及其他 Reader 元信息 |
| FTS / Inverted | 未压缩倒排 row ID | T 个 UInt64 词项/文档条目为 8 * T |
| FTS / Inverted | 未压缩倒排词频 | Float32 词频为 4 * T;ID 与词频合计 12 * T |
| FTS / Inverted | 位置数据,启用时 | O 次存储的 Int32 出现位置为 4 * O |
| FTS / Inverted | 位置偏移 | 实际保留的偏移数组字节数,独立于 4 * O |
| FTS / Inverted | 压缩倒排布局 | 使用实际保留的压缩块替代对应的未压缩布局公式 |
| FTS / Inverted | 评分 / 块元信息及分配开销 | 需测量:保留的块描述、评分数据、对象及闲置容量 |
| ZoneMap | 最小值 | Z 个加载的 zone、宽度为 w 的定长值,逻辑载荷为 Z * w |
| ZoneMap | 最大值 | 逻辑载荷为 Z * w;min/max 合计 2 * Z * w |
| ZoneMap | 计数及 zone 边界 | 需测量:保留的计数和边界;按各 Fragment 统计 zone,包括不足一个 zone 的尾部 |
| ZoneMap | NULL 跟踪及装箱值/对象开销 | 需测量:可选的 NULL 行集合及实际标量分配;可能大于 min/max 的逻辑字节数 |
| BloomFilter | 过滤器位数组 | 累加实际分配的过滤器字节数;Z 个等大的过滤器、每个 F 字节时为 Z * F |
| BloomFilter | Zone 描述 / 过滤器对象 | 需测量:保留的描述、过滤器参数及对象开销 |
| BloomFilter | NULL 跟踪 | 需测量:可选的 NULL 行集合 |
| RTree | 包围盒坐标 | 每个二维条目有四个 Float64 坐标,为 32 * Q;Q 统计所有缓存树层级的条目 |
| RTree | 行 / 子页面 ID | UInt64 ID 为 8 * Q;坐标与 ID 合计 40 * Q |
| RTree | 树元信息及页面偏移 | 需测量:保留的树描述及页面偏移缓冲区 |
| RTree | NULL 跟踪、Arrow/对象及闲置容量 | 需测量:实际额外分配 |
| JSON-path 包装类型 | 底层索引 | 使用所选标量索引类型的完整组件预算 |
| JSON-path 包装类型 | 路径及包装结构 | 需测量:在底层索引之外保留的路径和包装分配 |
BloomFilter 应使用构建器实际按块取整后的分配;仅凭行数和误判率不能描述所有布局。对于不熟悉或实验性的布局,应测量加载后的分配,避免套用其他索引类型的公式。匹配行数很少,也可能仍需较大的词典、树或文档元信息。
查询工作内存与上述缓存表格分开预留:临时交集/并集、结果 row ID 集合、近似过滤器复核、RTree 精确复核读取的原始几何值,以及输出列读取都需要单独预算。
底层结构可参考上游 BTree 格式、Bitmap 格式和 FTS 格式。这里估算的是加载后的载荷,不是压缩文件大小或查询总 RSS。
从索引大小推导每个 BE 的工作集
对于 Segment s,令 N_s 为索引向量数,P_s 为 IVF 分区数,H_s 为某个 BE 需要保留的不同分区数。分区较均衡时:
cached_vectors_on_BE ≈ sum over segments (N_s * H_s / P_s)
index_cache_target ≈ cached vector/row-ID payload
+ cached HNSW graphs
+ centroids, codebooks and other index allocations
+ measured headroom
即:缓存目标容量约为缓存向量及 row ID 载荷、HNSW 图、聚类中心与码本等索引分配,加上经测量确定的余量。分区倾斜时,应使用实际分区行数。H_s 是跨查询的工作集,不是某次查询的 nprobes;不同查询可能逐渐访问所有分区,重复或并发查询也可能共享同一分区缓存。
按不同的 (dataset, index segment, partition) 统计缓存项,并计入该 BE 访问的所有表、索引和版本。一个 BE 的缓存不会预热其他 BE,调度也不保证每个 Index Segment 始终在同一个 BE 上执行。除非实际分配和复用情况支持这一假设,否则不要直接用完整索引大小除以 BE 数量。
查询还会额外占用内存:正在加载的分区、被缓存淘汰但仍由查询持有的对象、Prefilter 状态、距离表、候选堆、解码批次、精排和二阶段回表。top_k、ef、refine_factor、扫描并发和 nprobes 会影响这些内存或访问的工作集,但不会改变每条向量的存储字节数。缓存配置不是 Lance 或 BE RSS 的硬上限,增大缓存也不会消除距离计算和每次查询的过滤工作。
配置示例与调优
假设一个 Index Segment 中有 1 亿个 FP32、768 维向量,索引为 IVF_PQ,m=96、b=8,分为 4096 个均衡分区。某个 BE 上的典型工作负载反复访问其中 1024 个不同分区:
Full code/row-ID payload = 100000000 * (96 + 8) = 10400000000 bytes ≈ 9.69 GiB
Cached payload = 10400000000 * 1024 / 4096 = 2600000000 bytes ≈ 2.42 GiB
IVF centroids = 4096 * 768 * 4 = 12582912 bytes = 12 MiB
PQ codebook = 256 * 768 * 4 = 786432 bytes = 0.75 MiB
如果这是该 BE 上唯一较大的索引工作集,且 BE 内存预算允许,可将 4 GiB 作为初始 Index Cache 容量,在计算出的载荷之外为其他索引分配留出空间。该值需要验证,不代表通用的开销比例。如果工作负载需要完整索引常驻,4 GiB 就不够;即使默认的 10 GiB,也已接近未计入其他分配的 9.69 GiB 载荷。
若 BE 在预留查询峰值、其他 Doris 工作负载、进程和操作系统余量后,仍可分配 4 GiB 索引缓存和 1 GiB 元数据缓存,可配置:
lance_index_cache_size_bytes = 4294967296
lance_metadata_cache_size_bytes = 1073741824
重启该 BE 后生效。应分别为每个 BE 做预算;这些是 BE 配置参数,不是 Catalog 属性或 SQL 会话变量。
- 使用有代表性的查询及并发预热,覆盖不同查询向量、过滤条件和表。仅重复同一个查询向量会低估多样化工作负载的工作集。
- 对比
doris_be_lance_session_index_cache_usage_bytes与doris_be_lance_session_index_cache_capacity_bytes、时间窗口内hits_total/misses_total的增量,以及每次查询的LanceIndexPartitionCacheMissLoads。使用量接近容量且持续未命中,可能意味着缓存反复淘汰;新分区、新索引版本或切换 BE 也会造成未命中。只有确认存在复用需求且内存允许时才增大容量。 - BE 内存预算允许时,Metadata Cache 可从默认的 1 GiB 开始,根据其
usage_bytes和命中/未命中指标调整。文件/Fragment 数量、Schema/页面元信息、删除信息和 row ID 映射决定其需求,不能按向量索引大小的固定比例配置。BE Metadata Cache 与 FE 表访问缓存相互独立。 - 增大任何一个缓存前,都应在目标并发下检查 BE 进程内存和查询峰值。缓存命中率高但查询仍慢,可能是评分、Prefilter、解码或回表开销,增大缓存不一定有效。
- 单独为精排和输出列回表等重复数据文件读取配置
lance_data_cache_disk_capacity_bytes。增大它不会扩大 Index Cache,也不能弥补索引缓存未命中:索引文件绕过这个磁盘数据缓存。lance_data_cache_read_block_size_bytes建议先保持默认的 1 MiB,再根据有效读取量与读放大调整。
4. 为共享缓存分配预算
每个 BE 使用共享的 Lance Session 和一份 Index Cache 容量。向量分区、BTree 页面、Bitmap/LabelList 条目和全文索引内容会竞争这份空间,同一 BE 访问的不同表、不同 Catalog 的索引也包括在内。缓存键用于区分条目身份,不代表预留容量。Lance BE 缓存参数没有提供按表或索引类型设置配额、固定驻留的控制能力。
只有加载后的内容占用缓存,仅创建多个索引不会自动将它们全部加载。查询开始使用这些索引后,加载一个索引可能淘汰另一个索引的条目。例如,大范围 BTree 扫描或多样化的全文查询可能替换已经预热的向量分区,后续向量查询就需要重新加载。索引版本变化也可能在旧条目仍驻留时引入新条目。缓存淘汰不会立即释放仍被运行中查询引用的对象。
应为混合工作集做预算,避免重复计算共享分配:
index_cache_target_on_BE ≈ vector working set
+ BTree working set
+ Bitmap / LabelList / NGram working sets
+ FTS and other index working sets
+ measured headroom
即将各类索引工作集相加,再预留经测量确定的余量。例如,测量和载荷估算得出向量占 2.5 GiB、BTree 占 0.25 GiB、Bitmap 占 0.5 GiB、FTS 占 0.75 GiB,且都已包含各自的索引结构,总工作集为 4 GiB。初始配置 5 GiB Index Cache 可为增长和估算误差留出 1 GiB,但不保证适合所有工作负载。如果 BE 还容得下 1 GiB Metadata Cache,以及查询峰值内存、其他 Doris 工作负载和进程/操作系统余量:
lance_index_cache_size_bytes = 5368709120
lance_metadata_cache_size_bytes = 1073741824
Index Cache 和 Metadata Cache 容量独立:元数据条目不会直接淘汰索引条目,增大其中一个也不会扩大另一个。但两者仍消耗同一个 BE 进程的内存。磁盘数据缓存是第三份预算,且不缓存索引文件。同一 BE 上拆分 Catalog 不能隔离 Index Cache;需要隔离的工作负载,应使用部署环境支持的独立 BE 资源和路由能力。
排查相互抢占时,可先预热一类查询,记录延迟和缓存指标增量;再运行另一类查询,随后重复第一类查询。在索引版本和 BE 分配具有可比性的前提下,如果接近容量时重新出现未命中或加载,说明可能存在淘汰压力。Session 命中/未命中指标汇总了多种工作负载,需要结合查询 Profile 分析。预热所有索引本身也可能淘汰希望保留的工作集。应优先使用有代表性的混合工作负载预热,仅在有复用需求且内存允许时增大容量。
5. 选择 Index Segment 粒度
Index Segment、数据 Fragment、IVF Partition 和 BTree 叶子页是不同单位。当前 Doris 向量搜索会将每个选中的物理 Index Segment 转换成一个扫描 Split,未被索引覆盖的 Fragment 增加 Flat Search Split。FTS 同样使用物理索引 Segment。单个向量 Segment 内的 IVF Partition 不会被拆成独立 Doris Split 分配到多个 BE,但 Lance 内部仍可以并行执行。普通标量扫描是否按 Segment 分组,取决于选中的索引和执行计划,不能假设所有标量索引都采用向量搜索的拆分策略。
设 B 为本次查询实际可用的 BE 数量,S 为选中的向量 Segment 数量。如果 S 小于 B,这条查询就没有足够的索引扫描 Split 分配到所有这些 BE。增加 S 会增加调度机会,但不保证所有 Split 同时执行;扫描器限制、Lance 内部并发、CPU、I/O、缓存驻留情况及其他并发查询都会影响结果。仅增加 Doris Pipeline 实例不会将一个 Index Segment 继续拆小。
对于较大数据集,可将 B、2 * B、4 * B 个相对均衡的向量 Segment 作为初始对比实验,不是官方最优值,也不是小数据集必须满足的要求。以一个示例性的 1 亿向量数据集、8 个可用 BE 为例:
| Segment 数量 | 平均每 Segment 向量数 | 评估重点 |
|---|---|---|
| 8 | 1250 万 | Split 足以覆盖 BE,每 Segment 的重复操作较少 |
| 16 | 625 万 | 调度更灵活,单个大 Segment 对耗时的影响较小 |
| 32 | 312.5 万 | 任务更多,但搜索、加载和合并开销可能增加 |
应尽量均衡预计搜索工作量和加载后的索引字节数,而不只是 Fragment 个数。对于维度和编码相同的向量,行数均衡是合理起点;HNSW 图大小、IVF 倾斜、标量过滤选择率和文档长度都可能改变负载。没有通用的每 Segment 行数或文件大小阈值。高吞吐场景可能适合减少每条查询的任务数,为并发查询保留资源;单查询延迟优先的场景则可能受益于更多 Split。
Segment 更多也意味着更多的逐 Segment 初始化、元数据/量化器对象和局部候选生成。每个向量/FTS Split 最多保留 top_k + offset 个候选,Doris 不会将这个上限除以 S。因此扫描侧候选上限为 S * (top_k + offset),同时受实际匹配数量限制。例如,top_k=1000、无 offset、32 个 Split,在 Doris 归并前最多有 32000 个候选。本地 TopN 可能减少网络传输,因此这不是网络行数的保证。top_k 较大时尤其要关注候选处理开销。
Segment 数应与每个 Segment 的 IVF num_partitions、查询 nprobes 一起调优。分区均衡时,粗略探测向量数为 sum(N_s * nprobes_s / P_s),每个 Segment 最多为 N_s。这不是 HNSW 比较次数的公式。重建 Segment 或改变分区数,即使 nprobes 相同也可能改变召回率;应在相近召回率下比较延迟。HNSW 的 ef 和精排配置也会影响对比。
使用 Lance 工具构建和维护 Segment,再提交为目标逻辑索引。多个名字不同、分别覆盖全表的索引,不能替代一个包含多个物理 Segment 的索引。优先按完整 Fragment 组成均衡分组,避免小批追加不断积累大量微小 Segment,并使用嵌入 Lance 版本支持的维护 API。构建后在 Doris EXPLAIN/Profile 中确认物理 Segment 数和未覆盖 Fragment 数,不要从 SHOW INDEX 的结果行数推断。将所有 Segment 合并成一个,可能降低 Doris 扫描并行度。构建及提交的概念可参考 Lance 分布式索引。
对于 BTree 点查或高选择性的 Bitmap 查询,过多 Segment 可能增加查找和打开开销,而每个 Segment 减少的工作量很少。FTS 会增加逐 Segment 的词典/倒排搜索和候选归并。应分别评估这些工作负载,不要为所有索引类型套用向量索引的 Segment 数。拆小 Segment 也不能消除不必要的逐行 Prefilter 构建,应独立确认 Reader 的过滤行为。
6. 验证混合工作负载
- 确认索引选择、物理 Split 和索引覆盖情况,将回退扫描和二阶段回表纳入延迟分析。
- 估算每个索引保留的载荷,将共享工作集相加。查询内存单独预留,包括并行加载、row ID 集合、候选堆和解码。
- 使用真实并发对比冷缓存和预热后的混合工作负载,跟踪 P95 延迟、吞吐量、适用时的召回率、各 BE 峰值内存、缓存使用量与命中/未命中增量、索引加载 I/O 以及扫描时间倾斜。
- 每次改变一个维度:缓存容量、Segment 数或搜索参数。若重建 Segment 改变了模型,应重新验证召回率。继续增加 Split 或缓存不再改善吞吐、延迟或内存表现时,停止增加。
- 大量追加数据、重建索引、扩缩 BE 或改变查询组合后重新评估。针对一个查询向量或一张表校准的配置,不能作为所有工作负载的固定预算。
7. 增加资源前先定位问题
| 现象 | 优先检查 | 可评估的调整 |
|---|---|---|
| 缓存接近容量,索引持续未命中 | 索引/版本变化、BE 分配和总工作集 | 内存允许时增大 lance_index_cache_size_bytes,减少无用预热,或用独立 BE 资源隔离工作负载 |
| 标量或全文查询后,向量查询变慢 | 按共享缓存预算重复跨工作负载淘汰检查 | 为两类查询一起做预算;同一 BE 上拆分 Catalog 不能隔离缓存 |
| 大型已索引数据集只有少量 BE 扫描 | 物理 Segment Split 数、可用 BE 和扫描器调度 | 对比更多且均衡的 Segment;仅增大 Pipeline 实例不能拆分 Segment |
| Segment 增多后 CPU、内存或归并耗时上升 | 每 Split 候选数、top_k + offset 和查询并发 | 减少 Segment,或避免请求超过应用需求的候选数量;搜索参数变化后重新检查召回率 |
| 缓存命中率高但延迟仍高 | 评分、Prefilter 工作、解码和二阶段回表耗时 | 优化主要耗时阶段;增大 Index Cache 不能消除计算或回表 |
| 元数据反复加载 | Metadata Cache 使用量及命中/未命中增量、Fragment/文件数和版本变化 | 有复用需求且内存允许时,单独调整 lance_metadata_cache_size_bytes |
| 缓存未满但进程内存很高 | 并发查询分配、正在加载的数据、仍被引用的对象和其他 BE 工作负载 | 根据测得的峰值降低并发或缓存预算;缓存容量不是进程内存上限 |
| 数据文件 I/O 持续较高 | 磁盘缓存计数、复用情况、读取块大小和输出列 | 调整数据缓存预算或读取粒度;索引文件不使用这个磁盘数据缓存 |
使用查看缓存效果中的 Profile 计数器和 BE 指标。仅凭汇总命中率无法判断哪个索引被淘汰,或哪个阶段占主要耗时。每次改变一个配置,记录每组对比的工作负载、索引版本、BE 分配、延迟、吞吐、召回率和峰值内存。