外观
路由观测
agent 直连 BIRD 控制 socket 采集的路由全表观测(约 5 min 一份),与 reconcile 健康完全分开。模型在 app/db/models/routing.py。
四张表是一套:
| 表 | 层 | 角色 | 实测 |
|---|---|---|---|
node_routing | L2 | 摘要与预聚合 | 10 行 / 344 kB |
node_route_entries | L2 | 逐路由明细 | 178 303 行 / 144 MB |
node_route_prefix_hashes | L2 | 差分写的门控基准 | 10 行 / 1288 kB |
node_routing_events | L2 | 规模与 churn 时间线 | 37 265 行 / 7488 kB |
这是全库最大的子系统(合计约 153 MB,占控制库 38%),其写路径也是全库约束最严的一条:整表内容哈希与 per-prefix 差分双重门控,机制见写入路径,实测效果是每节点每轮从约 17 800 行降到约 1 500 行。
读侧的聚合结果有一层 L5 缓存(routing:*,TTL 30 秒),见 KV 与缓存键索引。
明细数量远多于前缀数量:实测每节点 2 496 个唯一前缀对应 1.78 万行明细,即同一前缀存在来自不同对端的多条路径,平均约 7 条。该比例是理解差分写为何无法降至「几十行」量级的前提。
node_routing
每节点最新路由全表加预聚合,随上报 upsert。
| 列 | 类型 | 约束 / 默认 | 说明 |
|---|---|---|---|
node_id | String(64) | PK,FK→nodes.node_id(CASCADE) | 主键即外键 |
observation | String(16) | NOT NULL,默认 not-observed | 采集状态(observed / unavailable / not-observed) |
captured_at | DateTime(tz) | 可空 | 采集时刻 |
route_count | Integer | NOT NULL,默认 0 | 唯一前缀数(按最优路径去重) |
route_count_v4 / route_count_v6 | Integer | NOT NULL,默认 0 | 分族计数 |
rpki_valid / rpki_invalid / rpki_not_found | Integer | NOT NULL,默认 0 | RPKI 分布计数 |
aggregates | JSON | 可空 | 预聚合:origins、prefix_lengths、as_path_lengths、peers、routes_hash |
updated_at | DateTime(tz) | server_default now,带 onupdate | 更新时刻 |
aggregates.routes_hash 是差分写第一级门控用的整表内容哈希。
node_route_entries
逐路由明细,每路由一行。前缀检索走 SQL WHERE 加索引加 LIMIT。
| 列 | 类型 | 约束 / 默认 | 说明 |
|---|---|---|---|
id | Integer | PK,autoincrement | 主键 |
node_id | String(64) | FK→nodes.node_id(CASCADE),NOT NULL | 所属节点(不单独建索引,由复合索引最左前缀覆盖) |
prefix | String(64) | NOT NULL | 路由前缀 |
is_v6 | Boolean | NOT NULL,默认 False | 地址族 |
local | Boolean | NOT NULL,默认 False | 本地起源(static / direct / device),供 scope 过滤 |
primary | Boolean | NOT NULL,默认 False | 是否最优路径 |
origin_asn | Integer | 可空 | 起源 AS |
protocol | String(128) | 可空 | 协议来源 |
rpki | String(16) | 可空 | RPKI 状态 |
next_hop | String(128) | 可空 | 下一跳 |
as_path / communities / large_communities | JSON | 可空 | 路径与团体属性 |
复合索引(均以 node_id 打头):(node_id, is_v6)、(node_id, local)、(node_id, prefix)。
注意:四个索引合计 64 MB,接近表体的一半,但仅一个承担实际检索——(node_id, prefix) 被使用 312 万次,自增主键仅 15 次,另两个在三位数量级。清理判断见索引清单。
node_route_prefix_hashes
每节点 per-prefix 规范化哈希基准,是差分写的第二级门控。哈希算法为 FNV-1a64 over 定序数组 JSON(services/routing.py);基准缺失时自动退回一次无害的整表重写。
| 列 | 类型 | 约束 / 默认 | 说明 |
|---|---|---|---|
node_id | String(64) | PK,FK→nodes.node_id(CASCADE) | 主键即外键 |
hashes | JSON | NOT NULL,默认 {} | prefix → 规范化哈希 映射(整节点一行一份基准) |
updated_at | DateTime(tz) | server_default now,带 onupdate | 更新时刻 |
10 行占 1288 kB:单行 JSON 装着该节点的全部前缀哈希(千级键),是全库单行最大的活跃列。
本表尚未纳入迁移链,缺表时路由明细退化为整表重写。见已知偏差。
node_routing_events
append-only 路由表时间序列,每次全表快照一条并附 churn。按节点保留最近 17 280 条(_HISTORY_KEEP)——采集约 300 s 一次即约 60 天,与 L3 存档同窗口。
| 列 | 类型 | 约束 / 默认 | 说明 |
|---|---|---|---|
id | Integer | PK,autoincrement | 主键 |
node_id | String(64) | FK→nodes.node_id(CASCADE),NOT NULL,index | 所属节点 |
captured_at | DateTime(tz) | 可空 | 采集时刻 |
route_count / route_count_v4 / route_count_v6 | Integer | NOT NULL,默认 0 | 表规模计数 |
rpki_valid / rpki_invalid / rpki_not_found | Integer | NOT NULL,默认 0 | RPKI 计数 |
announced / withdrawn | Integer | NOT NULL,默认 0 | 相对上次快照的 churn(前缀集合的增减,不含路径属性变化) |
created_at | DateTime(tz) | server_default now,index | 写入时刻 |
本表虽是时间序列,却不在分区化目标之列:主键是自增 id,转换要动主键,而它只有约 3.7 万行。裁剪按条数而非时间,也不适用桶模型。