首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Milvus 3.0 Snapshot:不复制数据,怎么把集合按下暂停键

Milvus 3.0 Snapshot:不复制数据,怎么把集合按下暂停键

原创
作者头像
术哥
发布2026-07-18 10:10:11
发布2026-07-18 10:10:11
240
举报
文章被收录于专栏:运维有术运维有术

🚩 2026 年「术哥无界」系列实战文档 X 篇原创计划 第 170 篇,Milvus 最佳实战「2026」系列第 20

大家好,欢迎来到 术哥无界 | ShugeX | 运维有术

我是术哥,一名专注于 AI 编程、AI 智能体、Agent Skills、MCP、云原生、AIOps、Milvus 向量数据库的技术实践者与开源布道者

Talk is cheap, let's explore。无界探索,有术而行。

Milvus Snapshot 信息图封面
Milvus Snapshot 信息图封面

向量数据库里有个绕不开的问题:线上集合一直在写入、compact、重建索引。

但研发和运维又经常需要回到某个历史状态。比如验证一次模型回归,或者给恢复流程留一条退路。

直觉会把这件事归到 Backup:复制一份数据,放到另一处,出事再拿回来。Milvus 3.0 的 Snapshot 走了另一条路。

它更像给集合状态按了一下暂停键:不复制已有 segment 文件。

它只把当时能看到的 schema、index、segment/binlog 引用冻结下来,形成一个 point-in-time read-only view(时间点只读视图)。

这个设计看起来轻,但麻烦也藏在这里。

只要 Snapshot 引用了旧 segment 或 index 文件,GC 就不能把它删掉;只要 Restore 还在后台读这些文件,drop snapshot 也不能让源文件提前消失。

Snapshot 的难点落在 时间点视图、Copy Segment 恢复、GC/pin 保护 这三件事上:它们必须同时成立。

1. 先拆一个误会:Snapshot 不是 Backup

官方文档把 Snapshot 和 Backup 分得很清楚。Backup 是独立的数据副本,会复制全部数据文件,适合更长周期的灾备。

Snapshot 则面向天到周级别的快速回滚、版本化和测试。

创建时,它主要保存 metadata 与 manifest,引用对象存储里已经存在的 segment、binlog、index files。

这不是文字游戏。

Backup 要问的是:我有没有另一份完整数据。Snapshot 要问的是:未来某个时间,我还能不能重新解释这批旧文件。

所以 Snapshot 的边际存储成本可以很低,因为它不主动复制现有数据。但存储影响并不会凭空消失。

官方文档也说了,只要某个 segment 或 index file 被 snapshot 引用,Milvus 就不会 GC 这些文件。

除非相关 snapshot 被 drop,并且这些文件不再被 live collection 或其它 snapshot 引用。

这就解释了为什么不能把 Snapshot 写成传统备份。它没有把数据搬到保险柜,而是给对象存储里的一批文件贴上历史引用标签。标签还在,GC 就得绕开。

维度

Snapshot

Backup

数据形态

metadata + manifest 引用

独立完整副本

创建成本

主要写元数据和清单

复制全部数据文件

典型周期

天到周

周到年

关键风险

GC 误删历史引用

副本成本和恢复时长

这里的判断很直接:Snapshot 解决的是短期历史视图和快速恢复问题,不负责替代灾备体系。 如果对象存储本身整桶丢失,引用型 snapshot 也无能为力。

Snapshot 与 Backup 对比图
Snapshot 与 Backup 对比图

2. 时间点视图从哪里来

调研里有个容易写错的点:Snapshot 的 create_ts 不是跨 channel 的严格全局可见边界。

设计文档里写得更细:创建时会为 collection 的每个 channel 获取 MsgPosition

每个 channel 都有自己的 seek timestamp;create_ts 取这些 timestamp 的 min,主要用于兼容展示和排序。

决定一个 segment 是否进入 snapshot 的,是它所属 channel 的 seek position。

DataCoord 会遍历非 dropped segment,按该 segment 所在 channel 的边界判断是否纳入。

Snapshot 固定的对象并非抽象时间戳,而是一组 channel 边界加上一批 segment 引用。

如果要用 MVCC 这个词,边界也要讲清楚。

这里更准确的说法是 point-in-time read-only view / manifest view:某个时间点上,集合可见数据文件的只读清单。不要把它扩展成通用 MVCC 事务系统,官方资料没有支撑这层含义。

从读者角度看,Snapshot 像一次拍照。

从系统角度看,它更像写了一本账:这个 collection,当时有哪些 schema、index、segment、binlog、deltalog、statslog、index files。

未来 Restore 能不能跑通,要看这本账是否完整,也要看账上引用的文件是否还活着。

3. 为什么创建时不主动 flush

官方管理文档建议创建 snapshot 前 stop writing 并调用 flush()

但它也说得很明确:flush() 不是强制要求;如果跳过,snapshot 只包含已经 flushed 的数据。

源码也印证了这个取舍。

snapshotManager.CreateSnapshot() 没有主动 flush。它通过 handler.GenSnapshot(ctx, collectionID) 生成 snapshot data,再用 snapshotMeta.SaveSnapshot(ctx, snapshotData) 持久化。

也就是说,创建 Snapshot 的职责是固定已 sealed、已可引用的边界,而不是把正在写入的 growing segment 强行推到 sealed 状态。

这背后的取舍很工程化:Snapshot 要轻、要快、要低侵入。主动 flush 会把一次读视图创建变成写路径干预,带来额外延迟。

代价也摆在台面上:如果用户希望刚写入的数据也进入 snapshot,就要在语义上自己决定停写和 flush。系统不替你猜。

还有一个并发细节值得看。

源码里创建期间会调用 snapshotMeta.SetSnapshotPending(collectionID),注释说明它用于在 GenSnapshot -> SaveSnapshot 这个窗口阻止 compaction commit,避免并发 compaction drop 掉 snapshot 即将引用的 segment。

换成工程语言看,Snapshot 创建不是简单读一把元数据。

它要在对象存储、etcd、compaction 之间留出一段受保护的过渡区。

4. manifest 才是这套设计的账本

Milvus 3.0 Snapshot 的存储分成两层:etcd 保存 SnapshotInfo 这类基础信息,方便 list/query。

对象存储保存完整 collection description、index info、segment description。源码里的布局也很直观:

代码语言:markdown
复制
snapshots/{collection_id}/
├── metadata/
│   └── {snapshot_id}.json
└── manifests/
    └── {snapshot_id}/
        └── {segment_id}.avro

SnapshotWriter.Save() 的顺序是先写每个 segment 的 Avro manifest,再收集 StorageV2 manifest 路径,之后写 metadata JSON。

metadata 里包含 SnapshotInfoCollectionIndexesManifestListStoragev2ManifestListSegmentIdsBuildIds 等信息。

Avro manifest 是 segment 级别的事实载体。当前源码里 SnapshotFormatVersion = 4,并兼容读取 0/1/2/3/4 版本。

V4 entry 里可以看到 SegmentIDPartitionIDBinlogFilesDeltalogFilesStatslogFilesIndexFilesTextIndexFilesJSONKeyIndexFiles 等字段。

它还记录 StartPositionDmlPositionStorageVersionIsSortedCommitTimestamp

这个版本线索很有意思。

V2 加入 index_store_path_version,V3 加入 commit_timestamp,V4 加入 child_fieldsformat。这说明 Snapshot 不是薄薄一层 API。它已经变成持久化格式,要考虑历史版本可读、字段演进和未来恢复兼容。

这里还有一个容易被忽略的工程细节:metadata JSON 和 segment manifest 分别落在对象存储的不同路径里,etcd 里又有一份用于快速查询的 SnapshotInfo

这天然存在不一致窗口。比如 manifest 写了一半失败,或者 S3 写完了但 etcd 状态还没更新。

设计文档里把 SaveSnapshot() 写成类似两阶段提交:先记录 PENDING 状态,再写对象存储,之后把状态推进到 COMMITTED。

这个做法不花哨,但很实用。失败时,GC 至少知道这批文件属于哪个未完成 snapshot,而不是把它们当成来路不明的孤儿文件乱删。

Snapshot 的轻量不是随便写几条元数据。

它要用状态机把 etcd 和对象存储之间的灰区圈出来,让后续清理有账可查。

metadata 与 manifest 账本结构图
metadata 与 manifest 账本结构图

5. Restore 为什么走 Copy Segment

Restore 的反直觉点在这里:它不走 import,而是 Copy Segment。

官方文档给出的原因很具体:Restore 通过 copy-segment 直接复制 segment files,包括 binlogs、deltalogs、index files。

这样可以保留 field IDs 和 index IDs,避免 data rewriting 和 index rebuilding。

设计文档也把角色拆开了:Proxy 负责入口,RootCoord 负责创建新 collection/partition 和编排。

DataCoord 负责 copy segment job creation 与 index creation,DataNode 执行对象存储文件复制。

为什么不能 import?因为 snapshot 里的源数据已经是 Milvus 自己能解释的内部文件。

它不像外部 CSV、Parquet 或 JSON 那样等待解析;这些文件带着字段 ID、索引 ID、segment ID、partition/channel 边界。

再走 import,相当于把已经结构化的内部数据打散重来。

Copy Segment 也不是简单的对象存储 copy。CopySegmentJob 里有 IdMappingsSnapshotNameSourceCollectionIdPinIdTotalSegmentsCopiedSegmentsTotalRows 等字段。

CopySegmentTask 的数据流更细:读取 snapshot data,构造 CopySegmentRequest,DataNode 复制文件并生成新的 binlog paths。

随后 DataCoord 把 binlogs/indexes 同步到 segment metadata,再把目标 segments 标记为 Flushed。

难点是身份对齐。

设计文档要求恢复 collection metadata 时用 PreserveFieldId 保持字段 ID,用 PreserveIndexId 保持索引 ID。

原因不复杂:旧 binlog 和 index 文件里的数据身份依赖这些 ID。如果新 collection 重新分配字段或索引 ID,旧文件就有概率无法被新 schema 和 index definition 正确解释。

所以 Restore 的关键不在搬 bytes,而在让新 collection 继续读懂旧 snapshot 里的 bytes。这里包含 segment 文件、字段身份、索引身份、partition/channel 映射和后台任务状态的对齐。

Copy Segment 这条路径,正是为了少做重写,少做重建,同时保住内部文件语义。

这也解释了为什么 Restore 只能落到同一 cluster 中的新 collection。

Snapshot 记录的是 Milvus 内部对象的身份和引用关系,不是一个可随便搬家的中立文件包。目标 collection 要继承相同 schema、shard 数、partition 结构,还要接住旧文件里的 field/index 身份。换到完全陌生的环境,这些隐含前提就不一定成立。

所以 Restore 的边界其实很清楚:它不是跨集群迁移工具,也不是把历史文件导成通用数据集。

它更像在同一个系统语境里重建一个 collection,让新对象继续承认旧 snapshot 的账本。这个边界听起来保守,但换来的好处是路径短、少重算、少重建,恢复过程也能被 copy segment job 追踪进度。

Copy Segment 恢复流程图
Copy Segment 恢复流程图

6. GC 和 pin 保护才是硬骨头

Snapshot 真正改变系统行为的地方,是 GC。

没有 Snapshot 时,GC 主要看当前 live metadata:哪些 segment dropped 了,哪些 index build 不再需要,哪些对象存储文件可以清。引入 Snapshot 后,这套逻辑不够了。

一个 dropped segment 有时仍被某个 snapshot 的 manifest 引用;一个旧 index file 也会成为 Restore 必须读取的输入。

官方文档已经给出结论:一旦 Milvus 在 snapshot 中引用某个 segment 或 index file,就不会 GC 这些文件,除非 drop snapshot。

源码里能看到更具体的保护点:删除 binlog 前检查 snapshotMeta.IsSegmentGCBlocked(collectionID, segmentID);删除 segment index、index files、text index、JSON stats、JSON index 文件前,也会检查 segment 或 buildID 是否被 snapshot 保护。

这套判断不靠文件名约定硬猜,靠的是 snapshot metadata 中预计算的引用集合。源码注释提到 IsSegmentGCBlockedIsBuildIDGCBlocked 是 O(1) 检查。换句话说,Snapshot 让 GC 从 只看当前状态 变成 当前状态 + 历史引用

为什么还要保护 buildID?因为 Restore 不只读取 insert binlog。向量索引、标量索引、text index、JSON index 这些文件也会被 snapshot manifest 引用。只保护 segment 数据,不保护 index 文件,恢复时就会出现一种尴尬局面:数据还在,索引丢了,新 collection 无法按 snapshot 中记录的索引定义继续解释这些文件。

#47658 这类 issue 的价值就在这里。

它报出的 key not found 不是普通缺文件;它把 Snapshot 的隐含约束暴露出来:只要 manifest 记录了某个历史文件,GC 就必须把它当成活引用处理。否则 Snapshot 创建时看起来成功,Restore 时才发现账本上写的东西已经被清掉了。

这个点在 issue 里也有反面证据。GitHub issue #47658 描述过 RestoreSnapshot 在经过 compaction/index rebuild 的 collection 上失败,错误原因是 snapshot 引用的 index files 在 object storage 中不存在。

期望行为写得很直白:snapshot-referenced files including index files should be protected from GC and cleanup。这个 bug 线索说明,只保护 binlog 不够,index files 也必须进入引用保护。

Pin / Unpin 解决的是另一层并发问题。PinSnapshotData 的接口注释说,它会 pin snapshot,防止 GC 删除其 data files,并返回一个 pin ID;UnpinSnapshotData 通过 pin ID 移除 pin,所有 pin 都移除后 GC 才能回收。

这个语义和普通生命周期 API 不一样。

drop snapshot 表达的是用户不再需要这份历史视图;pin 表达的是某个后台流程正在读这份历史视图。两者会撞在一起。系统不能因为用户发起 drop,就把 restore/export 还没读完的源文件清掉。

Restore 过程中也会用 pin。源码注释解释了一个很现实的窗口:如果进程在 Pin 和 broadcast 之间崩溃,或者终态中 Unpin 失败,TTL 可以兜底。

成功路径里,pin ownership 会转移到 copy segment job;job 进入 terminal transition 时,再由 copy segment meta 调用 snapshotMeta.UnpinSnapshot(ctx, pinID)

所以 pin/unpin 更适合理解成后台读取流程的租约保护。restore/export 还在读 snapshot 数据时,即使有人 drop snapshot,GC 也不能立刻清理源文件。等读取流程结束,租约释放,GC 才重新评估这些文件是否还能删。

这里的设计取舍很克制。

Milvus 没有把 snapshot 引用永久锁死,也没有让后台任务裸奔。pin 给 restore/export 一个临时保护窗口,TTL 又给异常退出留了回收出口。正常路径靠 unpin 释放,异常路径靠过期时间兜底。这样做牺牲了一点状态管理复杂度,换来的是 GC 不误删、后台任务不中途断粮。

这也是 Snapshot 从功能变成系统能力的分界线。

少了这一层,快照就只剩下漂亮接口。

GC 与 pin 保护窗口示意图
GC 与 pin 保护窗口示意图

7. 演进线索:Snapshot 是系统约束,不是单点功能

把这些线索串起来,会发现 Snapshot 在 Milvus 3.0 里不是孤立功能。

创建时,它要处理 PENDING -> COMMITTED 的状态推进。

设计文档描述 SaveSnapshot() 使用类似两阶段提交的流程:先在 catalog 中保存 PENDING snapshot,写 S3 manifest 和 metadata JSON,再把 snapshot state 更新为 COMMITTED,并插入内存缓存。

源码注释也强调,这样做是为了创建原子化,失败时让 GC 能识别 orphan files。

恢复时,它把 collection meta restoration、index restoration、copy segment job、DataNode 文件复制、DataCoord 元数据同步串成后台任务。

CopySegmentTask 的生命周期是 Pending、InProgress、Executing、Completed/Failed、Cleanup。

进入 Completed 或 Failed 后,job 还会设置 cleanup timestamp,并依据 DataCoordCfg.CopySegmentTaskRetention 做保留。

清理时,它又要求 GC 理解 PENDING snapshots、DELETING snapshots,以及 collection 已 drop 后的 orphan snapshots。

后续 commit 里还出现了 collection lifecycle 绑定、pin/unpin、compaction protection 等补丁线索。

这些都说明一个事实:只要允许历史视图存在,整个系统就必须承认历史引用也是活的。

这也是 Snapshot 能和 Storage V3 manifest/payload reference 接上的地方。它没有重新发明存储层,而是把已有 segment/binlog/index 文件与 manifest 引用组织成可恢复的时间点账本。

创建时少复制,恢复时少重写,清理时多一层引用判断。

每一步都在省成本,但每一步也都把一致性压力转移到元数据、manifest 和 GC 保护上。

总结

Milvus 3.0 Snapshot 可以用一句话概括:它用 metadata 和 segment manifest 冻结 collection 的时间点只读视图。

恢复时,再用 Copy Segment 把这份视图恢复成新 collection,并让 GC/pin 保护这段历史引用。

它不是传统 Backup,因为它不复制完整数据副本。

它也不是简单的 API 开关,因为它牵动了 flush 边界、manifest 格式、field/index ID 保留、copy segment job、GC 引用索引和 pin 租约。

我更愿意把 Snapshot 看成 Milvus 3.0 对历史状态管理的一次补课:以前系统更关注当前集合还能不能写、能不能查。

Snapshot 加进来之后,系统还得回答另一个问题:过去某个时间点的集合状态,还能不能被安全地读回来。

这个问题不好答。

Milvus 3.0 给出的答案落在三件事上:冻结引用、保护引用、在恢复时复制必要的 segment 文件。轻量的代价,就是 GC 和后台任务都要懂这本历史账。

说明:本文内容基于 Milvus 3.0 官方源码、本地官方文档镜像、官方设计文档、Release Notes 以及 milvus-io GitHub Issue/Commit 线索分析整理而成。源码分析基于笔者本地仓库版本,未在生产集群中逐项完成全场景恢复验证。文中的配置模板和参数建议仅供参考,实际效果请以你的业务数据和环境测试结果为准。如果你已经在生产环境使用 Snapshot / Restore / Copy Segment,欢迎在评论区分享实际经验。

好啦,谢谢你观看我的文章,如果喜欢可以点赞转发给需要的朋友,我们下一期再见!敬请期待!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 先拆一个误会:Snapshot 不是 Backup
  • 2. 时间点视图从哪里来
  • 3. 为什么创建时不主动 flush
  • 4. manifest 才是这套设计的账本
  • 5. Restore 为什么走 Copy Segment
  • 6. GC 和 pin 保护才是硬骨头
  • 7. 演进线索:Snapshot 是系统约束,不是单点功能
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档