周三晚上,工程负责人盯着终端上那行 pg_restore exited 0,长出一口气。所有的表都回来了,行数完全吻合,整条 CI 管线都是绿的。团队提前下班,以为这次恢复和过去几十次演练一样,不会留下任何伤疤。二十来分钟后,第一条工单亮了:用户头像全部裂开,一张都加载不出来。接着是合同 PDF、嵌入页面的打样图、每一张用户在这两年里上传过的图片和附件,全部返回了 404。
这不是一次恢复失败。恰恰相反,后台从 Postgres 的角度看,没有任何一个步骤报错。所有的元数据都在,所有的行数都对,甚至 select count(*) 从对象元数据表里都返回了那个让人安心的数字。但你的用户数据已经不见了。这个场景有一个很具体的名字:指针还在,文件死了。而亲手创建出这条死亡通道的,正是很多开发者默认放心的 Supabase 内置备份。
正方会告诉你:我们做了每日自动备份,开了 PITR,还定期用 pg_dump 导出 SQL,这已经是教科书级别的数据保护。检查表的行数、跑一遍基础约束校验,没有一条记录丢失。任何基于 SQL 的恢复演练都会打出绿色的 PASS。数据安全,齐活。
反方会向你展示一个残酷的事实:你恢复出来的那个库,只不过是一本写满“你应该在这里”的目录,真正的物件还留在原来的仓库里,而那个仓库并不在你恢复的项目里。从逻辑上看,一切正常;从物理上看,所有的文件实体都消失了。当你把数据库还原到一个全新项目,或者旧的 S3 桶已经被清理,目录上每一条路径指向的都是虚空。404 不是恢复的故障,而是架构的疏忽。
我倾向于反方,不是因为错误有多么罕见,而是因为这一刀切得太干净。Supabase 的对象存储天生就是一个双系统:Postgres 里的 storage.objects 表只负责存元数据——桶名、路径、时间戳、所有者,像个图书管理员手里的卡片索引;真正的文件字节呆在 Supabase 运营的、完全位于数据库之外的 S3 兼容后端里。
你备份数据库,只能拿到卡片;文件书架从来不在导出行列里。无论是内置的每日备份、PITR,还是你手动执行的 pg_dump,这条裂缝都保持沉默,因为它们都是数据库层面的快照,物理文件并不参与。这种沉默才是最具欺骗性的。恢复之后,你可以登录 pgAdmin,一条条翻元数据表,确确实实看到那些熟悉的对象名和上传时间。没有任何约束报错,因为外键根本不指向实际文件。唯一的报警系统是终端用户:一个人发现头像裂了,然后一群人在工单里贴出一长串 S3 签名错误的截图。到这一步,数据库检查再优雅也于事无补。
如果你想亲身验证,五分钟就能复现这个错位。查询一下对象元数据,拿到最近 20 条记录的名称和时间戳。然后用 pg_dump 把整个库导出来,灌进本地的 Postgres,再跑一遍同样的查询。你会发现二十条记录整整齐齐,一条不差,但那二十个文件一个也没跟着过来。恢复通过了所有你能写出的 SQL 检验,却丢掉了用户真正在意的资产。
这引出一个更冷的思考:数据库的恢复仪式,是不是已经让我们习惯了只检查数据库的屋子,而忘了门后面的柜子也装着东西?如果你目前只依赖 Supabase 的备份策略,那么你实际上没有备份过任何用户上传的文件。
好消息是,文件并非无法外迁。Supabase 已经把这个 S3 兼容后端暴露了出来,只要配上密钥,就可以像操作普通 bucket 一样把文件同步出去。获取密钥路径很直接:进入 Dashboard → 对象存储 → Settings → S3 Access Keys,启用 S3 连接,生成一组访问密钥和密钥 ID。同一个页面会给出 endpoint 和 region,endpoint 会使用项目专用的 S3 风格地址。
有一个需要刻在脑子里的警告:这些密钥会绕过行级安全策略,一定要当成服务角色密钥处理,只放在服务器端,绝不要出现在客户端代码或环境变量里。有了密钥,你可以选择 rclone、s3cmd 或自己写一段简单的同步脚本,把桶里的所有对象定期拉取到独立的、你完全掌控的存储中。备份动作本身并不复杂——真正危险的是你以为已经做了,却从未验证过文件是否真的能被还原。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.