docs/design-docs/design_docs/20260609-external-snapshot-export-restore.md
This document is the single design record for external snapshot restore, cross-bucket snapshot export/restore, and root relocation of exported snapshot bundles.
Milvus snapshots were originally scoped to one cluster and one object-storage
bucket. RestoreSnapshot restores a snapshot that already exists in the target
cluster metadata. ExportSnapshot writes a self-contained bundle, but the copy
path and path validation were still built around one configured bucket.
The feature has four goals:
ExportSnapshot and RestoreExternalSnapshot across buckets when the
object-storage provider can perform provider-side copy.The feature has explicit non-goals:
<root>/snapshots/{collectionID}/metadata/{snapshotID}.json.Same-bucket external restore:
RestoreExternalSnapshot with the exported metadata
URI.Manual bundle relocation:
export-root/snapshots/100/metadata/1.json and export-root/files/....restored/x/snapshots/100/metadata/1.json and restored/x/files/....oldRoot from the
export-time metadata and newRoot from the restore-time metadata URI, then
rebases self-contained paths from oldRoot to newRoot.Export to a foreign bucket:
ExportSnapshot with a target_s3_path in a foreign
bucket.external_spec.Restore from a foreign bucket:
RestoreExternalSnapshot with a foreign metadata URI.Unsupported arbitrary layout:
restored/x/meta.json
restored/x/metadata/1.json
These paths do not contain the snapshots anchor. Without adding a new request
parameter or persisting a bundle-root field in the metadata, Milvus cannot infer
whether the root is restored, restored/x, or another ancestor. The request
must fail closed.
The public request carrier for foreign storage information is only
external_spec.
RestoreExternalSnapshotRequest contains:
db_name: database routing and namespace context.target_collection_name: collection created by the restore job.snapshot_metadata_uri: metadata file URI or object key.external_spec: optional JSON storage spec for the foreign source.RestoreExternalSnapshotResponse.job_id is the asynchronous restore job ID. The
caller uses it with GetRestoreSnapshotState.
ExportSnapshotRequest contains:
db_name: database routing and namespace context.collection_name: local source collection.snapshot_name: local snapshot to export.target_s3_path: destination root for the self-contained bundle.external_spec: optional JSON storage spec for the foreign target.ExportSnapshotResponse.snapshot_metadata_uri is the metadata URI of the
exported bundle.
The final API does not include foreign_storage_spec,
foreign_credential_ref, or external_credential_ref. Splitting storage config
and credential reference would create two credential models for one provider
copy request, so the API keeps one external_spec field aligned with external
table extfs shape and snapshot-specific validation.
The Go SDK exposes:
ExportSnapshot(ctx, NewExportSnapshotOption(...).WithExternalSpec(...))RestoreExternalSnapshot(ctx, NewRestoreExternalSnapshotOption(...).WithExternalSpec(...))GetRestoreSnapshotState(ctx, NewGetRestoreSnapshotStateOption(jobID))WithExternalSpec is optional. Empty external_spec means Layer 1 instance
credential resolution.
REST exposes:
POST /v2/vectordb/jobs/snapshot/export
POST /v2/vectordb/jobs/snapshot/restore_external
POST /v2/vectordb/jobs/snapshot/describe
POST /v2/vectordb/jobs/snapshot/list
REST uses camelCase request fields. The JSON field is externalSpec, and the
handler forwards it to the gRPC external_spec field.
Go SDK export:
metadataURI, err := client.ExportSnapshot(
ctx,
milvusclient.NewExportSnapshotOption(
"snapshot_20260608",
"source_collection",
"s3://foreign-bucket/export-root",
).WithExternalSpec(`{"extfs":{"cloud_provider":"aws","region":"us-west-2","use_iam":"true"}}`),
)
Go SDK external restore:
jobID, err := client.RestoreExternalSnapshot(
ctx,
milvusclient.NewRestoreExternalSnapshotOption(
"restored_collection",
"s3://foreign-bucket/export-root/snapshots/100/metadata/1.json",
).WithExternalSpec(`{"extfs":{"cloud_provider":"aws","region":"us-west-2","use_iam":"true"}}`),
)
Go SDK restore status:
info, err := client.GetRestoreSnapshotState(
ctx,
milvusclient.NewGetRestoreSnapshotStateOption(jobID),
)
REST export:
curl -X POST "$MILVUS_ADDR/v2/vectordb/jobs/snapshot/export" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"dbName": "default",
"collectionName": "source_collection",
"snapshotName": "snapshot_20260608",
"targetS3Path": "s3://foreign-bucket/export-root",
"externalSpec": "{\"extfs\":{\"cloud_provider\":\"aws\",\"region\":\"us-west-2\",\"use_iam\":\"true\"}}"
}'
REST external restore:
curl -X POST "$MILVUS_ADDR/v2/vectordb/jobs/snapshot/restore_external" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"dbName": "default",
"targetCollectionName": "restored_collection",
"snapshotMetadataURI": "s3://foreign-bucket/export-root/snapshots/100/metadata/1.json",
"externalSpec": "{\"extfs\":{\"cloud_provider\":\"aws\",\"region\":\"us-west-2\",\"use_iam\":\"true\"}}"
}'
REST restore status:
curl -X POST "$MILVUS_ADDR/v2/vectordb/jobs/snapshot/describe" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"jobId":"12345"}'
db_nameRestoreExternalSnapshot and ExportSnapshot are Global RBAC operations. The
source collection for external restore belongs to another cluster, and the
target collection may not exist when the request enters Proxy. Authorization
must therefore check a global privilege instead of treating either collection
name as the permission object.
db_name remains in both requests because the database interceptor and
namespace routing still need database context. It is not the RBAC object.
Snapshot storage credentials must align with Milvus instance storage config. The API does not add a generic credential abstraction.
Layer 1: instance credential plus bucket policy.
external_spec uses the Milvus instance object-storage credential.Layer 2: request external_spec.extfs.
use_iam, access key ID/value,
GCP service-account JSON for native GCS, or Azure account key fields when
those fields map to the same config structs Milvus already uses.role_arn, gcp_target_service_account, SAS, anonymous auth,
source-auth URLs, and independent dual credentials.Provider notes:
use_iam path. Runtime AWS role mechanisms may be provided by the
environment, but the snapshot request must not contain a generic role_arn.Restore persistence red line:
RestoreExternalSnapshot must propagate the source storage config from Proxy to
DataCoord, through WAL/meta, restore job state, copy segment job state, and
DataNode task execution. Raw secrets inside external_spec are therefore
persisted through WAL/meta/job/task state. Operators should prefer Layer 1 or
ambient identity fields such as use_iam=true. Logs and errors must use
redacted specs.
Exported self-contained bundles use this layout:
<root>/snapshots/{collectionID}/metadata/{snapshotID}.json
<root>/snapshots/{collectionID}/manifests/...
<root>/files/...
The snapshots directory is the root anchor. Restore derives:
oldRoot from the export-time metadata path stored in snapshot metadata.newRoot from the restore-time snapshot_metadata_uri.When the layout is self-contained and oldRoot != newRoot, restore rebases
paths from oldRoot to newRoot. This safely supports:
old:
export-root/snapshots/100/metadata/1.json
export-root/files/...
new:
restored/x/snapshots/100/metadata/1.json
restored/x/files/...
Root relocation is two-stage:
oldRoot.The design intentionally rejects metadata URIs that do not contain the
snapshots/.../metadata/... structure. Supporting arbitrary layouts would
require a new request parameter or a new persisted bundle-root field, and this
feature explicitly avoids both.
Cross-bucket copy is a provider-side copy capability. There is no streaming fallback.
The core invariant is:
There must exist one provider-side copy request whose credential can read the source object and write the destination object.
For restore, the destination is the local bucket. The copy credential must read the foreign source and write the local target. For export, the destination is the foreign bucket. The copy credential must read the local source and write the foreign target.
Provider limitations fail closed:
Metadata reads/writes and large object copy can use different helper objects, but the large object move itself must be one provider-side copy request.
Proxy:
db_name through the database interceptor.external_spec without logging raw secrets.DataCoord:
DataNode:
snapshotstorage:
external_spec.Internal proto and WAL propagation:
external_spec for export and external
restore.Data flow:
ExportSnapshot:
Proxy -> DataCoord -> local snapshot metadata -> provider-side copies ->
foreign target bundle -> snapshot_metadata_uri
RestoreExternalSnapshot:
Proxy -> DataCoord -> foreign metadata/manifests -> WAL restore message ->
copy segment job -> DataNode -> provider-side copies into local bucket
Path validation:
snapshots/.../metadata/....newRoot.Endpoint/provider compatibility validation:
Access probing:
Secret handling:
external_spec in logs and errors.Fail-closed behavior:
API contract tests:
external_spec.RestoreExternalSnapshot and ExportSnapshot use Global RBAC.db_name is filled by the database interceptor and is not treated as the RBAC
object.externalSpec is forwarded, and describe/list snapshot job routes map to
restore job state APIs.Resolver and validator tests:
external_spec resolves Layer 1 instance credential.external_spec.extfs resolves allowed storage-config-compatible fields.role_arn, gcp_target_service_account, SAS, anonymous auth, and dual
credentials are rejected.Root relocation tests:
oldRoot -> newRoot rebase updates metadata manifest paths before reads.snapshots/.../metadata/... anchor is rejected.Restore tests:
external_spec.Export tests:
Standalone client build:
client/ module both build against the published
milvus-proto version that contains the snapshot APIs.