Skip to content

数据库迁移

后端库是 PostgreSQL,迁移由 Alembic 管理。migrations/env.py 与 control-server 共享同一个 DSN 变量 DN42_CONTROL_DATABASE_URL

迁移链清单见 数据库参考


常用命令

从仓库根执行:

bash
export DN42_CONTROL_DATABASE_URL=postgresql+asyncpg://user:pass@host/db

alembic upgrade head        # 升到最新
alembic current             # 看当前版本
alembic history             # 看迁移链
alembic downgrade -1        # 回退一步(删列类多为不可逆)

迁移期驱动会被自动替换为同步版本:+aiosqlite 去除、+asyncpg+psycopg2+asyncmy+pymysql

⚠️ 现役生产置 auto_migrate = false(compose 里 DN42_CONTROL_DB_AUTO_MIGRATE=false),即启动走 create_all不是迁移链。原因见下一节:生产库由 create_all 建起、未被 alembic 纳管。

因此生产上的 schema 变更目前create_all 建新表 + 手工 ALTER 改既有表。要切到迁移链驱动,先按下一节 stamp 对齐,再把该项置 true


create_all 库首次接入 Alembic

dev 与 CI 默认走 Base.metadata.create_all——只建缺失的表,绝不应用 ALTER。历史上用 create_all 起的库没有 alembic_version 记录,直接 upgrade head 会从 base 重跑并撞上已存在的表。

正确做法是先对齐版本再升级:

bash
alembic stamp head      # 或 stamp <该库实际对应的 revision>
alembic upgrade head

判断该 stamp 到哪个 revision,办法是逐条比对迁移内容与库里的实际表结构。


已知的 ORM 与迁移偏差

从空库跑 alembic upgrade head 的结果与 ORM 定义当前不完全一致,缺三处:node_route_prefix_hashes 整张表,以及 node_statusbgp_summarypeer_rates 两列。

生产库历史上经 create_all 建起,这三处实际存在。从空库走迁移链的新部署会缺,补法:

sql
ALTER TABLE node_status ADD COLUMN bgp_summary jsonb;
ALTER TABLE node_status ADD COLUMN peer_rates jsonb;

node_route_prefix_hashescreate_all 补建即可(它只建缺失表,不动既有表)。

缺列时读侧会自动回退到解析完整快照的路径,功能不受影响、只是慢;缺哈希基准表则路由明细退化为整表重写。


FOR UPDATE 与 lazy-joined 关系

Node.dns_grouplazy="joined"可空关系,所以 session.get(Node, …, with_for_update=True) 会发出:

sql
SELECTFROM nodes LEFT OUTER JOIN dns_groups … FOR UPDATE

SQLite 直接忽略 FOR UPDATE 子句,这个问题在 SQLite 上永远不会暴露。切到 PostgreSQL 后 asyncpg 报:

FeatureNotSupportedError: FOR UPDATE cannot be applied to
the nullable side of an outer join

后果是所有 materialize、世代回滚、agent WG 公钥上报全部 500——provision、改接口、改拓扑、注册全被卡死。

修复是把行级锁限定到主表:with_for_update={"of": Node}(生成 FOR UPDATE OF nodes),不去锁外连接的可空侧。已统一修在 services/materializer.pyservices/generations.pyservices/wireguard_keys.py 三处。

通用规律:在 PostgreSQL 上对带 lazy="joined" 可空关系的实体做 with_for_update 时,必须用 of= 限定到非空主表。新增此类锁查询时照此处理。


SQLite 迁移到 PostgreSQL

deploy/docker/migrate_sqlite_to_postgres.py:按外键拓扑序整库拷贝(类型安全)并重置自增序列,源库全程只读。

迁移后按上一节的 alembic stamp 流程接入迁移链。


新增迁移

改了 ORM 就要补迁移,两者不同步是长期负担。

bash
alembic revision --autogenerate -m "描述"

autogenerate 的产物必须人工复核:它对 JSON 列默认值、部分索引、server_default 的推断都不完全可靠。特别注意:

  • ASN 列必须是 BigInteger——DN42 的 4242420000+ 超 int32。
  • 部分唯一索引(如 pending_registrationsWHERE status='pending')autogenerate 不一定生成,需手写。
  • 迁移文件里改了 server_default必须真的 ALTER 到线上,否则模型默认值与库默认值会长期分叉。