资讯动态

Milvus DataCoord Segment Manifest Commit 框架设计解析:以 segment 为粒度的 StorageV3 元数据提交协议

发布时间:2026/9/11 11:44:56 来源:尧图企业网站定制
Milvus DataCoord Segment Manifest Commit 框架设计解析以 segment 为粒度的 StorageV3 元数据提交协议【免费下载链接】milvusMilvus is a high-performance, cloud-native vector database built for scalable vector ANN search项目地址: https://gitcode.com/GitHub_Trending/mi/milvus本设计文档20260817-datacoord-segment-manifest-commit.md定义了 Milvus DataCoord 中一套全新的、以 segment 为序列化粒度的 manifest 提交框架用于解决 StorageV3 不可变版本化 manifest 在并发写入下的乱序发布问题。读完本文你将掌握该框架的完整协议流程、锁序约束、失败恢复语义、各调用方索引、GC、统计、压缩、flush、copy/import的迁移清单以及其在当前仓库中的落地实现与测试覆盖。背景StorageV3 的 manifest 与发布问题StorageV3 使用不可变、带版本号的 manifest 作为 segment 文件的唯一事实来源。一个 segment 的所有文件插入日志、索引、统计信息等被组织进一个版本化的 manifest 中SegmentInfo.manifest_path是这个 manifest 修订版对外可见的持久化指针——只有被这个指针指向的修订版才对 Milvus 的其余组件可见。在设计这套框架之前segment 文件的生命周期被分散在多个所有者手中彼此之间缺少统一的提交协调索引 worker可以把自己的索引条目追加进 manifest之后再把结果路径上报给 DataCoordDataCoord GC先从 manifest 中删除索引条目再单独发布新路径并清理文件与SegmentIndex元数据copy/restore在后续的UpdateManifest之前追加被拷贝的索引条目stats、flush/import、compaction通过多种形式的UpdateManifest发布 manifest。问题在于meta.segMu只保护UpdateSegmentsInfo的进程内执行并不覆盖创建 manifest 修订版的那次对象存储事务因此它不是一个真正意义上的 segment 提交锁。两个路径可以观察到同一个指针、各自独立创建修订版随后乱序发布各自的结果——一次稍晚的 etcd 写可能把指针回退到更旧的修订版或者发布一个遗漏了已完成变更的修订版。既有的indexMeta.keyLock(BuildID)职责也不相同它只串行化单个SegmentIndex作业的生命周期变更无法串行化一个被所有索引和统计共享的 manifest。反过来如果用一个全局segMu跨越整个对象存储 I/O又会把不相关的 segment 全部串行化让网络 I/O 阻塞所有元数据更新。目标与非目标框架的目标可以归纳为六条单个 segment 在进程内同一时刻至多有一个manifest 提交在进行segment 锁覆盖 manifest 修订版创建和catalog 发布两个阶段而非仅覆盖 etcd 写入DataNode 负责写物理数据、索引和统计文件DataCoord 拥有提交时刻的 manifest 事务以及结果的 etcd 发布一次成功的可见提交在一个 catalog 事务中更新所有描述该可见 manifest 的元数据失败可重试且绝不会在 manifest 存在之前发布 manifest 指针框架只对并发写者可能指向同一 segment的路径做串行化——即 post-flush 异步作业stats sort、索引构建、GC、压缩、批量 DDL单写者路径仍走内联UpdateManifest。同时明确划出三条非目标不做对象存储与 etcd 之间的分布式事务不可用也不允许用 protobuf 值 CAS 去模拟不做跨多个活跃 DataCoord leader 的分布式 per-segment 锁——Milvus 的 leadership 机制已保证只有活跃 DataCoord 能变更 catalog 元数据manifest 事务冲突处理负责 leader 切换、重试与意外外部写者的保护不串行化所有 segment 元数据更新——非 manifest 更新仍走UpdateSegmentsInfo只有 manifest 提交协议由这把新的 keyed lock 串行化。所有权模型谁写文件谁提交指针框架的核心分工清晰体现在其所有权模型图中DataNode / compactor writes immutable artifact files returns a structured manifest delta and its expected input manifest | v DataCoord meta.CommitSegmentManifest(segmentID, request) segment-scoped lock - read current SegmentInfo - validate expected base / segment state - execute packed manifest transaction - catalog transaction: SegmentInfo SegmentIndex/task state - install in-memory result | v Visible SegmentInfo.manifest_path关键在于凡是由 DataCoord 构建修订版的路径worker 不得返回一个已预先发布的 manifest 修订版。例如索引任务只返回索引文件与索引元数据而不是AddIndexInfoToManifest的结果DataCoord 在持有 segment 提交锁期间才把它转换为ManifestIndexInfo并亲自执行 packed 事务。这条规则适用于所有并发的 post-flush 路径——stats sort、索引构建、GC 索引移除、压缩、批量 DDL——因为它们操作的都是可能同时被其他路径推进的已 flush segment必须串行化。这些路径只能通过CommitSegmentManifest触达 manifest绝不调用UpdateManifest。作为对照两类单写者路径不做串行化、直接通过UpdateManifest内联发布growing/L0 segment 的 flushSaveBinlogPathsgrowing segment 在创建时就持有ManifestEarliest修订版见 segment_manager.go创建时通过packed.MarshalManifestPath(basePath, packed.ManifestEarliest)初始化每次 sync 都会推进 manifest但这些 sync 都来自该 segment VChannel 的唯一 WAL 拥有者、顺序应用不存在并发写者。过期重试与跨节点交接由SaveBinlogPaths中的 channel-owner 检查围栏保护重发的相同指针是无操作因此无需 base-match CAS。flusher 返回完整 manifest 指针DataCoord 直接记录。新 copy/import 目标的最终化目标 segment 预先以空 manifest 路径注册见 snapshot_manager.go、import_util.go并保持Importing状态——对以Flushed/Flushing为门槛的 stats/index/compaction 不可见——直到一次单一的Importing - Flushed最终化。其 worker 返回完整指针DataCoord 不做任何 manifest I/O。由于这些路径没有并发写者UpdateManifest不带任何 StorageV3 防护其实现见 meta.go注释明确写明Concurrent post-flush writers never reach this operator。一个 producer 可以写数据文件但一个要发布进并发 post-flush 窗口的作业不会自己去挑选可见的 manifest 修订版——它把结构化条目交给 DataCoord由 DataCoord 在锁下提交修订版。API 形态类型化请求替代任意回调框架在meta上增加一把 keyed lock随其余元数据状态一起初始化segmentManifestLocks *lock.KeyLock[int64]对外暴露面采用类型化请求而非任意回调回调可能做隐藏 I/O 或在持锁期间重入metatype SegmentManifestCommit struct { SegmentID int64 ExpectedManifest string // empty only for initial-manifest creation Mutation ManifestMutation CatalogMutation SegmentManifestCatalogMutation } func (m *meta) CommitSegmentManifest( ctx context.Context, commit SegmentManifestCommit, ) errorManifestMutation是一个封闭/类型化的集合初始包含CreateManifest— 从 worker 提供的结构化条目构造第一个修订版AddIndexes/DropIndexesAddStatsAppendData/ReplaceData— 供 flush 与 compaction 使用PublishPreparedManifest— 仅作为临时兼容适配器必须校验 base在所有 producer 都返回结构化条目后移除。CatalogMutation描述必须与 manifest 指针一起可见的元数据完成某个SegmentIndex、创建拷贝目标SegmentIndex记录、更新 segment 统计、变更 segment 状态等。它必须产出 catalog action而不是执行一次独立的 catalog 写。当前仓库中这套 API 已落地为具体的 Go 类型见 segment_manifest_commit.goManifestMutationType是封闭集合包含ManifestMutationCommitUpdates从结构化 packed updates 创建新修订版即常规 StorageV3 发布路径与ManifestMutationNoop发布由既有 producer 准备好的指针刻意不做对象存储 I/O作为迁移补丁SegmentManifestCommit还额外携带StorageConfig字段供 packed 事务构造对象存储属性。注意ManifestMutation中的NewFiles归调用者所有必须在CommitSegmentManifest返回后销毁。提交协议与锁序对单个 segment协议共六步对segmentManifestLocks[segmentID]加锁短暂持有segMu克隆当前SegmentInfo后释放segMu校验 segment 存在性、StorageV3、健康/状态以及操作依赖特定输入时的ExpectedManifest基于克隆/当前 manifest 执行packed事务。packed resolver 固定为OVERWRITE在 segment 锁下不存在竞争的本机写者而 resolver 又为重试/leader 交接竞争提供了确定性的 latest-manifest rebase重新获取segMu重载最新SegmentInfo并重新校验健康与ExpectedManifest把新指针和 catalog 变更应用到该最新克隆上从而保留 manifest I/O 期间发生的、无关的普通元数据更新仍在持有segMu时执行一次包含变更后SegmentInfo与全部关联SegmentIndex记录的catalog.Update事务——这与既有全记录UpdateSegmentsInfo的一致性模型一致最终 catalog 发布被串行化而不同 segment 较慢的 manifest I/O 保持并发。若 catalog 写失败不得改动内存在内存中安装克隆的元数据然后释放segMu与 segment 锁。锁序始终固定为segmentManifestLock(segmentID) - segMu - indexMeta.keyLock(buildID)segMu绝不能在对象存储 I/O 期间被持有。既需要 segment 状态又需要 index 状态的路径必须遵循此顺序任何代码都不得先拿 BuildID 锁再尝试 segment manifest 提交。多 segment 操作在加锁前先对 segment ID 排序compaction 尽可能创建独立的目标 segment而不是在一把锁下提交两个 segment。源码级验证CommitSegmentManifest的实现segment_manifest_commit.go严格遵循这一协议先locks.Lock(commit.SegmentID)并在defer中记录lockWait/lockHold指标随后segMu.RLock克隆 segment 快照并立即释放再进行对象存储 I/OcommitManifestMutation最后segMu.Lock重载最新记录、在isNewSegment/已存在两种分支下重新校验、应用 catalog 变更、执行m.catalog.Update成功后才m.segments.SetSegment安装内存。框架还实现了批量形式CommitSegmentManifestssegment_manifest_commit.go先用KeyLock.TryLockMany原子化地一次性获取全部目标 segment 的锁失败按 200µs 起步、20ms 封顶的指数退避重试超过 30s 阈值后升级到LockManyOrdered的 FIFO 阻塞路径以避免饿死再在segMu外并行生成各 segment 修订版最后通过一次UpdateSegmentsInfo调用在单个 segMu 临界区、单次 catalog 写中发布全部指针与额外 operator——L0 压缩正是通过它把多个目标 segment 的提交合并为一次原子操作见 compaction_task_l0.go。失败与恢复语义对象存储与 etcd 无法原子提交因此顺序被刻意设计为单向artifact files - manifest revision - etcd/catalog pointer - memoryartifact 生成失败不做任何 manifest 或 etcd 变更manifest 创建成功但 catalog 发布失败该修订版成为孤儿且不可见。重试从仍然发布的指针开始孤儿清理是安全的因为没有SegmentInfo引用它重试必须在逻辑变更层面幂等加索引不得产生重复的逻辑索引条目删除已删除的逻辑索引可以移除所有匹配的历史条目过期的期望 base 或 segment 状态变更是重试/丢弃结果而非覆盖当前指针的尝试。精确的ExpectedManifest冲突保留一个类型化的可重试错误加一个进程内 stale 标记让 Stats 等任务级消费者可以丢弃过期的 worker 输出而不必把无关的 service-unavailable 失败误判为 stale。在代码中这个 stale 标记就是errSegmentManifestStalesegment_manifest_commit.go对外仍包装为类型化的可重试 service-unavailable 错误staleSegmentManifestError、matchesExpectedManifest。对 Noop 兼容路径validatePreparedManifest强制同样的单调指针规则prepared 修订版的 base 必须匹配当前 base且版本号不得低于当前版本否则即报 stalesegment_manifest_commit.go。Drop 需要额外的持久化清理状态commit manifest without index mark index cleanup pending - delete index objects (retryable) - remove SegmentIndex metadata / complete cleanup marker框架能阻止这些步骤期间插入索引/统计提交但无法让对象删除与 etcd 删除在进程崩溃下原子化。pending 状态使剩余清理可被发现且幂等——这是 GC 迁移的核心要求详见下文迁移清单。调用方迁移清单设计文档为每个既有调用方给出了明确的迁移路径当前所有者/路径框架迁移方案task_index.go/ DataNode 索引任务返回索引 artifact 元数据。meta.CommitSegmentManifest(AddIndexes)创建修订版并原子完成SegmentIndex。garbage_collector.go使用DropIndexes在一个 catalog 事务中发布指针与清理意图随后执行可重试的对象清理。copy_segment_task.go、import_task_import.go/ restorecopy/import 目标是全新的、独占拥有的 segmentworker 返回完整指针DataCoord 通过UpdateManifest内联发布该首指针。无其他写者触碰的 segment 不需要CommitSegmentManifest串行化。task_stats.goText、JSON、sort 统计统一改用AddStats移除裸 sortUpdateManifest路径。flush /SaveBinlogPaths无需迁移。经UpdateManifest内联发布。growing/L0 segment 由其单一 WAL 拥有者顺序 flush即使 manifest 从ManifestEarliest起持续推进也无并发写者。compaction在 DataNode 生成输出文件、返回输出 manifest 条目再经框架发布每个输出 segment。external collection refresh从 segment 级迁移中延期。保留其作业级UpdateSegmentsInfo发布直到 collection 级 generation 边界能原子切换完整刷新结果与external_source/external_spec。snapshot/restore 与 recovery正常读取已发布指针并发的 post-flush 目标 manifest 写入走框架。仓库现状与迁移表一一对应UpdateManifest的内联调用点包括 copy_segment_task.go、import_task_import.go、services.goflush 响应路径以及 task_stats.gosort 统计路径正是设计文档要求移除的裸 sortUpdateManifest路径。迁移门禁UpdateManifest是单写者 manifest 路径的内联发布机制——包括所有 StorageV1/V2 写入以及 StorageV3 的 flushSaveBinlogPaths与 copy/import 最终化这些均无并发写者因此不带任何 StorageV3 防护。并发的 post-flush 路径stats、index、GC、compaction、批量 DDL永不调用UpdateManifest——它们构建修订版并通过CommitSegmentManifest推进指针。评审时对每个UpdateManifest(调用点和每个 packed manifest 变更的 grep 是必需的迁移门禁任何新增的UpdateManifest调用者必须是单写者路径。分阶段实现刻意不迁移 external collection refresh——如果逐 segment 发布返回结果会把一次作业级刷新变成部分可见的序列失败作业可能只发布了部分新 manifest。external collection refresh 是明确的后续工作在下述 end-state 验收标准达成前该 collection 级协议不算完成。实施计划clean worktree框架必须在基于master的干净分支上完成进行中的 manifest-index 迁移不是它的实现分支在meta.go中加入 keyed lock 与CommitSegmentManifest骨架附带锁序文档与聚焦的并发测试加入类型化 packed mutation 适配器及 add/drop/stats/create 测试。存储事务使用OVERWRITE不添加 protobuf 序列化的SegmentInfo值相等 CAS端到端迁移索引完成更新 DataNode 结果契约、移除 worker 侧索引 manifest 发布、由meta原子发布SegmentInfo加SegmentIndex用持久化清理状态与崩溃/重试测试迁移 GC迁移 stats含 sort 路径把 compaction 输出发布迁移到框架。flushSaveBinlogPaths与 copy/import 保持内联UpdateManifest单写者UpdateManifest保持无任何 StorageV3 防护添加可观测性锁等待/持有时长、提交结果、stale-base 拒绝、孤儿 manifest 计数、清理重试在 Milvus builder 容器中运行全部受影响的 DataCoord/DataNode 测试矩阵含覆盖 etcd 失败、stale 输入、并发索引完成、index-vs-GC、stats-vs-index 以及 drop 各阶段后重启的 race/fault-injection 测试。现有测试已经覆盖了协议的核心断言segment_manifest_commit_test.goTestCommitSegmentManifestPublishesOnlyAfterCatalogSuccess、TestCommitSegmentManifestLeavesMemoryUntouchedOnCatalogFailurecatalog 失败不改内存、TestCommitSegmentManifestSerializesSameSegment同 segment 串行化、TestCommitSegmentManifestDoesNotSerializeDifferentSegmentsDuringManifestIO不同 segment 的 manifest I/O 保持并发、TestCommitSegmentManifestRebasesCatalogMutationAfterManifestIOI/O 后 rebase 到最新克隆、TestCommitSegmentManifestFailsStaleWhenPointerAdvancesDuringManifestIOI/O 期间指针移动即 stale、TestCommitSegmentManifestCreatesSegmentWithInitialPointer新 segment 首指针、TestCommitSegmentManifestRejectsDroppedSegmentAsNotFounddrop 视为 not-found 而非内部错误以及TestUpdateManifestAllowsStorageV3Advancement/TestUpdateManifestAllowsStorageV3FirstPublication验证UpdateManifest对 V3 单写者路径保持可用。批量路径的失败注入测试见 segment_manifest_commit_batch_test.go 与 compaction_task_l0_test.go。与既有 manifest-index 工作的集成既有分支不应增量扩展进这套框架。干净分支完成后把 manifest-index 迁移 rebase 到框架分支上用CommitSegmentManifestaction 替换其本地的packed.AddIndexInfosToManifest、RemoveIndexInfosFromManifest与 manifest 指针发布逻辑为迁移兼容保留从 legacySegmentIndex.IndexFileKeys到 manifest 条目的读回退重跑完整生命周期审计build、load、query、copy/restore、snapshot、compaction、dropped-segment GC 与 recovery。这样的顺序避免了在同一 release 中稳定两套相互竞争的发布协议。底层事务细节OVERWRITE resolver 与 manifest 路径编码框架调用的 packed 事务在 manifest_commit.go 中实现CommitManifestUpdates对整包更新使用C.LOON_TRANSACTION_RESOLVE_OVERWRITE——统计更新依赖 overwrite-on-key 语义loon_transaction_update_stat会替换同 key 的既有条目而 column-group append 与 delta-log add 本身就不在 key 上冲突单一 resolve 模式简化了 API 并保持了既有AddStatsToManifest行为。manifest 指针本身是 basePath 加版本号的编码MarshalManifestPath/UnmarshalManifestPath见 ffi_common.go这也是validatePreparedManifest能做版本单调性比较的基础。空更新集时CommitManifestUpdates直接返回MarshalManifestPath(basePath, baseVersion)即同版本幂等推进。验收标准在 DataCoord segment 提交框架之外不存在任何 StorageV3 manifest 变更或指针发布同一 segment 上的两个并发操作不能乱序发布指针修订版不同 segment 的操作在 manifest I/O 上互不阻塞catalog 写失败从不更新内存元数据也从不暴露孤儿 manifest 修订版崩溃后的 GC 能收敛且不删除当前 manifest 引用的文件测试覆盖每种要求的竞争与失败场景而非仅覆盖成功的顺序执行。这套验收标准直接呼应了设计文档开篇的问题陈述segMu不覆盖对象存储事务、keyLock(BuildID)职责错位、全局锁会串行化无关 segment——而 segment-scoped manifest commit 框架正是为这三者同时提供答案的统一提交协议。【免费下载链接】milvusMilvus is a high-performance, cloud-native vector database built for scalable vector ANN search项目地址: https://gitcode.com/GitHub_Trending/mi/milvus创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价