我恢复过相当多不属于我的 Supabase 备份。不是因为出了什么问题,而是因为我构建了一个对它们进行恢复测试的工具,而几乎每一个新数据库都让我学到了一些关于备份如何悄无声息地变坏的东西。
模式总是相同的。备份任务显示绿色。文件在存储桶里。大小也合理。没有人有任何理由怀疑它。然后当有人第一次真正恢复它时,发现少了些东西。
下面这五种情况都不涉及罕见的故障。它们全都是默认设置,或者看起来合理的选择,而每一种都会产生一个在你真正使用之前看起来没问题的备份。
Supabase CLI 有一个 supabase db dump 命令,这是很多人首先会想到的。它在文档里,只需一行命令,就能生成一个很大的 .sql 文件。
默认情况下,该文件包含你的 schema,但不包含任何行。表、函数、策略,全都在。数据,一点也没有。你需要用 --data-only 再生成一个包含行的文件,而角色则需要用 --role-only 再运行一次。这个文件有数千行,看起来是完整的,这正是它能存活数月的原因:没有人会打开备份文件去检查 COPY 语句。
如果你的备份是基于 supabase db dump 构建的,请打开文件并搜索 COPY 或 INSERT。如果它们不在那里,那你备份的只是一张蓝图。
针对 Session pooler 连接字符串运行 pg_dump 没有这个问题,因为它默认会转储数据。只需确保其主版本至少不低于你项目的 Postgres 版本,否则它会拒绝运行,这至少是一个明显的失败。
Supabase Storage 将你上传的文件保存在数据库之外。数据库拥有的是 storage.objects,这是一张每个文件对应一行的表:bucket、path、size、timestamps。
因此,每一次数据库备份,无论是 Supabase 自带的每日备份、时间点恢复(point-in-time recovery),还是你的 pg_dump,都只包含描述文件的那些行,而不包含任何文件本身。Supabase 的文档明确说明了这一点,但很容易被忽略过去。
这种故障会在恢复之后显现出来。应用启动,所有查询都正常工作,但每一个头像、发票 PDF 和用户上传文件都变成了失效链接,因为数据库指向的是它从未拥有过的文件。
如果用户会上传任何内容,这些 bucket 就需要有自己的备份。Supabase 通过兼容 S3 的端点(Storage → Settings)暴露 Storage,因此用 rclone copy 将每个 bucket 复制到你自己的 bucket 中即可。每次运行时复制到一个新的带日期的文件夹;原地同步同一个文件夹意味着今晚在生产环境中删除的文件,今晚也会从你的备份中被删除。
在 Pro 计划中,Supabase 每天进行一次备份并保留七天。很多人把它当作自己的异地副本。
它并非异地,而且在当前大多数项目上你无法将其取出。使用物理备份流程的项目——即所有运行较新 Postgres 版本的项目以及所有启用了时间点恢复的项目——可以在仪表板中从这些备份恢复,但无法下载它们。如果问题出在项目本身(项目被删除、账户被锁定、账单过期),那么这些备份就处在问题的错误一侧。
Supabase 的备份在其设计用途上表现良好:在项目内部回滚糟糕的一天。它们不是你持有的副本。为此,你需要将自己的 dump 放在自己的 bucket 中,而在 Free 计划上根本没有自动备份,因此你自己的 dump 就是唯一存在的副本。
这个是我最喜欢的,因为恢复会“成功”。
Supabase schema 到处都在使用 auth.uid(),而一个非常自然的使用位置是列默认值:owner uuid default auth.uid()。该行会自动填充其 owner。很好。
当你测试恢复时,你会恢复到一个一次性的普通 Postgres 中。普通 Postgres 没有 auth schema。因此该表的 CREATE TABLE 会失败,其行的 COPY 会失败,而 pg_restore 会继续执行到结束,因为默认情况下它会报告错误并继续。最终你得到了除那张表之外的所有表。
错误行就在那里。但将 Supabase dump 恢复到普通 Postgres 中,总会产生一些预期内的报错,涉及仅存在于 Supabase 上的角色和 schema,第三次之后你就学会直接跳过任何提到 auth 的内容。正是这个习惯掩盖了这个问题。
修复方法很简单:在恢复之前,先创建三个角色(anon、authenticated、service_role)以及一个 auth schema,并定义返回 null 的桩函数 uid()、role()、email() 和 jwt()。不要创建 auth.users;关于指向它的外键报错确实只是沙箱噪音。如果你想看它在两个容器中实际发生的过程,我在 DEV 上写了完整的复现步骤。
最后一个问题不在于备份里有什么,而在于备份是什么时候的。
一个 cron job 或定时 GitHub Actions workflow 每晚运行。某天密码被轮换,或者 runner 镜像发生变化,或者数据库增长超过了超时限制。任务开始失败。没有任何人收到告警,因为静默失败正是定时任务的常态。bucket 里仍然有文件,只是都是旧的。
当你终于需要这份备份时,它已经过期六周了。
这是五个问题中唯一一个仅靠恢复测试无法发现的,因为旧备份可以完美恢复。你需要通过检查最新行来发现它:恢复最新的备份,然后对一张每天都有写入的表执行 select max(created_at)。如果结果不是昨晚的,说明任务已经坏了。
上述每种情况都有相同的形态:备份作为文件看起来没问题,但作为恢复却会失败。所以解决办法就是定期恢复它,并检查恢复出来的内容。
我的例行流程,手动操作大约需要十五分钟:
从你的 bucket 中拉取最新的备份。
在 Docker 中启动一个一次性的 Postgres,使用与你的项目相同的主版本号。
创建角色和 auth 桩,然后执行 pg_restore --no-owner --no-privileges。
将表数量与生产环境进行比较。
检查两到三张绝不能为空的表的行数。
对一张每天都有写入的表检查 max(created_at)。
从备份中挑选几个 Storage 文件,与生产环境比较校验和。
丢弃容器。
第 4 步捕获问题 4。第 5 步捕获问题 1。第 6 步捕获问题 5。第 7 步捕获问题 2。而第 1 步捕获问题 3,因为如果唯一的副本是你无法下载的,你根本做不了这件事。
每月执行一次。把它加入日历。第一次会花更长时间,因为你会发现一些问题。
我开发了 BackupDrill,它可以备份 Supabase 项目,并按计划对备份进行恢复测试。
转载声明:本文为转载整理,原文版权归原作者及原站点所有。
原文来源:Hacker Noon
原文作者:Super Lede;如有侵权,请联系管理员删除。