土法炼钢兴趣小组的算法知识备份

【向量检索引擎】生产排障:召回、延迟、堆积、OOM

文章导航

分类入口
databasestorage
标签入口
#milvus#troubleshooting#recall#latency#compaction#oom#replica#vector-engine

目录

排障时最容易犯的错:把一切归因于 ef / nprobe。本系列前 16 篇给出的机制说明,真实故障往往落在 可见性、段状态、过滤选择度、离线队列、对象存储与副本拓扑。本文先给一张症状到机制的决策树,把「感觉不对」翻译成「该看哪一篇、哪个组件」,再展开四类症状各自的检查单。不含未跑的集群数字

本文是「向量检索引擎」系列第 17 篇(共 19 篇)。→ 系列目录

版本锚定:机制结论沿用本系列第 2–14 篇已钉的 Milvus 2.6.x 官方文档结论;本篇不引入新的官方文档来源,只做交叉归纳。


一、先分清四种「不好」

症状标签 可能机制 优先篇章
召回差 度量/归一化、索引未加载、过滤杀候选、一致性过旧 2、8、10、11、12
延迟高 GuaranteeTs 等待、冷段加载、Proxy reduce、对象 GET、副本不均 4、6、7、9、12、14
写入失败/抖动 WAL 迁移 Wait for Ready、Coordinator 不可用 4、5、14
空间/内存爆 删除未 compaction、索引堆积、副本因子过高 10、13、14

四类症状不是互相排斥的分类——一次批量导入引发的索引堆积,可能同时表现为「召回差」(新段还没索引)和「延迟高」(走了慢路径),排障时先分别检查,再合并结论,不要看到一个可能原因就停下。


二、症状 → 机制决策树

把第一节的表格摊开成一张可以按图排查的决策树。四条主干对应四类症状,每条主干下的分支给出「先问什么问题,再决定看哪一篇」:

flowchart TD
  symptom["Observed symptom?"]

  symptom -->|"recall drop"| recallGate{"Flat/IDMAP baseline also low?"}
  recallGate -->|"yes"| metricIssue["Metric/normalization mismatch<br/>见第 2、8 篇"]
  recallGate -->|"no, only ANN path low"| filterGate{"query has filter?"}
  filterGate -->|"yes"| selectivityIssue["Selectivity breaks topk merge<br/>见第 11 篇"]
  filterGate -->|"no"| readinessIssue["Consistency watermark or index not handed off<br/>见第 10、12 篇"]

  symptom -->|"latency spike"| consistencyGate{"only slow under Strong?"}
  consistencyGate -->|"yes"| waitIssue["GuaranteeTs wait, not ANN compute<br/>见第 12 篇"]
  consistencyGate -->|"no"| coldGate{"first hit / occasional?"}
  coldGate -->|"yes"| coldLoadIssue["Cold segment load or replica reroute<br/>见第 7、14 篇"]
  coldGate -->|"no"| fanoutIssue["Proxy reduce fanout or candidate multiplier<br/>见第 9、11 篇"]

  symptom -->|"backlog / segment explosion"| queueGate{"build queue depth growing?"}
  queueGate -->|"yes"| dataNodeIssue["Data Node throughput below write peak<br/>见第 10 篇"]
  queueGate -->|"no"| compactionIssue["Compaction threshold or small-segment flush<br/>见第 3、10 篇"]

  symptom -->|"OOM"| oomGate{"which component?"}
  oomGate -->|"Query Node"| replicaMemIssue["segment data + index + replication factor<br/>见第 14 篇"]
  oomGate -->|"Proxy"| reduceBufferIssue["concurrency x reduce buffer<br/>见第 4、9 篇"]
  oomGate -->|"Data Node"| buildMemIssue["index build peak memory<br/>见第 8、10 篇"]

这张图的作用不是替代后面几节的检查单,而是先把「哪一类问题」定下来,避免在错误的层面调参——例如「延迟高」但只在 Strong 一致性下出现,直接跳到第三节的召回检查是浪费时间,该看的是第四节的等待窗口。

按图走一遍具体例子:假设某天监控报警「Query Node 内存使用率持续走高,即将触发 OOM」。沿 oomGate 分支先确认是哪个组件——如果是 Query Node,下一步不是立刻扩容,而是按第六节的公式拆解这部分内存由「段数据 + 段索引」乘「副本因子」构成:如果最近没有新增数据、也没有加副本,但内存还在涨,大概率是段数量本身在增长(第五节的堆积问题,新段没有被及时合并或旧段没有被正确释放),而不是”数据量变大”这个默认直觉;如果最近刚把副本因子从 1 调到 2 或 3,那八成是第六节 6.1 节讲的線性增量在生效,这属于预期行为,解决办法是评估是否真的需要这么高的副本因子,而不是当成异常报警去查。同一个症状标签(OOM)下的根因和处理方式完全不同,图的作用就是帮你在几秒内把问题限定到正确的分支,再进对应章节展开。


三、召回:对照金标准

  1. 同一度量与归一化:cosine/IP/L2 与入库预处理是否一致(frontier/08、第 2 篇)。
  2. Flat 金标准:小样本上 Flat/IDMAP 算 Recall@k,再比 HNSW/IVF(第 2、8 篇)。
  3. 过滤:估选择度 \(s\);低 \(s\) 时检查是否滤空局部 Top-\(k\)(第 11 篇)。
  4. 删除/TTL:结果是否应排除已删/过期实体(第 13 篇);bitset 是否传播到历史副本——这一步容易被忽略,第九节会展开一个具体案例。
  5. 索引就绪:Sealed 是否已建索并 handoff 到 Query Node(第 3、10 篇)——「能搜」可能走劣路径。

不要用「感觉答案不对」代替 Recall@k 与固定评测集:

\[ \mathrm{Recall@}k = \frac{|\text{ANN 返回的 Top-}k \cap \text{Flat 金标准的 Top-}k|}{k} \]

排障时按上表 1–5 逐项排除,每排除一项就重新算一次 Recall@k,观察数字是否回到预期区间——如果第 1、2 项(度量、Flat 基线)都对齐了,Recall@k 依然明显偏低,说明问题落在第 3–5 项(过滤、删除传播、索引就绪)之一,可以缩小到具体一个查询模板去对照哪个环节引入了差异,而不是笼统地怀疑”索引不准”。


四、延迟:拆等待 vs 计算 vs I/O

\[ \text{总延迟} \approx \text{一致性等待} + \text{段加载/对象 GET} + \text{Growing/Sealed 检索} + \text{多级 reduce} + \text{客户端} \]

把这条公式换成一次具体请求的时序,能更直接看出每一段延迟发生在哪个组件、该找谁的监控:

sequenceDiagram
  participant C as Client
  participant Px as Proxy
  participant SN as Streaming Node
  participant QN as Query Node
  C->>Px: search(consistency_level, topk, filter)
  Px->>Px: resolve GuaranteeTs
  Px->>SN: dispatch to relevant shards
  Note over SN: wait until ServiceTime catches up<br/>见第 12 篇 GuaranteeTs
  SN->>SN: search local Growing
  SN->>QN: fetch sealed candidates
  Note over QN: cold load from object store if not cached<br/>见第 6、7 篇
  QN-->>SN: segment-level topk
  SN-->>Px: shard-level reduce
  Px-->>C: final reduce + response

对应的检查表:

检查 动作
换 Eventually/Bounded 后是否明显下降 若是,主因是 ServiceTime 等待(第 12 篇),不是 ANN
仅首次/偶发尖刺 冷 handoff、缓存失效、副本改派(第 7、14 篇)
topk/shard 数线性变差 Proxy 最终 reduce 或候选倍数过大(第 9、11 篇)
与对象存储错误率相关 第 6 篇寻址;查 MinIO/S3 慢请求与 LIST/GET
Streaming 写入后立即 Strong 搜索 预期等待;改 Session 是否满足产品(第 12 篇)

五、堆积:建索与 compaction

信号 含义
段很多、索引任务队列长 Data Node 跟不上写入(第 10 篇)
删除后磁盘不降 软删未 compaction(第 13 篇)
小段爆炸 flush 过碎或 compaction 落后(第 3、10 篇)

处理方向:扩 Data Node、降写入突发、调段大小策略、给 compaction 留 I/O 预算——加 Query Node 副本不会消化建索队列(第 10 篇已用最小故事说明:索引堆积的根因是写入峰值速率超过建索处理速率,不是数据总量)。


六、OOM 与容量

\[ \text{Query Node 内存} \approx \sum_{\text{已加载段}} (\text{段向量数据} + \text{段索引}) \times \text{副本因子} \]

  1. Query Node:上式是主要开销来源;副本因子每加一档,这部分内存近似线性增长(第 14 篇)。
  2. Proxy:超大并发 \(\times\) 大候选归并缓冲(第 4、9 篇)——topk 越大、shard 数越多,单次请求的归并缓冲越大,高并发下容易在 Proxy 侧先 OOM。
  3. Data Node:建索峰值(向量矩阵 + 图结构),与数据量、维度、索引类型相关(第 8、10 篇)。
  4. Growing:Streaming Node 内存策略与 flush 是否触发(第 5 篇)——flush 延迟等于 Growing 常驻内存时间变长。

对象存储「便宜」不降低 内存工作集;副本用 QPS 换的是 RAM,这是一条容量规划时经常被忽略的等式两端关系。

6.1 把公式代入一组占位数字

光看公式容易低估量级增长的速度,代入一组教学用的占位数字(不是任何实测或官方基准,只用于建立直觉):设一个 collection 有 1000 万行 768 维 float32 向量。原始向量数据大小约为

\[ 10^7 \times 768 \times 4\text{ bytes} \approx 30.7\text{ GB} \]

如果索引结构(图或量化码本等)额外再占向量数据的 30%~100%(具体倍数随索引类型剧烈变化,这里只取一个示意区间),单份数据加索引的常驻内存量级在 40 GB 到 60 GB 之间。副本因子设为 1 时,Query Node 集群至少要有这么多可用内存;副本因子提到 3,仅这一个 collection 就要 120 GB 到 180 GB——如果同一个集群上还跑着其它几个规模相近的 collection,“副本因子从 1 提到 3”这个看起来只是调一个数字的操作,实际对应的是数倍的硬件采购或云资源账单。这组数字的目的不是给出”精确内存需求”(真实数据分布、索引参数、压缩方式都会让实际数字明显偏离),而是说明副本因子这个旋钮的成本敏感度远高于它在配置项里看起来的”轻量”,调整前值得先按上面的公式估一个量级,而不是凭感觉设置。


七、写入路径检查单

  1. Coordinator 是否单活跃且健康(第 4 篇)。
  2. 是否处于 WAL 迁移 / Wait for Ready(第 5、14 篇)。
  3. shard/channel 是否倾斜到单 Streaming Node(第 3 篇)。
  4. Woodpecker 模式是否为 MemoryBuffer 导致写延迟达数百毫秒量级(第 5 篇官方模式说明)。

八、最小故事:写入偶发超时,被误判成网络问题

另一类容易走错排查方向的场景:某集群的 insert 请求在几分钟窗口内偶发超时,运维第一反应是查网络——ping 延迟正常、带宽也没跑满,网络团队排查一圈无果,问题过一会又自己消失了,几天后同样的模式再次出现。

按第七节的检查单重新过一遍:网络本身正常排除了链路问题,接下来看 Coordinator 与 WAL。查看 Streaming Coordinator 的事件日志,发现超时窗口正好对应一次 Streaming Node 的滚动升级或节点漂移——第 14 篇已经拆开过这个机制:升级触发 WAL 从旧节点迁到新节点,中间有一段”旧节点拒绝写、新节点尚在恢复”的窗口,客户端在这段窗口里的写请求会进入 Wait for Ready 状态,如果客户端超时设置得比这段恢复窗口更短,就会表现为”偶发超时”,恢复完成后一切又恢复正常——这不是网络问题,是第 14 篇 fencing 机制预期内的行为,只是触发它的运维动作(滚动升级)和”网络问题”这个第一直觉之间没有直接线索,需要主动去查 Coordinator 事件日志才能对上号。

这个故事想强调的是:写入路径的偶发抖动,优先看是否有节点级事件(升级、漂移、故障转移)与超时窗口时间重合,而不是先假设是网络或硬件问题。检查单第 1、2 两项(Coordinator 健康、WAL 迁移状态)通常能在几分钟内排除或确认这类原因,比等网络团队排查一整圈更快定位。


九、最小故事:同一个查询,两次结果不一样

设想一个配置了多副本的 Collection:客户端刚删除了一批文档对应的向量,几秒后用同一条查询反复请求,发现有些请求里已删除的向量还出现在结果中,有些请求里没有——现象看起来像「一致性随机丢失」,运维第一反应是怀疑 Proxy 缓存坏了或者一致性级别配置有问题。

按第 13、14 篇已经钉住的机制重新拆解:Delete 由承载该 shard WAL 的 Streaming Node 上的 Query Delegator 负责,向持有该 sealed 段的其它 Query Node 广播 Delete(第 14 篇)。如果 Collection 配置了多副本,广播需要传播到每一个持有该段副本的节点,这中间存在一个短暂的传播窗口——不同副本收到广播的时刻不完全同步。同时,Proxy 侧维护一份 segment→Query Node 的路由缓存,并会在负载不均时把请求改派到「同样持有该段、更空闲」的副本(第 14 篇 Failover 逻辑)。

把两个机制叠在一起看,现象就有了确定的解释:两次一模一样的查询,如果恰好被 Proxy 路由到收到删除广播的时间点不同的两个副本,就会出现「同一个查询,不同结果」——这不是随机故障,而是软删广播窗口副本路由的不确定性叠加后的必然结果。定位方法是查 Delegator 广播延迟指标(是否有明显滞后)与 Proxy 路由改派频率(是否该 collection 副本因子较高、改派频繁),而不是去怀疑一致性级别配置错了。


十、常见误解

误解一:召回或延迟异常,先调 ef / nprobe 总不会错。 第一节已强调:真实故障多数落在可见性、段状态、过滤选择度、离线队列这几层,和索引参数无关。在没有用第二节的决策树定位到「确实是 ANN 计算本身慢/不准」之前先调索引参数,往往只是掩盖了真实问题(例如把 ef 调大掩盖了本该修复的索引堆积)。

误解二:多副本能同时解决延迟、召回一致性和高可用三个问题,副本越多越好。 第九节的故事说明副本本身可能引入一类新的不一致窗口(软删广播传播延迟)。副本主要解决的是读吞吐扩展与故障转移(第 14 篇),不是免费的一致性增强手段;副本数增加也直接推高第六节的 Query Node 内存占用。

误解三:对象存储费用低,所以对象存储相关的问题不值得优先排查。 第六节已指出「便宜」不等于「不影响内存工作集」;第四节的延迟时序图也显示冷段加载走的正是对象存储 GET 路径。对象存储的慢请求、LIST/GET 错误率,是延迟尖刺里经常被忽略但影响直接的一环(第 6 篇)。

误解四:只要监控显示”索引已建好”,召回就应该达到 HNSW/IVF 的理论水平。 “索引已建好”只覆盖第 10 篇讲的第一次交付(Data Node 写回对象存储);召回是否达到预期还要看第 3 步(第三节)——度量/归一化是否一致、过滤选择度是否打穿归并假设、软删是否已经正确传播。索引建好只排除了”没有索引”这一种可能,不代表其它四类召回问题都已排除。

误解五:写入偶发超时,第一时间应该联系网络团队排查链路。 第八节的故事说明这类问题优先看节点级事件(滚动升级、故障转移)是否与超时窗口重合,Coordinator 与 WAL 迁移日志通常几分钟内就能给出答案,比协调跨团队排查网络链路更快。网络问题当然存在,但不应该是默认的第一假设——第七节的检查单把”Coordinator 是否健康”“是否处于 WAL 迁移”排在更靠前的位置,正是因为这两类原因更常见、定位更快。

误解六:容量规划时,副本因子从 1 调到 2 只是”多一份保险”,内存增量可以忽略不计再评估。 6.1 节的占位数字已经说明,副本因子的内存增量是线性的,且基数(单份数据加索引的常驻内存)在十万到千万行规模下很容易达到几十 GB。调副本因子前先用公式估一个量级,比事后被账单或 OOM 惊到再回头查代价要便宜得多。


十一、学术谱系与开放问题

11.1 谱系

排障方法论本身也有可对照的学术源头:分布式系统的可观测性——把一次跨组件请求的耗时拆成可归因的分段——最早由 Google 的 Dapper 系统化(Sigelman et al., Dapper, a Large-Scale Distributed Systems Tracing Infrastructure, Google Technical Report, 2010),核心思想是给一次请求分配全局 trace id,跨服务传播 span,事后能按 span 重建调用树。本文第四节的延迟时序图正是这种「先拆分段、再定位瓶颈段」的方法论在向量引擎排障上的具体化;第六节的 OOM 容量公式则对应经典排队论里「工作集 = 请求数 \(\times\) 单请求资源占用」的直觉(Little’s Law 的资源占用变体,不是严格意义上的排队等待时间公式,这里只借用其「用负载 \(\times\) 单位代价估算资源占用」的思路,不代入原始公式的到达率/服务率假设)。

11.2 开放问题

  1. 统一「召回 / 延迟 / 队列深度」的红黄绿看板模板——目前需要跨第 9、10、12 篇分别查看不同组件的指标,缺一个把 GuaranteeTs 等待、建索队列深度、reduce 扇出统一到同一时间轴的可观测性方案。
  2. 自动区分 GuaranteeTs 等待与 Knowhere 计算的 tracing span——如果能像 Dapper 那样给「等待水位」和「向量距离计算」分配不同的 span 类型,第四节的时序图就能直接从 tracing 系统里重建,而不需要靠经验猜测延迟卡在哪一段。
  3. 多集群联邦排障时的时钟与 TSO 对齐——当多个 Milvus 集群通过应用层联邦时,各自的 TSO 是独立的逻辑时钟,跨集群比较「谁的数据更新」缺乏统一定义。
  4. 软删广播延迟的可观测性——第九节的故事说明 Delegator 广播窗口是真实存在的不一致来源,但目前官方文档未给出该窗口的典型量级或监控指标,运维只能通过间接现象反推。
  5. 写入路径节点级事件与超时窗口的自动关联——第八节的故事说明”滚动升级导致的偶发超时”需要人工去对照 Coordinator 事件日志和客户端超时日志的时间戳,是否能有一个自动化的关联规则(例如”任意 Streaming Node 漂移事件发生时,自动标注该时间窗口内的写超时为预期行为”),目前没有公开的官方方案。

十二、小结

三句话小结:先用症状到机制的决策树把「感觉不对」归到召回、延迟、堆积、OOM 四类中的具体分支,而不是直接假设是索引参数问题;召回和延迟异常的根因大多落在可见性水位、段状态、过滤选择度、离线队列这几层,索引堆积和 OOM 则要按组件分别核算资源占用;多副本、对象存储「便宜」这类看起来只带来好处的设计,各自都有对应的排障新盲区(广播窗口、内存工作集),需要单独确认而不是默认已解决。

选型与阅读地图见 第 18 篇


参考资料

  1. Sigelman, B. H. et al., Dapper, a Large-Scale Distributed Systems Tracing Infrastructure, Google Technical Report, 2010(分布式追踪方法论,与本文延迟拆分思路对照)。
  2. 本系列第 2–14 篇机制结论(见各篇参考资料)。
  3. 系列 indexPLAN.md

返回 系列目录 | 上一篇:Lance 对照 | 下一篇:选型与阅读地图

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-07-12 · database / storage

【向量检索引擎】Data Node:compaction 与 index build

说明 Milvus 2.6.x 中 Data Node 作为离线 Worker 如何承接 Coordinator 下发的建索引与 compaction,输入输出如何进出对象存储;用最小故事、常见误解与队列积压图说明索引堆积如何拖垮查询新鲜度与资源争用。


By .