7.1 KiB
PostgreSQL Database Migration Runbook
项目:coding/comi-logic/autumn/server
目标:将当前旧 PostgreSQL 数据库迁移到新的 PostgreSQL 数据库,并把 Autumn Server 的运行配置切换到新库。
本文不保存数据库密码。执行时用本机 shell 变量或密钥系统注入连接串。
结论
默认推荐路径:
预检 -> 备份 -> 目标库清空/确认 -> pg_dump 自定义格式 -> pg_restore -> Drizzle 迁移历史校验 -> 应用配置切换 -> 健康检查 -> 保留旧库只读观察
如果业务不能接受最终停写窗口,再使用 PostgreSQL logical replication:
目标库初始化 -> 建 publication/subscription -> 等待追平 -> 短暂停写 -> 最后校验 -> 切换应用 -> 取消订阅
当前项目使用 drizzle-orm、postgres.js 和自定义 bun db 迁移 CLI。项目根的 ../scripts/db/README.md 明确说明生产/预发迁移 CLI 尚未完整接入,迁移后需要确认 drizzle.__drizzle_migrations 与 ../shared/drizzle/meta/_journal.json 一致。
预检
先在安全终端中设置变量:
export OLD_DATABASE_URL='<old-postgres-url>'
export NEW_DATABASE_URL='<new-postgres-url>'
export MIGRATION_DIR="$PWD/.migration-artifacts/postgres-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$MIGRATION_DIR"
确认本机客户端版本:
psql --version
pg_dump --version
pg_restore --version
建议使用等于或高于源库主版本的 PostgreSQL 客户端。当前本机已安装 /opt/homebrew/opt/libpq/bin/psql、pg_dump、pg_restore,版本为 PostgreSQL 18.3。
采集源库和目标库状态:
psql "$OLD_DATABASE_URL" -v ON_ERROR_STOP=1 -c "select version();"
psql "$NEW_DATABASE_URL" -v ON_ERROR_STOP=1 -c "select version();"
psql "$OLD_DATABASE_URL" -v ON_ERROR_STOP=1 -Atc \
"select current_database(), pg_size_pretty(pg_database_size(current_database()));"
psql "$NEW_DATABASE_URL" -v ON_ERROR_STOP=1 -Atc \
"select current_database(), pg_size_pretty(pg_database_size(current_database()));"
psql "$OLD_DATABASE_URL" -v ON_ERROR_STOP=1 -Atc \
"select schemaname, count(*) from pg_tables where schemaname not in ('pg_catalog','information_schema') group by 1 order by 1;"
检查目标库是否为空或是否允许覆盖:
psql "$NEW_DATABASE_URL" -v ON_ERROR_STOP=1 -Atc \
"select schemaname || '.' || tablename from pg_tables where schemaname not in ('pg_catalog','information_schema') order by 1 limit 50;"
如果目标库已有业务数据,不要直接 restore。先新建目标数据库或做快照。
全量迁移
先导出 cluster-level 对象。托管数据库通常不允许恢复所有角色/表空间,但这一步能留下审计材料:
pg_dumpall --globals-only "$OLD_DATABASE_URL" > "$MIGRATION_DIR/globals.sql"
导出业务库,使用 custom archive,便于 pg_restore 并行恢复、列出对象、选择性恢复:
pg_dump "$OLD_DATABASE_URL" \
--format=custom \
--blobs \
--no-owner \
--no-acl \
--verbose \
--file="$MIGRATION_DIR/autumn.dump"
生成对象清单:
pg_restore --list "$MIGRATION_DIR/autumn.dump" > "$MIGRATION_DIR/autumn.dump.list"
恢复到新库:
pg_restore "$MIGRATION_DIR/autumn.dump" \
--dbname="$NEW_DATABASE_URL" \
--clean \
--if-exists \
--no-owner \
--no-acl \
--jobs=4 \
--verbose
如果目标库不是空库,--clean --if-exists 会删除 dump 中对应对象。生产迁移前必须先确认目标库可覆盖。
校验
基础校验:
psql "$OLD_DATABASE_URL" -v ON_ERROR_STOP=1 -Atc \
"select count(*) from information_schema.tables where table_schema not in ('pg_catalog','information_schema');"
psql "$NEW_DATABASE_URL" -v ON_ERROR_STOP=1 -Atc \
"select count(*) from information_schema.tables where table_schema not in ('pg_catalog','information_schema');"
对所有普通表生成行数对比 SQL:
psql "$OLD_DATABASE_URL" -v ON_ERROR_STOP=1 -Atc \
"select format('select %L as table_name, count(*) from %I.%I;', schemaname || '.' || tablename, schemaname, tablename)
from pg_tables
where schemaname not in ('pg_catalog','information_schema')
order by schemaname, tablename;" > "$MIGRATION_DIR/counts.sql"
psql "$OLD_DATABASE_URL" -v ON_ERROR_STOP=1 -f "$MIGRATION_DIR/counts.sql" > "$MIGRATION_DIR/counts-old.txt"
psql "$NEW_DATABASE_URL" -v ON_ERROR_STOP=1 -f "$MIGRATION_DIR/counts.sql" > "$MIGRATION_DIR/counts-new.txt"
diff -u "$MIGRATION_DIR/counts-old.txt" "$MIGRATION_DIR/counts-new.txt"
校验 Drizzle 迁移历史:
psql "$NEW_DATABASE_URL" -v ON_ERROR_STOP=1 -Atc \
"select count(*) from drizzle.__drizzle_migrations;"
在 repo 根执行 dry-run,不应有意外 pending migration:
cd /Volumes/sker/resources/coding/comi-logic/autumn
AUTUMN_DB_DIRECT=1 DATABASE_URL="$NEW_DATABASE_URL" bun db migrate:dry --env=prod
如 dry-run 显示需要补跑迁移,先判断是目标库缺少历史记录还是确有新 DDL。不要盲目 mark-applied。
应用切换
当前 Autumn Server 的直接 PostgreSQL URL 只使用 DATABASE_URL。
同时 src/db/initDrizzle.ts 会优先使用 env.HYPERDRIVE.connectionString,否则回落到 env.DATABASE_URL。因此切换时需要确认 Cloudflare Hyperdrive 绑定和 DATABASE_URL 都指向新库。
建议切换顺序:
- 给新库执行一次只读 smoke query。
- 更新密钥系统 / Cloudflare vars / Hyperdrive 连接。
- 部署 Worker。
- 请求
/ready,确认 PostgreSQL ready check 通过。 - 观察错误率、连接池错误、关键业务 API 和迁移任务队列。
低停机备选
如果最终停写窗口不可接受,采用 logical replication:
- 源库确认
wal_level=logical、复制权限、publication 权限。 - 在目标库先创建 schema,通常来自
pg_dump --schema-only或全量 restore。 - 源库创建 publication。
- 目标库创建 subscription,允许初始数据复制。
- 等待 replication lag 归零。
- 短暂停写,做最后行数/关键表校验。
- 切换应用连接到新库。
- 观察通过后删除 subscription/publication。
logical replication 不会自动复制所有 DDL、角色、权限和某些对象,仍然需要 schema/role/extension 预置和变更冻结。
回退
热窗口内优先回退应用配置:
新库异常 -> 把 Worker/Hyperdrive/vars 切回旧库 -> 部署 -> /ready 和关键 API 验证
切换后不要马上写入旧库或清理旧库。保留旧库只读观察至少一个业务周期。若新库已接受写入且需要回退,必须先决定是否做反向同步、数据补偿或从备份恢复,不能只改连接串。
官方依据
- PostgreSQL
pg_dump: https://www.postgresql.org/docs/current/app-pgdump.html - PostgreSQL
pg_restore: https://www.postgresql.org/docs/current/app-pgrestore.html - PostgreSQL
pg_dumpall: https://www.postgresql.org/docs/current/app-pgdumpall.html - PostgreSQL logical replication: https://www.postgresql.org/docs/current/logical-replication.html
- PostgreSQL libpq connection strings, SSL, channel binding: https://www.postgresql.org/docs/current/libpq-connect.html