Files

290 lines
15 KiB
Markdown
Raw Permalink Normal View History

2026-08-04 14:46:12 +08:00
# 架构评审与修订建议
> 评审对象:《03-通用场景应用方案》《08-三系统职责划分》,以及工程侧 `04-architecture.md`
> 状态:**全部为建议,均未生效。** 每条给出现状、问题、建议、影响面和我的把握程度,供逐条裁决。
> 裁决后:采纳的条目改写对应文档正文,本文保留为决策依据;未采纳的条目保留并记明理由,避免以后重复讨论。
> 日期:2026-08-04
---
## 0. 总评
架构的骨架是对的,尤其三处:
1. **控制面/数据面分离解 NAT** —— 隧道只走 ONVIF 控制流量,视频边缘主动推、绝不过隧道,且明确"控制面丢失不停视频"。多数团队会把视频塞进 VPN 然后在带宽和单点上栽跟头
2. **水平触发对账器,只收敛不回滚** —— DB 单一真相源、幂等、退避、独立信号量、10% 孤儿删除安全闸。"不做跨系统回滚"是分布式里最易做错的决定,这里做对了
3. **Event / Alert 分离** —— 事件不可变,告警是带状态机的响应过程,投递三个事实分开存。多数系统合成一个 `notified` 布尔,然后永远回答不了"到底有没有人管"
下面 13 条是我认为需要修订或补空的地方。**A 组影响架构决策,B 组是数据层空缺。**
---
## A 组 · 影响架构决策
### A-1 Bell 的职责边界应按变化频率再切一刀
**把握程度:中**(这是判断,不是硬伤)
**现状**:Bell = 事件校验/存储 + 规则引擎 + 告警状态机 + 投递 + 反馈闭环 + 租户 RBAC + 审计 + 配额真相源 + 管理后台 + 前端 + 大屏 + 场景包。
**问题**:Sense 与 Brain 的边界很干净——按运行时和语言切,边界落在阻抗真正变化的地方。Bell 没有用同一把尺子。它内部有一条清楚的断层:
| | 事件/告警内核 | 管理面 |
| --- | --- | --- |
| 内容 | ingest、event、rule、alert、deliver、feedback | tenant、RBAC、audit、配额、web、packs |
| 变化频率 | 低 | 高 |
| 正确性要求 | 极高(状态机、幂等、重启续跑) | 常规 CRUD |
| 出错后果 | 告警丢失 | 页面报错 |
捆在一起意味着改一个权限要碰告警内核的部署。
**建议**:不必现在拆成两个部署单元,但**在 `Bell/internal/` 内部先立起这条边界**:核心包不得依赖管理面包,管理面通过接口调用核心。M4 若确需拆分,成本接近零;不拆也没有损失。
**影响面**:《08》§1.1 Bell 条目、§2.3 目录结构;`04-architecture.md` §2。
---
### A-2 配额不该走跨系统 API,应改为同库只读视图
**把握程度:高**
**现状**:《08》§3 边界 7、`04-architecture.md` §5 第 7 条——Bell 拥有 `site.max_video_channels`,Sense 通过"版本化内部 API 或只读投影"读取,并定义了配额服务不可达时的降级语义;`api.md` §2 已把它列为待 M2 设计的接口。
**问题**:为了一个整数,引入了三个活动部件(接口、超时重试、降级态)。而架构第 5 条已经定了**一个 PostgreSQL 实例,schema 分离**——同实例意味着这次跨界根本不需要网络。
现在的设计同时付两份成本:既没有独立库的隔离好处,又背上了微服务的仪式感。
**建议**:改为只读视图,视图名带版本号以保住"版本化"这个要求:
```sql
CREATE VIEW bell.site_quota_v1 AS
SELECT id, tenant_id, max_video_channels FROM bell.sites;
GRANT SELECT ON bell.site_quota_v1 TO sense_app;
```
Bell 改表结构时只要视图签名不变,Sense 不受影响——这正是版本化想要的效果。
**连带收益**:省掉一个内部接口、一套超时重试,以及《12》S-09 里一半的降级态工作量(区域策略那半仍需保留,因为它可能来自 Sense 自己的写路径)。
**影响面**:《08》§3 边界 7;`04-architecture.md` §4 步骤 1、§5 第 7 条、§7;`api.md` §2 删去 `Sense → Bell 读取站点视频配额` 一行;《12》S-09 缩小范围。
> ⚠️ 若未来确定 Sense 与 Bell 分库部署,本条自动失效,回退到 API 方案。裁决时请一并确认"一个实例"这个前提的有效期。
---
### A-3 「Brain 无状态」的表述需要收紧
**把握程度:高**
**现状**:`04-architecture.md` §2 称 Brain「Python/CUDA,业务无状态」;《08》§1 称「无状态」。
**问题**:判定内核是 `NORMAL → SUSPECT → CONFIRMED → RECOVERING`,**每个 track 一份、带时间窗**。这就是状态。文档说的"业务无状态"实际含义是"无 DB schema",但两者不等价,差别会在 M4 分片时显现:
- 进程在某人处于 SUSPECT 时重启,那次判定怎么办?
- 分片再平衡时,track 状态机迁不迁移?
- 同一个人从 media-01 机位走到 media-02,两个 worker 各持一份状态,如何不重复报警?
**建议**:把表述改为——
> Brain **无持久化业务状态**;判定状态机是**进程内易失状态**,重启即丢失。已接受的代价是:重启瞬间正在进行的单次判定丢失,不影响已产出事件与后续判定。跨 worker 的同一目标关联由 `anon_id`(ReID)在 Bell 侧聚合承担,不依赖 Brain 之间共享状态。
若这个代价不可接受,需在 M4 分片前给出方案,不能靠"无状态"这个词绕过去。
**影响面**:《08》§1 总表与 §1.1 Brain 条目;`04-architecture.md` §2 表格。
---
### A-4 M1 必须引入一个假 reader,否则对账器在真空里开发
**把握程度:高**
**现状**:`04-architecture.md` §10——M1、M2 只动 Sense,M3 才有 Brain 和 Bell。
**问题**:对账器的全部意义是把 mediamtx 收敛到 DB 期望态。但 `sourceOnDemand: yes` 的语义是**有 reader 才拉流**,而 Brain 不在就没有 reader。于是 M1–M2 的对账器是在对着一个什么都不做的系统收敛。
四条硬约束里最要命的两条——**改 path 配置会踢掉当前发布者**、**推理 source 挂上去就是一个 reader**——都要到 M3 才第一次真正暴露。M2 的出口标准会给出虚假的安全感。
**建议**:M1 引入一个最小假 reader(`ffmpeg -i rtsp://… -f null -`,或一个只拉不解码的 gortsplib 客户端),作为 Sense 测试装置的一部分。目的不是功能,是**让 sourceOnDemand 的语义在 M1 就受力**:验证有 reader 时才拉流、卸载 reader 后 30s 自动停、改 path 配置会踢掉发布者。
**影响面**:`04-architecture.md` §10;《03》§6.1 M1 骨架增加 `testutil/fakereader`;M1/M2 出口标准增加一条。
---
### A-5 先做纵向切片,不要等到 M3 才有端到端
**把握程度:中高**(这是排期建议,不是架构缺陷)
**现状**:`Sense/`、`Brain/`、`Bell/` 是三个空目录。围绕它们已有 13 份 raw 文档、12 份工程文档、一份冻结契约(schema + 负面测试 + 三个夹具)、两个原型、三轮原型评审、一份 24 条待办清单。按现有顺序,第一个端到端价值出现在 M3。
**问题**:设计密度显著领先于证据。所有架构判断——分片、配额、对账、多租户——在 M3 前都拿不到反馈。
缓解因素是真实的:silver_pose 已验证、文档大量源自一个交付过的项目。所以这不是空想,但风险形状很具体。
**建议**:在 M1 内插入一条**纵向切片**,复用 silver_pose 已跑通的链路:
```
1 路摄像头 → mediamtx → silver_pose(出 v0.1 事件)→ 最小 Bell(一张表 + 一条通知)
```
对账器可以只有 50 行,Bell 可以只有一张表。目的不是交付,是**让契约、sourceOnDemand 语义、事件流转在 M1 就受一次真实的力**。
现有架构完全支持这么做——三系统边界清楚、契约已冻结。缺的不是设计,是把"先纵切一刀"排进里程碑。
**影响面**:`04-architecture.md` §10;`06-tasks.md` 路线图。
---
### A-6 L2/L3 的分层边界是名义上的,真边界只有一条
**把握程度:中**
**现状**:分层 L0–L5,L2 是流水线(Savant),L3 是能力(检测/姿态/跟踪/ReID)。
**问题**:Savant 的模型就挂在 pipeline 里,这条线在实现时会消失。把它当作可独立替换的边界会产生错误预期。
**建议**:文档中明确——L2/L3 是**认知分层**,便于讨论职责;**唯一可独立替换的技术边界是 `Detector` / `PoseEstimator` 接口**。那条接口设计得早、目的明确(AGPL 逃生),是真的边界,应单独强调而不是淹没在六层里。
**影响面**:《03》§1.1–1.2 分层说明;`04-architecture.md` §1。
---
## B 组 · 数据层空缺
### B-1 设备遥测的历史数据没有归属 · 建议优先处理
**把握程度:高**
**现状**:原型已展示「时间漂移 +3.8s」「重连历史」「最后遥测」「分片实际码率」。这些是**时序业务数据**,当前文档中无任何归属。
**问题**:
- 全量塞 Postgres:128 路 × 每分钟一条 ≈ 一年 6700 万行。会成为库里最大的表,却是价值密度最低的数据
- 塞 Prometheus:那是运维指标,保留期短,且不适合按设备做业务查询("这台摄像头上个月的漂移趋势")
**建议**:
| 数据 | 存哪 |
| --- | --- |
| 当前值(最后一次漂移、当前分片、实际态) | `sense` schema,随设备行 |
| 最近 N 条(默认 100)事件式记录:重连、校时、状态跃迁 | `sense` schema,独立表 + 定期裁剪 |
| 连续趋势(码率、漂移曲线) | Prometheus |
| 若确需长期业务查询 | **TimescaleDB 扩展**——仍是同一个 PG 实例,不违反"一个实例"的决定 |
**影响面**:《08》§1.1 Sense 条目;`04-architecture.md` §8;`03-tech-stack.md` 增加 TimescaleDB 作为条件性选项。
### B-2 Brain 的本地重试队列形态未定
**把握程度:高**
**现状**:`04-architecture.md` §7「Brain 投递失败落本地队列重试,不阻塞实时推理主链路」——只有语义,没有形态。
**问题**:Brain 号称无 schema、无持久化状态,**这个队列是它唯一的持久化**,而且直接决定 Brain 崩溃时丢不丢事件。
**建议**:明确选型(文件追加 / SQLite / BadgerDB 任一皆可,但必须选定),并定义:队列上限、超限后的丢弃策略(**建议丢最旧,且丢弃必须产生运维告警**)、重启后的恢复顺序、重复投递由 Bell 侧 `source_event_id` 去重。
**影响面**:《08》§2.2 Brain 目录(`emit/publisher.py` 旁增加队列实现);`04-architecture.md` §7;`api.md` §2。
### B-3 边缘节点的断网缓存形态未定,且需与环形缓冲区分
**把握程度:高**
**现状**:需求 §3.5「断网时边缘缓存事件,恢复后补传」;《03》另有"边缘环形缓冲"用于 pre-roll。
**问题**:这是**两种不同的东西**,文档中容易混为一谈:
| | 环形缓冲 | 事件缓存 |
| --- | --- | --- |
| 内容 | 视频(最近 N 秒) | 结构化事件 + 元数据 |
| 覆盖策略 | 循环覆盖,丢失可接受 | **不可静默丢失** |
| 用途 | pre-roll 证据回捞 | 断网续传 |
**建议**:文档中显式区分两者,并为事件缓存定义容量上限、超限策略与补传进度的可观测指标(对应《12》S-10)。
**影响面**:《03》§2.6 证据回捞;《08》§1.1 Sense 条目。
### B-4 M1 SQLite → Postgres 的切换时机未定
**把握程度:高**
**现状**:《03》§6.1 与《08》§2.1 都写「M1 先 SQLite,schema 与生产 Postgres 保持一致」,但没写何时切、是否一次性、有无迁移脚本。
**建议**:钉在 **M2 入口**,且切换前 schema 必须已用 Postgres 语法编写(SQLite 只作为运行时,不作为 schema 方言的来源)。迁移脚本从第一天就写 Postgres 版本,SQLite 通过兼容子集运行。
**影响面**:`04-architecture.md` §10;《08》§5 里程碑表。
### B-5 用数据库角色强制 schema 边界
**把握程度:高**
**现状**:架构第 7 条写「不跨 schema 直接写」。
**问题**:只靠约定,迟早会被一个赶工的 JOIN 破掉,而且破掉时没有任何信号。
**建议**:在 DB 层强制:
```sql
CREATE ROLE sense_app; GRANT USAGE ON SCHEMA sense TO sense_app;
CREATE ROLE bell_app; GRANT USAGE ON SCHEMA bell TO bell_app;
-- sense_app 对 bell schema 无任何权限,A-2 的 site_quota_v1 视图除外
```
这样"不跨 schema 写"从纪律问题变成权限问题——写错了连不上,而不是上线后才发现。
**影响面**:`04-architecture.md` §5 第 5、7 条;部署清单。
### B-6 建库、建角色、建 extension 的归属无人认领
**把握程度:高**
**现状**:`sense` schema 的迁移在 `Sense/`,`bell` 的在 `Bell/`。但**建数据库本身、建角色、安装 pgvector / TimescaleDB extension** 不属于任何一方。
**建议**:归入独立部署清单(与 MediaMTX、MinIO、Prometheus 同级),在 M2 之前指定归属。否则会变成"谁先跑谁建",各环境不一致。
**影响面**:《08》§1.2 基础设施行;部署清单。
### B-7 人脸相关存储的位置需在 M5 前复核
**把握程度:中**
**现状**:《02》定人脸底库「独立 schema、独立加密、独立审计」,`face_vectors.embedding` 用 `VECTOR`(pgvector)。
**问题**:「独立 schema」与「独立加密」在同一个 PG 实例内能做到什么程度,需要具体方案(列级加密?TDE?还是独立实例?)。现在的表述在合规评审时会被追问。
**建议**:M5 前明确——若"独立加密"要求达到密钥与业务库分离的程度,则人脸库应是**独立 PG 实例**,此时它是"一个实例"决定的合法例外(因为它不参与对账,不破坏单一真相源前提)。
**影响面**:《02》§11 SQL 模型;`04-architecture.md` §5 第 5 条增加例外说明。
---
## 汇总:需要修订的文档章节
| 建议 | 《03》 | 《08》 | `04-architecture.md` | 其他 |
| --- | --- | --- | --- | --- |
| A-1 Bell 内部边界 | — | §1.1、§2.3 | §2 | — |
| A-2 配额改视图 | — | §3 边界 7 | §4、§5-7、§7 | `api.md` §2 删一行;《12》S-09 缩范围 |
| A-3 Brain 状态表述 | — | §1、§1.1 | §2 | — |
| A-4 M1 假 reader | §6.1 | — | §10 | M1/M2 出口标准 |
| A-5 纵向切片 | — | — | §10 | `06-tasks.md` |
| A-6 L2/L3 边界 | §1.1–1.2 | — | §1 | — |
| B-1 遥测归属 | — | §1.1 | §8 | `03-tech-stack.md` |
| B-2 Brain 队列 | — | §2.2 | §7 | `api.md` §2 |
| B-3 两种缓存 | §2.6 | §1.1 | — | — |
| B-4 SQLite 切换 | §6.1 | §5 | §10 | — |
| B-5 DB 角色 | — | — | §5 | 部署清单 |
| B-6 建库归属 | — | §1.2 | — | 部署清单 |
| B-7 人脸存储 | — | — | §5 | 《02》§11 |
---
## 建议裁决顺序
| 顺序 | 条目 | 理由 |
| --- | --- | --- |
| 1 | **A-2**(配额视图) | 影响 `api.md` 待设计接口清单与《12》S-09 的范围,越早定省的工作越多 |
| 2 | **A-4**(M1 假 reader) | 影响 M1 任务拆分,且成本极低 |
| 3 | **B-1、B-2、B-3、B-4**(数据层四空缺) | 都是补空不是改决策,无争议 |
| 4 | **B-5、B-6**(DB 角色与建库归属) | 部署侧,M2 前必须有 |
| 5 | **A-3**(Brain 状态表述) | 改表述,但会牵出 M4 分片的真问题,宜早不宜迟 |
| 6 | **A-5**(纵向切片) | 排期决策,需与商务节奏一起看 |
| 7 | **A-1、A-6、B-7** | 判断性/远期,可延后 |