
🚩 2026 年「术哥无界」系列实战文档 X 篇原创计划 第 170 篇,Milvus 最佳实战「2026」系列第 20 篇
大家好,欢迎来到 术哥无界 | ShugeX | 运维有术。
我是术哥,一名专注于 AI 编程、AI 智能体、Agent Skills、MCP、云原生、AIOps、Milvus 向量数据库的技术实践者与开源布道者!
Talk is cheap, let's explore。无界探索,有术而行。

向量数据库里有个绕不开的问题:线上集合一直在写入、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 保护 这三件事上:它们必须同时成立。
官方文档把 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 的 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 能不能跑通,要看这本账是否完整,也要看账上引用的文件是否还活着。
官方管理文档建议创建 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 之间留出一段受保护的过渡区。
Milvus 3.0 Snapshot 的存储分成两层:etcd 保存 SnapshotInfo 这类基础信息,方便 list/query。
对象存储保存完整 collection description、index info、segment description。源码里的布局也很直观:
snapshots/{collection_id}/
├── metadata/
│ └── {snapshot_id}.json
└── manifests/
└── {snapshot_id}/
└── {segment_id}.avroSnapshotWriter.Save() 的顺序是先写每个 segment 的 Avro manifest,再收集 StorageV2 manifest 路径,之后写 metadata JSON。
metadata 里包含 SnapshotInfo、Collection、Indexes、ManifestList、Storagev2ManifestList、SegmentIds、BuildIds 等信息。
Avro manifest 是 segment 级别的事实载体。当前源码里 SnapshotFormatVersion = 4,并兼容读取 0/1/2/3/4 版本。
V4 entry 里可以看到 SegmentID、PartitionID、BinlogFiles、DeltalogFiles、StatslogFiles、IndexFiles、TextIndexFiles、JSONKeyIndexFiles 等字段。
它还记录 StartPosition、DmlPosition、StorageVersion、IsSorted、CommitTimestamp。
这个版本线索很有意思。
V2 加入 index_store_path_version,V3 加入 commit_timestamp,V4 加入 child_fields 和 format。这说明 Snapshot 不是薄薄一层 API。它已经变成持久化格式,要考虑历史版本可读、字段演进和未来恢复兼容。
这里还有一个容易被忽略的工程细节:metadata JSON 和 segment manifest 分别落在对象存储的不同路径里,etcd 里又有一份用于快速查询的 SnapshotInfo。
这天然存在不一致窗口。比如 manifest 写了一半失败,或者 S3 写完了但 etcd 状态还没更新。
设计文档里把 SaveSnapshot() 写成类似两阶段提交:先记录 PENDING 状态,再写对象存储,之后把状态推进到 COMMITTED。
这个做法不花哨,但很实用。失败时,GC 至少知道这批文件属于哪个未完成 snapshot,而不是把它们当成来路不明的孤儿文件乱删。
Snapshot 的轻量不是随便写几条元数据。
它要用状态机把 etcd 和对象存储之间的灰区圈出来,让后续清理有账可查。

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 里有 IdMappings、SnapshotName、SourceCollectionId、PinId、TotalSegments、CopiedSegments、TotalRows 等字段。
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 追踪进度。

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 中预计算的引用集合。源码注释提到 IsSegmentGCBlocked 和 IsBuildIDGCBlocked 是 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 从功能变成系统能力的分界线。
少了这一层,快照就只剩下漂亮接口。

把这些线索串起来,会发现 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 删除。