对象存储数据迁移(R2 → MinIO / 跨厂商搬迁)
对象存储之间迁移数据(如 Cloudflare R2 → MinIO),主流做法是用 S3 兼容工具(rclone、aws s3 sync、mc mirror)直接对拷,无需写业务代码。 本文以 rclone 为主线,覆盖迁移流程、配置要点与客户端兼容性注意事项。 各厂商差异背景见 供应商对比(Vendors)。
为什么"搬迁"是 S3 兼容的生态红利
- R2 / MinIO / AWS 都走 S3 兼容 API:源与目标都是 S3 系时,用同一个工具、 同一套命令即可对拷,数据层面零转换
- 迁移的本质 = 在两端各配一个"remote",然后 copy / sync 对象(key、数据、 元数据)
- 对比:OSS 走原生 API,跨厂商搬迁要先用工具导出成标准格式,或依赖云厂商 自己的迁移服务,麻烦得多;Supabase 虽有 S3 协议但能力是子集,能直接对拷 但受限(详见下文「Supabase 特例」)
工具选型
| 工具 | 适用场景 | 说明 |
|---|---|---|
| rclone | 通用跨厂商(首选) | 支持几乎所有对象存储后端,配置抽象统一,copy / sync / check 一套走完 |
aws s3 sync | S3 兼容之间 | AWS 官方 CLI,覆盖 endpoint 即可用于 R2 / MinIO |
mc mirror | MinIO ↔ 其他 S3 兼容 | MinIO 客户端,语法简单 |
| 云迁移服务 | 同云大规模迁移(如 AWS→AWS) | AWS DataSync、云厂商自带迁移工具;跨厂商一般不如 rclone 灵活 |
rclone 迁移流程
1. 安装并配置两个 remote
rclone config 交互式创建,或直接写 ~/.rclone.conf:
# 源:Cloudflare R2(S3 兼容)
[r2]
type = s3
provider = Cloudflare
access_key_id = <R2_ACCESS_KEY_ID>
secret_access_key = <R2_SECRET_ACCESS_KEY>
endpoint = https://<ACCOUNT_ID>.r2.cloudflarestorage.com
region = auto
# 目标:MinIO(S3 兼容,本地/自托管)
[minio]
type = s3
provider = Minio
access_key_id = <MINIO_ROOT_USER>
secret_access_key = <MINIO_ROOT_PASSWORD>
endpoint = http://127.0.0.1:9000
force_path_style = true
要点:
- provider 字段:
Cloudflare/Minio等预设值让 rclone 自动套用各家的 默认行为(签名、endpoint 风格) - R2 的 region 传
auto(被忽略);endpoint 由 Account ID 派生 - MinIO 必须
force_path_style = true(本地无虚拟主机 DNS) - 凭证放本机 rclone 配置,不要写进任何仓库文件
2. 先 dry-run,再正式迁移
# 预览将复制的对象(不实际传输)
rclone copy r2:my-bucket minio:my-bucket --dry-run
# 正式全量复制(同名覆盖,不删除目标多余对象 —— 安全)
rclone copy r2:my-bucket minio:my-bucket --progress
# 校验:默认对比大小与哈希(严格);--size-only 仅比大小(快但不严格)
rclone check r2:my-bucket minio:my-bucket
copy vs sync vs move
| 命令 | 行为 | 风险 |
|---|---|---|
copy | 单向复制,不删除目标多余对象 | 低(推荐起步) |
sync | 目标与源对齐,删除目标中源没有的对象 | 高(先 dry-run) |
move | 复制后删除源对象 | 高(源被清空) |
第一次迁移永远用
copy;确认无误、且确实需要目标与源完全一致时,再考虑sync。切流量后的增量补齐用sync最合适。
sync 详解:让目标对齐 + 增量同步的原理
rclone sync src:path dst:path 的官方语义是把源同步到目标,只修改目标一端 (changing the destination only):
- 数据流仍是单向(只从源读、往目标写),但目标与源完全对齐:源有目标没 有的 → 复制;两边都有但判定不同(size / modtime / hash)的 → 覆盖;目标有源没有的 → 删除
- 因为会删目标端多余对象,是破坏性命令(可能丢数据),务必先
--dry-run或--interactive预览确认;且出现任何错误时 rclone 不会执行删除 - 被 filter 排除的文件默认不会被删(除非加
--delete-excluded) - 不想删目标多余文件就用
copy,它是 sync 的非破坏版本
为什么 sync 能"增量"同步:它并不是盲目全量重传,而是先对账、再只动 差异:
- 列清单:先通过两端后端的 List API(S3 系即 ListObjects)拿到源、目标 各自的对象清单:key + 大小 + 修改时间(+ 校验和)
- 算差异:在本地内存里对比两份清单,把每个对象归入四类 —— 仅源有 (新增)、两边都有但 size / modtime(或 hash)不同(变更)、仅目标有 (多余)、完全相同(跳过)
- 只动差异:仅对"新增 / 变更"发起实际数据传输,对"多余"执行删除, 完全相同的对象零数据传输、直接跳过
- 幂等可重跑:第二次运行同一命令时,上次已同步的对象会被识别为"相同" 而直接跳过,只补新产生的增量 —— 所以增量同步不需要任何"断点 / 状态记 录",每次都是从两端当前现状对账得出差异,天然支持"切流量后用 sync 补 齐窗口期增量"
"相同"怎么判断(决定哪些对象被跳过,即增量同步的依据):
| 判断依据 | 说明 |
|---|---|
| 默认:大小 + 修改时间 | 两端 size 与 modtime 一致(容差 --modify-window,默认 1ns)即视为相同 |
--checksum(-c) | 改为按 大小 + 校验和 判断(S3 系小对象 ETag=MD5 可用;multipart 大对象不是) |
--size-only | 只比大小,快但不严格 |
--update(-u) | 目标端 mtime 比源新的文件跳过,适合目标有更新、不能回滚的场景 |
--ignore-times(-I) | 不跳过任何"看似相同"的对象,无条件全量传输 |
相关安全参数:--check-first 先完成全部对账再开始传输 / 删除(缩小风险 窗口)、--max-delete 限制单次最多删除对象数、--backup-dir 把被覆盖 / 删除的对象先移入备份目录,作为 sync 的"后悔药"。
S3 系场景的细节:modtime 比较依赖 rclone 上传时写入的
x-amz-meta-mtime元数据(浮点秒、1ns 精度),该元数据不在 List 响应里,默认每个对象要多发 一次 HEAD 请求才能读到(对象多时是笔不小的开销)。若对象由第三方工具上传、 没有该元数据,或想省掉 HEAD 开销,可用--use-server-modtime改用服务端 Last-Modified(上传时间)比较,或改--checksum按校验和判定。
注意事项
1. Client 兼容性(迁移后客户端如何跟着切)
迁移不只是数据搬走,客户端指向也要换。S3 兼容客户端 (@aws-sdk/client-s3、aws cli、mc、rclone)切换只涉及 4 个配置项, 业务代码零改动:
| 配置项 | R2(源) | MinIO(目标) |
|---|---|---|
endpoint | https://<ACCOUNT_ID>.r2.cloudflarestorage.com | http://<minio-host>:9000 |
| credentials | R2 API Token | MinIO root / 自定义用户 |
region | auto(忽略) | us-east-1(默认) |
forcePathStyle | false(默认 virtual-hosted) | true |
排查清单:
- endpoint 是否硬编码:代码 / CI / 前端里若写死了
xxx.r2.cloudflarestorage.com,统一改成环境变量注入 - forcePathStyle:本地 MinIO 必须
true;云服务(R2 / AWS)用默认 virtual-hosted —— 同一份代码两套值,配置项要可覆盖 - 签名 URL:迁移后原签名 URL 全部失效(域名、密钥、签名上下文都变), 必须用新客户端重新签发;已下发给客户端 / 缓存 / CDN 的旧 URL 要清理
- CORS:若原本给 R2 bucket 配了 CORS(浏览器直传 / 直读),MinIO 的 bucket 也要配,否则前端直传报跨域错误
- 非 S3 系客户端:若原业务用的是
ali-oss/@supabase/supabase-js等原生 SDK,迁到 S3 兼容服务需要重写客户端(方法名、返回结构不同) - 本地开发环境:切换后开发环境的 endpoint 配置也要同步(如
.env/.env.example),避免本地还指向旧服务
2. 数据层面的坑
- 元数据 / Content-Type:rclone 的元数据复制默认关闭(需
--metadata/-M开启;开启后个别后端字段仍可能丢失,如自定义 x-amz-meta-*、存储类型);迁移后抽查关键对象的 Content-Type 与元数据 - 校验方式:
rclone check可用大小(--size-only)或校验和;S3 系的 ETag 对小对象等于 MD5,大对象(multipart)不是 —— 追求严格校验时用 rclone 自己计算的哈希,不要只看 ETag - 大文件:rclone 默认自动分片(multipart),S3 兼容两端通常无兼容问题; 注意设置合理的
--transfers/--checkers并发控制带宽 - 版本与删除标记:源若开启版本控制,rclone 默认不迁移历史版本(列目录时 用
--s3-versions才看得到);要迁移历史版本需显式开启,且目标也要开启 版本控制才能保留(否则多版本同 key 互相覆盖) - 存储类型 / 生命周期:冷热分层(如 Glacier / 归档)迁移后通常退化为 标准存储,存储费用与访问行为会变;对象存储本身的生命周期规则不会跟着 对象走,需要在新端重建
- ACL / 权限:rclone 默认不复制桶级 ACL / Policy,目标 bucket 的权限 体系(用户、Policy、公开/私有)要单独重建
- 大小写与命名:S3 系 key 大小写敏感;从 OSS(大小写不敏感)迁出时, 同名不同大小写的 key 可能冲突,需先清理
3. 网络与成本
- 出口流量:从云上迁出会吃出口流量费(R2 免费、多数云计费);从 本地 MinIO 迁到云端则是上行,通常免费
- 带宽瓶颈:本地 → 云端(或反向)的迁移速度受上传/下载带宽限制;大量 小对象时请求数(QPS)也可能成为瓶颈
- 迁移窗口:全量复制期间源端可能持续有新写入 —— 正确节奏是先全量
copy,切流量后再sync补齐窗口期增量
4. 推荐的迁移节奏(双跑 → 切换 → 回滚窗口)
flowchart LR
A["① 全量 copy
(dry-run 先行)"] --> B["② rclone check 校验"]
B --> C["③ 客户端改配置
指向新端(双写/只读验证)"]
C --> D["④ 增量 sync 补齐
窗口期数据"]
D --> E["⑤ 切流量
(DNS / 配置 / 环境变量)"]
E --> F["⑥ 保留源一段时间
(回滚窗口)"]
- 迁移期间建议双写或只读窗口:让新客户端先读 MinIO 验证,写入仍走源 或两边都写,降低一次性切换的风险
- 切流量后不要立刻删源桶,保留一段回滚窗口(按数据重要性定,如 1~4 周)
- 验证手段:
rclone check+ 业务探针(下载几个代表对象对比大小/哈希)
Supabase 特例(S3 协议是子集实现)
Supabase Storage 的 S3 兼容不是完整实现,官方维护一张 S3 兼容性表: 核心端点齐全(ListBuckets、ListObjects V1/V2、Get/Put/Delete/DeleteObjects、 CopyObject、完整 Multipart 流程),但很多 S3 特性明确不支持,迁移时要有预期:
1. 不支持的特性(复制不走、校验需降级)
- 版本控制:S3 versioning 不支持,删除即永久删除,无法恢复
- ACL / Tagging / Object Lock / Storage Class / 生命周期 / SSE:均不支持, 迁入后这些属性无从保留
- CORS 配置:没有 S3 的 CORS API,浏览器直传/直读的 CORS 要在 Supabase Dashboard 配置
- Checksums:不支持
Content-MD5/x-amz-checksum-*,rclone check的校验和对比会退化(详见下文 rclone 配置)
2. 凭证模型:全桶级 + 绕过 RLS
- 项目级 S3 Access Keys(Settings → Storage → S3):一对 key 管所有 bucket、所有操作,并绕过 RLS —— 只能放服务端,别进浏览器
- 对比 R2 的 Token 可按桶/按操作收缩,Supabase 的 S3 key 没有这个粒度; 多租户 / 按用户限权要改用 Session Token + 用户 JWT(走 RLS)
- 迁入后若要 anon 访问,仍需先建 RLS policy(新版默认无 policy,全拒)
3. 端点与配置(rclone 无 Supabase 预设)
rclone 的 S3 provider 列表里没有 Supabase,需手动配通用 S3 remote:
[supabase]
type = s3
# 无 Supabase 预设:不设 provider,走通用 S3 签名
endpoint = https://<PROJECT_REF>.storage.supabase.co/storage/v1/s3
force_path_style = true
region = <PROJECT_REGION> # 本地开发填 local
access_key_id = <S3_ACCESS_KEY_ID>
secret_access_key = <S3_SECRET_ACCESS_KEY>
- 必须
force_path_style = true:endpoint 路径挂在/storage/v1/s3下, 没有 virtual-hosted 形态 - region 填项目 region(官方示例如此);本地开发用
local - 大文件上传推荐用 storage 专用 hostname (
<ref>.storage.supabase.co而非<ref>.supabase.co),性能更好 - 因不支持 checksum 头,
rclone check的校验和对比会退化(大概率降为大小 比较),关键数据可另用 REST API 下载后比 MD5
4. 文件大小与配额
- 单对象上限 5GB(标准上传);bucket 级有可配置的「最大文件大小」限制, 迁大文件前先确认/调大
- 免费层存储配额小(如 1GB),迁移前确认目标额度足够
5. URL 形态
- 公共 URL:
…/storage/v1/object/public/<bucket>/<path>;S3 presign 用 SigV4 query-param —— 域名与路径和 R2/MinIO 不同,迁移后 URL / 签名 URL 全部失效需重签,前端引用要同步改
参考
- 相关文档:供应商对比(Vendors)、 基本用法(Basic Usage)、 签名 URL(Signed URL)、 挂载 Bucket(FUSE Mount)
- 工具文档:rclone(
rclone copy / sync / check、S3 provider 配置)、aws s3 sync、mc mirror
Backlinks (3)
flowchart LR
n0["Collection"]
n1["Maps"]
n2["Knowledge"]
n3["Cloud"]
n4["对象存储基本用法(Bucket / Object / 常用操作)"]
n5["对象存储数据迁移(R2 → MinIO / 跨厂商搬迁)"]
n6["Object Storage"]
n7["挂载 Bucket 为本地文件系统(FUSE Mount)"]
n8["对象存储签名 URL(Signed URL)原理与实战"]
n9["对象存储供应商对比(S3 / R2 / OSS / Supabase / MinIO)"]
n10["Infrastructure"]
n11["Prototypes"]
n12["Research"]
n13["Better Auth 源码阅读指南"]
n14["DuckDB 实战研究"]
n15["英语学习 Dashboard"]
n16["Jellyfin 源码阅读指南"]
n17["Lux 资料整理"]
n18["Nest Commander 学习资料"]
n19["NestJS 源码阅读指南"]
n20["Protomaps 自建底图研究"]
n21["自制 PMTiles 地图(最简单例子)"]
n22["MapLibre 集成 Protomaps"]
n23["PMTiles 格式与工具链"]
n24["上海地区底图项目"]
n25["Redash 源码阅读指南"]
n26["Rust 学习计划"]
n27["shadcn/ui 源码阅读指南"]
n28["TRIP 项目核心原理与代码阅读指南"]
n0 --> n1
n0 --> n12
n2 --> n10
n3 --> n6
n4 --> n5
n4 --> n8
n4 --> n9
n4 --> n11
n5 --> n4
n5 --> n7
n5 --> n8
n5 --> n9
n6 --> n4
n6 --> n5
n6 --> n7
n6 --> n8
n6 --> n9
n7 --> n4
n7 --> n8
n7 --> n9
n8 --> n11
n9 --> n4
n9 --> n5
n9 --> n8
n9 --> n11
n10 --> n3
n11 --> n4
n11 --> n7
n11 --> n8
n11 --> n9
n11 --> n20
n12 --> n13
n12 --> n14
n12 --> n15
n12 --> n16
n12 --> n17
n12 --> n18
n12 --> n19
n12 --> n20
n12 --> n25
n12 --> n26
n12 --> n27
n12 --> n28
n14 --> n12
n20 --> n1
n20 --> n21
n20 --> n22
n20 --> n23
n20 --> n24
n21 --> n23
n22 --> n11
n22 --> n24
n23 --> n21
n23 --> n24
n24 --> n22
n24 --> n23
click n0 "../../../../../collection/" "Collection"
click n1 "../../../../../collection/maps/" "Maps"
click n2 "../../../../" "Knowledge"
click n3 "../../" "Cloud"
click n4 "../basic-usage/" "对象存储基本用法(Bucket / Object / 常用操作)"
click n5 "./" "对象存储数据迁移(R2 → MinIO / 跨厂商搬迁)"
click n6 "../" "Object Storage"
click n7 "../mount-bucket/" "挂载 Bucket 为本地文件系统(FUSE Mount)"
click n8 "../signed-url/" "对象存储签名 URL(Signed URL)原理与实战"
click n9 "../vendors-comparison/" "对象存储供应商对比(S3 / R2 / OSS / Supabase / MinIO)"
click n10 "../../../" "Infrastructure"
click n11 "../../../../../prototypes/" "Prototypes"
click n12 "../../../../../research/" "Research"
click n13 "../../../../../research/topics/better-auth/" "Better Auth 源码阅读指南"
click n14 "../../../../../research/topics/duckdb/" "DuckDB 实战研究"
click n15 "../../../../../research/topics/english/" "英语学习 Dashboard"
click n16 "../../../../../research/topics/jellyfin/" "Jellyfin 源码阅读指南"
click n17 "../../../../../research/topics/lux/" "Lux 资料整理"
click n18 "../../../../../research/topics/nest-commander/" "Nest Commander 学习资料"
click n19 "../../../../../research/topics/nestjs/" "NestJS 源码阅读指南"
click n20 "../../../../../research/topics/protomaps/" "Protomaps 自建底图研究"
click n21 "../../../../../research/topics/protomaps/make-own-map/" "自制 PMTiles 地图(最简单例子)"
click n22 "../../../../../research/topics/protomaps/maplibre/" "MapLibre 集成 Protomaps"
click n23 "../../../../../research/topics/protomaps/pmtiles/" "PMTiles 格式与工具链"
click n24 "../../../../../research/topics/protomaps/shanghai-map/" "上海地区底图项目"
click n25 "../../../../../research/topics/redash/" "Redash 源码阅读指南"
click n26 "../../../../../research/topics/rust/" "Rust 学习计划"
click n27 "../../../../../research/topics/shadcn-ui/" "shadcn/ui 源码阅读指南"
click n28 "../../../../../research/topics/trip/" "TRIP 项目核心原理与代码阅读指南"