LoadDiff 机制详解:Milvus Segment 自管理加载与 Reopen 增量更新架构
【免费下载链接】milvusMilvus is a high-performance, cloud-native vector database built for scalable vector ANN search项目地址: https://gitcode.com/GitHub_Trending/mi/milvus
本文基于 Milvus 查询节点(QueryNode)与 segcore 的 LoadDiff 设计文档,系统讲解 Milvus 如何把 sealed segment 的加载编排从 Go 层下沉到 C++ segcore 层,使 segment 自治管理自身的加载过程,并通过 diff 计算支持不重载整段的增量更新(Reopen)。读完你将对SegmentLoadInfo、LoadDiff、ApplyLoadDiff、Storage V1/V2/V3 存储模式适配以及 QueryCoord 侧的触发链路形成完整的工程认知。
背景与动机:segment 加载为什么需要"自己管理自己"
Milvus 的查询节点需要把位于对象存储上的 sealed segment(含字段数据、向量/标量索引、删除记录、文本/JSON 统计等)加载进内存以服务查询。在引入 LoadDiff 之前,整条加载链路的编排职责全部集中在 Go 层。
旧架构:Go 层大包大揽,segcore 被动接收
根据设计文档(docs/design-docs/design_docs/segcore/20260204-loaddiff_based_segment_load.md),旧设计中 Go 层segment_loader.go需要负责:
- 资源估算与申请(按 segment 大小与字段配置申请内存/磁盘配额);
- 并发控制;
- 加载编排:走
Load → LoadSegment → loadSealedSegment/LoadMultiFieldData这条较长调用链; - 等待与同步。
而 C++ segcore 层完全被动:
- 被动接收数据(
LoadFieldData、LoadIndex、LoadDeletedRecord); - 不具备自主调度能力。
由此带来的典型问题是:加载相关状态(已加载哪些字段、哪些索引、索引是否包含原始数据 raw data)散落在 Go 侧的对象中,Go 与 C++ 之间围绕"下一步该传什么"需要大量跨语言调用与状态同步;一旦目标数据源发生局部变化(例如新增一个字段、索引从"仅有向量"变为"携带原始数据"、manifest 更新),往往只能整体卸载并重载整段,开销大、链路长。
新架构的三大目标
- 让segment 自己管理加载(对应 issue #45060);
- 支持在 QueryNode 上Reopen(增量重开)segment(对应 issue #46358);
- 把"从目标状态推导需要做哪些变更"抽象为统一的LoadDiff 差异计算,让初始加载(Load)与增量更新(Reopen)走同一套执行引擎。
说明:以下关键实现均可在仓库中找到实证,核心路径位于
internal/core/src/segcore/(C++ segcore)与internal/querycoordv2/、internal/querynodev2/(Go 层)。
新架构总览:三层协作 + C API 桥接
设计文档给出了清晰的端到端分层结构:
┌─────────────────────────────────────────────────────────────────────────────┐ │ QueryCoord │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ SegmentChecker │ │ │ │ - Detects segment state changes (lacks, redundancies, updates) │ │ │ │ - Creates Load/Reopen/Reduce tasks based on diff with target │ │ │ │ - getSealedSegmentDiff() checks ManifestPath changes │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────────┘ │ │ SegmentLoadInfo ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ QueryNode (Go) │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ segmentLoader │ │ │ │ - Resource estimation and protection │ │ │ │ - Delegates actual loading to segcore via C API │ │ │ │ - ReopenSegments() for incremental updates │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ LocalSegment │ │ │ │ - Load() / Reopen() pass SegmentLoadInfo to C++ │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────────┘ │ │ C API (SegmentLoad / ReopenSegment) ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ Segcore (C++) │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ SegmentLoadInfo │ │ │ │ - Wraps protobuf SegmentLoadInfo with caching │ │ │ │ - ComputeDiff(new_info) → LoadDiff │ │ │ │ - GetLoadDiff() → LoadDiff (for initial load from empty state) │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ ChunkedSegmentSealedImpl │ │ │ │ - SetLoadInfo(proto) stores segment load configuration │ │ │ │ - Load() calls GetLoadDiff() + ApplyLoadDiff() │ │ │ │ - Reopen(new_info) calls ComputeDiff() + ApplyLoadDiff() │ │ │ │ - ApplyLoadDiff() executes incremental changes │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────────┘各层职责如下:
- QueryCoord(调度中枢):
SegmentChecker周期性对照"分布式目标(target)"与"节点当前分布(dist)",发现缺失(lacks)、冗余(redundancies)与需要更新的 segment(updates),据此产出 Load / Reopen / Reduce 任务,并把SegmentLoadInfo下发到 QueryNode。实际实现见 internal/querycoordv2/checkers/segment_checker.go。 - QueryNode(Go 协调层):
segmentLoader只负责资源估算与保护,真正的加载通过 C API 委托给 segcore;对增量场景调用ReopenSegments()。入口可参考 internal/querynodev2/handlers.go 中由LoadSegmentsRequest触发的reopenSegments逻辑,其接口定义位于 internal/querynodev2/segments/segment_loader.go。Go 侧LocalSegment的Load()/Reopen()负责把SegmentLoadInfo透传给 C++。 - Segcore(C++ 执行层):
SegmentLoadInfo包装 protobuf 消息并提供差异计算能力;ChunkedSegmentSealedImpl是 sealed segment 的核心实现,负责SetLoadInfo、Load、Reopen与ApplyLoadDiff的具体执行。
值得一提:这条下沉链路的落地对应一批已合入的 PR(#45061 新增
NewSegmentWithLoadInfoAPI、#45488 把加载逻辑从 Go 层迁入 segcore、#46359 支持因数据/schema 变化触发的 reopen、#46394 让 QueryCoord 在 manifest 路径变化时触发 reopen、#46536 统一 Load 与 Reopen、#46598 处理 v1 旧 binlog 格式、#47061 支持 index-含-raw-data 字段的懒加载、#47412 把默认值填充纳入 LoadDiff)。
核心组件一:SegmentLoadInfo 类(加载信息包装与缓存)
设计文档中该类位于 internal/core/src/segcore/SegmentLoadInfo.h 与同目录SegmentLoadInfo.cpp,是一个对 protobufSegmentLoadInfo消息的 C++ 包装层,能力包括:
- 访问器:
GetSegmentID()、GetNumOfRows()、GetIndexInfos()、GetBinlogPaths()等; - 缓存:内部通过
BuildCache()构建按字段快速查找的结构(文档注释称其 cache 为"对可变计算的 memoization",见 SegmentLoadInfo.h 中GetColumnGroups()的可重入说明); - 索引转换:把
FieldIndexInfo转换为带 mmap 配置的LoadIndexInfo; - 差异计算:
ComputeDiff()与GetLoadDiff()。
设计文档给出的类骨架如下:
class SegmentLoadInfo { public: explicit SegmentLoadInfo(const ProtoType& info, SchemaPtr schema); // Compute diff between current and new load info (for Reopen) LoadDiff ComputeDiff(SegmentLoadInfo& new_info); // Compute diff from empty state (for initial Load) LoadDiff GetLoadDiff(); // Column groups support (Storage V3) std::shared_ptr<milvus_storage::api::ColumnGroups> GetColumnGroups(); private: void BuildCache(); void ComputeDiffIndexes(LoadDiff& diff, SegmentLoadInfo& new_info); void ComputeDiffBinlogs(LoadDiff& diff, SegmentLoadInfo& new_info); void ComputeDiffColumnGroups(LoadDiff& diff, SegmentLoadInfo& new_info); void ComputeDiffReloadFields(LoadDiff& diff, SegmentLoadInfo& new_info); };从实际源码(SegmentLoadInfo.h)看,私有 diff 计算函数比文档骨架更丰富,还包括ComputeDiffDefaultFields(默认值字段差异)、ComputeDiffTextIndexes(文本索引差异)与ComputeDiffJsonKeyStats(JSON key 统计差异),可见"差异类型"会随功能演进持续扩展。
核心组件二:LoadDiff 结构体("变更清单")
LoadDiff表示两个 segment 加载状态之间的差异,是整个机制的"变更清单"。设计文档给出基础版:
struct LoadDiff { // Indexes to load (field_id -> list of LoadIndexInfo) std::unordered_map<FieldId, std::vector<LoadIndexInfo>> indexes_to_load; // Binlogs to load (Storage V1/V2) std::vector<std::pair<std::vector<FieldId>, proto::segcore::FieldBinlog>> binlogs_to_load; // Column groups to load (Storage V3 manifest mode) std::vector<std::pair<int, std::vector<FieldId>>> column_groups_to_load; // Column groups for lazy loading (index has raw data) std::vector<std::pair<int, std::vector<FieldId>>> column_groups_to_lazyload; // Fields to reload (when index raw data availability changes) std::vector<FieldId> fields_to_reload; // Fields to fill with default values (schema evolution) std::vector<FieldId> fields_to_fill_default; // Indexes to drop std::set<FieldId> indexes_to_drop; // Field data to drop std::unordered_set<FieldId> field_data_to_drop; // Manifest update flag bool manifest_updated = false; std::string new_manifest_path; bool HasChanges() const; std::string ToString() const; // For debugging };实际实现(SegmentLoadInfo.h)在文档基础上还引入了大量"replace 类"与"细分统计类"条目,每个条目的语义注释都值得精读:
indexes_to_loadvsindexes_to_replace:前者用于"字段尚无索引"的新增加载,后者用于"字段已有索引"时的替换加载;binlogs_to_load/binlogs_to_replace:仅在 current 与 new 都处于 binlog 模式时填充;column_groups_to_load/column_groups_to_replace:对应字段新建/字段在列组间移动或列组数据变化的场景;同一个 group 可因不同配置重复出现;column_groups_to_lazyload/column_groups_to_lazyreplace:懒加载路径(含"index 携带 raw data"的字段);json_indexes_to_drop:由于 JSON 字段可含多个嵌套路径索引,字段级 drop 会丢失"保留兄弟路径"所需的标识,故按field_id -> 嵌套路径集合精确下发;- 文本/JSON 统计系列:
text_indexes_to_load(从预构建文件装载)、text_indexes_to_create(从 raw data 现场创建,服务于 enable_match 字段)、json_stats_to_load/replace/drop; load_external_manifest:外部集合(external collection)的列名是 parquet 字段名而非数字 field_id,应绕过ComputeDiffColumnGroups,在ApplyLoadDiff中直接LoadColumnGroups(manifest_path)。
HasChanges()会聚合上述全部条目(含manifest_updated与load_external_manifest),ToString()则为每个条目输出人类可读摘要,方便定位"这个 reopen 到底改了什么"。
一个需要特别留意的约束(代码注释中明确写出):binlog 模式与 manifest 模式之间不支持跨类别变更——SegmentLoadInfo只能在同一类别内演进,即 binlog → binlog,或 manifest → manifest。
核心组件三:ApplyLoadDiff —— 统一执行增量变更
设计文档给出的执行骨架位于 internal/core/src/segcore/ChunkedSegmentSealedImpl.cpp:
void ChunkedSegmentSealedImpl::ApplyLoadDiff( SegmentLoadInfo& segment_load_info, LoadDiff& diff, milvus::OpContext* op_ctx) { // 1. Load new indexes if (!diff.indexes_to_load.empty()) { LoadBatchIndexes(trace_ctx, diff.indexes_to_load, op_ctx); } // 2. Reload fields (MUST before drop index) if (!diff.fields_to_reload.empty()) { ReloadColumns(diff.fields_to_reload, op_ctx); } // 3. Drop indexes (MUST after reload to maintain data availability) if (!diff.indexes_to_drop.empty()) { for (auto field_id : diff.indexes_to_drop) { DropIndex(field_id); } } // 4. Load column groups (eager + lazy) if (!diff.column_groups_to_load.empty()) { LoadColumnGroups(..., diff.column_groups_to_load, true, op_ctx); } if (!diff.column_groups_to_lazyload.empty()) { LoadColumnGroups(..., diff.column_groups_to_lazyload, false, op_ctx); } // 5. Load field binlogs (Storage V1/V2) if (!diff.binlogs_to_load.empty()) { LoadBatchFieldData(trace_ctx, diff.binlogs_to_load, op_ctx); } // 6. Fill default values for schema evolution if (!diff.fields_to_fill_default.empty()) { FillDefaultValueFields(diff.fields_to_fill_default); } // 7. Drop field data if (!diff.field_data_to_drop.empty()) { for (auto field_id : diff.field_data_to_drop) { DropFieldData(field_id); } } }实现层面(ChunkedSegmentSealedImpl.cpp)有两点超出文档骨架、需要补充的事实:
- 每个阶段都做了可取消性(cancellation)检查。源码在第 7243、7253、7268、7347 行等多处调用
CheckCancellation(op_ctx, id_, "ChunkedSegmentSealedImpl::ApplyLoadDiff()"),与 Go 侧的资源保护配合,保证加载过程可被中止回收,避免长时间占用资源。 - 内部存在"已发布状态"(PublishedState)快照与写时复制(COW)提交机制。例如
Load()开头会CapturePublishedState()再构造可变副本,文档注释说明"ApplyLoadDiff 产生的运行时更新在数据加载完成后通过 COW helper 提交",保证并发读(search/retrieve)在加载过程中始终看到一致状态。索引/字段的 drop 也有DropIndexFromState(...)、DropFieldData(..., &runtime)这类带状态快照的重载版本,具体可参见 ChunkedSegmentSealedImpl.cpp 中DropFieldData与DropIndex的定义。
Load 与 Reopen:同一 diff 机制的两个入口
这是本次设计的核心统一点:初始 Load 与增量 Reopen 共用ApplyLoadDiff()执行管线,差异只在于 diff 的来源。
初始 Load:从空状态取 diff
void ChunkedSegmentSealedImpl::Load( milvus::tracer::TraceContext& trace_ctx, milvus::OpContext* op_ctx) { auto diff = segment_load_info_.GetLoadDiff(); ApplyLoadDiff(segment_load_info_, diff, op_ctx); }实际的Load()实现(ChunkedSegmentSealedImpl.cpp)与 Reopen 共享reopen_mutex_以保证串行化,并通过CapturePublishedState()捕获快照后,把"已用默认值填充的字段、已创建的文本索引"等运行时状态标记进可变副本,再GetLoadDiff()计算"从空 → 目标"的完整 diff 并打日志(如Loading segment {id} with {rows} rows),最后交给ApplyLoadDiff。
Reopen:对当前状态计算增量 diff
void ChunkedSegmentSealedImpl::Reopen( const milvus::proto::segcore::SegmentLoadInfo& new_load_info, SchemaPtr new_schema) { auto current = std::atomic_load(&segment_load_info_); SegmentLoadInfo current_mutable(*current); SegmentLoadInfo new_seg_load_info(new_load_info, new_schema); auto diff = current_mutable.ComputeDiff(new_seg_load_info); std::atomic_store(&segment_load_info_, std::make_shared<const SegmentLoadInfo>(new_seg_load_info)); ApplySchemaForReopen(new_schema); ApplyLoadDiff(new_seg_load_info, diff); }从实现可以看到segment_load_info_是一个原子共享指针(std::atomic_load/std::atomic_store),当前加载配置会被整体替换成新配置,同时ApplySchemaForReopen(new_schema)更新段内 schema,最后ApplyLoadDiff只执行差异部分。源码中Reopen系列包含多个重载(见 ChunkedSegmentSealedImpl.cpp),分别覆盖"仅 schema 变化""携带 OpContext 的 schema 变化"以及"携带完整新 load info + schema"的路径。
Schema 感知的 Reopen
文档特别强调:schema 变更不再走"直接填充缺失字段"的独立 sealed-segment 路径。QueryNode 会把最新集合 schema 连同SegmentLoadInfo一起传给 segcore,于是ComputeDiff()在计算 load / 默认值填充 / drop 动作时能"看见"新增或删除的字段。
对于仅由 query/retrieve 计划触发的"schema-only 懒检查",Reopen(SchemaPtr)会复用当前 load info,与较新的 schema 做差异计算;默认值填充、字段数据删除、索引删除因此都被建模进LoadDiff,统一由ApplyLoadDiff()执行。这也让段内字段与查询计划所需字段的一致性维护完全收敛到 C++ 层。
三种存储模式的 diff 适配
Milvus 的对象存储布局历史上存在多种格式,LoadDiff 机制对它们做了分层适配:
| 存储模式 | 数据组织特征 | diff 计算入口 | 说明 |
|---|---|---|---|
| Storage V1(遗留 binlog) | 每个字段拥有独立 binlog 文件;child_fields为空,field_id == group_id | ComputeDiffBinlogs() | 文档中称为 legacy binlog,另有 PR #46598 专门处理 v1 格式 |
| Storage V2(列组 Column Groups) | 多个字段可共享同一列组;child_fields列出组内字段 ID | ComputeDiffBinlogs() | 仍以 binlog 路径集合表达变更 |
| Storage V3(manifest 模式) | 使用Loon manifest描述列组元数据;支持"index 携带 raw data"字段的懒加载 | ComputeDiffColumnGroups() | ManifestPath变化会触发 segment reopen |
设计文档明确指出 V3 的 diff 类型划分为"加载/替换列组"与"懒加载/懒替换列组"两对动作,且该能力由 SegmentLoadInfo.cpp 中GetColumnGroups()与milvus_storage::api::ColumnGroups支撑。
Proto 定义与字段演进
SegmentLoadInfo的线格式定义于 pkg/proto/segcore.proto(QueryCoord 调度侧还有一份同名的querypb.SegmentLoadInfo,见 pkg/proto/query_coord.proto,两者在跨层转发时互转)。设计文档收录的消息体:
message SegmentLoadInfo { int64 segmentID = 1; int64 partitionID = 2; int64 collectionID = 3; int64 dbID = 4; int64 flush_time = 5; repeated FieldBinlog binlog_paths = 6; int64 num_of_rows = 7; repeated FieldBinlog statslogs = 8; repeated FieldBinlog deltalogs = 9; repeated int64 compactionFrom = 10; repeated FieldIndexInfo index_infos = 11; string insert_channel = 13; int64 readableVersion = 14; int64 storageVersion = 15; bool is_sorted = 16; map<int64, TextIndexStats> textStatsLogs = 17; repeated FieldBinlog bm25logs = 18; map<int64, JsonKeyStats> jsonKeyStatsLogs = 19; common.LoadPriority priority = 20; string manifest_path = 21; // Storage V3 manifest path }对照仓库实际文件(pkg/proto/segcore.proto),该消息已进一步演进,后续新增字段包括:
use_take_for_output(字段 22):为 true 时输出字段检索改用take()API 而非bulk_subscript;estimated_bytes_per_row(字段 23):来自 Take API 的每行平均内存字节采样值,在格式元数据缺乏 size stats 时作为回退估算;commit_timestamp(字段 24):import / CDC segment 的事务时间戳。非零时 segcore 会覆写内存中的时间戳列,使 MVCC / TTL / 删除逻辑统一按 commit 时间而非行内原始时间戳生效;- 原文档中
compactionFrom(字段 10)注释为 "segmentIDs compacted from",即该 segment 由哪些 compaction 源段合并而来。
FieldIndexInfo(同文件 L100-L115)则描述了每个索引的index_name/indexID/buildID、index_params、index_file_paths、index_size、index_version、num_rows、current_index_version与标量索引版本等信息,是 diff 中"索引加载/替换/删除"判定的数据基础。
QueryCoord 集成:谁来决定何时 Reopen
SegmentChecker 与差异检测
Go 侧负责发现更新需求的是 internal/querycoordv2/checkers/segment_checker.go 中的SegmentChecker。设计文档给出了关键逻辑:
func (c *SegmentChecker) getSealedSegmentDiff(...) (toLoad, loadPriorities, toRelease, toUpdate) { isSegmentUpdate := func(segment *datapb.SegmentInfo) bool { segInDist, existInDist := distMap[segment.ID] return existInDist && segInDist.ManifestPath != segment.GetManifestPath() } // ... detect updated segments and create reopen tasks } func (c *SegmentChecker) createSegmentReopenTasks(...) []task.Task { // Creates ActionTypeReopen tasks for updated segments }实际实现印证了这一描述:getSealedSegmentDiff(segment_checker.go)在判断是否需要 update 时调用了packed.CompareManifestPath(segInDist.ManifestPath, segment.GetManifestPath())(segment_checker.go),对"dist 中段"与"目标段"的 manifest 路径做语义比较,并在日志中同时输出 distManifest 与 targetManifest;而 reopen 任务最终通过task.NewSegmentAction(s.Node, task.ActionTypeReopen, s.GetInsertChannel(), s.GetID())创建(segment_checker.go)。
新增 ActionType:Reopen
为承载增量更新,任务动作类型在既有基础上新增了一类:
const ( ActionTypeGrow ActionType = iota + 1 ActionTypeReduce ActionTypeUpdate ActionTypeReopen // New action type for segment reopen )任务到达 QueryNode 后,由 handler 层路由到 loader 的ReopenSegments(internal/querynodev2/handlers.go),该能力在 loader 接口ReopenSegments(ctx context.Context, loadInfos ...)中暴露(internal/querynodev2/segments/segment_loader.go),最终把SegmentLoadInfo交给 segcore 执行 C++ 侧的Reopen。
执行顺序:保证"查询永远有可用数据源"
ApplyLoadDiff()内的动作顺序是正确性关键。设计文档的权威顺序如下:
- 加载/替换索引—— 新索引或替换索引最先就位;
- 重载列(ReloadColumns)—— 恢复因"索引不再携带 raw data"而失去 raw data 覆盖的字段;
- 加载/替换列组—— Storage V3 的 eager 与 lazy 两路;
- 加载/替换字段 binlog—— Storage V1/V2 数据面;
- 丢弃索引—— 只有在新数据/新索引可用后才 drop 旧索引;
- 加载文本/JSON stats 索引—— 应用预构建的 text 与 JSON 统计变更;
- 填充默认值—— 面向无数据源的 schema 演进字段;
- 从 raw data 现场创建文本索引—— 服务于没有预构建索引文件的 enable_match 字段;
- 丢弃字段数据—— 清理被删除的字段。
设计文档特别强调:"当前实现总是先加载/替换 raw 字段数据、再 drop 旧索引,因此在索引切换过程中,查询永远可以回退到某个合法数据源。"这个可用性保证是第 2、3、4 步必须排在 drop 动作(第 5、9 步)之前的根本原因。
当前能力矩阵
设计文档以表格形式总结了 LoadDiff 机制已覆盖的能力(均标注为已实现,可在 SegmentLoadInfoTest.cpp 等测试中找到对应用例):
| Feature | Status | Description |
|---|---|---|
| Initial Load | ✅ Implemented | 从空状态经GetLoadDiff()加载 segment |
| Index Load | ✅ Implemented | 加载新索引(向量/标量) |
| Index Drop | ✅ Implemented | 目标中移除的索引被 drop |
| Binlog Load | ✅ Implemented | 加载字段 binlog(Storage V1/V2) |
| Column Group Load | ✅ Implemented | 加载列组(Storage V3) |
| Lazy Loading | ✅ Implemented | 跳过携带 raw data 的索引字段的加载 |
| Field Reload | ✅ Implemented | 索引 raw data 可用性变化时重载字段 |
| Schema Evolution | ✅ Implemented | 为新增字段填充默认值 |
| Manifest Update | ✅ Implemented | manifest 路径变化时执行 reopen |
| Legacy Format | ✅ Implemented | 兼容 V1 binlog 格式 |
单元测试覆盖
差异计算的单元测试集中在 internal/core/src/segcore/SegmentLoadInfoTest.cpp(仓库中约 3955 行),设计文档概括的测试维度包括:
- 索引的 diff 计算;
- binlog 的 diff 计算(覆盖 V1 与 V4 两种格式);
- 列组的 diff 计算;
- 混合格式处理(V1/V2 binlog 与 V3 manifest 并存场景下的模式约束);
- schema 演进场景(新增带默认值字段、字段数据 drop、索引 drop 的组合)。
与之配套的段级测试还有ChunkedSegmentSealedStorageV2Test.cpp、ChunkedSegmentSealedTest.cpp、ChunkedSegmentSealedBinlogIndexTest.cpp与LoadCancellationTest.cpp等,读者可沿 internal/core/src/segcore/ 目录继续深入。
收益总结
设计文档归纳的架构收益可概括为七点:
- 职责清晰:加载逻辑完全由 segment 自己管理,Go 层退化为"资源看门人";
- 性能优化:显著减少 Go ↔ C++ 的跨语言调用次数(编排不再逐字段往返);
- 资源管理更精准:C++ 层直接处理资源分配,避免中间层重复估算;
- 缓存深度整合:segment 可直接与缓存层交互;
- schema 演进更主动:segment 掌握自身全部信息,能自主决策填充/删除;
- 增量更新:局部变化(新增字段、索引 raw data 状态翻转、manifest 更新)无需整体卸载重载;
- API 统一:单一
ApplyLoadDiff()覆盖所有加载场景,Load 与 Reopen 共享一条执行管线。
未来演进方向
文档明确该机制的终极目标是:让所有加载状态变更都走 LoadDiff。已规划的扩展方向包括:
- 在线索引构建:检测到某字段新增索引 → reopen 段并加载新索引 → 若索引携带 raw data 则 drop 字段数据;
- 在线 schema 变更:新增带默认值字段、drop 字段(删除字段数据)、修改字段属性;
- 索引类型变更:在不同索引类型之间切换并处理参数变化;
- Compaction 集成:compaction 之后更新 segment、处理段合并;
- 分层存储(Tiered Storage):数据在不同存储层级间移动、更新存储位置。
如何继续阅读源码
建议按以下路径沿"决策 → 编排 → 执行"的调用链阅读:
- 调度与差异检测:internal/querycoordv2/checkers/segment_checker.go(
getSealedSegmentDiff与ActionTypeReopen任务创建); - 线格式定义:pkg/proto/segcore.proto(
SegmentLoadInfo、FieldIndexInfo)与 pkg/proto/query_coord.proto; - Go 编排层:internal/querynodev2/segments/segment_loader.go(
ReopenSegments)、internal/querynodev2/handlers.go; - C++ 差异计算:internal/core/src/segcore/SegmentLoadInfo.h 与同目录
SegmentLoadInfo.cpp(ComputeDiff*系列、GetLoadDiff); - C++ 执行主体:internal/core/src/segcore/ChunkedSegmentSealedImpl.cpp(
Load/Reopen/ApplyLoadDiff/DropIndex/DropFieldData); - 测试印证:internal/core/src/segcore/SegmentLoadInfoTest.cpp。
提示:LoadDiff 机制面向 sealed segment 的加载编排,其行为与对象存储格式版本(Storage V1/V2/V3)、索引是否携带 raw data、schema 演进状态强相关,跨 binlog/manifest 类别的变更当前不受支持,阅读代码与设计时需留意上述前提。
【免费下载链接】milvusMilvus is a high-performance, cloud-native vector database built for scalable vector ANN search项目地址: https://gitcode.com/GitHub_Trending/mi/milvus
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考