Milvus 3.0 开源解读之Snapshot | 无需复制embedding数据的Milvus Collection视图
生产环境里的 Collection,不会因为你需要一个稳定版本数据就暂停写入。商品在上下架,价格在变化,新文档会不断进入,Agent 也可能持续回写。
但另一边,团队又经常要基于这份数据:测新的 Embedding 模型、验证 Backfill 逻辑、搭压测集群、跑夜间批处理…… 这些任务有个共同要求:输入数据得是固定的,不能跑着跑着数据就变了。
过去,要获得这样一个固定版本的数据,最直接的办法全量拷一份。如果是季度级别的迁移备份,数据要长期留着用,这么做没毛病。
但问题在于,不能模型每更新一次,我们就把同样的操作重来一次。等 Collection 涨到几亿、几十亿条,光数据拷贝吃掉的对象存储和网络 IO,再加索引重建耗的 CPU/GPU 和时间,成本就会成为大问题。
这正是我们推出 Milvus Snapshot 的原因。
简单说,Snapshot 就是 Collection 在某个时间点的只读快照。不用完整复制数据集,Snapshot 只记录那一刻生效的数据、索引和元数据的引用。
在实际业务落地中,我们可以在一些风险较高的变更前创建 Snapshot,之后既可以用它回滚,也可以把同一个版本恢复到另一个集群,还可以将其直接交给批处理任务使用。
原本需要三份数据副本、三条准备链路才能完成的事,现在可以围绕同一个 Snapshot 展开。
以下是关于这次 Snapshot 升级的完整说明。
为什么传统的 Backup 已经不够用?
先聊个问题:Milvus 本来就有 milvus-backup 工具,能做容灾和长期留存,为什么还要搞 Snapshot?
因为真实的 AI 工程里,除了防数据丢失,更多痛点来自频繁迭代带来的状态隔离需求。
| 需求 | 常见做法 | backup 的主要问题 |
|---|---|---|
| 频繁创建恢复点 | 定期执行完整备份 | 数据量越大,复制时间和 I/O 开销越高 |
| 模型或数据版本回滚 | 导回旧数据并重新构建索引 | 回滚链路长,还会重复消耗计算资源 |
| 准备测试和压测环境 | 重新导出、传输并导入生产数据 | 每个环境都要维护一套数据准备链路 |
| 运行长时间 batch job | 先把线上数据复制到离线环境 | 生产仍在写入,任务输入与线上版本容易错位 |
以上这四件事看着不一样,底层需求其实完全相同:数据要一份固定、可追溯,还能被其他集群、其他计算任务直接读取的 Milvus Collection 版本。
以电商搜索为例。
假设 Milvus 中有一个 products Collection,保存商品属性和 Embedding。搜索时,系统会把用户查询的 Embedding 与商品 Embedding 做向量检索;与此同时,商品上下架、价格调整等更新全天都在发生。
但现在,团队准备用一套更好的 Embedding 模型。这意味着整个商品库都要重新生成向量,并替换线上 Collection 中原有的 Embedding。
在这种不好撤销的变更发生之前,团队通常会有三个需求:
- 留一份旧版本,以便出了问题能够回滚;
- 在隔离集群里用同一批商品做负载测试;
- 给 Spark 一份固定的商品集合,让新旧模型真正跑在相同输入上。
没有 Snapshot 时,这三个需求往往对应三份数据副本和三条数据准备链路。
Milvus Snapshot 到底记录了什么?
Snapshot 能做到在不复制数据的情况下,创建Collection 的稳定只读版本的前提在于Milvus 存储本身已有的一个特性:文件写入后不会再被原地修改。
在Milvus 中,Sealed Segment、索引文件和 Delete Log 一旦写入,就保持不变。Compaction 也不会去改旧文件,而是生成新文件,再把旧文件淘汰掉。
有了这个前提,某个时间点的 Collection 状态其实不需要复制数据也能保存下来。
既然文件本身不会变化,那么一个 Collection 在某一时刻是什么状态,就可以由“那一刻仍然有效的文件集合”完整描述。 要保住这个版本,只需记录当时有哪些文件,并保证这些文件在 Snapshot 存续期间不会被回收。
CreateSnapshot 做的正是这件事。
它会在不复制向量数据库的情况下,直接记录这次操作对应的数据边界、文件引用,以及 Collection 的元数据,包括 Schema、Partition、Properties 和索引信息。每个 Segment 会生成一份 Apache Avro manifest,Snapshot 自身的元数据则以 JSON 保存。
而基于同一个只读版本,Milvus 3.0 提供了四种 Snapshot 能力:
| 能力 | 执行方式 | 用户得到什么 |
|---|---|---|
| 轻量快照 | CreateSnapshot 记录数据、索引和元数据引用,不复制已有数据文件 | 可以更频繁地创建恢复点,不必每次完整搬走 collection |
| 快速恢复 | 恢复时复制快照引用的数据和索引文件 | 避开完整数据导入和索引重建,缩短恢复链路 |
| 跨集群流转 | 目标集群从 snapshot metadata URI 恢复;存储不可直达时再按需导出 | 同一份数据版本可以用于迁移、压测和隔离环境复制 |
| 稳定视图 | External Collection 和 Spark connector 读取固定版本 | Batch job 运行期间,生产 collection 可以继续接收写入 |
(图注:四项能力共享同一份 Snapshot,但Snapshot 不必遵循“创建 → 导出 → 恢复 → 查询”的固定流程。创建快照之后,同一个数据版本可以直接被多个系统消费, 可以在原集群恢复,也可以交给另一个集群、External Collection 或 Spark。怎么组合取决于实际任务,甚至可以只创建 Snapshot 暂时不用。但无论采用哪条路径,消费者拿到的都是同一组文件引用,因此不用再为每个任务单独准备一份输入数据。)
轻量快照:创建成本只取决于文件数量,不在于数据总量
完整备份通常要搬运整个 collection 完整的数据。
Snapshot 则主要记录本次创建操作对应的数据边界、文件引用和 collection 元数据,形成一份只读版本,不需要复制已有的数据文件和索引文件。
因此它的开销取决于 Segment、索引文件和 manifest 条目的数量,以及对象存储的请求延迟,而不是 Collection 一共有多少的数据。
时间成本上,CreateSnapshot 通常可以在毫秒级完成。不过,这个数字会随着 Segment 数量和对象存储请求延迟发生变化,不适合作为固定指标来做容量规划。
另外,CreateSnapshot 本身不会触发 Flush。如果刚写入的数据必须包含在这个版本里,就应该先执行 Flush,并等待 Flush 完成后再创建 Snapshot。
原因和前面的机制是一致的:Growing Segment 还没有真正落成文件,也就不存在可以写进 manifest 的文件引用。因此,创建 Snapshot 时仍处于 Growing 状态的数据,默认不会进入这次 Snapshot 的数据边界。
如果并不要求包含最新写入数据,例如只是创建一个定时恢复点,那么不 Flush 也完全合理。
这里的关键不是“一定要不要 Flush”,而是你要清楚这次 Snapshot 的边界是什么。
flushTask, err := client.Flush(ctx,
milvusclient.NewFlushOption("products"))
if err != nil {
return err
}
if err := flushTask.Await(ctx); err != nil {
return err
}
err = client.CreateSnapshot(ctx,
milvusclient.NewCreateSnapshotOption(
"before_model_v2",
"products",
).WithDescription("Recovery point before model v2"))
依旧是前文在商品推荐的例子,创建完成后,before_model_v2 对应的就是一组固定文件:准确地说,是任何人开始修改 Embedding 之前,那一刻的完整商品目录。之后,回滚、隔离集群压测和 Spark 评测都可以围绕这一份 Snapshot 展开。
不过值得一提的是,轻量创建不等于完全没有存储成本。 Milvus 只有在某个 Segment 或索引文件不再被任何对象引用时,才能把它回收。而Snapshot 本身就是一份引用,因此,只要 Snapshot 还存在,它记录的文件就不能被回收。
与此同时,线上 Collection 仍然会继续运行:新的数据不断 Flush 成 Segment,Compaction 会把旧 Segment 合并成新文件,Delete 也会持续产生变化。
于是,随着时间推移,Snapshot 所引用的文件集合会和线上 Collection 当前使用的文件集合逐渐分叉。
这部分“分叉出来、但又不能删”的数据,才是 Snapshot 真正的存储成本。
如果一个 Collection 更新非常频繁,而某个 Snapshot 又保留得足够久,它最终占用的额外空间可能接近一份完整副本。在某些情况下,一个 Snapshot 甚至可能让对象存储占用接近翻倍。
因此,Snapshot 带来的主要运维问题并不是“怎么创建”,而是“留多久”。
命名时最好直接写清用途,例如:before_model_v2;而不是
snapshot_3。再配合 ListSnapshots 和 DropSnapshot,及时清理已经不再值得继续占用存储空间的 Snapshot。
快速恢复:已经建好的索引,不必再算一遍
传统恢复中成本最高的通常是两件事:重新导入数据,以及重新构建索引。
对于大规模 Collection,尤其是在索引构建成本较高的情况下,这两个阶段都会持续占用 CPU、网络和对象存储 I/O。
RestoreSnapshot 会直接复制 Snapshot 引用的数据文件和索引文件,再围绕这些文件恢复 Collection 元数据。索引文件来自已有构建结果,不需要再次计算。
(要注意,恢复仍会在对象存储中创建新的数据副本,因此它不是零拷贝;省下的是重新解析数据、重新编码和重建索引的计算过程。)
jobID, err := client.RestoreSnapshot(ctx,
milvusclient.NewRestoreSnapshotOption(
"before_model_v2",
"products",
"products_rollback",
))
在设计中,恢复任务可以异步执行,而且不会破坏原 Collection。
调用接口后会先返回 Job ID,恢复任务在后台继续运行。应用不需要一直阻塞等待,而是通过 GetRestoreSnapshotState 查询进度;任务完成后,再加载新的 Collection。
另外,恢复的目标也是一个新的 Collection。
上面的例子恢复到的是 products_rollback,而不是原来的 products。线上 Collection 不会被覆盖。
这意味着团队可以先检查恢复结果,与当前线上版本做比较,确认没有问题后再决定要不要切流量。
毕竟,能先验证再决定是否回滚,和一开始就覆盖现有状态,在实际生产操作里完全是两回事。
跨集群流转:不用搬数据,目标集群即可访问到数据
很多跨集群方案一开始就假设:要把 collection 放到另一个集群,必须先把全部数据导出去。但把一个固定版本流转到另一个集群时,核心问题其实不是“怎么把数据运过去”,而是:目标集群能不能访问这些数据原本所在的位置。
借助Snapshot ,全量数据迁移操作彻底成为历史。只要有访问权限和可达的网络路径,数据就不需要先搬一次。
RestoreExternalSnapshot 可以直接接收 Snapshot metadata URI。只要目标集群能够访问这份 metadata 以及它引用的数据和索引文件,就可以创建新的 collection。
snapshot, err := sourceClient.DescribeSnapshot(关键不是搬数据,而是让目标集群访问到数据,
milvusclient.NewDescribeSnapshotOption(
"before_model_v2",
"products",
))
if err != nil {
return err
}
jobID, err := targetClient.RestoreExternalSnapshot(ctx,
milvusclient.NewRestoreExternalSnapshotOption(
"products_staging",
snapshot.GetS3Location(),
))
如果目标集群本来就能访问源 Snapshot 所在的存储,到这里整个流程就已经结束了。
但如果当两个集群之间存在 bucket、根路径或访问权限隔离时,我们就需要 ExportSnapshot:它会将 Snapshot 导出为一份自包含的数据包,并存放到目标集群可访问的位置。
因此,Export 更适合作为存储不可直达时的兜底路径,而不是跨集群恢复的默认前置步骤。如果不分场景一律优先执行 Export,相当于在大量本无需搬运数据的流程中,平白增加了一次全量数据复制开销。
此外,Export 底层依赖对象存储服务原生的服务端拷贝(Server-side Copy)能力,数据全程无需经过 Milvus 集群中转。但如果服务端拷贝本身不可用 —— 比如跨不同对象存储服务商,或是两个存储端点之间无法直接寻址互通 ——Export 操作会直接终止,而非让全量数据集绕行 Milvus 完成中转。这类场景下,需要使用自有存储工具完成跨地域、跨云厂商的对象数据复制,再从复制完成后的目标地址执行 Restore 操作。
在这套机制的边界内,同一份快照可同时供多个目标集群复用,源 Collection 也能持续正常提供服务。无论是生产数据同步至测试环境、多集群数据分发、迁移演练,还是隔离环境验证,都无需再各自维护独立的数据准备流水线。
稳定视图:只想读数据,就不必先 Restore
前面几条路径最终都会创建一个新的 Collection。
但如果任务本身只需要读数据,恢复出一个完整的新 Collection 有时候反而是多此一举。
Snapshot 已经描述了一组固定在对象存储里的文件。只要消费者能够理解这份描述,就可以直接读取这些文件。
整个过程中,生产 Collection 仍然可以继续接收写入。
| 消费方式 | 怎么使用 | 适合的任务 |
|---|---|---|
| Milvus External Collection | StorageV3 内表快照作为 milvus-table 外部源,通过外表直接共享和查询数据 | 历史版本检索、回归验证、审计分析 |
| Spark connector | 读取 snapshot metadata URI 及其引用文件,形成固定的 batch 输入 | A/B 评测、去重、聚类、质量检查和字段回填 |
目前,在Milvus中,只读数据主要有两条路径:
一种是 Milvus External Collection:把 StorageV3 内表的快照当成 milvus-table 外部源,通过外表直接查数据,适合历史版本检索、回归验证、审计分析这类场景。
在 Milvus 中基于 Snapshot 创建 External Collection,需要提供三类信息。
external_source 指向 snapshot metadata URI,external_spec 指定 milvus-table 格式和存储访问配置,每个字段则通过 external_field 映射到源快照中的字段。
External Collection 创建完成后,先执行 Refresh,再根据实际查询需求创建索引并 Load。
另一种是 Spark Connector:可以保证一次批处理任务从头到尾,读的都是同一批数据。
基于Snapshot 启动的 Spark 任务,不会跟着线上 Collection 的更新往前追。从任务开始到结束,它读的永远是快照定义的那组固定数据 —— 哪怕这几个小时里 products 又灌了几十万条新数据,这次任务的输入也不会有任何变化。
text
Snapshot metadata URI
-> Spark connector
-> DataFrame / batch job
-> A/B evaluation, deduplication, or quality checks
-> data lake, reports, or optional Milvus backfill
还是用模型升级的例子。如果两个模型比较时,使用的数据一直在变,那其实就谈不上真正的同条件比较。
有了 before_model_v2 以后,Batch Job 可以读取 Snapshot 边界内那批商品的 ID、标题、类目和旧模型 Embedding,再为同一批商品生成新 Embedding,最后输出 A/B 评测结果、重复商品分组和异常记录。
下周重新跑一次,读到的仍然是同一批商品。
计算结果则可以继续留在 Data Lake 或报表系统中。
如果还需要更新 Milvus 里的字段,再通过 Backfill 接口显式执行回填。过程中,读取 Snapshot 本身不会修改源 Collection。
另外,这条链路目前还有三个需要注意的边界:
- 基于 Snapshot 的 External Collection 是只读数据源。高频在线 Insert 和 Delete 仍然应该放在普通 Collection 上。
milvus-table目前支持把 StorageV3(Loon)内表 Snapshot 作为 External Source;External Table 的 Snapshot 不能继续串联,作为新的milvus-tableSource。- 长时间运行的任务应该使用
PinSnapshotData固定 Snapshot 引用的数据,避免 Retention Policy 在任务尚未完成时回收文件;任务结束后再释放 Pin。
尾声 Snapshot 的价值和边界
Snapshot 的价值不是简单的备份快了多少倍,而是从根本上改变了执行路径。
| 阶段 | 传统链路需要做什么 | Snapshot 改变了什么 | 仍然存在的成本 |
|---|---|---|---|
| 创建恢复点 | 复制 collection 数据 | 只写元数据、文件清单和文件引用 | 对象存储请求、元数据处理和旧文件保留 |
| 恢复 collection | 导入数据并重新构建索引 | 复制已有数据和索引文件 | 数据副本、对象存储复制和网络吞吐 |
| 运行 batch job | 先复制线上数据,再启动计算 | 直接读取固定的 metadata 和文件集合 | 数据扫描和 Spark 计算资源 |
至于最终能快多少,还是要看具体环境。毕竟Segment 数量、索引规模、对象存储性能、网络状况以及任务并发度,都会影响实际耗时。
但可以确定的是,Snapshot 非常适合以下场景:
- 模型或数据版本上线前创建恢复点,需要快速验证,也希望出了问题能够尽快回滚。
- 把生产数据复制到测试、压测、灰度或其他隔离集群。
- 给 Spark、评测平台或数据治理任务提供固定输入。
- 通过 External Collection 查询历史版本,用于回归验证或审计分析。
但 Snapshot 不是独立备份的替代品。Snapshot 引用的仍然是线上 Collection 自己的文件。它和生产数据使用同一个 Bucket、同一套 Credentials、同一种存储服务,也处在同一个垃圾回收体系之下。
换句话说,Snapshot 是生产数据的一份视图,不是一份独立副本。如果底层存储整体丢失,那么存放在其中的 Snapshot 也会和原 Collection 一起丢失。
因此,Snapshot 和 Backup 的边界在于:
- Snapshot 解决逻辑故障。例如模型上线后效果退化、Backfill 写错了数据,或者一次迁移只执行了一半。
- Backup 解决物理故障。它需要在独立故障域保存真正独立的数据副本,用于长期留存、合规以及底层存储整体丢失等情况。此时仍然应该使用 Milvus Backup 或其他独立备份方案。
Snapshot 也不是一套连续的版本历史。
只有在你显式创建 Snapshot 的时间点,才会留下对应版本;创建之后,它就是不可变的。
保存一个时间点版本也仍然是一个主动动作。Snapshot 真正解决的问题,其实是让这个动作便宜到可以更频繁地做。
另外,使用中需要注意
- 创建快照不会主动 flush,growing 数据默认不在快照中。
- 直接进行跨集群 Restore 时,目标集群必须能够访问 Snapshot metadata URI 及其引用文件,包括具备相应的存储 Credentials 和网络链接。
ExportSnapshot不会通过 Milvus 中转数据。因此跨 Region 或跨云厂商时,仍然需要在存储层执行普通对象复制:先用自己的存储工具把对象复制过去,再从新的位置 Restore。milvus-table目前只支持 StorageV3 内表快照作为外部源,外表快照不能继续作为新的milvus-table外表源。- 长时间运行的 batch job 可以使用
PinSnapshotData保护引用。任务结束后应解除 pin,并根据保留策略执行DropSnapshot。
当前,Snapshot 创建、同集群恢复和稳定视图目前已经可以在 Milvus 3.0 中使用。
跨集群 Restore 和 Export 也即将推出。
Snapshot 只是 Milvus 3.0 这次大版本更新的一部分。与此同时,Milvus 3.0 还带来了 External Collection、Spark Connector,以及支撑这些能力的全新 StorageV3 存储引擎。
如果本文提到的这些工作负载正好也是你需要解决的问题,这几项能力不妨一试。






