外观
路线图与状态
本文是前瞻性待办:当前两条主线是「控制信息单一真相源清理」与「控制面 Cloudflare Workers 迁移」,外加若干 1.0 后增强项。已交付能力以代码与测试为准,原理见 internals/architecture.md。
已交付(摘要)
- 控制面闭环:整节点 provision、CRUD+materialize 单事务、materialize 行级锁串行化、generation 保留窗口/读取/diff/回滚、节点退役-收敛、peering 聚合根 + 一键化、健康五态强类型视图。
- Agent 闭环:source→render→observe→plan→execute→report;内容寻址
dn42.config_hash最小扰动;tri-state 采集治"假绿";Session 401 自愈;WG 私钥本地生成 + RSA-OAEP 托管 + 公钥 409 闸门;reconcile 指标落盘 +--doctor;声明式 underlay 重建;WG endpoint 周期重解析;L3 漂移自愈(退避 + 熔断);route collector 喂送;自更新(控制面分发 wheel + 健康门回滚,见 guides/upgrades-and-migrations.md)。 - 安全:token 仅存 SHA-256、过期/轮换、注册审批闸门、Admin API fail-closed、append-only 审计日志。
- 可视化/运维:SvelteKit 管理 UI(独立仓库 dn42-control-webui)+ 一键互联向导、UI 专用 BFF(趋势 / 仪表盘 / 榜单 / 实时拨测)、内部互联(iBGP/OSPF)合成视图与一致性不变量(见 internals/internal-routing.md)、共享 DNS 组 + anycast(见 guides/dns-and-anycast.md)。
主线:控制面 Cloudflare Workers 迁移(已切流,2026-07-14)
TS 面(apps/control-server-ts:Hono + drizzle/Hyperdrive + Durable Objects,入口 api.natlan.io,2026-07-14 由 beta.api.natlan.io 转正)已全功能对齐并承接全部 9 节点 agent 流量;Python 面退为温备(入口 legacy.api.natlan.io,回滚 = 逐节点恢复 <node>.env.bak-py-plane)。两面行为变更仍须锁步,直至 Python 面正式退役。 wrangler.jsonc 的 routes 已同步为 api.natlan.io,旧 beta.api.natlan.io 域名已下线。
主线:数据库入 Cloudflare(下一步,压缩运营成本)
目标:控制面数据层脱离租赁主机/数据库服务,终态基础设施 = Workers Paid($5/月, 内含额度基本覆盖)+ 节点租金,无独立控制面主机。
关键约束(按 2026-07 实测数据):Cloudflare 存储(D1 / DO-SQLite)按行读写计费 (写 $1/百万行,月内含 5000 万行;删除也计写)——完整价目与逐项成本测算见 reference/cloudflare-billing.md。现有两个"整表替换" 写路径在该模型下不可负担,是迁移的真正前置:
| 写路径 | 旧现状(PG 无所谓) | 直接搬 CF 的月成本 | 改造(2026-07-14 已双面上线) |
|---|---|---|---|
| 路由明细整表重写 | 12.8k 行/节点,哈希门控,~12-88 次重写/时 | $200+ /月 | 差分写:per-prefix FNV-1a64 哈希基准表 node_route_prefix_hashes,只 delete+insert 变化/消失的前缀(每轮几十行);无基准首轮整表重写自举 |
| prefix-flap 打分整表替换 | 200 行/节点 × 每 60s 全量 | ~$155/月 | 热态出 SQL:打分条目+路径证据存 KV(TS 面)/ Redis(Python 面)单节点 blob pflap:<node>;SQL 只留 meta+速率存档;存储不可用自动回退旧 SQL 路径 |
其余表(rollup upsert、状态、事件)写率无碍;全库 118MB,远低于 10GB 上限。
选型倾向:Durable Object + SQLite(而非 D1):
- DO-SQLite 的同步 SQL +
transactionSync提供真·交互式事务——materializer / wireguard key 传播 / mitigations 的"读-算-条件写"原子性是本项目当初选择单 Worker 的立项理由,D1 只有 batch 事务(预构造语句列表),十来条写路径要全部重构成乐观并发; - drizzle 有
durable-sqlite驱动(迁移可跑在 DO 里);30 天 point-in-time recovery 内置; - 架构形态:单 "ControlDB" DO 承载数据层 + 事务性服务层,Worker 经粗粒度 RPC 调用 (本项目写并发极低,单点 DO 即等价今天的单 PG 源);观测热态并入该 DO 内存;
- 代价:单点位(无全球读复制,与今天单 PG 等价)、schema 换 SQLite 方言、数据层全量改 drizzle 驱动。D1 的全球读复制对单操作员 + agent 工作负载价值有限。
分阶段:
- P0(零代码,立即省钱):Python 面观察期结束后退役 GCP compose 栈;
dn42-ts-pg迁至任一现役节点(机器反正在租,118MB 库无压力),Hyperdrive 换源即可(已演练); 每日pg_dump→ R2 备份;GCP 主机退租。hkg1 冻结库同步退役。 状态(2026-07-14 决策):GCP 主机预算尚余约 80 天(至 2026-10 初),暂不迁移; 窗口期内优先完善 Worker(P1 已完成),到期前执行迁库 + 退役。前置事实:agent 已 零连接、Python 面纯温备,主机上唯一硬依赖是dn42-ts-pg(Hyperdrive 源库)。 - P1(写路径瘦身,双赢)✅ 2026-07-14 完成:路由明细差分写 + prefix-flap 热态 出 SQL,两面同步实现并上线(跨面哈希算法逐字节一致;读侧四函数统一走热态装载器, blob 缺失逐节点回退旧 SQL 行)。上线验证:打分 SQL 表冻结、榜单/告警/时序保持新鲜。
- P2(存储迁移):ControlDB DO 落地(schema 方言 + 数据层驱动 + 事务写路径迁入 DO), parity harness 对拍护航。
- P3(切换):沿用 2026-07-14 切流剧本(导出→灌入→对拍→切换→留回滚)。
主线:控制信息单一真相源清理(进行中)
北极星
控制面里存了很多"事实"。其中副本——同一事实的独立第二份,没有机制保证一致,renumber / 改密钥 / 改配置时"东改一处、漏改一处"且改不干净(历史事故 pvg2 postmortem,运维叙事档案已移出本仓库)。
北极星:把"副本"逐个变成"派生"(render/normalize/materialize 时从唯一真相源算出,不落独立存储)。概念分类与地址层面的完整盘点见 reference/addressing-model.md。范例 _normalize_dns_anycast:DnsGroup.bind_addresses 单源 → 派生 dns-anycast 接口地址 + 任播前缀。
已完成(行为保持,golden 逐字节对账绿)
| 重复的事实 | 改法 |
|---|---|
节点 LLA fe80::X 各存一份在每条外部 eBGP 接口 addresses | NodeSpec.link_local 单源;materialize 派生注入外部 eBGP WG 接口;backfill_node_lla.py 剥副本 |
内部对端 wireguard_peer.public_key 存 spec 副本 | materialize 按 peering.remote_node_id 从对端 Node.wireguard_public_key 派生注入 |
base_template.node.* 是 nodes 表列的陈旧副本 | provision 剥掉被 _node_payload 覆盖的身份字段 |
索引列 name/kind/remote_asn/enabled ↔ spec JSON 双写漂移 | WgInterface/BgpSession.apply_spec 从校验 spec 单源投影索引列 |
node_routing.routes 死列(明细已在 node_route_entries) | 迁移 f2a3b4c5d6e7 删列 + 从模型移除 |
待定(需人参与,不宜盲改)
每条都触及活跃 fleet 行为 / 存量数据 / agent 协议契约 / 寻址设计决策,应配合单节点灰度或一次显式决策再做。
| 重复的事实 | 待做 | 为何需人参与 |
|---|---|---|
internal_topology.hosts[*].ownip/ownip6 ← 各节点 Node.loopback_* | materialize + UI + 渲染三处都从 Node.loopback_* 派生 hosts;并加"某节点 loopback 变更时重物化所有引用它的节点"编排 | 多消费者 + 牵动全 fleet iBGP,须逐节点灰度验证收敛(不可逆风险) |
bgp_sessions[*].source_address ← 绑定 interface.addresses | source_address 改可选 + 从绑定接口按地址族派生 + 强校验 | 牵动寻址模型决策(per-link /31 vs 节点单 /32+LLA,见 reference/addressing-model.md),须先拍板 |
router_id 与 loopback_ipv4 常恒等却分存 | 二选一为源,另一派生 | 破坏性 schema 变更,影响存量节点数据 |
NodeSpec.region → Bird2ConfigSpec.region | 改名 region_override 使覆盖意图显式 | StrictModel extra=forbid 需 alias/迁移 |
BgpSessionSpec.neighbor → peer_routes 镜像 | 加 validator:neighbor host 路由 ∈ peer_routes | 硬校验可能拒绝存量配置,须先全量核查 |
applied_generation → observed_generation 术语混淆 | schema 注释/改名澄清 | 改 agent 上报协议字段 = 版本契约,需协调升级 |
推进方式:每条独立成 PR,配合单节点灰度 / 一次设计决策 / 存量迁移 / agent 协调升级;每消灭一个副本,更新 reference/addressing-model.md 的副本清单。
1.0 后(其它增强)
| 事项 | 说明 |
|---|---|
| 多副本控制面 | EventBus 换 Redis Pub/Sub + 默认 Postgres,支持横向扩展(当前单进程 MVP,agent 周期 reconcile 兜底) |
| 批量 / fleet 重物化 | 模板升级后一键全节点重物化 |
| Admin RBAC / 多管理员 | 当前单 admin token |
| per-service toggle | 可选叶子服务(dns / debug-shell)的单独启用/禁用端点 |
| Agent 自身 healthz HTTP 探针 | 指标已落盘 + doctor 已就绪;如需被外部探活再补本地 HTTP 端点 |
| Agent token 形状强制 | 自动生成 token 形如 <id>.<secret> 与 base64url 字面量不同形,统一后在鉴权侧强制 |
| 更多 runtime backend | 纯 systemd 网络管理等 |