数据库迁移、备份与恢复
用「扩展—迁移—收缩」让结构变化可回退,用一次真实恢复演练证明备份有效。含可直接用的 pg_dump/恢复命令。
数据库最危险的时刻不是服务器坏掉,而是一次看起来很简单的上线:改个字段名、跑个脚本、导一批数据,然后发现旧代码和新结构不兼容,而这时候用户正在用。
备份的价值也不在"每天生成成功",而在需要时能恢复出一个可用的业务。一个从没恢复过的备份,和没有备份的区别只是你心里更踏实一点——而这种踏实是假的。
快速版
- 每次结构变化都要有版本化的迁移文件,不在生产库里手点。
- 改字段用「扩展—迁移—收缩」三步,不要一步到位。
- 上线前先在真实数据副本上演练一遍迁移。
- 备份覆盖三类:数据库、用户文件、恢复所需配置。
- 明确写下能接受丢多久数据、断多久服务,别自我安慰。
- 做一次真实恢复演练,把用时和手工步骤记进运行手册。
迁移要能重复执行
每次结构变化都应该有版本记录。不要只在生产数据库里手工点几下——那样你既无法回滚,也无法在另一台机器上重建同样的结构。
一条迁移要能说明五件事:
| 要说明的 | 为什么 |
|---|---|
| 增加、修改或删除了什么 | 出问题时判断影响面 |
| 旧版本代码是否还能运行 | 决定能不能先发数据库、后发应用 |
| 数据怎么回填或转换 | 空字段和错数据都会让功能失效 |
| 失败后如何停止和恢复 | 迁移跑一半失败是常态 |
| 预计锁表时间、耗时、磁盘影响 | 大表加索引可能锁几分钟,等于停业 |
第五项在小项目里经常被忽略,直到某次对着十万行的表加了一个非空字段,站卡了三分钟。
用「扩展—迁移—收缩」降低风险
以重命名一个关键字段为例。不要一步删掉旧字段,按五步走:
1. 扩展:加新字段,旧字段保持可用 ← 此时新旧代码都能跑
2. 双写:发布同时读写新旧结构的代码
3. 回填:分批把历史数据写进新字段并核对
4. 切读:读取切到新字段,旧字段只写不读 ← 此时可以随时切回去
5. 收缩:观察稳定几天后,删掉旧字段步骤更多,但换来一个关键能力:应用和数据库可以分别回滚。一步到位的改法里,应用回滚了但数据库回不去,你就只能硬着头皮往前修。
第 5 步的"观察几天"不要跳。删字段是唯一不可逆的一步。
备份至少覆盖三类
只备份数据库是不够的,恢复的时候你会发现缺东西:
| 类别 | 具体内容 | 漏掉的后果 |
|---|---|---|
| 数据库 | 结构 + 业务记录 | 显然 |
| 用户文件 | 上传的图片、附件、对象存储 | 数据库里有引用,文件没了,页面全是破图 |
| 配置说明 | 环境变量清单、部署步骤、依赖的外部服务 | 数据恢复了,但起不来 |
密钥不要和数据备份放在同一个公开位置,但恢复负责人必须知道怎么重新取得它们。写一份"密钥在哪里取"的说明,而不是把密钥本身放进备份。
具体命令
Postgres 在 Docker 里的备份和恢复,可以直接用:
# 备份(压缩,带日期)
docker compose exec -T db pg_dump -U postgres myapp \
| gzip > ~/backups/db-$(date +%F-%H%M).sql.gz
# 恢复到另一个库验证(不要直接恢复到生产)
docker compose exec -T db createdb -U postgres myapp_restore_test
gunzip -c ~/backups/db-2026-08-31-1200.sql.gz \
| docker compose exec -T db psql -U postgres myapp_restore_test
# 验证恢复结果:查几张关键表的行数
docker compose exec -T db psql -U postgres myapp_restore_test \
-c "select count(*) from users; select count(*) from orders;"注意 exec -T:没有 -T 的话 Docker 会分配伪终端,管道里的数据会被破坏,你会得到一个看起来正常但实际损坏的备份文件。
另外,上线前顺手做一次全量快照比只备数据库更省心。摆摊每次部署前打一个整目录的 tarball,文件名带上当前 commit:
tar czf ~/backups/app-pre-$(git rev-parse --short HEAD).tgz -C ~/ myapp这样回滚不只是回代码,是回到一个确定的、能跑的状态。
定义恢复目标
先诚实回答两个问题:
- 最多能接受丢失多久的数据?(RPO)
- 最多能接受服务中断多久?(RTO)
早期产品不必做昂贵的秒级容灾,但必须把真实能力告诉自己。如果你每天备份一次,就不要假装只会丢五分钟数据——真出事的时候,这个自我欺骗会变成对用户的欺骗。
一个务实的早期配置:每天自动备份一次,保留最近 7 天 + 每周一份保留 4 周。恢复目标写成"最多丢一天数据,最多断两小时",然后验证你真的能在两小时内恢复。
定期做恢复演练
按固定周期,在隔离环境里走完五步:
- 选一个备份(随机选,不要总选最新的)
- 恢复数据库和文件
- 启动应用
- 检查账号能登录、核心记录在、订单和权益对得上
- 记录用时、缺失项、需要手工干预的步骤
只检查备份文件大小不等于恢复测试。 损坏、权限错误、版本不兼容、缺少扩展,这些问题只有真的恢复一次才会暴露。
第 5 步里"需要手工干预的步骤"是最有价值的产出——那些步骤就是你下次故障时会卡住的地方。
常见卡点
| 卡在哪 | 可能原因 | 试试这样 |
|---|---|---|
| 备份文件恢复时报错 | docker exec 没加 -T,数据被 TTY 破坏 | 加 -T 重新备份 |
| 恢复出来的库缺扩展 | 生产库装了扩展,恢复环境没装 | 把扩展列进配置说明,恢复前先装 |
| 数据恢复了但应用起不来 | 只备了数据,没备配置和环境变量清单 | 补第三类备份 |
| 迁移跑一半失败,库处于中间状态 | 迁移不是原子的,也没有回滚脚本 | 大迁移拆小步,每步单独可回滚 |
| 加字段导致站卡住 | 大表加非空字段会锁表 | 先加可空字段,再回填,再加约束 |
| 备份一直成功但从没恢复过 | 没有演练机制 | 现在就做一次,见本章动作 |
| 备份和密钥放在一起丢了 | 图省事放同一个位置 | 备份存数据,密钥另外管,写清怎么取 |
验收标准
- 每次结构变化都有版本化的迁移文件
- 关键字段的变更走过「扩展—迁移—收缩」,不是一步改
- 备份覆盖数据库、用户文件、配置说明三类
- 备份是自动的,不依赖你记得手动跑
- 写下了能接受的数据丢失窗口和服务中断时长
- 至少做过一次完整恢复演练,并记录了用时
- 恢复步骤写进了运行手册,别人照着能做
- 密钥不在数据备份里,但恢复负责人知道去哪里取
本章动作
今天生成一份备份,恢复到另一个数据库实例,然后完成一条真实的主路径查询(比如"查出某个用户的订单和权益")。
把三样东西写进运行手册:恢复命令、所需权限、实际用时。
如果这次演练花了两小时,那你的真实恢复时间就是两小时——而不是你之前以为的十分钟。
下一步
- 监控、日志与故障处理——出问题时怎么第一时间知道
- 自建服务器部署——部署更新流程里的备份步骤
- 上线前验收——包含回滚方案的上线记录