Skip to content
返回维护与 LTS

迁移回滚

让每次迁移可逆 — 使你敢激进迁移并从失败恢复的安全网。

为什么

不可逆迁移是单向门:若新 schema 破坏应用,唯一选择是从备份恢复,丢迁移后所有写。可逆迁移(expand-contract 带真 DOWN)变双向门:若新代码在新 schema 上失败,你回退代码且旧代码仍工作(因 expand-contract 保旧 schema 兼容)。关键是 DB 迁移和代码部署是分开步骤,且迁移总先扩展(加性、非破坏)使旧代码续工作。排练回滚 — 在 staging 跑 DOWN — 是使其凌晨 3 点工作的方式,因你证了 down 迁移是真的非 no-op。

何时使用

每次迁移部署前。把回滚排练作为迁移 PR 完成定义的一部分在 staging 做。

如何做

1. 写真 DOWN 迁移(非 no-op)逆 UP — 测:UP、DOWN、UP 须全成
2. 用 expand-contract:UP 是加性(加列/表),故旧代码在新 schema 上仍工作
3. 先部署迁移(UP);旧代码在扩展 schema 上续跑
4. 部署新代码;若失败回退代码(非 DB)— 旧代码在新 schema 上工作
5. 仅 schema 本身坏(如坏约束)才跑 DOWN — 且旧代码仍工作
6. staging 排练:跑 UP、部署新代码、跑 DOWN、部署旧代码 — 确认每步应用工作
7. 收缩阶段保旧代码可部署(最终收缩迁移后才删)

示例

# 可逆迁移(Knex)— 加 NOT NULL 带默认,再回填
exports.up = async (knex) => {
  // 扩展:加可空列(旧代码忽略它)
  await knex.schema.alterTable('users', t => t.text('display_name').nullable());
  // 回填
  await knex.raw('UPDATE users SET display_name = name');
  // 收缩:变 NOT NULL(回填后安全)
  await knex.schema.alterTable('users', t => t.text('display_name').notNullable());
};
exports.down = async (knex) => {
  // 逆:变可空再删(旧代码无它也工作)
  await knex.schema.alterTable('users', t => t.text('display_name').nullable());
  await knex.schema.alterTable('users', t => t.dropColumn('display_name'));
};

# staging 排练(迁移 PR 的 CI 作业):
#   1. npx knex migrate:latest          # UP
#   2. 部署新代码;跑冒烟测试
#   3. npx knex migrate:rollback        # DOWN
#   4. 部署旧代码;跑冒烟测试  # 须仍通过
# 任一步失败则迁移不发。

# 生产回滚(若新代码在新 schema 上失败):
#   kubectl rollout undo deployment/app   # 仅回退代码
#   (DB 留在 UP;旧代码在扩展 schema 上工作)
# 仅 schema 坏时:
#   npx knex migrate:rollback            # DOWN DB

常见问题

  • ⚠no-op DOWN 迁移 — 你以为能回退,凌晨 3 点发现 down 啥也不做 schema 卡住。
  • ⚠一步合并 DB 迁移和代码部署 — 失败时不知回退哪个且需不同回退命令。
  • ⚠从不排练回滚 — DOWN 有 bug(引用已删表)只在最需要时暴露。

最佳实践

  • ✓总写真 DOWN 迁移并在 staging 测 UP→DOWN→UP — no-op down 是凌晨 3 点失败的谎。
  • ✓分开部署 DB 迁移(扩展)和代码 — 先回退代码;仅 schema 本身坏才回退 DB。
  • ✓把回滚排练作 CI 门控:UP、部署、DOWN、重部署旧 — 每步应用不工作则迁移不发。