外观
迁移与演进
schema 由 ORM 变为库中表的两条路径,以及现役生产库的实际状态。操作步骤、alembic stamp 的使用时机与回滚见数据库迁移。
两条建表路径
迁移脚本位于 migrations/versions/(41 个 revision),配置见 alembic.ini(script_location = migrations,file_template = %(rev)s_%(slug)s)。migrations/env.py 从 DN42_CONTROL_DATABASE_URL 取 DSN(与运行时共享同一变量),并将异步驱动统一替换为同步驱动执行迁移;target_metadata = Base.metadata。
schema 初始化由启动期 _init_schema(app/main.py)按 db_auto_migrate 分流:
| 配置 | 行为 | 适用 |
|---|---|---|
db_auto_migrate=True | alembic upgrade head,迁移失败即启动中止(fail-fast) | 全新部署 |
db_auto_migrate=False(默认) | Base.metadata.create_all——只建缺失的表,不应用任何 ALTER | dev、CI,以及现役生产 |
create_all 的行为边界需明确,因为生产依赖它:创建缺失的表与索引,不修改已存在的表。新增一张表会自动出现;为已有表增列、改类型、改可空性、删列,一律不会发生。
生产库的实际状态
现役生产为 db_auto_migrate=false,但库中存在一行 alembic_version,两者叠加出一个需要说明的状态:
| 事实 | 取值 |
|---|---|
alembic_version 停留于 | e0f1a2b3c4d5(portal_login_sessions) |
| 迁移链 head | b1c2d3e4f5a6(drop_registry_mirror_tables) |
| 相差 | 6 个 revision |
| 实际 schema | 由 create_all 建到 ORM 当前形态,超前于版本号 |
即版本号陈旧而表结构较新。直接执行 alembic upgrade head 会从 f1a2b3c4d5e6 开始重放,第一步即因「表已存在」而失败。接管的前提是先执行 alembic stamp 对齐,见数据库迁移。
由此得出两条日常纪律:
- 新增表:修改 ORM 即可,生产重启时
create_all会创建。迁移文件仍需编写——新部署走迁移链。 - 改列、删列、改约束:
create_all不会执行,必须手工ALTER到线上,否则生产与 ORM 静默分叉。
同一原因下,迁移中的 DROP TABLE 在生产从未执行:peering_challenges、peering_sessions 与 registry_* 系列早已从 ORM 与迁移链移除,生产库中仍保留空表。它们不占用资源、不参与读写,清理属于可选动作。
已知的 ORM 与迁移偏差
反向情况同样存在:从空库执行 alembic upgrade head 的结果与 ORM 定义当前不完全一致,缺失三处:
| 缺失项 | 所在表 |
|---|---|
node_route_prefix_hashes 整张表 | — |
bgp_summary 列 | node_status |
peer_rates 列 | node_status |
生产库历史上经 create_all 建立,这三处实际存在;从空库走迁移链的新部署则会缺失。补充迁移之前,新部署的处置方式是先 create_all 建库再 alembic stamp head,或在 upgrade head 后手动补充:
sql
ALTER TABLE node_status ADD COLUMN bgp_summary jsonb;
ALTER TABLE node_status ADD COLUMN peer_rates jsonb;缺列时读侧自动回退到解析完整快照的路径,功能不受影响,仅延迟上升;缺 node_route_prefix_hashes 表则路由明细退化为整表重写(见写入路径)。
时序表分区
L3 时序存档按保留期裁剪,做法是周期性 DELETE 旧行。随机 DELETE 持续产生死元组与索引膨胀,依赖 autovacuum 回收;按时间分区后裁剪转为 DROP TABLE <分区>,耗时在毫秒级且不产生垃圾。
目标表(三张的主键都已包含 bucket_start,满足 PG「分区键必须在唯一约束内」的硬要求,无需改主键):
| 表 | 规模(参考基线) | 分区键 |
|---|---|---|
node_iface_traffic_rollup | 777 120 行 / 127 MB | bucket_start(epoch 秒) |
prefix_flap_rate_rollup | 462 604 行 / 61 MB | 同上 |
node_traffic_rollup | 119 306 行 / 15 MB | 同上 |
不在目标之列的三张时序表各有原因:node_metrics_rollup 与 autopeer_route_rollup 规模小一个量级且死元组为零;node_routing_events 主键是自增 id,转换要动主键,而它只有约 3.7 万行;node_route_entries 是差分写的活跃集(L2),没有时间列。
分区按周切分:分区序号 = epoch // 604800,可逆、可排序且与时区无关;分区名为 <表名>_p<序号>。
两个组件:
app/services/partitions.py的PartitionMaintainer—— 随控制面启动的日常维护循环,补出未来两周的分区、DROP掉保留期(10 周)之外的。表尚未转换为分区表时整体 no-op,非 PostgreSQL 也跳过,因此可以先于迁移上线。scripts/db/partition_timeseries.py—— 一次性在线迁移(建分区母表 → 拷数据 → 改名,旧表留成<表名>_old)。
为什么需要脚本而非迁移文件:现役生产由 create_all 建起(db_auto_migrate=false),而 create_all 只建缺失的表、绝不 ALTER——在模型上标注 postgresql_partition_by 对生产库没有任何效果。PostgreSQL 也不支持把普通表原地改成分区表。
转换完成后在 control-server.toml 置 [database] partitioned_timeseries = true,按行裁剪的 DELETE 随即全部停掉。必须在转换实际完成之后才置真——未分区却置真等同于旧行永不回收。步骤与回滚见 deploy/docker/STORAGE-ROLLOUT.md,收益评估见性能与容量。
迁移链
当前 head:b1c2d3e4f5a6。按链表顺序:
| revision | slug | 内容 |
|---|---|---|
a0043f410bda | initial_schema | 初始 schema:nodes 及核心表 |
b1f2c3d4e5a6 | node_runtime_status | 新增 node_status / node_status_events |
c2a3b4c5d6e7 | token_hash_pending_registrations | agent_tokens 加 token_hash / expires_at 与唯一索引;新增 pending_registrations |
d4e5f6a7b8c9 | admin_audit_enrollment_hash | 新增 admin_audit_log;enrollment_tokens 改哈希存储 |
e5f6a7b8c9d0 | wireguard_key_escrow | nodes 加 wireguard_public_key 与 wireguard_private_key_escrow |
f6a7b8c9d0e1 | remove_compose_runtime_fields | 从存量 JSON 剥 runtime.adapter / templates.compose / templates.systemd |
a7b8c9d0e1f2 | slim_build_spec | 从存量 JSON 剥 build.context / build.dockerfile |
b8c9d0e1f2a3 | node_lifecycle | nodes 加 lifecycle |
c9d0e1f2a3b4 | node_route_entries | 新增 node_route_entries 索引表,明细从单 JSON 列拆出 |
d0e1f2a3b4c5 | dns_groups | DNS 改共享组模型:dns_groups / dns_group_zones / dns_records,nodes 加 dns_group_id |
e1f2a3b4c5d6 | drop_legacy_compat_shims | 剥 DB 残留以删运行时垫片 |
f2a3b4c5d6e7 | drop_node_routing_routes | 删死列 node_routing.routes |
a3b4c5d6e7f8 | drop_rpki_unknown | 删死列 rpki_unknown |
a4b5c6d7e8f9 | node_link_local | nodes 加 link_local |
b5c6d7e8f9a0 | pending_reg_unique_pending | pending_registrations 加部分唯一索引 |
c0d1e2f3a4b5 | node_traffic_rollup | 新增 node_traffic_rollup |
d1e2f3a4b5c6 | node_traffic_last_sample | 新增 node_traffic_last_sample |
e2f3a4b5c6d7 | registry_mirror | 新增本地 registry 副本表 |
f3a4b5c6d7e8 | iface_traffic_rollup | 新增 node_iface_traffic_rollup / node_iface_traffic_last |
a5b6c7d8e9f0 | node_metrics_rollup | 新增 node_metrics_rollup / node_metrics_last |
b6c7d8e9f0a1 | bgp_session_flaps | 新增 bgp_flap_state / bgp_flap_transitions |
c7d8e9f0a1b2 | prefix_flaps | 新增 node_prefix_flaps / node_prefix_flap_meta |
d8e9f0a1b2c3 | flap_alerts | 新增 flap_alerts |
e9f0a1b2c3d4 | prefix_flap_origin_asn | node_prefix_flaps 加 origin_asn |
f0a1b2c3d4e5 | prefix_flap_paths | 新增 node_prefix_flap_paths |
a1b2c3d4e5f6 | flap_stats_rollup | 新增 prefix_flap_rate_rollup / flap_activity_rollup;node_prefix_flaps 加 recent_changes / rate_per_s |
b2c3d4e5f6a7 | admin_accounts | 新增 admin_users / admin_sessions |
c3d4e5f6a7b8 | registry_mntner | 新增本地 registry_mntner |
c4d5e6f7a8b9 | registry_person | 新增本地 registry_person |
c5d6e7f8a9b0 | registry_mntner_tech_c | registry_mntner 加 tech_c |
c6d7e8f9a0b1 | peering_challenges | 新增 peering_challenges / peering_sessions |
c7e8f9a0b1c2 | registry_key_cert | 新增本地 registry_key_cert |
d9e0f1a2b3c4 | drop_peering_challenge_tables | 删挑战与会话表(验证流迁至 auth-server) |
e0f1a2b3c4d5 | portal_login_sessions | 新增 portal_login_transactions / portal_sessions ← 生产 stamp 停在这里 |
f1a2b3c4d5e6 | autopeering_requests | 新增 autopeering_node_policies / peering_requests |
f4a5b6c7d8e9 | autopeering_parity_a | 节点策略元数据 + autopeer_route_rollup + 探测结果列 |
f5b6c7d8e9f0 | node_keys_port_pools | 新增 node_wireguard_keys / port_pools |
f6c7d8e9a0b1 | peer_naming_capacity | max_sessions 改可空(容量由端口池派生) |
a9b0c1d2e3f4 | autopeering_request_unify | 请求行连接参数列改可空;新增会话形态列 |
b1c2d3e4f5a6 | drop_registry_mirror_tables | 删控制库内的 registry 副本表(副本已拆为独立服务) |
registry_* 系列表曾在控制库内维护,随 registry-server 拆分已从链上移除。控制面现经 HTTP 客户端查询副本服务,不再落库。