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

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

文章导航

分类入口
databasestorage
标签入口
#milvus#data-node#compaction#index-build#handoff#sealed#vector-engine

目录

Streaming Node 解决 提交与实时可搜第 5 篇);Query Node 解决 历史段检索第 7 篇)。重型、可延迟的工作——向量/标量索引构建历史数据整理(compaction)——落在 Data Node,由 Coordinator 调度(第 4 篇)。一个常见的排障误区是「写入之后能搜到,说明这批数据已经进入正常服务状态」——本文要说明的恰恰是:能搜到搜得又快又准 之间,隔着 Data Node 这条离线队列。

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

版本锚定:Milvus 2.6.x Architecture / Data Processing(A 级)。compaction 动机的历史叙述引用官方博客(B 级)。


一、Data Node 在四层中的位置

官方定义:Data Node 负责历史数据的 离线处理,例如 compactionindex building。它是存算分离下的无状态 Worker:听 Coordinator 指令,读写对象存储,不永久持有可变用户状态。

与另外两类 Worker 的分工:

Worker 在线性 典型工作
Streaming Node 在线写 + Growing 查询 WAL、flush、Delegator
Query Node 在线历史查询 加载 Sealed、段级 search
Data Node 离线 建索、compaction

Architecture 插入流的末步:Data Node 在 Sealed 上建索引并写入对象存储后,Query Node 加载新索引并替换对应增长数据视图——中间由 Coordinator handoff 缝合(第 3 篇)。

和 Query Node 一样,Data Node 也是无状态 Worker:它不持有跨任务的可变状态,一次建索/compaction 任务失败可以直接重新调度到任意存活的 Data Node 上重跑,而不需要「恢复」某个特定节点上的中间状态。这也是为什么扩容 Data Node 池能直接提升建索并发——不像 Query Node 扩容还要考虑 data view 重新分配的过程,新加入的 Data Node 只需要从 Coordinator 的任务队列里领任务即可开始工作。


二、Index build:每段一份索引

Data Processing · Index building(A 级要点):

  1. 为避免数据一更新就全局重建,Collection 再分为 segment,每段自有索引
  2. 可为向量字段、标量字段、主键字段建索引。
  3. 输入与输出都经对象存储:Data Node 加载段日志快照 → 反序列化 → 建索 → 序列化 → 写回对象存储。
  4. 向量建索是计算与内存密集型,依赖 SIMD;官方强调弹性对成本的重要性。

不同字段类型的建索资源画像并不相同,规划 Data Node 容量时不能只按「一种任务」估算:

建索对象 资源特征 与本系列的衔接
向量字段 计算/内存密集,依赖 SIMD,部分类型可用 GPU Knowhere VecIndex(第 8 篇)
标量字段 相对轻量:Bloom、哈希、树、倒排等经典结构 官方列举,本篇不展开实现
主键字段 通常与标量索引同类,但要求唯一性约束 影响 upsert/delete 的定位效率(第 13 篇)

这与 Knowhere「对 segment 内数据 train 并 build」(第 8 篇)直接对接:Data Node 是调度与 I/O 壳,Knowhere 是向量索引内核。容量规划时把向量建索单独拎出来看,因为它是这张表里唯一可能需要 GPU、且耗时对数据分布最敏感的一项。

flowchart LR
  coord["Coordinator schedule"]
  dn["Data Node"]
  obj["Object storage"]
  know["Knowhere Train/Build"]
  qn["Query Node load"]
  coord --> dn
  obj -->|"load snapshot"| dn
  dn --> know
  know -->|"write index"| obj
  coord -->|"handoff"| qn
  obj --> qn

2.1 新鲜度与「有数据无好索引」

flush 已使数据进入 Sealed 并可能被查询路径看到,但 ANN 索引对象可能仍在构建队列中。此时执行可能退化为更慢的路径或临时索引策略(具体行为随版本与 queryNode.segcore.interimIndex.* 等配置变化,见官方 Query Node 配置;第 7 篇提及)。排障「写入后能搜但很慢 / 召回不稳」应同时看:

这两个检查点对应两次独立的「交付」:Data Node 把索引写回对象存储是第一次交付(索引已存在),Coordinator 完成 handoff 并让 Query Node 加载是第二次交付(索引已生效)。只确认了第一次交付就断言「索引已经建好可以搜了」,会漏掉两次交付之间的延迟窗口。


三、Compaction:清理与合并

3.1 动机(历史官方叙述)

官方博客 How to Compact Data in Milvus?(2022,B 级)将 compaction 概括为:合并小段、清理逻辑删除,降低存储占用;由协调组件触发、Data Node 执行。并区分:

维度 binlog compaction segment compaction
作用范围 单个段内部 多个 Sealed 小段之间
主要收益 回收该段内软删占用的日志空间 减少段数、降低对象 API 与归并扇出
触发时机 该段删除比例达到一定水平 段数或段大小达到调度阈值
与 handoff 的关系 完成后同样触发 handoff(第 3.2 节) 完成后同样触发 handoff(第 3.2 节)

两者共享同一个 Data Node 任务队列与调度器;binlog compaction 更「本地」,segment compaction 更「全局」,但排障时都要回到第四节的资源池模型去看——不必特意区分「是哪种 compaction 慢」,先看队列本身是否堆积。

这与列存 merge、LSM compaction 同属「不可变文件 + 后台整理」家族(对照 columnar-enginerocksdb),但整理目标还包括 软删空间回收段数可控(段数影响第 9 篇归并扇出与第 6 篇对象 API 次数)。这类「前台可变结构 + 后台不可变文件整理」的分工,经典源头是 O’Neil et al. 提出的 Log-Structured Merge-Tree(The Log-Structured Merge-Tree (LSM-Tree), Acta Informatica 33(4), 1996):论文关心的是磁盘随机写代价,用分层归并把随机写变成顺序写;Milvus segment compaction 面对的磁盘代价已经被对象存储的按请求计费取代,但「延迟整理、批量重写换取更低平均代价」的结构完全一致。leveled 还是 tiered 的写放大/读放大权衡(Dayan & Idreos, Dostoevsky: Are Your LSM Key-Value Stores Optimized?, SIGMOD 2017)在 Milvus 侧没有对应的公开分层策略描述——本篇不假设 Milvus 的 segment compaction 采用了这两种策略中的某一种,只指出这是同一类问题的姊妹领域。

LSM 社区量化这类后台整理代价时常用写放大(Write Amplification,WA)

\[ WA = \frac{\text{compaction 过程中实际重写的字节数}}{\text{用户逻辑写入的字节数}} \]

leveled compaction 倾向于更低的空间占用与更好的读性能,但每层合并都要重写数据,WA 偏高;tiered(在 Milvus 语境接近 segment compaction 直接合并小段)倾向于减少重写次数,WA 偏低,但读时要扫更多未合并的小文件,付出的是读放大(Read Amplification,RA)。Milvus 官方文档没有公开给出 segment compaction 的 WA/RA 数值或权衡策略,本篇不代入具体数字,只借这个量化框架说明:「多合并几次段」和「少合并、多存几个小段」是同一枚硬币的两面,选择哪一面依赖对象存储的读写代价比,而不是「compaction 越多越好」。

3.2 与 handoff 的衔接(A 级)

Data Processing:当 Growing flush 成 Sealed,或 Data Node 完成 compaction 后,Coordinator 发起 handoff,重新分布 Sealed 到 Query Node,并释放冗余。

因此 compaction 不是「只在桶里改文件」:它会驱动 查询视图迁移。compaction 高峰期可能与查询加载争抢对象存储带宽与 Node CPU。

3.3 实现提示(不替代源码阅读)

Data Node compaction 实现中可见对 SegmentBinlogs 的计划驱动合并、可选 merge-sort 等分支(源码路径随 release 变化)。本系列不绑定某一 commit 的函数名表;升级时以当前 internal/datanode/compactor 与官方发布说明为准。


四、资源与调度:离线队列如何拖累在线 SLO

Coordinator「Historical Data Management」把 compaction 与建索当作可分发任务。工程上常见张力:

张力 表现
建索慢于写入 索引堆积;查询走劣路径或等待
compaction 落后 小段爆炸;删除空间不回收;对象文件数上升
与查询争抢 Query Node 加载与 Data Node 拉取同一桶前缀
GPU 建索与 CPU 查询混部 若共享物理机,资源争用从对象存储带宽扩展到显存/PCIe
多 Collection 共享 Data Node 池 一个 Collection 的批量导入可能挤占其它 Collection 的建索窗口

容量规划应把 Data Node 当成 独立池:不能假设「加 Query Node」自动消化建索队列。这个池的产出能力由三个因素共同决定——单任务耗时(数据量、维度、索引类型)、并发任务数上限(内存/CPU/GPU 配额)、任务排队策略(是否有优先级)。只调其中一个因素而忽略另外两个,观察到的效果往往不稳定。

把这条张力画成因果链,更容易在监控里定位「新鲜度变差」到底卡在哪一环——建索速率一旦低于 flush 产生新 Sealed 段的速率,队列深度会持续增长,而查询侧只能退化到更慢的兜底路径,用户感知到的是「新数据搜得到但慢、或者召回看起来不稳」:

flowchart TB
  flush["Growing flush 成 Sealed(持续产生)"]
  queue["Data Node 建索 / compaction 队列"]
  backlog["队列深度持续增长<br/>(建索速率 < flush 速率)"]
  interim["Query Node 退化到<br/>interim index / 更慢兜底路径"]
  stale["用户感知:新数据搜得慢或召回不稳"]
  flush --> queue
  queue -->|"enqueue 快于 drain"| backlog
  backlog --> interim
  interim --> stale
  backlog -.->|"compaction 任务也排在同一队列"| queue

图中的自环(虚线)是容易被忽视的一点:compaction 本身也要占用同一个 Data Node 任务队列。当建索已经堆积时,如果继续把 compaction 任务往里塞,只会让队列更深——这也是为什么第四节强调 Data Node 该当独立资源池看待,建索与 compaction 之间还需要一层优先级调度,而不是先来先服务。


五、最小故事:一次索引堆积如何被误诊成「Knowhere 变慢」

设想一次批量导入:短时间内产生了 50 个新 Sealed segment,而 Data Node 建索的处理速率是每分钟 5 个。运维在导入完成 10 分钟后开始压测查询,观察到部分查询延迟明显偏高、部分召回结果和预期不一致,第一反应是「怀疑 HNSW 的 ef 参数设小了」或「怀疑 Knowhere 哪个 SIMD 路径没启用」。

按第二、四节的模型重新拆解这次现象:10 分钟内 Data Node 大概只能处理完 50 个任务里的一小部分,意味着这时仍有大量新 Sealed segment 还没有向量索引。查询命中这些段时,要么走暴力/临时路径(延迟高),要么召回口径和已建好索引的段不一致(表现为「召回不稳」)。这次现象的根因是 建索队列还没消化完这批导入,而不是索引参数或 SIMD 配置的问题——正确的排障动作是查 Data Node 任务队列深度与建索任务耗时,而不是先去改 ef

同一个场景换一个变量,结论会不同:如果这批导入分 20 次、每次间隔 5 分钟小批量写入,而不是一次性写完,Data Node 的建索速率(每分钟 5 个)足以在下一批到达前消化掉上一批,队列深度不会持续累积,也就不会出现上面的现象。这说明索引堆积不是「数据量大」本身导致的,而是 写入速率的峰值 超过了建索处理速率——同样的总数据量,写入节奏不同,是否触发堆积可能完全不同。容量规划时应该按峰值写入速率对齐 Data Node 吞吐,而不是按日均写入量估算。


六、延迟归因:从症状定位到 Data Node 哪一环

第 7、9 篇分别给过 Segcore/Knowhere 层面与跨节点归并层面的延迟归因表;离线数据面的症状对应关系不同,容易被误判成在线路径的问题:

症状 更可能对应 先检查什么
批量导入后一段时间内召回不稳、延迟偏高,随后恢复正常 建索队列堆积(第五节故事) Data Node 任务队列深度、建索任务平均耗时
小文件数、对象存储 API 调用量持续上升 compaction 落后 segment compaction 任务积压、触发阈值是否合适
Data Node 与 Query Node 同时抢占对象存储带宽,两侧都变慢 建索/compaction 与查询加载争用同一对象存储 是否命中同一批段的桶前缀、带宽限额配置
只有极少数 Collection 表现异常,其它正常 该 Collection 的段划分策略不合理 segment 数是否偏多(第四节「段数 ≈ 索引任务数」)、是否频繁触发小段 flush
GPU 建索任务排队但 GPU 利用率看起来不高 调度未按硬件类型区分任务队列 是否所有建索任务(含不需要 GPU 的)都挤在同一队列里排队
调大/调小 compaction 触发阈值后现象没有变化 binlog 与 segment compaction 互相挤占(3.1 节) 是否只调了一种 compaction 的参数,另一种仍占用队列窗口
对象存储侧确认索引已写入,但查询仍走旧路径 两次交付之间的窗口期(2.1 节) Coordinator handoff 状态、Query Node data view 是否已更新

这张表的落点是:离线队列的问题会以「在线查询变慢/不稳」的方式暴露出来,但根因往往不在 Query Node 或 Knowhere,而在 Data Node 这一层——排障顺序应该先看队列深度,再看具体的索引参数。

这也解释了为什么第 7、9 篇的延迟归因表都把「是否命中冷段加载 / 是否与批量写入窗口重叠」列为检查项:那两张表覆盖的是在线路径,本表覆盖的是离线路径,三者拼起来才是完整的排障地图。


七、常见误解

误解一:compaction 只是后台清理磁盘空间,不会影响查询。 第三节 3.2 已明确:compaction 完成后 Coordinator 会发起 handoff,重新分布 Sealed 到 Query Node——compaction 直接驱动查询视图迁移,高峰期还会与查询加载争抢对象存储带宽和节点 CPU。

误解二:只要 flush 完成,segment 立刻就能用上向量索引搜索。 flush 只代表数据进入 Sealed 并可能被查询路径看到;向量索引的构建是 Data Node 队列里的异步任务,可能滞后。第五节的最小故事就是这类「有数据、无好索引」现象被误诊的典型场景。

误解三:查询变慢或堆积时,加 Query Node 就能缓解。 Query Node 解决的是「加载和检索」的并发与内存容量,不解决「建索/compaction 处理速率」的问题——后者是 Data Node 独立资源池的产出能力。批量导入后的索引堆积只能靠扩容 Data Node、调整调度优先级或放慢写入速率来缓解。

误解四:段越小、compaction 越频繁,系统整体表现总是更好。 第三节的写放大讨论已经说明:更频繁的 compaction 意味着更多重写字节数,即更高的 WA;段变小还会让「段数 ≈ 索引任务数」这条关系(第四节)更早触发建索风暴。压缩策略是权衡,不是「越勤快越好」。

误解五:binlog compaction 和 segment compaction 是两套互相独立的机制,可以分别单独调优。 两者共享同一个 Data Node 任务队列(3.1 节表格),调大其中一种的触发频率会占用另一种的调度窗口。孤立地调其中一个参数,观察到的效果可能来自另一个机制被挤占,而不是这个参数本身生效。

误解六:监控看到「索引对象已写入对象存储」,就可以认为这批数据已经在用新索引搜索。 第二节 2.1 已拆开两次交付:索引写回对象存储只是第一次交付,Query Node 完成 handoff 加载才是第二次交付。两次交付之间的窗口期,查询可能仍在用旧视图或兜底路径,只看对象存储侧的写入确认会得出「已生效」的错误结论。


八、学术谱系、工程间隙、开放问题

8.1 谱系

工作负载 经典机制 Milvus 落点
不可变文件整理 O’Neil et al., LSM-Tree, Acta Informatica 1996 Data Node compaction
leveled vs tiered 权衡 Dayan & Idreos, Dostoevsky, SIGMOD 2017 暂无公开分层策略描述;本篇不代入
写放大量化 \(WA\) 公式(第三节) Milvus 未公开等价量化框架
批量构建索引 离线 ANN 构建流水线 每 Sealed segment 建 Knowhere 索引
视图切换 快照发布 / handoff Coordinator handoff
无状态离线 Worker 批处理任务重调度(不依赖节点本地状态) Data Node 失败任务可直接重新调度(第一节)

Wang et al. (SIGMOD 2021) 强调动态更新与查询并存;2.6 用 Streaming/Query/Data 三分把「在线可搜」与「离线重活」拆开,避免在查询节点上同步跑满 SIMD 建索。这个三分本身也回答了 LSM 论文里没有覆盖的一个问题:O’Neil et al. 的原始模型只区分「内存 MemTable」与「磁盘 SST」两层,没有「向量索引构建」这种计算密集、需要独立硬件资源池的后台任务类型——Milvus 把 Data Node 单独拆出来,某种程度上是给 LSM 式后台整理再加一层计算密集子任务的工程扩展。

8.2 工程间隙

8.3 开放问题

  1. 建索优先级如何相对 compaction、相对查询加载自动调度?
  2. GPU 建索与 CPU 查询并存时的集群分区策略(第 8、18 篇)?
  3. 多向量字段导致单段多索引时,部分索引成功部分失败的对外语义?
  4. 能否借鉴 LSM 分层调参的思路,给 segment compaction 补一套可观测的放大系数(重写字节数 / 有效数据字节数),让容量规划从「经验阈值」变成「可核算的曲线」?
  5. binlog compaction 与 segment compaction 共享同一队列时,是否需要像现代 LSM 实现那样区分优先级(例如高删除率的段优先做 binlog compaction),而不是简单的先来先服务?
  6. 标量索引(Bloom、哈希、树、倒排)建索资源需求远小于向量索引,是否值得在调度层单独拆出一条轻量队列,避免标量索引任务被向量建索任务长期阻塞?

九、小结

三句话小结

  1. Data Node 是离线臂膀:从对象存储拉 Sealed 快照,经 Knowhere 写回索引,并做 compaction 控制段数与删除空间;Coordinator handoff 把结果交给 Query Node。
  2. flush 完成不等于索引就位——建索是异步队列任务,批量导入后的「查询变慢/召回不稳」经常是索引堆积(取决于写入峰值速率是否超过建索处理速率),而不是参数或 SIMD 配置的问题。
  3. Data Node 应当被当成独立资源池规划:加 Query Node 解决不了建索/compaction 处理速率不足的问题。

在线 SLO 往往死在 离线队列与对象存储争用,而不是又一个 ef 参数——排障顺序应该是「先看队列深度和两次交付状态,再看索引参数」。

下一批进入过滤与一致性语义(第 11–14 篇);对照与选型见第 15–18 篇。

排障时把「延迟落在哪一层」的问题按顺序过一遍——Segcore/Knowhere(第 7、8 篇)、跨节点归并(第 9 篇)、离线队列(本篇)——往往比直接猜参数更快找到根因。


参考资料

核心论文

  1. O’Neil, P., Cheng, E., Gawlick, D., O’Neil, E., The Log-Structured Merge-Tree (LSM-Tree), Acta Informatica 33(4), 1996(不可变文件 + 后台整理的经典源头)。
  2. Dayan, N., Idreos, S., Dostoevsky: Are Your LSM Key-Value Stores Optimized?, SIGMOD 2017(leveled vs tiered 的 WA/RA 权衡;本篇仅作姊妹领域对照,不代入 Milvus 具体实现)。
  3. Wang et al., Milvus: A Purpose-Built Vector Data Management System, SIGMOD 2021。

文档

  1. Milvus Documentation v2.6.x, Architecture Overview
  2. Milvus Documentation v2.6.x, Storage/Computing Disaggregation(Data Node 职责)。
  3. Milvus Documentation v2.6.x, Data Processing(index building、handoff 与 compaction 触发叙述)。
  4. How to Compact Data in Milvus?, Milvus Blog, 2022(binlog/segment compaction;B 级)。
  5. 第 3 篇 Segment 状态机第 4 篇 Proxy 与 Coordinator
  6. 第 6 篇 对象存储布局第 8 篇 Knowhere
  7. 第 13 篇 Delete·Upsert·TTL系列 index

返回 系列目录 | 上一篇:分布式 search 归并 | 下一篇:混合过滤

同主题继续阅读

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

2026-07-12 · database / storage

【向量检索引擎】Query Node 与 Segcore:段级 search 如何执行

按 Milvus 2.6.x Data Processing 与 Architecture 拆解 Query Node 对 Sealed 的加载与段级检索,说明 Streaming Node 上 Growing 路径与 Query Delegator 如何拼成一次 search,用最小故事与常见误解钉住 Segcore 与 Knowhere 的层次边界。


By .