Files
MoShiro/docs/project-layout.md
T
justme dcf096431d ci: add dependency-aware main fastlane
Add Linux amd64 fast-lane OCI builds selected by project dependencies.

Co-authored-by: JustMe <mr.toing@outlook.com>
2026-10-05 17:22:26 +00:00

24 KiB
Raw Blame History

项目结构与工程约束

1. 目标

  1. 三个独立项目加一个前端:Probe、Agent、Backend 各自拥有解决方案,可以单独构建、测试和发布;Web 是独立的 Vite 项目(web/)。
  2. 通用能力只写一次:TCP/UDP/TLS/ICMP 采集积木(类似 iperf3 的能力,但只记录原始数据)放在 Monitor.Measurement 中,Probe 和 Agent 共用;采集方法(Kind)由积木组合声明而成。
  3. 节点机制通用化:注册、身份、连接、RPC、可靠上报、状态同步、宿主、更新,由与业务无关的 Fleet 框架提供(见 fleet/overview.md)。Monitor 只写业务,其他产品也可以复用 Fleet。
  4. 改一处,整条链路受益:协议、原始数据格式、测试方案、指标、错误码、版本号、编译设置、代码风格都只有一个真源。改动之后,下游要么自动更新,要么在编译期或 CI 中直接报错。
  5. 模块化:Probe/Agent 由可插拔的模块组成;Backend 按层分成多个项目,层之间的依赖在编译期强制。
  6. Probe/Agent 轻量:运行时只依赖 BCL 和仓库内的库,不引用任何 NuGet 包。

2. 仓库布局

monitor/
├─ README.md  .gitignore
├─ docs/   docs/fleet/
├─ scripts/                                   # publish / package / smoke / stability 等脚本(check-deps 等检查也放这里)
├─ packaging/                                 # install.sh、systemd 单元、OpenWrt 文件
├─ web/                                       # 前端:Vite + React + TypeScript(自写),只依赖 backend/openapi/v1.json
│
├─ global.json  Directory.Build.props  Directory.Packages.props  Monitor.slnx
├─ .editorconfig  BannedSymbols.txt           # 风格与禁用 API(构建期强制)
├─ BannedSymbols.Node.txt                     # 仅用于常驻节点进程:纯采集边界
│
└─ backend/                                   # 所有 C# 代码(构建产物在 backend/artifacts/)
   ├─ fleet/                                  # 通用节点框架(不认识 Monitor,以后可以拆成独立仓库)
   │  ├─ src/     Fleet.Core Fleet.Identity Fleet.Rpc Fleet.Link Fleet.Hosting Fleet.Update
   │  │           Fleet.Enrollment.Client  Fleet.Server.{Enrollment,Link,Releases,Sqlite}
   │  ├─ tools/   fleet-enroll  fleet-updater  fleet-release
   │  ├─ samples/ EchoNode  EchoServer
   │  └─ tests/
   ├─ shared/                                 # Monitor 自己的共享库
   │  ├─ Monitor.Protocol/      # 契约真源:Kind/Plan 目录、任务集、monitor.* 方法、UDP 包、RawChunk、错误码
   │  ├─ Monitor.Measurement/   # 采集引擎:积木、Recorder、任务调和(见 measurement.md)
   │  └─ tests/
   ├─ probe/   src/Monitor.Probe/    tests/   # 家宽端采集器
   ├─ agent/   src/Monitor.Agent/    tests/   # VPS 端采集器
   ├─ src/                                    # Backend(面板)
   │  ├─ Monitor.Backend/                     # 宿主与组合根(Program.cs)
   │  ├─ Monitor.Backend.Api/                 # API 层:HTTP 端点、DTO、错误映射
   │  ├─ Monitor.Backend.Gateway/             # 在 Fleet.Server.Link 上注册 Monitor 的方法、状态与收件箱
   │  ├─ Monitor.Backend.Application/         # 用例层 + 端口接口
   │  ├─ Monitor.Backend.Analysis/            # 分析(纯计算,见 analysis.md)
   │  └─ Monitor.Backend.Storage.Sqlite/      # 端口接口的实现,同时实现 IFleetStore
   ├─ tests/
   ├─ openapi/v1.json                         # 构建生成并提交
   └─ Monitor.Backend.slnx

节点机器上的发布物是三个独立进程,使用不同的系统用户:monitor-agent(或 monitor-probe)、fleet-enroll(一次性运行)、fleet-updater(常驻或定时运行)。

3. 依赖关系

 ┌────────┐ ┌────────┐   ┌──────────────────────────────┐          ┌─────┐
 │ Probe  │ │ Agent  │   │ Backend (Api/Gateway/App/    │─openapi─>│ Web │
 └─┬──┬───┘ └─┬──┬───┘   │ Analysis/Storage)            │          └─────┘
   │  └───┬───┘  │       └───┬───────────────┬──────────┘
   │      ▼      │           │               │
   │ ┌───────────┴┐          │               │
   │ │Measurement │          │               │
   │ └─────┬──────┘          │               │
   ▼       ▼                 ▼               ▼
 ┌──────────────────────────────────┐   ┌─────────────────────────┐
 │         Monitor.Protocol         │   │ Fleet.Server.*          │
 └────────────────┬─────────────────┘   └───────────┬─────────────┘
                  ▼                                 ▼
 ┌──────────────────────────────────────────────────────────────────┐
 │ Fleet(节点侧):Core · Identity · Rpc · Link · Hosting              │
 └──────────────────────────────────────────────────────────────────┘
      反向依赖一律禁止:fleet/** ✗→ Monitor.*

简要来说:

Probe, Agent  ──> Measurement ──> Monitor.Protocol ──> Fleet.Core / Fleet.Rpc
Probe, Agent  ──> Fleet.Link / Fleet.Hosting / Fleet.Identity
Backend.*     ──> Monitor.Protocol + Fleet.Server.*                (不引用 Measurement)
fleet-enroll  ──> Fleet.Enrollment.Client ──> Fleet.Identity / Core
fleet-updater ──> Fleet.Update ──> Fleet.Core                      (不引用 Link,也不读取节点身份)
Web           ──> openapi/v1.json
项目 允许依赖 禁止
Fleet.*(节点侧) BCL、其他节点侧 Fleet 库 任何 NuGet 运行时包;任何 Monitor.*
Fleet.Server.* ASP.NET Core、Microsoft.Data.Sqlite(仅 Sqlite 项目)、节点侧 Fleet 库 任何 Monitor.*
Monitor.Protocol Fleet.Core、Fleet.Rpc 任何 I/O、套接字、日志
Monitor.Measurement Monitor.Protocol、Fleet.Core NuGet;日志(只通过计数接收器上报);读取配置;统计或解释逻辑
Probe / Agent 上述库,以及 Fleet.Link / Hosting / Identity NuGet 运行时包;彼此互相引用;Fleet.Update;Fleet.Enrollment.Client
Backend.* Monitor.Protocol、Fleet.Server.*,以及第 5.2 节列出的包 Measurement、EF Core 等依赖反射的库

分析器类包(PrivateAssets="all")不受限制。scripts/check-deps 在 CI 中检查上述规则。

4. 共享库

4.1 Fleet(通用节点框架)

组件、扩展点与接入方式见 fleet/overview.md,连接细节见 fleet/link.md,更新见 fleet/update.md。原来的 Monitor.Common 和 Monitor.NodeHost 已经并入 Fleet:

原位置 现位置
Monitor.Common(Result、缓冲区、VarInt、有界队列、重放窗口、退避、ID、令牌哈希) Fleet.Core
Monitor.NodeHost(模块宿主、日志、配置、ConfigStore、服务外壳) Fleet.Hosting
节点密钥与签名 Fleet.Identity
JSON-RPC 信封与路由 Fleet.Rpc
Agent 注册 Fleet.Enrollment.Client + fleet-enroll

4.2 Monitor.Protocol(Monitor 的契约真源)

Monitor.Protocol/
├─ ProtocolVersion  ProtocolLimits  MonitorErrorCode(在 Fleet 错误码基础上扩展)
├─ Ids/          AgentId(= Fleet NodeId)、ProbeId、TaskId、TestId、ChunkId、StreamId
├─ Catalog/
│  ├─ Kinds/     KindSpec 声明(积木组合 + ParamSpec + 记录列 + 预算)
│  ├─ Plans/     多阶段测试方案
│  └─ ParamSpec  参数定义(类型、默认值、上下限),节点与 Backend 共用同一套校验
├─ Tasks/        TaskSet(Fleet 期望状态的文档类型)、调和结果
├─ Rpc/          monitor.* 方法名、params/result、二进制通道号(从 16 开始)
├─ Wire/         UDP 包、数据连接首帧
├─ Raw/          自描述 RawChunk:头部、列类型、列编解码、Brotli、crc32c
└─ Json/         AgentRpcContext、ProbeRpcContext(按信道拆分,便于裁剪)

累加器、直方图等统计类型不在 Protocol 中,只属于 Backend.Analysis。

4.3 Monitor.Measurement(采集引擎)

包含:

  • 积木:Transport、Pacer、Payload、Behavior、Sampler;
  • 根据列定义自动生成的 Recorder;
  • KindAssembler;
  • TaskReconciler(实现 Fleet 的 IStateReconciler<TaskSet>);
  • BudgetGuard。

详见 measurement.md 第 6 节。

5. 模块化

5.1 Agent 与 Probe:模块组装

模块在 Program.cs 中显式组装,不使用 DI 容器,也不做反射扫描。连接、认证、重连、发件箱、状态同步都由 Fleet 提供,Monitor 只写业务模块:

Agent Program.cs
  identity = NodeIdentity.Load(dir)                          ← 由 fleet-enroll 生成
  link     = FleetLink.ToServer(cfg, identity, AgentRpcContext.Default)
               .WithState<TaskSet>(…, taskReconciler)         期望任务集 → 调和
               .WithOutbox<RawChunk>(…, 64 MiB)                原始块可靠上报
               .On(monitor.test.run / monitor.test.cancel, …)
  peers    = FleetLink.PeerListener(cfg.Listen, identity, ProbeRpcContext.Default)   Probe 会话
  host     = new ModuleHost(log)
  ├─ link、peers                  (Fleet)
  ├─ ProbeDirectoryRelay          转发 monitor.probe.agents / agents_changed
  ├─ UdpMux / TcpAcceptor         UDP/TCP/TLS 数据端口(Measurement 共享),按 (session, stream) 分发给监听方积木;AgentAdmission 提供重放、限速
  ├─ ChunkModule                  合并 Probe 与本端的列 → 切块 → link.Outbox
  └─ TestModule                   按需测试的阶段编排

Probe Program.cs
  ├─ AgentDirectoryModule         维护 Agent 列表(bootstrap → monitor.probe.agents → agents_changed)
  ├─ PeerPool(Fleet)             每个 Agent 一个 FleetLink.ToPeer:认证、重连
  ├─ TaskReconciler               每个会话各自的期望状态(与 Agent 相同的代码)
  ├─ ColumnShipper                本端的列通过会话的 Outbox 发送(二进制通道 17)
  └─ TestModule

模块之间通过构造函数传入的接口或 Channel<T> 通信,禁止通过静态全局状态共享数据。

新增 RPC 方法:在 Monitor.Protocol/Rpc 中定义 params/result,然后在组装处加一行 .On(Methods.X, HandleX)。

5.2 Backend:分层与 API 层

        HTTP (public | admin)     wss /api/agent/v1/rpc(Fleet.Server.Link)   SSE /api/public/live
                │                              │                                    ▲
 ┌──────────────▼───────────────┐  ┌───────────▼──────────┐                         │
 │ Backend.Api                  │  │ Backend.Gateway      │                         │
 │ Public/ Admin/               │  │ 注册 monitor.* 方法    │                         │
 │ 认证(Cookie/CSRF/令牌)       │  │ TaskSet 状态提供者     │                         │
 │ 错误映射 · OpenAPI · SSE 端点  │  │ RawChunk 收件箱        │                         │
 └──────────────┬───────────────┘  └───────────┬──────────┘                         │
                └───────────────┬──────────────┘                                    │
                                ▼                                                   │
 ┌─────────────────────────────────────────────────────────┐    ┌───────────────────┴──┐
 │ Backend.Application                                     │───>│ LiveHub(Application)│
 │ Agents · Probe · Tasks(有效任务集的计算与下发)            │    │ 内存快照 + 主题订阅   │
 │ Tests · Ingest(chunk → 存储 → 分析队列)· SeriesQuery     │    └──────────────────────┘
 │ AnalysisScheduler · Downsampler · Auth · Releases 视图    │ 调用 ─> Backend.Analysis
 └───────────────────────────▲─────────────────────────────┘         纯计算:算子/指标/模型
                             │ 实现端口
 ┌───────────────────────────┴─────────────────────────────┐
 │ Backend.Storage.Sqlite:段文件 + SQLite(只做读写)         │
 │ 同时实现 IFleetStore(Fleet 的节点和令牌表放在 monitor.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、认证过滤器、ProblemFactory、EndpointConventions)。每类资源一个端点文件,新增资源时在 ApiModule.Map 中加一行。
  • 注册端点、签名认证、连接注册表、状态推送、收件箱 ACK、发布通道都来自 Fleet(MapFleetEnrollment、MapFleetLink、MapFleetReleases)。Monitor 只提供:
    • IFleetStore 的实现;
    • TaskSet 的状态提供者;
    • RawChunk 的收件箱;
    • monitor.* 方法。
  • public 层的 DTO 单独定义,只暴露可以公开的字段,并统一经过“可见性过滤器”。
  • 请求校验:简单字段约束使用 .NET 10 内置的 Minimal API 校验;任务参数统一用 ParamSpec 校验(与节点共用);跨字段规则放在 Application 层。
  • Backend 使用 ASP.NET Core 自带的 DI 容器,只在组合根显式注册。

Backend.Analysis 只依赖 Monitor.Protocol 和 Fleet.Core,没有 I/O,可以单独做成离线 CLI 使用。

Backend 允许的包:ASP.NET Core 共享框架、Microsoft.Data.Sqlite、Microsoft.AspNetCore.OpenApi、Microsoft.Extensions.ApiDescription.Server(仅构建期使用)。

6. “改一处,整条链路受益”的机制

改动 真源 链路上的效果
采集密度(频率、包大小、采样间隔) 任务参数(API) 经 fleet.state.sync 下发后立即生效,无需发布
新增采集方法(现有积木可以组合) Protocol/Catalog/Kinds 中加 1 条 节点执行、Recorder 自动生成列、Backend 校验和目录、Analysis 按列名读取
新增采集方法 在 Kinds 里加声明(含流的 Driver/Key),在 Measurement/Blocks 里实现它的拨号方和监听方并登记到 BlockCatalog Probe 和 Agent 同时获得,合并不需要写代码
原始数据格式 Protocol/Raw(自描述) 新增列不需要升级版本;四处使用同一份编解码器
新增统计指标 Analysis/Metrics 中加 1 行 MetricDef 存储、降采样、API、SSE、目录全部自动处理;回填 30 天历史
服务评分 Analysis/Models 查询时计算,立即对全部历史生效
新增 RPC 能力 Monitor.Protocol/Rpc + 组装处加 1 行 通过 Fleet 的 capabilities 协商,旧节点不受影响
注册、认证、重连、发件箱、状态同步 backend/fleet/ Monitor 和所有其他产品同时受益
更新机制 Fleet.Update + fleet-updater 所有产品共用;产品只提供发布清单
REST DTO 与端点 Backend.Api 生成 OpenAPI,CI 检查 diff,前端的 TS 类型随之更新
错误码 Fleet 错误码 + MonitorErrorCode REST、JSON-RPC、OpenAPI、TS 全部一致
版本、编译设置、风格、禁用 API backend/ 下的 props、.editorconfig、BannedSymbols*.txt 所有项目

7. 编译设置

backend/Directory.Build.props(所有项目):

<Project>
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <LangVersion>latest</LangVersion>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <AnalysisLevel>latest-recommended</AnalysisLevel>
    <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
    <IsAotCompatible>true</IsAotCompatible>
    <InvariantGlobalization>true</InvariantGlobalization>
    <UseArtifactsOutput>true</UseArtifactsOutput>
    <Deterministic>true</Deterministic>
    <Version>0.1.1</Version>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" PrivateAssets="all" />
    <AdditionalFiles Include="$(MSBuildThisFileDirectory)BannedSymbols.txt" />
  </ItemGroup>
</Project>

backend/fleet/src/* 另外启用 Microsoft.CodeAnalysis.PublicApiAnalyzers(PublicAPI.Shipped.txt / PublicAPI.Unshipped.txt),公开 API 的任何变更都会在 PR 中显示出来。

backend/probe/、backend/agent/ 下的 Directory.Build.props(先导入根 props,再追加轻量设置):

<PropertyGroup>
  <PublishAot>true</PublishAot>
  <OptimizationPreference>Size</OptimizationPreference>
  <UseSystemResourceKeys>true</UseSystemResourceKeys>
  <EventSourceSupport>false</EventSourceSupport>
  <DebuggerSupport>false</DebuggerSupport>
  <HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
  <ServerGarbageCollection>false</ServerGarbageCollection>
  <ConcurrentGarbageCollection>false</ConcurrentGarbageCollection>
  <!-- 保留 StackTraceSupport:线上崩溃需要可读的调用栈 -->
</PropertyGroup>
<ItemGroup>
  <!-- 纯采集边界:编译期禁止 -->
  <AdditionalFiles Include="$(MSBuildThisFileDirectory)../BannedSymbols.Node.txt" />
</ItemGroup>

BannedSymbols.Node.txt 适用于常驻节点进程(Probe、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)。

fleet-enroll 和 fleet-updater 是独立的工具,使用各自的禁用清单:

  • fleet-updater 允许写安装目录、调用服务管理接口,但禁止读取节点身份目录;
  • fleet-enroll 允许 HTTP,允许写入身份目录。

8. 轻量预算

M0 阶段先测出基线,之后作为 CI 的回归门槛(超出 10% 即失败):

程序 发布体积 空闲常驻内存 负载
Probe ≤ 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 — 一次性运行
fleet-updater ≤ 4 MB ≤ 16 MB 空闲时每小时轮询一次
Backend 不设硬性限制 ≤ 150 MB —

为满足预算,实现上需要遵守:

  • 热路径使用 Span<T> 和 ArrayPool<byte>,不逐包分配;
  • UDP 使用 SocketAddress 系列非分配 API;
  • stream 和 bulk 发送使用预先填充的缓冲区,只统计计数,不复制数据;
  • 不使用 LINQ。

9. 发布

  • 目标平台:
程序 RID 说明
Agent linux-x64、linux-arm64 VPS
Probe linux-x64、linux-arm64、win-x64 普通 PC 或服务器
Probe(OpenWrt) linux-musl-x64、linux-musl-arm64 正式支持;linux-musl-arm(armv7)视 M0 的工具链验证结果决定是否支持
fleet-enroll / fleet-updater 与所在节点相同
Backend linux-x64、linux-arm64、win-x64
  • MIPS 不支持(NativeAOT 没有 MIPS 目标)。使用 MIPS 路由器(例如 MT7621)时,把 Probe 运行在局域网内的其他设备上,比如 NAS、树莓派或 PC,测量结果同样代表这条家宽。
  • NativeAOT 需要目标平台的原生工具链:musl 目标在 Alpine 容器中构建,arm64 使用交叉编译镜像。CI 矩阵为 Windows、Linux glibc、Linux musl。
  • scripts/publish.(sh|ps1) <probe|agent|backend|fleet-enroll|fleet-updater> <rid> 输出到 artifacts/publish/<project>/<rid>/。
  • 发布时由 fleet-release 在 CI 中生成并签名清单(见 fleet/update.md),节点通过 fleet-updater 更新。
  • Linux 上 TLS 依赖系统 OpenSSL(libssl.so.3)。

9.1 OpenWrt 上的 Probe

事项 做法
C 库 musl:使用 linux-musl-* 目标发布
TLS .NET 在 Linux 上依赖 OpenSSL,而 OpenWrt 默认使用 mbedTLS/wolfSSL。需要安装 libopenssl3(安装脚本自动检查并提示)。启动时自检,缺少时明确报错,不会静默降级
全球化 已启用 InvariantGlobalization,不依赖 ICU
服务管理 procd:提供 /etc/init.d/monitor-probe(procd_set_param respawn,前台运行,日志输出到 logd);Fleet.Hosting.ServiceShell 在 procd 下不需要 sd_notify
存储 闪存很小,而且需要避免频繁写入:二进制放在 /usr/lib/monitor/,配置和身份放在 /etc/monitor/(sysupgrade 时通过 /lib/upgrade/keep.d/ 保留);测量数据本来就不落盘;ConfigStore 只在 Agent 列表变化时写入
内存 预算 ≤ 32 MB,适合 ≥ 256 MB 内存的路由器;在本地配置中限制重负载测试的码率(limits.max_test_mbps),防止路由器 CPU 成为瓶颈,反过来影响测量结果
测量偏差 Probe 运行在网关上时,自己没有 NAT;一旦路由器 CPU 饱和,结果就会失真。因此 Probe 在 monitor.event 中上报自身的 CPU 繁忙标记(仅进程级别),分析层据此标注置信说明
打包 提供 tar.gz 和 procd 脚本;包格式(ipk 或 apk)按目标 OpenWrt 版本提供
更新 fleet-updater 使用 procd 适配器;keep_versions = 1,下载前检查剩余空间,避免写满闪存

10. Web(待开发)

唯一输入是 backend/openapi/v1.json:用 openapi-typescript 生成类型,openapi-fetch 作为请求客户端;技术栈等前端立项时再决定。