外观
分层与存储角色
数据放在哪一层、依据是什么。这是数据层最上游的决策,表定义与写入路径都是它的推论。
五层数据模型
从最权威、最持久,到最易失、最可重建。层次决定备份策略、丢失后果与恢复方式——这三者构成分层的实际意义。
| 层 | 内容 | 存储 | 生命周期 | 丢失后果 | 恢复方式 |
|---|---|---|---|---|---|
| L1 配置事实源 | 节点身份、Peering、DNS、密钥、端口池、管理员账号 | PostgreSQL | 常驻 | 全站不可用,且不可重建 | 仅能从备份恢复 |
| L2 运行观测 | 各节点最新健康、路由全表、差分基准 | PostgreSQL | 常驻(覆盖式 upsert[1]) | 视图空窗 | agent 下一轮上报即重建 |
| L3 时序存档 | 流量、指标、路由事件、flap 速率的降采样序列 | PostgreSQL | 保留 60 天 | 历史趋势断档 | 不可重建(既往采样无法追溯) |
| L4 持久瞬态 | flap 热态、agent 心跳、登录限速、WS 订阅、会话与凭据 | Valkey | TTL 秒至天 | 一次重登录或一格时序断档 | 功能自愈,无需干预 |
| L5 派生缓存 | desired-state 物化结果、健康与路由聚合、ASN 名、token 解析 | Redis | TTL 10 秒至 1 小时 | 仅延迟上升,不产生错误 | 下次读取时自动回填 |
三条推论,落到日常操作:
- 备份范围为 L1–L3 加 Cap 所属的那部分 L4(见备份边界)。备份 L5 意味着分层划分有误。
- L4 与 L5 的分界不是持久与否,而是是否为唯一副本。 L5 的每一条都能从 L1–L3 重算;L4 的 flap 热态无法重算——它是对一份已经过去的上报所做的差分基准。
- L3 是唯一「不可重建但可以丢失」的一层:丢失不影响功能,仅历史曲线缺失一段。
层是属性,不是目录
真实子系统通常跨层。流量同时占据 L2(差分基准)、L3(5 min 存档)与 L5(Redis 热窗口);flap 占据 L4(打分热态)、L3(速率存档)与事实层(告警与转移)。若按层切分文档,任一子系统都会被拆散,因此 tables/ 按子系统分册,层标注在每张表上。
存储实例与角色
| 实例 | 配置 | 角色 | 判据 |
|---|---|---|---|
postgres | PG 16,三个 database | L1–L3,事实源 | 需要事务、JOIN 或审计追溯,或丢失后不可重建 |
valkey | noeviction[2],RDB[3] save 300 100,maxmemory 256mb | L4,持久小状态 + Cap 的事实源 | 键值可寻址、必须带 TTL、需跨进程共享 |
redis | allkeys-lru[4],maxmemory 256mb,不落盘 | L5,可丢弃缓存 | 删除整个实例后功能仅变慢、不出错 |
注意:两者的差别在角色而非协议。 Redis 会淘汰键且不落盘,写入唯一副本等同于静默丢数据;Valkey 落盘且不淘汰,写入可丢弃缓存则长期占用 noeviction 额度,额度耗尽会连带导致 Cap 的验证码签发报 OOM,表现为登录中断。
db 编号分配
Redis 与 Valkey 各有 16 个逻辑 db。按应用分配,且同一应用在两个实例上使用同一编号——由编号即可判定归属,FLUSHDB 也能精确清除单个应用而不影响其他。
| db | 应用 | valkey(L4) | redis(L5) |
|---|---|---|---|
| 0 | Cap | 事实源:签名密钥对、sitekey、配置 | 不使用 |
| 1 | control-server | flap 热态、心跳、限速、WS 订阅、拨测中转、门户登录事务 | desired-state、健康、routing、流量窗口、ASN 名、token |
| 2 | auth-server | OAuth 授权码、WebAuthn ceremony | 预留 |
| 3 | registry-server | 预留 | 只读查询缓存 |
注意:db 仅隔离键名空间。 maxmemory、淘汰策略、持久化与 CPU(单线程)均为实例级配置,分库不等于资源隔离。
编号由 apps/control-server/app/tests/test_storage_discipline.py 静态检查约束:将某应用配置到其他应用的 db 上,测试会明确指出失败位置。
新数据的准入决策
按顺序判断,首个满足条件的分支即为结论:
三条硬性纪律,均由 test_storage_discipline.py 静态约束:
*_strict系列仅可用于 Valkey。 「失败即抛出」意味着调用方将其视为主存储,而 Redis 会淘汰键且不落盘。- 写入 Valkey 的每个键都必须带 TTL。 实例为
noeviction,无 TTL 的键将永久占用额度。 - PostgreSQL 中不存放寿命短于一天且无审计价值的行。 此类数据只会制造死元组[5]与 vacuum[6] 抖动。
四个已落地的判例
判据抽象,判例具体。以下四次迁移可直接对照:
| 数据 | 迁移方向 | 依据 |
|---|---|---|
| flap 打分热态(两级) | PostgreSQL → L4 | 短命、可重建,且为唯一副本,不能置于会淘汰的 L5 |
| 门户登录事务 | PostgreSQL → L4 | 30 分钟内消费一次即作废,无审计价值,不参与关联查询 |
| agent 心跳、登录限速 | 进程内存 → L4 | 多 worker 下必须跨进程共享 |
| 拨测与日志流中转 | 进程内存 → L4 | 同上,且单次作业天然横跨三个请求 |
反向判例同样重要:时序存档不迁移(不可重建,置于可丢失介质违反分层原则);告警的 started_at 与 peak_score 不迁移(需审计追溯)。取舍详见性能与容量。
备份边界
| 对象 | 是否备份 | 理由 |
|---|---|---|
dn42_control / dn42_auth / dn42_registry | 必备 | L1–L3,事实源 |
valkey 的 dump.rdb | 必备 | db 0 为 Cap 的事实源(14 个无 TTL 的键,含签名密钥对);db 1/2 一并覆盖 |
redis | 不备份 | L5 的定义即「删除后仅变慢不出错」,备份它意味着分层划分有误 |
脚本见 deploy/tools/backup_control_plane.sh;上线与恢复步骤见 deploy/docker/STORAGE-ROLLOUT.md。
注意: 备份集中敏感度最高的两张表是 node_wireguard_keys(全 fleet 隧道私钥,base64 明文)与 portal_sessions(上游 OIDC 令牌明文),访问控制应按此定级。
upsert:插入或更新的合并操作。行不存在则插入,存在则按主键更新,用于「每节点最新态」这类只关心当前值的写入。 ↩︎
noeviction:Redis / Valkey 的淘汰策略之一。内存达到maxmemory后拒绝所有写入并返回 OOM 错误,而非淘汰既有键。适用于存放唯一副本的实例。 ↩︎RDB:Redis / Valkey 的快照式持久化。按
save <秒> <变更数>触发全量落盘,与增量追加的 AOF 相对。 ↩︎allkeys-lru:内存达到上限时按最近最少使用(Least Recently Used)顺序淘汰任意键。适用于纯缓存实例,代价是任何键都可能在任何时刻消失。 ↩︎死元组(dead tuple):PostgreSQL 的 MVCC 机制下,UPDATE 与 DELETE 不就地修改数据,而是保留旧版本行。旧版本在无事务引用后成为死元组,占用空间直至被 vacuum 回收。 ↩︎
vacuum / autovacuum:PostgreSQL 回收死元组并更新统计信息的维护过程。autovacuum 为后台自动触发版本,其回收速度跟不上产生速度时,表体与索引会持续膨胀。 ↩︎