外观
数据库迁移
后端库是 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_status 的 bgp_summary 与 peer_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_hashes 用 create_all 补建即可(它只建缺失表,不动既有表)。
缺列时读侧会自动回退到解析完整快照的路径,功能不受影响、只是慢;缺哈希基准表则路由明细退化为整表重写。
FOR UPDATE 与 lazy-joined 关系
Node.dns_group 是 lazy="joined" 的可空关系,所以 session.get(Node, …, with_for_update=True) 会发出:
sql
SELECT … FROM nodes LEFT OUTER JOIN dns_groups … FOR UPDATESQLite 直接忽略 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.py、services/generations.py、services/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_registrations的WHERE status='pending')autogenerate 不一定生成,需手写。 - 迁移文件里改了
server_default,必须真的 ALTER 到线上,否则模型默认值与库默认值会长期分叉。