Files
MoShiro/docs/dev/project-layout.md
justme a6f9759238
ci/woodpecker/push/web-build Pipeline was successful
ci/woodpecker/push/verify Pipeline was successful
ci/woodpecker/push/dev-image Pipeline failed
ci: build musl only for arm64 and reuse toolchain cache [CI]
2026-10-21 12:41:58 +08:00

262 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 项目结构与工程约束
## 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)。