Skip to content

迁移与演进

schema 由 ORM 变为库中表的两条路径,以及现役生产库的实际状态。操作步骤、alembic stamp 的使用时机与回滚见数据库迁移


两条建表路径

迁移脚本位于 migrations/versions/(41 个 revision),配置见 alembic.iniscript_location = migrationsfile_template = %(rev)s_%(slug)s)。migrations/env.pyDN42_CONTROL_DATABASE_URL 取 DSN(与运行时共享同一变量),并将异步驱动统一替换为同步驱动执行迁移;target_metadata = Base.metadata

schema 初始化由启动期 _init_schemaapp/main.py)按 db_auto_migrate 分流:

配置行为适用
db_auto_migrate=Truealembic upgrade head,迁移失败即启动中止(fail-fast)全新部署
db_auto_migrate=False(默认)Base.metadata.create_all——只建缺失的表,不应用任何 ALTERdev、CI,以及现役生产

create_all 的行为边界需明确,因为生产依赖它:创建缺失的表与索引,不修改已存在的表。新增一张表会自动出现;为已有表增列、改类型、改可空性、删列,一律不会发生。

生产库的实际状态

现役生产为 db_auto_migrate=false,但库中存在一行 alembic_version,两者叠加出一个需要说明的状态:

事实取值
alembic_version 停留于e0f1a2b3c4d5(portal_login_sessions)
迁移链 headb1c2d3e4f5a6(drop_registry_mirror_tables)
相差6 个 revision
实际 schemacreate_all 建到 ORM 当前形态,超前于版本号

即版本号陈旧而表结构较新。直接执行 alembic upgrade head 会从 f1a2b3c4d5e6 开始重放,第一步即因「表已存在」而失败。接管的前提是先执行 alembic stamp 对齐,见数据库迁移

由此得出两条日常纪律:

  • 新增表:修改 ORM 即可,生产重启时 create_all 会创建。迁移文件仍需编写——新部署走迁移链。
  • 改列、删列、改约束create_all 不会执行,必须手工 ALTER 到线上,否则生产与 ORM 静默分叉。

同一原因下,迁移中的 DROP TABLE 在生产从未执行:peering_challengespeering_sessionsregistry_* 系列早已从 ORM 与迁移链移除,生产库中仍保留空表。它们不占用资源、不参与读写,清理属于可选动作。

已知的 ORM 与迁移偏差

反向情况同样存在:从空库执行 alembic upgrade head 的结果与 ORM 定义当前不完全一致,缺失三处:

缺失项所在表
node_route_prefix_hashes 整张表
bgp_summarynode_status
peer_ratesnode_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_rollup777 120 行 / 127 MBbucket_start(epoch 秒)
prefix_flap_rate_rollup462 604 行 / 61 MB同上
node_traffic_rollup119 306 行 / 15 MB同上

不在目标之列的三张时序表各有原因:node_metrics_rollupautopeer_route_rollup 规模小一个量级且死元组为零;node_routing_events 主键是自增 id,转换要动主键,而它只有约 3.7 万行;node_route_entries 是差分写的活跃集(L2),没有时间列。

分区按周切分:分区序号 = epoch // 604800,可逆、可排序且与时区无关;分区名为 <表名>_p<序号>

两个组件

  • app/services/partitions.pyPartitionMaintainer —— 随控制面启动的日常维护循环,补出未来两周的分区、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。按链表顺序:

revisionslug内容
a0043f410bdainitial_schema初始 schema:nodes 及核心表
b1f2c3d4e5a6node_runtime_status新增 node_status / node_status_events
c2a3b4c5d6e7token_hash_pending_registrationsagent_tokenstoken_hash / expires_at 与唯一索引;新增 pending_registrations
d4e5f6a7b8c9admin_audit_enrollment_hash新增 admin_audit_logenrollment_tokens 改哈希存储
e5f6a7b8c9d0wireguard_key_escrownodeswireguard_public_keywireguard_private_key_escrow
f6a7b8c9d0e1remove_compose_runtime_fields从存量 JSON 剥 runtime.adapter / templates.compose / templates.systemd
a7b8c9d0e1f2slim_build_spec从存量 JSON 剥 build.context / build.dockerfile
b8c9d0e1f2a3node_lifecyclenodeslifecycle
c9d0e1f2a3b4node_route_entries新增 node_route_entries 索引表,明细从单 JSON 列拆出
d0e1f2a3b4c5dns_groupsDNS 改共享组模型:dns_groups / dns_group_zones / dns_recordsnodesdns_group_id
e1f2a3b4c5d6drop_legacy_compat_shims剥 DB 残留以删运行时垫片
f2a3b4c5d6e7drop_node_routing_routes删死列 node_routing.routes
a3b4c5d6e7f8drop_rpki_unknown删死列 rpki_unknown
a4b5c6d7e8f9node_link_localnodeslink_local
b5c6d7e8f9a0pending_reg_unique_pendingpending_registrations 加部分唯一索引
c0d1e2f3a4b5node_traffic_rollup新增 node_traffic_rollup
d1e2f3a4b5c6node_traffic_last_sample新增 node_traffic_last_sample
e2f3a4b5c6d7registry_mirror新增本地 registry 副本表
f3a4b5c6d7e8iface_traffic_rollup新增 node_iface_traffic_rollup / node_iface_traffic_last
a5b6c7d8e9f0node_metrics_rollup新增 node_metrics_rollup / node_metrics_last
b6c7d8e9f0a1bgp_session_flaps新增 bgp_flap_state / bgp_flap_transitions
c7d8e9f0a1b2prefix_flaps新增 node_prefix_flaps / node_prefix_flap_meta
d8e9f0a1b2c3flap_alerts新增 flap_alerts
e9f0a1b2c3d4prefix_flap_origin_asnnode_prefix_flapsorigin_asn
f0a1b2c3d4e5prefix_flap_paths新增 node_prefix_flap_paths
a1b2c3d4e5f6flap_stats_rollup新增 prefix_flap_rate_rollup / flap_activity_rollupnode_prefix_flapsrecent_changes / rate_per_s
b2c3d4e5f6a7admin_accounts新增 admin_users / admin_sessions
c3d4e5f6a7b8registry_mntner新增本地 registry_mntner
c4d5e6f7a8b9registry_person新增本地 registry_person
c5d6e7f8a9b0registry_mntner_tech_cregistry_mntnertech_c
c6d7e8f9a0b1peering_challenges新增 peering_challenges / peering_sessions
c7e8f9a0b1c2registry_key_cert新增本地 registry_key_cert
d9e0f1a2b3c4drop_peering_challenge_tables删挑战与会话表(验证流迁至 auth-server)
e0f1a2b3c4d5portal_login_sessions新增 portal_login_transactions / portal_sessions生产 stamp 停在这里
f1a2b3c4d5e6autopeering_requests新增 autopeering_node_policies / peering_requests
f4a5b6c7d8e9autopeering_parity_a节点策略元数据 + autopeer_route_rollup + 探测结果列
f5b6c7d8e9f0node_keys_port_pools新增 node_wireguard_keys / port_pools
f6c7d8e9a0b1peer_naming_capacitymax_sessions 改可空(容量由端口池派生)
a9b0c1d2e3f4autopeering_request_unify请求行连接参数列改可空;新增会话形态列
b1c2d3e4f5a6drop_registry_mirror_tables删控制库内的 registry 副本表(副本已拆为独立服务)

registry_* 系列表曾在控制库内维护,随 registry-server 拆分已从链上移除。控制面现经 HTTP 客户端查询副本服务,不再落库。