262 lines
19 KiB
Markdown
262 lines
19 KiB
Markdown
# 项目结构与工程约束
|
||
|
||
## 1. 目标
|
||
|
||
1. 两个独立程序加一个前端:Agent、Backend 各自拥有解决方案,可以单独构建、测试和发布;Web 是独立的 Vite 项目(`web/`)。
|
||
2. 通用能力只写一次:TCP/QUIC Datagram/TLS/ICMP 采集积木(类似 iperf3 的能力,但只记录原始数据)放在 `MoShiro.Measurement` 中,Agent 共用;采集方法(Kind)由积木组合声明而成。
|
||
3. 节点机制通用化:注册、身份、连接、RPC、可靠上报、状态同步、宿主,由与业务无关的 Fleet 框架提供(见 [Fleet 文档](../../backend/fleet/README.md))。MoShiro 只写业务,其他产品也可以复用 Fleet。
|
||
4. 单一真源:协议、原始数据格式、测试方案、指标、错误码、版本号、编译设置、代码风格都只有一处定义,改动后下游自动更新,或在编译期、CI 中报错。
|
||
5. 模块化:Agent 由可插拔的模块组成;Backend 按层分成多个项目,层之间的依赖在编译期强制。
|
||
6. 轻量:运行时只依赖 BCL 和仓库内的库,不引用任何 NuGet 包。
|
||
|
||
## 2. 仓库布局
|
||
|
||
```text
|
||
moshiro/
|
||
├─ README.md AGENTS.md .gitignore
|
||
├─ docs/
|
||
├─ scripts/ # publish / package / smoke / stability 等脚本
|
||
├─ packaging/ # install.sh、systemd 单元、OpenWrt 文件
|
||
├─ web/ # 前端:Vite + React + TypeScript(自写),只依赖 backend/openapi/v1.json
|
||
│
|
||
├─ global.json Directory.Packages.props MoShiro.slnx # SDK、NuGet 版本和全部 .NET 项目入口
|
||
│
|
||
├─ ci/MoShiro.Tar/ # CI 归档工具,也加入根解决方案
|
||
└─ backend/ # 服务和节点 C# 代码(构建产物在 backend/artifacts/)
|
||
├─ Directory.Build.props MoShiro.slnx # 后端编译设置和后端目录解决方案
|
||
├─ .editorconfig BannedSymbols.txt # 风格与禁用 API(构建期强制)
|
||
├─ BannedSymbols.Node.txt # 仅用于常驻节点进程:限制节点能力面
|
||
├─ fleet/ # 通用节点框架(不认识 MoShiro,以后可以拆成独立仓库)
|
||
│ ├─ src/ Fleet.Core Fleet.Identity Fleet.Rpc Fleet.Link Fleet.Hosting
|
||
│ │ Fleet.Enrollment.Client Fleet.Server.{Abstractions,Enrollment,Link}
|
||
│ ├─ tools/ fleet-enroll
|
||
│ └─ tests/
|
||
├─ shared/ # MoShiro 自己的共享库
|
||
│ ├─ MoShiro.Protocol/ # 契约真源:Kind 目录、任务集、moshiro.* 方法、测量包、RawChunk、错误码
|
||
│ ├─ MoShiro.Measurement/ # 采集引擎:积木、行表、合并、预算(见 measurement.md)
|
||
│ └─ tests/
|
||
├─ agent/ src/MoShiro.Agent/ tests/ # VPS 端采集器
|
||
├─ src/ # Backend(面板)
|
||
│ ├─ MoShiro.Backend/ # 宿主与组合根(Program.cs)
|
||
│ ├─ MoShiro.Backend.Api/ # API 层:HTTP 端点、DTO、错误映射
|
||
│ ├─ MoShiro.Backend.Gateway/ # 在 Fleet.Server.Link 上注册 MoShiro 的方法、状态与收件箱
|
||
│ ├─ MoShiro.Backend.Application/ # 用例层 + 端口接口
|
||
│ ├─ MoShiro.Backend.Analysis/ # 分析(纯计算,见 analysis.md)
|
||
│ └─ MoShiro.Backend.Storage.Sqlite/ # 端口接口的实现,同时实现 IFleetStore
|
||
├─ tests/
|
||
├─ openapi/v1.json # 构建生成并提交
|
||
└─ (解决方案见 backend/MoShiro.slnx)
|
||
```
|
||
|
||
节点机器上的发布物是两个独立进程,注册和采集分开:`fleet-enroll`(一次性运行)和 `moshiro-agent`(常驻)。更新:Quadlet 装的 Agent 由节点退出后 systemd 重新拉镜像,Backend 和一行脚本装的节点由宿主机上的 `moshiro-update` 完成,见 [deployment.md](../ops/deployment.md)。
|
||
|
||
## 3. 依赖关系
|
||
|
||
```text
|
||
┌────────┐ ┌────────┐ ┌──────────────────────────────┐ ┌─────┐
|
||
│ 拨号方 │ │ Agent │ │ Backend (Api/Gateway/App/ │─openapi─>│ Web │
|
||
└─┬──┬───┘ └─┬──┬───┘ │ Analysis/Storage) │ └─────┘
|
||
│ └───┬───┘ │ └───┬───────────────┬──────────┘
|
||
│ ▼ │ │ │
|
||
│ ┌───────────┴┐ │ │
|
||
│ │Measurement │ │ │
|
||
│ └─────┬──────┘ │ │
|
||
▼ ▼ ▼ ▼
|
||
┌──────────────────────────────────┐ ┌─────────────────────────┐
|
||
│ MoShiro.Protocol │ │ Fleet.Server.* │
|
||
└────────────────┬─────────────────┘ └───────────┬─────────────┘
|
||
▼ ▼
|
||
┌──────────────────────────────────────────────────────────────────┐
|
||
│ Fleet(节点侧):Core · Identity · Rpc · Link · Hosting │
|
||
└──────────────────────────────────────────────────────────────────┘
|
||
反向依赖一律禁止:fleet/** ✗→ MoShiro.*
|
||
```
|
||
|
||
简要来说:
|
||
|
||
```text
|
||
Agent ──> Measurement ──> MoShiro.Protocol ──> Fleet.Core / Fleet.Rpc
|
||
Agent ──> Fleet.Link / Fleet.Hosting / Fleet.Identity
|
||
Backend.* ──> MoShiro.Protocol + Fleet.Server.* (不引用 Measurement)
|
||
fleet-enroll ──> Fleet.Enrollment.Client ──> Fleet.Identity / Core
|
||
Web ──> openapi/v1.json
|
||
```
|
||
|
||
| 项目 | 允许依赖 | 禁止 |
|
||
| --- | --- | --- |
|
||
| `Fleet.*`(节点侧) | BCL、其他节点侧 Fleet 库 | 任何 NuGet 运行时包;任何 `MoShiro.*` |
|
||
| `Fleet.Server.*` | ASP.NET Core、`Microsoft.Data.Sqlite`(仅 Sqlite 项目)、节点侧 Fleet 库 | 任何 `MoShiro.*` |
|
||
| `MoShiro.Protocol` | Fleet.Core、Fleet.Rpc | 任何 I/O、套接字、日志 |
|
||
| `MoShiro.Measurement` | MoShiro.Protocol、Fleet.Core | NuGet;日志(只通过计数接收器上报);读取配置;统计或解释逻辑 |
|
||
| Agent | 上述库,以及 Fleet.Link / Hosting / Identity | NuGet 运行时包;彼此互相引用;Fleet.Enrollment.Client |
|
||
| Backend.* | MoShiro.Protocol、Fleet.Server.*,以及第 5.2 节列出的包 | Measurement、EF Core 等依赖反射的库 |
|
||
|
||
分析器类包(`PrivateAssets="all"`)不受限制。`ArchitectureTests` 在构建时检查上述规则。
|
||
|
||
## 4. 共享库
|
||
|
||
### 4.1 Fleet(通用节点框架)
|
||
|
||
组件、扩展点和协议细节见 [../backend/fleet/README.md](../../backend/fleet/README.md)。
|
||
|
||
### 4.2 MoShiro.Protocol(MoShiro 的契约真源)
|
||
|
||
```text
|
||
MoShiro.Protocol/
|
||
├─ ProtocolLimits MoShiroErrors(在 Fleet 错误码基础上扩展) IpVersions PublicAddress Ipv6Diagnostic
|
||
├─ Ids/ AgentId(= Fleet NodeId)、NodeId、TaskId、TestId
|
||
├─ Catalog/
|
||
│ ├─ Kinds/ Kind 声明(积木组合 + ParamSpec + 记录列 + 预算)
|
||
│ ├─ KindSpec Kind 与记录流(StreamSpec)的类型定义
|
||
│ └─ ParamSpec 参数定义(类型、默认值、上下限),节点与 Backend 共用同一套校验
|
||
├─ Tasks/ TaskSet(Fleet 期望状态的文档类型)、调和结果
|
||
├─ Rpc/ moshiro.* 方法表与 params/result、二进制通道号(从 16 开始)
|
||
├─ Wire/ QUIC Datagram 测量包、TCP 数据连接首帧
|
||
└─ Raw/ 自描述 RawChunk:头部、列类型、列编解码、Brotli、crc32c
|
||
```
|
||
|
||
累加器、直方图等统计类型只属于 `Backend.Analysis`,不在 Protocol 中。测试方案(Plan)只存在于 Backend(`Application/Tests/Plans.cs`)。
|
||
|
||
### 4.3 MoShiro.Measurement(采集引擎)
|
||
|
||
包含:
|
||
|
||
- 积木:Transport、Pacer、Payload、Behavior、Sampler;
|
||
- 按 Kind 声明驱动的行表(`RowStore`)与列块合并(`FragmentMerger`、`ChunkFinalizer`);
|
||
- `BlockCatalog`:Kind 到拨号方 / 监听方积木的映射;
|
||
- `BudgetGuard`:参数校验与本地硬上限。
|
||
|
||
任务调和在 Agent 的 `TaskManager` 与 拨号方 中完成。
|
||
|
||
详见 [measurement.md](../design/measurement.md) 第 6 节。
|
||
|
||
## 5. 模块化
|
||
|
||
### 5.1 Agent:组装
|
||
|
||
节点组装点为 `AgentApp`,显式连接各模块,不使用 DI 容器或反射扫描:
|
||
|
||
```text
|
||
AgentApp
|
||
FleetLink -> Backend;TaskSet、DialTargets、心跳和 RawChunk 发件箱
|
||
DialerHost / DialDirectory -> 每个目标及 IP 族一个 DialConnection
|
||
DialConnection -> TLS 控制会话、TaskLink、任务角色和列块发件箱
|
||
PeerListener / PeerHub <- 其他 Agent 的接入会话
|
||
QUIC / TCP <- 数据面监听(listen.enabled 默认 true)
|
||
TaskManager / TestRunner / ChunkAssembler
|
||
RestartUpdater / OpenWrtUpdater
|
||
```
|
||
|
||
拨出和接入可同时运行。Backend 的线路策略默认建立所有可拨号 Agent 之间的双向有向线路,排除自连接。节点通过同一条 FleetLink 上报两侧状态。模块使用构造参数或 `Channel<T>` 通信。
|
||
|
||
### 5.2 Backend:分层与 API 层
|
||
|
||
```text
|
||
HTTP (public | admin) wss /api/agent/v1/rpc(Fleet.Server.Link) SSE /api/public/live
|
||
│ │ ▲
|
||
┌──────────────▼───────────────┐ ┌───────────▼──────────┐ │
|
||
│ Backend.Api │ │ Backend.Gateway │ │
|
||
│ Public/ Admin/ │ │ 注册 moshiro.* 方法 │ │
|
||
│ 认证(Cookie/CSRF/令牌) │ │ TaskSet 状态提供者 │ │
|
||
│ 错误映射 · OpenAPI · SSE 端点 │ │ RawChunk 收件箱 │ │
|
||
└──────────────┬───────────────┘ └───────────┬──────────┘ │
|
||
└───────────────┬──────────────┘ │
|
||
▼ │
|
||
┌─────────────────────────────────────────────────────────┐ ┌───────────────────┴──┐
|
||
│ Backend.Application │───>│ LiveHub(Application)│
|
||
│ Agents · 拨号方 · Tasks(有效任务集的计算与下发) │ │ 内存快照 + 主题订阅 │
|
||
│ Tests · Ingest(chunk → 存储 → 分析队列) │ └──────────────────────┘
|
||
│ Analysis(AnalysisScheduler 等)· Query · Auth · Updates │ 调用 ─> Backend.Analysis
|
||
└───────────────────────────▲─────────────────────────────┘ 纯计算:算子/指标/模型
|
||
│ 实现端口
|
||
┌───────────────────────────┴─────────────────────────────┐
|
||
│ Backend.Storage.Sqlite:段文件 + SQLite(只做读写) │
|
||
│ 同时实现 IFleetStore(Fleet 的节点和令牌表放在 moshiro.db 中)│
|
||
└─────────────────────────────────────────────────────────┘
|
||
|
||
端口:IAgentStore、ITaskStore、IRawStore、IMetricStore、ITestStore、IAuthStore
|
||
```
|
||
|
||
规则:
|
||
|
||
- Api 不能引用 Storage(编译期保证);Api 和 Gateway 只调用 Application。
|
||
- Application 返回 `Result<T>`。错误码到 HTTP 状态的映射只在 Api 层做一次(`ErrorCodeHttpMap`);到 JSON-RPC 错误的映射由 Fleet.Rpc 统一完成。
|
||
- Api 层按调用方分目录:`Public/`、`Admin/`,公共部分放在 `Common/`(分页、ETag、认证过滤器、Problems、`EndpointConventions`)。每类资源一个端点文件,新增资源时在 `ApiModule.Map` 中加一行。
|
||
- 注册端点、签名认证、连接注册表、状态推送、收件箱 ACK 都来自 Fleet(`MapFleetEnrollment`、`MapFleetLink`)。MoShiro 只提供:
|
||
- `IFleetStore` 的实现;
|
||
- `TaskSet` 的状态提供者;
|
||
- `RawChunk` 的收件箱;
|
||
- `moshiro.*` 方法。
|
||
- public 层的 DTO 单独定义,只暴露可以公开的字段,并统一经过“可见性过滤器”。
|
||
- 请求校验:简单字段约束使用 .NET 10 内置的 Minimal API 校验;任务参数统一用 `ParamSpec` 校验(与节点共用);跨字段规则放在 Application 层。
|
||
- Backend 使用 ASP.NET Core 自带的 DI 容器,只在组合根显式注册。
|
||
|
||
Backend.Analysis 只依赖 MoShiro.Protocol 和 Fleet.Core,没有 I/O。
|
||
|
||
Backend 允许的包:ASP.NET Core 共享框架、`Microsoft.Data.Sqlite`、`Microsoft.AspNetCore.OpenApi`、`Microsoft.Extensions.ApiDescription.Server`(仅构建期使用)。
|
||
|
||
## 6. 编译设置
|
||
|
||
- 所有 .NET 项目共用 `backend/Directory.Build.props`(目标框架、可空引用、警告当错误、最新分析器、确定性构建、禁用 API 清单 `BannedSymbols.txt`、版本号)和根目录的 `Directory.Packages.props`(NuGet 版本、SQLite 源码版本),SDK 版本在根目录的 `global.json`。
|
||
- `backend/agent/` 另有 `Directory.Build.props`:NativeAOT、体积优先、关闭 EventSource、调试支持和 Server/Concurrent GC 等,具体开关以文件为准;保留 StackTraceSupport,线上崩溃需要可读的调用栈。
|
||
- `BannedSymbols.Node.txt` 适用于常驻节点进程(Agent,以及 Fleet 的节点侧运行库),主要条目:
|
||
- `System.Diagnostics.Process`;
|
||
- `System.Reflection.Assembly.Load*`;
|
||
- `System.Runtime.InteropServices.NativeLibrary.Load*`;
|
||
- `System.IO.File.Write*`、`File.Create`、`FileStream` 构造函数(只有 `Fleet.Hosting.ConfigStore` 可以通过 `#pragma` 豁免);
|
||
- `System.Net.Http.HttpClient`(没有豁免:注册在 `fleet-enroll` 里,它是独立的一次性工具,允许 HTTP 和写入身份目录)。
|
||
|
||
## 7. 轻量预算
|
||
|
||
体积和内存目标如下,表中「实测」为某次测量值:
|
||
|
||
| 程序 | 发布体积 | 空闲常驻内存 | 负载 |
|
||
| --- | --- | --- | --- |
|
||
| 拨号方 | ≤ 8 MB(Linux/OpenWrt,静态链接了 .NET 的加密、压缩、网络库;Windows 实测 5.6 MB) | ≤ 32 MB(Linux 实测约 21 MB) | 同时连接 15 个 Agent 并运行默认模板时 CPU < 2%;`hy2_ladder` 100 Mbps 时 CPU < 50%(单核) |
|
||
| Agent | ≤ 10 MB(Linux 实测 8.4 MB) | ≤ 48 MB(Linux 实测约 23 MB) | 运行默认模板时 ≤ 64 MB;同时跑一个 200 Mbps 测试时 CPU < 60%(1 vCPU) |
|
||
| fleet-enroll | ≤ 4 MB | — | 一次性运行 |
|
||
| Backend | 不设硬性限制 | ≤ 150 MB | — |
|
||
|
||
为满足预算,实现上需要遵守:
|
||
|
||
- 热路径使用 `Span<T>` 和 `ArrayPool<byte>`,不逐包分配;
|
||
- UDP 使用 `SocketAddress` 系列非分配 API;
|
||
- stream 和 bulk 发送使用预先填充的缓冲区,只统计计数,不复制数据;
|
||
- 不使用 LINQ。
|
||
|
||
## 8. 发布
|
||
|
||
- 目标平台:
|
||
|
||
| 程序 | RID | 说明 |
|
||
| --- | --- | --- |
|
||
| Agent | `linux-x64`、`linux-arm64` | VPS |
|
||
| 拨号方 | `linux-x64`、`linux-arm64`、`win-x64` | 普通 PC 或服务器 |
|
||
| 拨号方(OpenWrt) | `linux-musl-arm64` | 正式支持;`linux-musl-arm`(armv7)视工具链验证结果决定是否支持 |
|
||
| fleet-enroll | 与所在节点相同 | |
|
||
| Backend | `linux-x64`、`linux-arm64`、`win-x64` | |
|
||
|
||
- MIPS 不支持(NativeAOT 没有 MIPS 目标)。使用 MIPS 路由器(例如 MT7621)时,把 拨号方 运行在局域网内的其他设备上,比如 NAS、树莓派或 PC,测量结果同样代表这条家宽。
|
||
- NativeAOT 需要目标平台的原生工具链:Linux amd64/arm64 发布 glibc,OpenWrt ARM64 发布静态 musl;arm64 使用交叉编译镜像。CI 矩阵为 Windows、macOS、Linux glibc amd64/arm64 和 Linux musl arm64。
|
||
- `scripts/publish.sh <agent|backend|fleet-enroll> <rid>` 输出到 `backend/artifacts/publish/<project>/<rid>/`。
|
||
- 容器镜像由 CI 构建并对镜像 digest 签名,节点的宿主机更新器验签后更新,见 [deployment.md](../ops/deployment.md);纯二进制节点的更新见 [路线图](../roadmap.md)。
|
||
- Linux 上 TLS 依赖系统 OpenSSL(`libssl.so.3`)。
|
||
|
||
### 8.1 OpenWrt 上的 拨号方
|
||
|
||
| 事项 | 做法 |
|
||
| --- | --- |
|
||
| C 库 | musl:使用 `linux-musl-*` 目标发布 |
|
||
| TLS | .NET 在 Linux 上依赖 OpenSSL,而 OpenWrt 默认使用 mbedTLS/wolfSSL。需要安装 `libopenssl3`(安装脚本自动检查并提示)。启动时自检,缺少时明确报错,不会静默降级 |
|
||
| 全球化 | 已启用 `InvariantGlobalization`,不依赖 ICU |
|
||
| 服务管理 | procd:提供 `/etc/init.d/moshiro-agent`(`procd_set_param respawn`,前台运行,日志输出到 logd);`Fleet.Hosting.ServiceShell` 在 procd 下不需要 `sd_notify` |
|
||
| 存储 | 闪存很小,而且需要避免频繁写入:二进制放在 `/usr/lib/moshiro/`,配置和身份放在 `/etc/moshiro/`(sysupgrade 时通过 `/lib/upgrade/keep.d/` 保留);测量数据本来就不落盘;`ConfigStore` 只在 Agent 列表变化时写入 |
|
||
| 内存 | 预算 ≤ 32 MB,适合 ≥ 256 MB 内存的路由器;在本地配置中限制重负载测试的码率(`limits.max_test_mbps`),防止路由器 CPU 成为瓶颈,反过来影响测量结果 |
|
||
| 测量偏差 | 拨号方 运行在网关上时,自己没有 NAT;一旦路由器 CPU 饱和,结果就会失真。因此 拨号方 在 `moshiro.event` 中上报自身的 CPU 繁忙标记(仅进程级别),分析层据此标注置信说明 |
|
||
| 打包 | 提供 `tar.gz` 和 procd 脚本;包格式(ipk 或 apk)按目标 OpenWrt 版本提供 |
|
||
| 更新 | 纯二进制节点的更新器还没有实现([路线图](../roadmap.md));实现时 `keep_versions = 1`,下载前检查剩余空间,避免写满闪存 |
|
||
|
||
## 9. Web
|
||
|
||
`web/` 是 npm workspaces:`display`(展示端,匿名可读)、`admin`(管理端)、`shared`(API 客户端、图表、组件、主题)。Vite 8、React 19、TypeScript,图表用 uPlot;唯一的契约是 `backend/openapi/v1.json`,用 `openapi-typescript` 生成类型(`npm run gen:api`)。颜色只能写在 `shared/theme/tokens.css`,由 stylelint 强制。详见 [web/README.md](../../web/README.md)。
|