docs: rewrite README and AGENTS.md etc..
This commit is contained in:
1 parent
cc64df3e70
commit
a9bf849697
22 files changed
+514
-665
No files matched your search
+12
-12
@@ -16,11 +16,11 @@
|
||||
指标窗口(1m) → 5m → 1h API:metrics{} + scores{}
|
||||
```
|
||||
|
||||
三层都采用**注册制**,由代码中的注册表驱动;API 的 `/catalog/metrics`、`/catalog/models` 也直接由注册表生成。
|
||||
三层都采用注册制,由代码中的注册表驱动;API 的 `/catalog/metrics`、`/catalog/models` 也直接由注册表生成。
|
||||
|
||||
## 2. 累加器(固定的少数几种)
|
||||
|
||||
所有指标的中间状态只能是下面几种累加器之一。**合并规则只写一次**,存储、降采样、按时间范围聚合都通用处理:
|
||||
所有指标的中间状态只能是下面几种累加器之一。合并规则只写一次,存储、降采样、按时间范围聚合都通用处理:
|
||||
|
||||
| 累加器 | 状态 | 合并 | 可导出的值 |
|
||||
| --- | --- | --- | --- |
|
||||
@@ -79,14 +79,14 @@ public static class LinkMetrics
|
||||
```
|
||||
|
||||
- `from` 写成 `kind.stream`,`*` 表示所有具有该 stream 的 Kind;某个窗口里没有对应的输入时,该指标在这个窗口就不存在。
|
||||
- **TCP 长尾**:均值和 p95 看不到偶发的重传尖峰(一次丢包后的 RTT 聚在 RTO 附近),所以 `tcp.rtt` 还导出 `p999`、`max` 和长尾比 `tail`(p99 / p50,从同一个直方图导出,窗口合并后仍然精确);`tcp.slow` 是慢回显的占比——阈值 `max(1.8 × 基线, 基线 + max(15 ms, 0.35 × 基线))`,基线是**同一个 10 秒块**里 RTT 的中位数(所以每个观测只依赖自己所在的块,窗口合并仍然精确;块里少于 20 个样本不产生观测)。把它和 `tcp.retrans`(内核计数,Agent 一侧反映下行)、`tcp.stall` 对照,可以区分"重传造成的尖峰"和"队头阻塞"。
|
||||
- TCP 长尾:均值和 p95 看不到偶发的重传尖峰(一次丢包后的 RTT 聚在 RTO 附近),所以 `tcp.rtt` 还导出 `p999`、`max` 和长尾比 `tail`(p99 / p50,从同一个直方图导出,窗口合并后仍然精确);`tcp.slow` 是慢回显的占比——阈值 `max(1.8 × 基线, 基线 + max(15 ms, 0.35 × 基线))`,基线是同一个 10 秒块里 RTT 的中位数(所以每个观测只依赖自己所在的块,窗口合并仍然精确;块里少于 20 个样本不产生观测)。把它和 `tcp.retrans`(内核计数,Agent 一侧反映下行)、`tcp.stall` 对照,可以区分"重传造成的尖峰"和"队头阻塞"。
|
||||
- 导出值的名字为 `指标id.统计量`,例如 `rtt.p95`、`loss.up`、`tcp.stall.per_min`。
|
||||
|
||||
**新增一个统计 = 新增一行 `MetricDef`**,如果需要新的算子,再加一个算子类。存储、降采样、API 输出、目录说明都不需要改。新指标上线后,会自动对保留期内(30 天)的原始数据回填(见第 7 节)。
|
||||
新增一个统计只需要新增一行 `MetricDef`,需要新算子时再加一个算子类。存储、降采样、API 输出、目录说明都不需要改。新指标上线后,会自动对保留期内(30 天)的原始数据回填(见第 7 节)。
|
||||
|
||||
## 5. 模型(服务体验评分)
|
||||
|
||||
模型是一组指标的评分函数,**在查询时计算**,不存储。改动评分公式后,立即对全部历史生效。
|
||||
模型是一组指标的评分函数,在查询时计算,不存储。改动评分公式后,立即对全部历史生效。
|
||||
|
||||
```csharp
|
||||
public sealed class McModel : IServiceModel
|
||||
@@ -117,7 +117,7 @@ chunk 入库 ─┬─> 流式路径:按 1s 桶计算累加器 → 热层环
|
||||
|
||||
两条路径调用同一份 `MetricDef` 和算子代码,所以实时值与历史值使用的是同一套口径。迟到的块(例如 Agent 重连后补传)会同时更新热层(如果仍在 1 小时内)、`metric_10s`,并重新进入持久路径。
|
||||
|
||||
**指标分组(preset)** 与指标一样采用注册制:`Presets.Udp = [LinkMetrics.LossUp, …]`,由 `/api/public/catalog/presets` 下发。前端按分组展示,新增指标只需要把它加进分组。查询规则见 [query.md](query.md)。
|
||||
指标分组(preset)同样是注册制:`MetricRegistry.Presets` 中的 `PresetDef(id, label, metricIds)`,由 `/api/public/catalog/presets` 下发。前端按分组展示,新增指标只需要把它加进分组。查询规则见 [query.md](query.md)。
|
||||
|
||||
## 7. 版本与重算
|
||||
|
||||
@@ -136,12 +136,12 @@ chunk 入库 ─┬─> 流式路径:按 1s 桶计算累加器 → 热层环
|
||||
|
||||
```text
|
||||
backend/src/MoShiro.Backend.Analysis/ 依赖:MoShiro.Protocol、Fleet.Core(纯计算,没有 I/O)
|
||||
├─ Accumulators/ Counter Ratio Sum Min Max Histogram RunLength Series + AccumulatorCodec
|
||||
├─ Ops/ Rtt Loss OwdVar LossRuns Reorder Gap KernelDelta Rate PhaseMs Outcome ChangePoint
|
||||
├─ Metrics/ LinkMetrics TcpMetrics UdpTrainMetrics ConnectMetrics …(只包含声明)
|
||||
├─ Models/ BaseModel McModel Hy2Model RealityModel
|
||||
├─ Engine/ ChunkView(列视图) WindowEvaluator TestEvaluator MetricRegistry ModelRegistry
|
||||
├─ Accumulators/ Accumulator(Counter、Ratio、Sum、Min、Max、RunLength、Series)、HistogramAcc、AccumulatorCodec
|
||||
├─ Ops/ Ops(通用算子)、EchoOps、SessionOps
|
||||
├─ Metrics/ LinkMetrics、TcpMetrics、SessionMetrics(只包含声明)、MetricRegistry(指标与分组的注册表)
|
||||
├─ Models/ ServiceModel(基类)、Models(base、mc、hy2、reality)
|
||||
├─ Engine/ ChunkView(列视图)、MetricDef、WindowEvaluator、TestEvaluator、ChangePoint
|
||||
└─ Clock/ ClockDriftFit(估计两端的时钟偏移和漂移)
|
||||
```
|
||||
|
||||
分析库没有 I/O,可以直接用于离线 CLI `tools/moshiro-analyze`,对下载的原始块做实验。
|
||||
分析库没有 I/O,可以脱离 Backend 单独引用。
|
||||
+35
-35
@@ -2,11 +2,13 @@
|
||||
|
||||
## 1. 目标与定位
|
||||
|
||||
一个**只关注网络层**的线路质量面板:单体面板(Backend)+ 轻量节点(Probe、Agent)+ 长连接 RPC + 任务下发。核心能力:
|
||||
只关注网络层的线路质量面板:单体面板(Backend)、轻量节点(Probe、Agent)、长连接 RPC 与任务下发。
|
||||
|
||||
- 在**若干 Probe**(家宽等需要观测的出口,可以有多个)与**多台海外 VPS Agent**(10 几台)之间,采集**最原始的逐包、逐事件数据**;哪些 Probe 连哪些 Agent 由面板决定(自动全连,也可以手动选择);
|
||||
- 由 Backend 的分析层推算:**如果在某条线路上部署 REALITY 类、Hysteria2 类或 Minecraft 服务,体验会怎样**,并对多条线路排序和对比;
|
||||
- **主机层信息只上报只读的汇总数字**(2026-10 起,由用户确认放开):Agent 和 Probe 读 `/proc` 得到 CPU 占用、内存、根分区、网卡累计流量与瞬时网速、运行时长,不采集 CPU 型号、主机名、网卡名和任何 IP,不采集进程;Probe 的上报可以在 `probe.json` 里关掉(`reporting.host_stats: false`)。展示页用卡片画出每个 Agent 和 Probe 的主机状态,线路单独放在卡片下面的面板里。节点只在连接面板时自报粗粒度的系统、架构和构建信息(版本、tag、提交、构建时间),用于展示和更新;国家和运营商由 Backend 用节点的来源地址查询,节点自己不访问任何外部服务。展示页看不到任何 IP。Agent 和 Probe 仍然是**纯采集器**,不具备 SSH、远程执行、终端、文件读写、自更新等能力:更新由节点宿主机上的更新器完成,节点进程只往数据目录放面板转来的发布清单和更新请求。
|
||||
- 采集:多个 Probe(家宽等需要观测的出口)与多台海外 VPS Agent 之间记录逐包、逐事件的原始数据。哪些 Probe 连哪些 Agent 由面板决定,默认全连,也可以手动选择。
|
||||
- 分析:Backend 的分析层推算在某条线路上部署 REALITY 类、Hysteria2 类或 Minecraft 服务的体验,并对多条线路排序和对比。
|
||||
- 主机状态:Agent 和 Probe 读取 `/proc`,上报 CPU 占用、内存、根分区、网卡累计流量与瞬时网速、运行时长这类汇总数字。不采集 CPU 型号、主机名、网卡名、IP 和进程。Probe 的上报可在 `probe.json` 中用 `reporting.host_stats: false` 关闭。展示页以卡片呈现每个节点的主机状态,线路在卡片下方的面板中。
|
||||
- 构建信息:节点连接面板时自报系统、架构和构建信息(版本、tag、提交、构建时间),用于展示和更新。国家和运营商由 Backend 依据节点来源地址查询,节点不访问任何外部服务,展示页不含任何 IP。
|
||||
- 节点边界:Agent 和 Probe 是纯采集器,没有 SSH、远程执行、终端、文件读写和自更新。更新由节点宿主机上的更新器完成,节点进程只把面板转来的发布清单和更新请求写入数据目录。
|
||||
|
||||
## 2. 拓扑
|
||||
|
||||
@@ -34,8 +36,8 @@
|
||||
更新(Agent、Probe、Backend):面板登记 CI 签名的清单 → 管理端确认 → 节点(Backend 先备份数据库)有序退出 → systemd Pull=always 拉最新镜像重启
|
||||
```
|
||||
|
||||
- **线路(line)** = 一对(Probe,Agent)。可以有多个 Probe(身份由 Probe 自己生成并用加入令牌注册,可以吊销、删除),每个 Probe 连接多个 Agent,每个 Agent 同时服务多个 Probe;一条线路的数据按 `line:<probe_id>` 单独存放,互不混合。对外的线路 id 是 `<agent_id>~<probe_id>`。
|
||||
- **目标任务(target)** = 某个 Agent → 指定的 IP:端口(ICMP、TCP/TLS 建连),只能访问 Agent 本地允许的网段。
|
||||
- 线路(line):一对(Probe,Agent)。可以有多个 Probe(身份由 Probe 自己生成并用加入令牌注册,可以吊销、删除),每个 Probe 连接多个 Agent,每个 Agent 同时服务多个 Probe;一条线路的数据按 `line:<probe_id>` 单独存放,互不混合。对外的线路 id 是 `<agent_id>~<probe_id>`。
|
||||
- 目标任务(target):某个 Agent → 指定的 IP:端口(ICMP、TCP/TLS 建连),只能访问 Agent 本地允许的网段。
|
||||
|
||||
## 3. 数据链路
|
||||
|
||||
@@ -58,9 +60,9 @@
|
||||
|
||||
| 主题 | 要点 | 文档 |
|
||||
| --- | --- | --- |
|
||||
| 节点框架 | 注册、身份、连接、RPC、可靠上报、状态同步、宿主都由与业务无关的 **Fleet** 提供;MoShiro 只写业务,其他产品也能复用 | [../backend/fleet/README.md](../../backend/fleet/README.md) |
|
||||
| 节点框架 | 注册、身份、连接、RPC、可靠上报、状态同步、宿主都由与业务无关的 Fleet 提供;MoShiro 只写业务,其他产品也能复用 | [Fleet 文档](../../backend/fleet/README.md) |
|
||||
| 注册与身份 | 使用 `fleet-enroll` 和加入令牌自动注册,之后用签名认证;Probe 用自己的身份直连面板,Agent 列表由面板下发 | [enrollment.md](../security/enrollment.md) |
|
||||
| 更新 | 只接受发布密钥签名的清单,更新策略由节点本地决定,失败自动回滚。CI 对镜像 digest 签名;Backend 和一行脚本装的节点由宿主机上的 `moshiro-update` 验签、按 digest 拉取、失败回滚;Podman Quadlet 装的 Agent、Probe 更简单:节点有序退出,systemd 用 `--pull always` 重新拉最新镜像,节点自己不下载、不验证清单;纯二进制节点的更新是待办([TODO.md](../../TODO.md)) | [deployment.md](../ops/deployment.md) |
|
||||
| 更新 | 只接受发布密钥签名的清单,更新策略由节点本地决定,失败自动回滚。CI 对镜像 digest 签名;Backend 和一行脚本装的节点由宿主机上的 `moshiro-update` 验签、按 digest 拉取、失败回滚;Podman Quadlet 装的 Agent、Probe 更简单:节点有序退出,systemd 用 `--pull always` 重新拉最新镜像,节点自己不下载、不验证清单;纯二进制节点的更新见[路线图](../roadmap.md) | [deployment.md](../ops/deployment.md) |
|
||||
| 任务模型 | `Task = {kind, target, params, schedule}`;持续采集、按需测试、目标任务统一建模;下发“期望任务集”,由节点自行调和 | [measurement.md](measurement.md) |
|
||||
| 采集扩展 | Kind 由积木组合声明;采集密度靠参数调整;原始块自描述 | [measurement.md](measurement.md) |
|
||||
| 统计扩展 | 指标注册制:少数几种累加器 + 统计算子;一行 `MetricDef` 就是一个新指标 | [analysis.md](analysis.md) |
|
||||
@@ -91,15 +93,15 @@ Backend:不自动,只有管理员点「更新并重启」;退出之前先
|
||||
|
||||
### 4.1 回显的四个时间戳
|
||||
|
||||
回显类 Kind 记录 `t1..t4`(Probe 发出、Agent 收到、Agent 回复、Probe 收到)。借此可以扣除 Agent 的处理耗时得到 RTT,可以分别判断上行和下行的丢包,并在去除时钟偏移和漂移后,得到**每个方向各自的抖动**,不需要两端时钟同步。
|
||||
回显类 Kind 记录 `t1..t4`,用于扣除 Agent 处理耗时、分别判断上下行丢包和计算各方向的抖动,不要求两端时钟同步。推导见 [measurement.md](measurement.md) 第 3.4 节。
|
||||
|
||||
### 4.2 NAT / CGNAT 适配
|
||||
|
||||
所有连接和 UDP 流都由 Probe 发起;Agent 把回复发往请求的**实际源地址**。会话以 `session_id` 和 MAC 认证,不绑定五元组,NAT 映射变化后自动跟随新地址。持续的 UDP 回显本身就起到保活作用。
|
||||
所有连接和 UDP 流都由 Probe 发起;Agent 把回复发往请求的实际源地址。会话以 `session_id` 和 MAC 认证,不绑定五元组,NAT 映射变化后自动跟随新地址。持续的 UDP 回显本身就起到保活作用。
|
||||
|
||||
## 5. 安全边界:纯采集
|
||||
|
||||
> 这一节是现状。「Backend 被攻破、节点被入侵」之后的威胁模型、现有的几处缺口(例如 Probe 会拨 Backend 给的任意地址、测量数据没有产生者签名)和每项的状态,见 [security.md](../security/security.md);预握手敲门、Probe 自己生成密钥、操作员集合与服务器身份已经实现。
|
||||
> 本节描述现状。Backend 或节点被攻破后的威胁模型、现有缺口(例如 Probe 会拨 Backend 给的任意地址、测量数据没有产生者签名)及状态见 [security.md](../security/security.md)。
|
||||
|
||||
```text
|
||||
Agent / Probe 能做的 执行编译在二进制中的 Kind;监听和访问本地配置允许的端口与网段;受本地硬上限约束
|
||||
@@ -108,15 +110,15 @@ Agent / Probe 能做的 执行编译在二进制中的 Kind;监听和访问
|
||||
也推不出恶意更新(更新清单必须由发布密钥签名,节点宿主机上的更新器用本机公钥验签,并且只认本机配置的镜像仓库)
|
||||
```
|
||||
|
||||
- **本地硬上限**写在节点本机的配置文件中,Backend 下发的任务如果超出,节点直接拒绝:允许访问的目标网段(默认为空,即不开放目标任务)、总码率和包速率上限、允许监听的端口。
|
||||
- **构建期强制**:常驻节点进程使用 `BannedSymbols.Node.txt`,禁用 `System.Diagnostics.Process`、`Assembly.Load*`、`NativeLibrary.Load`、`HttpClient`,以及 `ConfigStore` 以外的文件写入 API,详见 [project-layout.md](../dev/project-layout.md) 第 7 节。
|
||||
- **权限分离**:注册(`fleet-enroll`,一次性运行)、采集(`moshiro-agent` / `moshiro-probe`)、更新是不同的进程。容器节点里,采集进程在容器内以 UID 1654 运行(只读文件系统、不带额外能力),更新由宿主机上 root 运行的 `moshiro-update` 完成:它只读取节点放进 `update/` 的普通文件、复制到自己的临时目录后验签,状态写在节点只读的 `update-status/`;采集进程改写不了自己的二进制,更新器也读不到节点身份私钥。纯二进制节点的更新是待办(见 [TODO.md](../../TODO.md))。
|
||||
- **容器运行**:Agent、Probe 的镜像基于 Alpine(musl,NativeAOT),宿主机用 rootful podman 加 systemd 运行:UID/GID 1654、只读文件系统、`--cap-drop all`、`no-new-privileges`、`/tmp` 用 tmpfs;配置和身份放在挂载的数据目录里。
|
||||
- **最小权限运行**:非 root 用户;只在需要时授予 `CAP_NET_RAW`(ICMP);默认端口都高于 1024,不需要 `CAP_NET_BIND_SERVICE`,只有在 Agent 本地显式开启 443 时才授予。
|
||||
- **身份**:节点使用本地生成的 ECDSA P-256 密钥对签名认证(Agent 和 Probe 都自己生成;Probe 有两把独立的密钥,一把连面板、一把连 Agent),Backend 只存公钥,数据库泄露也无法冒充节点。
|
||||
- **防放大**:未认证的包静默丢弃;回复长度与请求相等;每个流按声明速率限速;使用重放窗口。
|
||||
- **容器里的主机指标**:容器里的 Agent 通过只读挂载宿主机的 `/proc`、`/sys`、根目录和 `/etc/os-release`(环境变量 `HOST_PROC`、`HOST_SYS`、`HOST_ROOT`)读取宿主机的资源占用和系统名;读不到的项返回空值,不会退回去读容器自己的数据。Probe 的 compose 也一样挂载这几项。
|
||||
- **自身状态**:节点上报进程内的计数(会话数、包速率、队列、丢弃数);Agent 另外每隔几秒用 `moshiro.host` 上报一份主机资源占用的汇总数字(可以在 `agent.json` 里用 `reporting.host_stats = false` 整个关掉),最新值实时推给展示页,Backend 每分钟存一个样本(默认保留 7 天,`retention.host_days`)。
|
||||
- 本地硬上限写在节点本机的配置文件中,Backend 下发的任务如果超出,节点直接拒绝:允许访问的目标网段(默认为空,即不开放目标任务)、总码率和包速率上限、允许监听的端口。
|
||||
- 构建期强制:常驻节点进程使用 `BannedSymbols.Node.txt`,禁用 `System.Diagnostics.Process`、`Assembly.Load*`、`NativeLibrary.Load`、`HttpClient`,以及 `ConfigStore` 以外的文件写入 API,详见 [project-layout.md](../dev/project-layout.md) 第 7 节。
|
||||
- 权限分离:注册(`fleet-enroll`,一次性运行)、采集(`moshiro-agent` / `moshiro-probe`)、更新是不同的进程。容器节点里,采集进程在容器内以 UID 1654 运行(只读文件系统、不带额外能力),更新由宿主机上 root 运行的 `moshiro-update` 完成:它只读取节点放进 `update/` 的普通文件、复制到自己的临时目录后验签,状态写在节点只读的 `update-status/`;采集进程改写不了自己的二进制,更新器也读不到节点身份私钥。纯二进制节点的更新见[路线图](../roadmap.md)。
|
||||
- 容器运行:Agent、Probe 的镜像基于 Alpine(musl,NativeAOT),宿主机用 rootful podman 加 systemd 运行:UID/GID 1654、只读文件系统、`--cap-drop all`、`no-new-privileges`、`/tmp` 用 tmpfs;配置和身份放在挂载的数据目录里。
|
||||
- 最小权限运行:非 root 用户;只在需要时授予 `CAP_NET_RAW`(ICMP);默认端口都高于 1024,不需要 `CAP_NET_BIND_SERVICE`,只有在 Agent 本地显式开启 443 时才授予。
|
||||
- 身份:节点使用本地生成的 ECDSA P-256 密钥对签名认证(Agent 和 Probe 都自己生成;Probe 有两把独立的密钥,一把连面板、一把连 Agent),Backend 只存公钥,数据库泄露也无法冒充节点。
|
||||
- 防放大:未认证的包静默丢弃;回复长度与请求相等;每个流按声明速率限速;使用重放窗口。
|
||||
- 容器里的主机指标:容器里的 Agent 通过只读挂载宿主机的 `/proc`、`/sys`、根目录和 `/etc/os-release`(环境变量 `HOST_PROC`、`HOST_SYS`、`HOST_ROOT`)读取宿主机的资源占用和系统名;读不到的项返回空值,不会退回去读容器自己的数据。Probe 的 compose 也一样挂载这几项。
|
||||
- 自身状态:节点上报进程内的计数(会话数、包速率、队列、丢弃数);Agent 另外每隔几秒用 `moshiro.host` 上报一份主机资源占用的汇总数字(可以在 `agent.json` 里用 `reporting.host_stats = false` 整个关掉),最新值实时推给展示页,Backend 每分钟存一个样本(默认保留 7 天,`retention.host_days`)。
|
||||
|
||||
## 6. 可扩展性目标
|
||||
|
||||
@@ -142,10 +144,10 @@ Kind 目录编译在二进制中,节点不接受远程下发代码(安全边
|
||||
|
||||
| # | 决策 | 理由 |
|
||||
| --- | --- | --- |
|
||||
| D1 | 只关注网络层;节点是纯采集器,没有远程执行能力 | 由用户确认;防止权限泄露 |
|
||||
| D2 | 拓扑为多个 Probe 连接多个 Agent(10+);线路 = (Probe, Agent),数据按 Probe 隔离 | 由用户确认 |
|
||||
| D1 | 只关注网络层;节点是纯采集器,没有远程执行能力 | 防止权限泄露 |
|
||||
| D2 | 拓扑为多个 Probe 连接多个 Agent(10+);线路 = (Probe, Agent),数据按 Probe 隔离 | 拓扑需求 |
|
||||
| D3 | Agent 用加入令牌自动注册,之后用密钥对签名认证 | 批量部署省事;Backend 不保存可用于冒充节点的秘密 |
|
||||
| D4 | Probe 直连 Backend:身份、Agent 列表(期望状态 `probe_agents`)、状态和更新都走这条链路,测量仍然只走 Probe → Agent(2026-10 由「Probe 不认识 Backend、经 Agent 同步列表」改成直连,因为面板本来就要对外暴露,直连让更新和管理少一跳)。哪些 Probe 连哪些 Agent 由面板决定,`fleet.auto_link` 打开时两两连,也可以手动选择,Probe 始终主动拨出 | 新增 Agent 后自动覆盖,不需要改 Probe 配置;Probe 的版本、更新和线路都能在面板上管理 |
|
||||
| D4 | Probe 直连 Backend:身份、Agent 列表(期望状态 `probe_agents`)、状态和更新都走这条链路,测量仍然只走 Probe → Agent。哪些 Probe 连哪些 Agent 由面板决定,`fleet.auto_link` 打开时两两连,也可以手动选择,Probe 始终主动拨出 | 新增 Agent 后自动覆盖,不需要改 Probe 配置;Probe 的版本、更新和线路都能在面板上管理 |
|
||||
| D5 | 只采集逐包、逐事件的原始数据,解释全部放在 Backend | 模型和指标可以事后重算;采集端最轻 |
|
||||
| D6 | 统一的任务模型 + 期望任务集调和 | 新增任务类型时协议不变 |
|
||||
| D7 | Kind 由积木组合声明;原始块自描述 | 调整密度或新增采集方法只改一处 |
|
||||
@@ -155,20 +157,18 @@ Kind 目录编译在二进制中,节点不接受远程下发代码(安全边
|
||||
| D11 | API 分为 public / admin / agent 三层;实时数据用 SSE | 前端只读场景足够;反向代理和 CDN 更友好 |
|
||||
| D12 | 单仓库,三个独立项目加 Web(展示端、管理端);契约以 C# 代码为真源 | 改一处,编译期即可发现不一致 |
|
||||
| D13 | Probe/Agent 运行时只依赖 BCL;.NET 10 NativeAOT | 轻量 |
|
||||
| D14 | 节点机制抽成与业务无关的 Fleet 框架,放在同一仓库的 `backend/fleet/` 目录,暂不独立(由用户确认;不设拆分时间表,需要时再评估;不使用 PublicApiAnalyzers) | 可复用;不引用任何 `MoShiro.*` 的约束由 `ArchitectureTests` 在构建时检查 |
|
||||
| D14 | 节点机制抽成与业务无关的 Fleet 框架,放在同一仓库的 `backend/fleet/` 目录,暂不独立(不设拆分时间表,需要时再评估;不使用 PublicApiAnalyzers) | 可复用;不引用任何 `MoShiro.*` 的约束由 `ArchitectureTests` 在构建时检查 |
|
||||
| D15 | 注册独立为 `fleet-enroll` 工具;常驻进程不包含注册代码 | 常驻进程去掉 HttpClient,边界更纯 |
|
||||
| D16 | 更新与业务进程分离:发布密钥签名、本地策略、健康确认与回滚。已实现:宿主机 `moshiro-update`(Backend、脚本装的节点,清单里是镜像 digest),Quadlet 节点则靠退出后由 systemd 重新拉镜像;纯二进制节点的更新见 [TODO.md](../../TODO.md) | 面板被攻破时也无法推送恶意二进制 |
|
||||
| D17 | Probe 支持 OpenWrt:正式支持 `linux-musl-x64`、`linux-musl-arm64`;armv7 视工具链验证结果决定;MIPS 不支持,改为在局域网设备上运行 Probe | 由用户确认;NativeAOT 不支持 MIPS |
|
||||
| D18 | 默认测量端口不使用 443:控制 7443/tcp,QUIC Datagram 数据 7443/udp,TCP 数据 7444/tcp;443 只在 Agent 本地显式开启时才允许使用 | 由用户确认;避免与 VPS 上的真实服务冲突;结果会记录实际端口 |
|
||||
| D19 | 展示查询最细 1s:最近 1 小时来自内存热层,更早的数据来自 10s/1m/5m/1h 各层,现场合并;时间段内的汇总由累加器精确合并得出 | 由用户确认;5 分钟、15 分钟这类视图足够精细且快 |
|
||||
| D20 | 时序响应采用列式格式(`t[]` + `values{}`,时间轴使用 Unix 毫秒) | 由用户确认;体积小,可以直接交给图表库 |
|
||||
| D16 | 更新与业务进程分离:发布密钥签名、本地策略、健康确认与回滚。已实现:宿主机 `moshiro-update`(Backend、脚本装的节点,清单里是镜像 digest),Quadlet 节点则靠退出后由 systemd 重新拉镜像;纯二进制节点的更新见[路线图](../roadmap.md) | 面板被攻破时也无法推送恶意二进制 |
|
||||
| D17 | Probe 支持 OpenWrt:正式支持 `linux-musl-x64`、`linux-musl-arm64`;armv7 视工具链验证结果决定;MIPS 不支持,改为在局域网设备上运行 Probe | NativeAOT 不支持 MIPS |
|
||||
| D18 | 默认测量端口不使用 443:控制 7443/tcp,QUIC Datagram 数据 7443/udp,TCP 数据 7444/tcp;443 只在 Agent 本地显式开启时才允许使用 | 避免与 VPS 上的真实服务冲突;结果会记录实际端口 |
|
||||
| D19 | 展示查询最细 1s:最近 1 小时来自内存热层,更早的数据来自 10s/1m/5m/1h 各层,现场合并;时间段内的汇总由累加器精确合并得出 | 5 分钟、15 分钟这类视图足够精细且快 |
|
||||
| D20 | 时序响应采用列式格式(`t[]` + `values{}`,时间轴使用 Unix 毫秒) | 体积小,可以直接交给图表库 |
|
||||
| D21 | 国家和运营商只由 Backend 向 IP 归属服务查询(缺省 `https://ip.007995.xyz`,可关闭),只查公网地址;节点不访问任何外部服务;公开接口不返回 IP、城市、ASN | 客户的防火墙常常要审计节点的出站连接;访客拿不到敏感信息(有测试保证) |
|
||||
| D22 | 构建信息(版本、tag、提交、构建时间)由节点自报,访客是否可见由设置 `site.show_build` 控制(默认关);提交号只在管理端 | 展示页可以选择不暴露版本;管理员能看出哪个节点没升级 |
|
||||
| D23 | 零信任方向:信任锚点从 Backend 移到操作员 passkey(操作员集合,节点安装时固定创世指纹和属主);Probe 自己生成密钥;Agent 的 TCP 端口预握手敲门;敏感能力以后放进独立 extension,用属主 passkey 签名的能力令牌授权。不做进程沙箱(节点是 1 核 1 GB 的小机器),代码风险靠 PR 评审 | 由用户确认;做法和各缺口的状态见 [security.md](../security/security.md)、[access.md](../security/access.md) |
|
||||
| D23 | 零信任方向:信任锚点从 Backend 移到操作员 passkey(操作员集合,节点安装时固定创世指纹和属主);Probe 自己生成密钥;Agent 的 TCP 端口预握手敲门;敏感能力以后放进独立 extension,用属主 passkey 签名的能力令牌授权。不做进程沙箱(节点是 1 核 1 GB 的小机器),代码风险靠 PR 评审 | 做法和各缺口的状态见 [security.md](../security/security.md)、[access.md](../security/access.md) |
|
||||
|
||||
## 9. 仍待确认
|
||||
## 9. 未决事项
|
||||
|
||||
待确认事项:
|
||||
|
||||
1. **告警**:首版不包含,是否需要预留 Webhook?
|
||||
2. **与真实服务共用 443**(例如 SNI 分流):首版不做,默认使用非 443 端口;以后如有需要再评估。
|
||||
- 告警:首版不包含,是否预留 Webhook 见[路线图](../roadmap.md)。
|
||||
- 与真实服务共用 443(例如 SNI 分流):默认使用非 443 端口,需要时再评估。
|
||||
+30
-25
@@ -2,9 +2,9 @@
|
||||
|
||||
## 1. 原则
|
||||
|
||||
- **只采集原始数据**:逐包、逐消息、逐事件,TCP 另加周期性的内核采样;不计算任何统计,解释全部放在 [analysis.md](analysis.md)。
|
||||
- **一处声明,处处生效**:采集方法(Kind)由积木组合**声明**而成。Probe 和 Agent 共用同一套积木(`backend/shared/MoShiro.Measurement`)。
|
||||
- **密度即参数**:频率、包大小、采样间隔全部是任务参数,由 API 下发,不需要改代码。
|
||||
- 只采集原始数据:逐包、逐消息、逐事件,TCP 另加周期性的内核采样;不计算任何统计,解释全部放在 [analysis.md](analysis.md)。
|
||||
- 一处声明,处处生效:采集方法(Kind)由积木组合声明而成。Probe 和 Agent 共用同一套积木(`backend/shared/MoShiro.Measurement`)。
|
||||
- 密度即参数:频率、包大小、采样间隔全部是任务参数,由 API 下发,不需要改代码。
|
||||
|
||||
## 2. 任务模型
|
||||
|
||||
@@ -22,9 +22,9 @@ schedule continuous 持续运行,按时间切块
|
||||
once 按需测试,由 API 触发,也就是一次 Test
|
||||
```
|
||||
|
||||
- **线路任务模板**:`agent_id` 为空的 `line` 任务会自动应用到所有 `active` 的 Agent。新 Agent 注册后自动获得这些任务;也可以针对单个 Agent 覆盖参数或停用。
|
||||
- **测试方案(Plan)**:多个阶段的 `once` 任务组合,例如 `hy2_ladder` 是 `udp_train` 在 10、25、50、100 Mbps 下各运行 10 秒。Plan 定义在 `Protocol/Catalog/Plans`。
|
||||
- **期望任务集**:Backend 为每个 Agent 计算“有效任务集”(模板 + 覆盖 + 目标任务),带版本号,作为 Fleet 的期望状态(`fleet.state.sync`,文档类型 `TaskSet`)下发。Agent 的 `TaskReconciler` 与当前运行的任务对比后,启动、停止或修改参数;其中线路任务再经过控制通道同步给 Probe。**新增任务类型时,协议不需要任何改动。**
|
||||
- 线路任务模板:`agent_id` 为空的 `line` 任务会自动应用到所有 `active` 的 Agent。新 Agent 注册后自动获得这些任务;也可以针对单个 Agent 覆盖参数或停用。
|
||||
- 测试方案(Plan):多个阶段的 `once` 任务组合,例如 `hy2_ladder` 是 `udp_train` 在 10、25、50、100 Mbps 下各运行 10 秒。Plan 定义在 `backend/src/MoShiro.Backend.Application/Tests/Plans.cs`。
|
||||
- 期望任务集:Backend 为每个 Agent 计算“有效任务集”(模板 + 覆盖 + 目标任务),带版本号,作为 Fleet 的期望状态(`fleet.state.sync`,文档类型 `TaskSet`)下发。Agent 的 `TaskManager` 与当前运行的任务对比后,启动、停止或修改参数;其中线路任务再经过控制通道同步给 Probe。新增任务类型时协议不变。
|
||||
|
||||
## 3. Kind 目录:积木组合
|
||||
|
||||
@@ -37,9 +37,9 @@ schedule continuous 持续运行,按时间切块
|
||||
| Payload(负载形态) | `fixed`、`uniform`、`choice` | `size`、`min`/`max`、`sizes[]` |
|
||||
| Behavior(对端行为) | `echo`、`sink`、`source`、`connect`、`hold` | `direction`(`fwd`/`rev`/`bidir`)、`streams` |
|
||||
| Sampler(采样器,可挂到任意连接上) | `tcp_info`、`socket_counters` | `every_ms` |
|
||||
| Recorder(记录器) | 根据 Kind 声明的列**自动生成**的列式记录器 | — |
|
||||
| Recorder(记录器) | 根据 Kind 声明的列自动生成的列式记录器 | — |
|
||||
|
||||
**两种角色互相独立**:连接角色(Probe 永远是拨号方,适配 NAT)与数据角色(发送端、接收端、回显端,由 `direction` 决定)无关。每种积木只实现一次,Probe 和 Agent 共用。
|
||||
两种角色互相独立:连接角色(Probe 永远是拨号方,适配 NAT)与数据角色(发送端、接收端、回显端,由 `direction` 决定)无关。每种积木只实现一次,Probe 和 Agent 共用。
|
||||
|
||||
### 3.2 Kind 声明(`MoShiro.Protocol/Catalog/Kinds`)
|
||||
|
||||
@@ -64,10 +64,10 @@ public static readonly KindSpec TcpEcho = new(
|
||||
|
||||
这一条声明会被四个地方使用:
|
||||
|
||||
- **Probe/Agent**:组装积木执行任务;
|
||||
- **Recorder**:根据列定义记录数据;
|
||||
- **Backend**:校验参数和预算,并在 `/catalog/kinds` 中展示;
|
||||
- **Analysis**:按列名读取数据。
|
||||
- Probe/Agent:组装积木执行任务;
|
||||
- Recorder:根据列定义记录数据;
|
||||
- Backend:校验参数和预算,并在 `/catalog/kinds` 中展示;
|
||||
- Analysis:按列名读取数据。
|
||||
|
||||
### 3.3 内置 Kind
|
||||
|
||||
@@ -91,7 +91,7 @@ Probe 单调时钟 Agent 单调时钟
|
||||
t4 收到 <───────────── t3 回复
|
||||
```
|
||||
|
||||
RTT = `(t4 − t1) − (t3 − t2)`。缺少 `t2` 表示上行丢失;有 `t2`/`t3` 但缺少 `t4` 表示下行丢失。`t2 − t1` 和 `t4 − t3` 的**变化量**在去除偏移和漂移后,就是每个方向各自的时延抖动。
|
||||
RTT = `(t4 − t1) − (t3 − t2)`。缺少 `t2` 表示上行丢失;有 `t2`/`t3` 但缺少 `t4` 表示下行丢失。`t2 − t1` 和 `t4 − t3` 的变化量在去除偏移和漂移后,就是每个方向各自的时延抖动。
|
||||
|
||||
## 4. 原始块(自描述)
|
||||
|
||||
@@ -108,20 +108,20 @@ RawChunk
|
||||
└─ crc32c
|
||||
```
|
||||
|
||||
- **列定义写在块头中**:新增列或新增 Kind 时,**不需要升级 `container_version`**。只有容器格式本身变化时才升级。Backend 可以通用地解码任何块,不认识的列会被保留,但会被分析层忽略。
|
||||
- **切块规则**:持续任务每 **10 秒**切一块,或者记录数达到上限(默认 50 000)时提前切块。这样采集密度提高后,单块大小仍然有界,实时显示和持久化也共用同一条链路。
|
||||
- 列定义写在块头中:新增列或新增 Kind 时不需要升级 `container_version`。只有容器格式本身变化时才升级。Backend 可以通用地解码任何块,不认识的列会被保留,但会被分析层忽略。
|
||||
- 切块规则:持续任务每 10 秒切一块,或者记录数达到上限(默认 50 000)时提前切块。这样采集密度提高后,单块大小仍然有界,实时显示和持久化也共用同一条链路。
|
||||
- 按需测试按阶段切块,阶段内同样受记录数上限约束。
|
||||
- 编解码器位于 `MoShiro.Protocol/Raw`,Probe、Agent、Backend、离线工具使用同一份代码。
|
||||
|
||||
## 5. 列的归属与合并
|
||||
|
||||
> **两侧各自记录完整行的流**(`StreamSpec.Union`,例如 `tcp_info`):Probe 和 Agent 各自采样自己的套接字(Linux `TCP_INFO`,通过 `Socket.GetRawSocketOption`,不需要 P/Invoke),每一行带 `side` 标明属于哪一侧;合并时直接拼接,不连接,Agent 一侧的行每行只交出一次。Probe 的重传反映上行方向,Agent 的重传反映下行方向(代理场景里大流量走这个方向)。非 Linux 平台没有这类采样,对应的指标在窗口里就是没有数据。
|
||||
两侧各自记录完整行的流(`StreamSpec.Union`,例如 `tcp_info`):Probe 和 Agent 各自采样自己的套接字(Linux `TCP_INFO`,通过 `Socket.GetRawSocketOption`,不需要 P/Invoke),每一行带 `side` 标明属于哪一侧;合并时直接拼接,不连接,Agent 一侧的行每行只交出一次。Probe 的重传反映上行方向,Agent 的重传反映下行方向(代理场景里大流量走这个方向)。非 Linux 平台没有这类采样,对应的指标在窗口里就是没有数据。
|
||||
|
||||
`tcp_info` 的可空 `conn` 列标识当前记录器内的一条 TCP 连接,不包含地址。Backend 按 `(side, conn)` 分别求累计计数差,避免把重连前后或同时存活的套接字混为一个序列;时间不递增的样本被忽略,任一计数器回退时重新建立基线。旧数据没有 `conn`,仍按 `side` 分组,无法事后恢复丢失的连接归属。升级时先更新 Backend 和 Agent,再更新 Probe;旧 Agent 合并采样时会丢弃尚不认识的列。
|
||||
|
||||
TCP 回显的长度头和正文合为一次写入,避免在启用 `TCP_NODELAY` 时为长度头单独发送一个小段。`tcp.retrans` 仍按内核的重传段数与发送段数求比,不由应用消息数估算。
|
||||
|
||||
采集层只产生**原始列**(`seq`、`size`、`t1…t4`、`t_send`/`t_recv`、`outcome`……),不做任何统计,也不认识上层的方案或协议;丢包率、RTT、抖动由 Backend 的分析层从这些列推算。
|
||||
采集层只产生原始列(`seq`、`size`、`t1…t4`、`t_send`/`t_recv`、`outcome`……),不做任何统计,也不认识上层的方案或协议;丢包率、RTT、抖动由 Backend 的分析层从这些列推算。
|
||||
|
||||
每个记录流(`StreamSpec`)声明:
|
||||
|
||||
@@ -129,17 +129,17 @@ TCP 回显的长度头和正文合为一次写入,避免在启用 `TCP_NODELAY
|
||||
- `Key`:连接键(通常是 `seq`);
|
||||
- 每一列属于哪一侧由它的时钟决定:`TsProbe` 属于 Probe,`TsAgent` 属于 Agent,其余列属于主导方。
|
||||
|
||||
两端各自把**本侧的列**记在一张按键索引的环形行表里(`RowStore`),Probe 每 10 秒把已完成的行切成列块经控制通道交给**对应的 Agent**,Agent 按声明合并成完整的 RawChunk 后上传:
|
||||
两端各自把本侧的列记在一张按键索引的环形行表里(`RowStore`),Probe 每 10 秒把已完成的行切成列块经控制通道交给对应的 Agent,Agent 按声明合并成完整的 RawChunk 后上传:
|
||||
|
||||
- `Driver = Probe`(回显):行取自列块,Agent 的列按键补上,缺失就是缺失(上行丢失);
|
||||
- `Driver = Agent`(单向数据流):行取自 Agent 的记录(按列块头里的序号范围),Probe 的列按键补上,缺失就是缺失(丢失);
|
||||
- 没有 `Key`,或只有主导方一侧有列:原样透传。
|
||||
|
||||
新增一种记录流只需要写声明,不需要写合并代码。Probe 的列在规定时间内没有送达时,目前 Agent 带着已有的数据继续(不上传只有 Agent 一侧的 `partial` 块,见已知缺口)。
|
||||
新增一种记录流只需要写声明,不需要写合并代码。Probe 的列在规定时间内没有送达时,Agent 带着已有的数据继续,不上传只有 Agent 一侧的块。
|
||||
|
||||
## 6. 代码结构(`backend/shared/MoShiro.Measurement`)
|
||||
|
||||
同一种采集方法的两端是**一对角色**写在同一个文件里:拨号方(Probe 永远是拨号方,适配 NAT)和监听方(Agent)。两端共用包的构造和校验、时钟、行表和合并;Probe 和 Agent 只负责组装自己那一侧。
|
||||
同一种采集方法的两端是一对角色写在同一个文件里:拨号方(Probe 永远是拨号方,适配 NAT)和监听方(Agent)。两端共用包的构造和校验、时钟、行表和合并;Probe 和 Agent 只负责组装自己那一侧。
|
||||
|
||||
```text
|
||||
MoShiro.Measurement/ 依赖:MoShiro.Protocol(仅 BCL,零 NuGet)
|
||||
@@ -149,18 +149,23 @@ MoShiro.Measurement/ 依赖:MoShiro.Protocol(仅 BCL,
|
||||
│ ├─ MonoClock.cs 单调时钟(微秒)与 UTC 锚点
|
||||
│ └─ RunningBlock.cs 一个运行中的积木(取消令牌 + 后台任务)
|
||||
├─ Net/
|
||||
│ ├─ QuicDatagramTransport.cs、MeasurementDatagramRouter.cs MsQuic 数据面与逐流认证分发
|
||||
│ ├─ MsQuic*.cs、QuicDatagramTransport.cs、MeasurementDatagramRouter.cs MsQuic 数据面与逐流认证分发
|
||||
│ ├─ LinuxIcmpEcho.cs、TcpInfo.cs ICMP 回显;Linux `TCP_INFO` 采样
|
||||
│ └─ Tcp.cs TcpLink(拨号:TLS 按指纹固定 + 认证首帧)、TcpAcceptor(监听:TLS 自动识别 + 认证首帧)
|
||||
├─ Blocks/ 一个文件 = 一种采集方法的两个角色
|
||||
│ ├─ BlockBase.cs 积木的公共基类
|
||||
│ ├─ Echo.cs DatagramEchoDialer/Listener、TcpEchoDialer/Listener
|
||||
│ ├─ Session.cs ConnectDialer、HoldDialer、IcmpEchoSolo(没有对端)、IdleListener
|
||||
│ ├─ Train.cs TrainDialer(接收,记 t_recv)、TrainListener(按码率发包,记发送行)
|
||||
│ ├─ Bulk.cs `tcp_bulk` 的两个角色
|
||||
│ └─ BlockCatalog.cs Kind → (拨号方, 监听方);capabilities、本地预算的支持列表都从这里推出来
|
||||
├─ Merging/ FragmentMerger(声明驱动的按键合并)、ChunkFinalizer(盖章:块头、时钟锚点、起止时间)
|
||||
└─ Execution/ BudgetGuard(参数校验 + 本地硬上限,超出即拒绝)
|
||||
├─ Execution/ BudgetGuard(参数校验 + 本地硬上限,超出即拒绝)
|
||||
├─ Platform/ 主机汇总采样、IPv6 诊断、本机地址、平台识别
|
||||
└─ Update/ RestartUpdater(收到更新请求后有序退出)
|
||||
```
|
||||
|
||||
**边界**(由架构测试强制):采集层的任何代码里不能出现 REALITY、Hysteria2、Minecraft 这些业务概念,也没有测试方案——方案(例如 `hy2_ladder`、`reality_session`)只是若干采集原语阶段的编排,只存在于 Backend。监听方的准入策略(重放窗口、令牌桶、源地址学习)是安全边界,通过 `IAdmission` 由 Agent 实现,积木本身不含策略。
|
||||
边界(由架构测试强制):采集层的任何代码里不能出现 REALITY、Hysteria2、Minecraft 这些业务概念,也没有测试方案——方案(例如 `hy2_ladder`、`reality_session`)只是若干采集原语阶段的编排,只存在于 Backend。监听方的准入策略(重放窗口、令牌桶、源地址学习)是安全边界,通过 `IAdmission` 由 Agent 实现,积木本身不含策略。
|
||||
|
||||
## 7. 资源与安全上限
|
||||
|
||||
@@ -181,10 +186,10 @@ MoShiro.Measurement/ 依赖:MoShiro.Protocol(仅 BCL,
|
||||
| QUIC Datagram 数据(`udp_echo`、`udp_train`) | 7443/udp | Agent 固定监听;Probe 每次新连接轮换本地源端口 |
|
||||
| TCP/TLS 数据(`tcp_echo`、`tcp_bulk`、`connect`、`hold`) | 7444/tcp | 按首帧中的 `stream_id` 分发 |
|
||||
|
||||
- **默认不使用 443**,避免与 VPS 上已有的 REALITY 或 Hysteria2 服务冲突,也不需要特权端口权限。
|
||||
- 默认不使用 443,避免与 VPS 上已有的 REALITY 或 Hysteria2 服务冲突,也不需要特权端口权限。
|
||||
- 任务参数中的 `port` 只能取 Agent 本地配置 `listen` 列表中的端口。要用 443 测量,需要节点所有者在本机把 443 加入 `listen`(并授予 `CAP_NET_BIND_SERVICE`),然后在任务中指定 `port: 443`。
|
||||
- 每个原始块的头部都记录实际使用的端口。当测量端口与被推算服务的常用端口(443)不同时,分析结果附带 `port_differs_from_service` 置信说明,因为运营商的 QoS 可能按端口区别对待。
|
||||
|
||||
## 9. 边界
|
||||
|
||||
采集端只模拟流量特征(端口、包大小、速率、握手、方向),**不实现** REALITY 或 Hysteria2 协议本身,分析结果会附带置信说明。用户态看不到 TCP 的逐段收发,所以 TCP 能拿到的最原始数据是“逐消息时间戳 + 内核采样”;pcap/eBPF 不在首版范围内。
|
||||
采集端只模拟流量特征(端口、包大小、速率、握手、方向),不实现 REALITY 或 Hysteria2 协议本身,分析结果附带置信说明。用户态看不到 TCP 的逐段收发,TCP 的原始数据是逐消息时间戳加内核采样;pcap / eBPF 见[路线图](../roadmap.md)。
|
||||
+11
-11
@@ -2,10 +2,10 @@
|
||||
|
||||
MoShiro 的两条控制链路都建立在 Fleet 之上:
|
||||
|
||||
- **Agent ↔ Backend**:Fleet 连接服务端(wss),见 [../backend/fleet/README.md](../../backend/fleet/README.md);
|
||||
- **Probe ↔ Agent**:Fleet 节点间点对点会话(TLS),见 [../backend/fleet/README.md](../../backend/fleet/README.md) 的「点对点会话」一节。
|
||||
- Agent ↔ Backend:Fleet 连接服务端(wss),见 [Fleet 文档](../../backend/fleet/README.md);
|
||||
- Probe ↔ Agent:Fleet 节点间点对点会话(TLS),见 [Fleet 文档](../../backend/fleet/README.md) 的「点对点会话」一节。
|
||||
|
||||
认证、心跳、重连、能力协商、可靠发件箱、期望状态同步、JSON-RPC 信封都由 Fleet 负责。本文只定义 **MoShiro 自己的内容**:`moshiro.*` 方法、状态文档、二进制通道,以及 Fleet 之外的数据面(QUIC Datagram 和 TCP 数据连接)。所有类型都放在 `MoShiro.Protocol` 中。
|
||||
认证、心跳、重连、能力协商、可靠发件箱、期望状态同步、JSON-RPC 信封都由 Fleet 负责。本文只定义 MoShiro 自己的内容:`moshiro.*` 方法、状态文档、二进制通道,以及 Fleet 之外的数据面(QUIC Datagram 和 TCP 数据连接)。所有类型都放在 `MoShiro.Protocol` 中。
|
||||
|
||||
## 1. Fleet 能力的映射
|
||||
|
||||
@@ -15,8 +15,8 @@ MoShiro 的两条控制链路都建立在 Fleet 之上:
|
||||
| 下发 Agent 列表给 Probe | 期望状态 `fleet.state.sync`(Probe 直连面板) | 文档类型 `probe_agents`:`version`、`agents[]`(`agent_id`、`host`、`tcp_port`、`quic_ports`、`public_key`),`public_key` 是 Agent 面向 Probe 的 TLS 证书公钥(SPKI,base64):Probe 用它固定证书并派生敲门密钥;没有上报公钥的 Agent 不会下发。已按线路开关过滤;Agent 集合或线路开关变化时重新推送 |
|
||||
| 定时重启 | 通知 `moshiro.schedule`(B→A、B→P) | `{restart_at}`,形如 `04:30+08:00`,空表示关闭;连上时补发、管理端改了设置就群发。节点本地写了 `restart_at` 或 `MOSHIRO_RESTART_AT` 时以本地为准;节点只是到点自己有序退出,由服务管理器拉起 |
|
||||
| 更新 | 通知 `moshiro.update.offer` / `moshiro.update.apply`(B→A、B→P),心跳附加字段 `update` | `offer` 带 CI 签名的发布清单原文和签名,节点只写进数据目录;`apply` 请求更新;`update` 是宿主机更新器的状态(`state`、`current`、`available`、`error`、`at`),见 [deployment.md](../ops/deployment.md) |
|
||||
| 上报原始块 | 发件箱,二进制通道 **16** | `RawChunk`(自描述,见 [measurement.md](measurement.md) 第 4 节),`msg_id = chunk_id` |
|
||||
| Probe 交付列块 | 点对点会话的发件箱,二进制通道 **17** | 列块:`u32 stream_id \| u32 first_seq \| u32 count \| 列数据(Brotli)` |
|
||||
| 上报原始块 | 发件箱,二进制通道 16 | `RawChunk`(自描述,见 [measurement.md](measurement.md) 第 4 节),`msg_id = chunk_id` |
|
||||
| Probe 交付列块 | 点对点会话的发件箱,二进制通道 17 | 列块:`u32 stream_id \| u32 first_seq \| u32 count \| 列数据(Brotli)` |
|
||||
| 节点自身状态 | `fleet.heartbeat` 的附加字段 | Agent:`sessions`、`probes[]`、`pps_in/out`、`dropped_invalid`、`dropped_rate_limited`、`queue_bytes`、`update`;Probe:只有 `update` |
|
||||
| 版本与能力 | `fleet.hello` | capabilities 包括 `moshiro.kinds:<列表>`、`moshiro.blocks:<列表>`;Agent 附加 `platform`(系统、架构、版本、tag、提交、构建时间)、`tls_pubkey`(面向 Probe 的证书公钥,面板自己算指纹)、`addresses`(本机公网地址)、`probe_ports`、`local_limits` 摘要;Probe 附加 `platform` |
|
||||
|
||||
@@ -53,7 +53,7 @@ MoShiro 的两条控制链路都建立在 Fleet 之上:
|
||||
|
||||
### 列块(Probe → Agent,通道 17)
|
||||
|
||||
Probe 只记录自己一侧的列,Agent 在数据面记录另一侧,由 Agent 合并、盖章、上传(见 `measurement.md` 第 5 节)。合并**由 Kind 的流声明驱动**(`StreamSpec`:`Driver`、`Key`,每一列的归属由它的时钟决定),没有按 Kind 手写的模式:
|
||||
Probe 只记录自己一侧的列,Agent 在数据面记录另一侧,由 Agent 合并、盖章、上传(见 `measurement.md` 第 5 节)。合并由 Kind 的流声明驱动(`StreamSpec`:`Driver`、`Key`,每一列的归属由它的时钟决定),没有按 Kind 手写的模式:
|
||||
|
||||
| 流声明 | 例子 | Probe 的列块 | Agent 补充 |
|
||||
| --- | --- | --- | --- |
|
||||
@@ -63,7 +63,7 @@ Probe 只记录自己一侧的列,Agent 在数据面记录另一侧,由 Agen
|
||||
|
||||
单块最多 `MaxRecordsPerChunk`(50 000)条记录:Probe 必须把大的阶段按序号范围切成多个列块(例如 100 Mbit/s、1000 B 的 10 秒阶段是 125 000 个包 → 4 个列块),`phase_end` 的应答报告总块数。块头里 Probe 必须带上 `agent_id`(它知道自己在和哪个 Agent 通信)。
|
||||
|
||||
新增方法只需要在 `MoShiro.Protocol/Rpc` 中定义,并在节点组装处加一行 `.On(...)`。旧节点会通过 capabilities 协商自动避开不支持的方法。
|
||||
新增方法在 `MoShiro.Protocol/Rpc` 中定义,并在节点组装处加一行 `.On(...)`。旧节点通过 capabilities 协商避开不支持的方法。
|
||||
|
||||
## 3. 数据面(不属于 Fleet)
|
||||
|
||||
@@ -82,7 +82,7 @@ Probe 只记录自己一侧的列,Agent 在数据面记录另一侧,由 Agen
|
||||
28+N 16 mac HMAC-SHA256(session_key, 前面全部字节),截断至 16 字节
|
||||
```
|
||||
|
||||
**接收方处理规则**(任一步失败即静默丢弃并计数,**不回复**):
|
||||
接收方处理规则(任一步失败即静默丢弃并计数,不回复):
|
||||
|
||||
1. 长度、magic、version、type 合法;
|
||||
2. `session_id` 和 `stream_id` 都处于活跃状态;
|
||||
@@ -90,7 +90,7 @@ Probe 只记录自己一侧的列,Agent 在数据面记录另一侧,由 Agen
|
||||
4. `seq` 不在重放窗口中;
|
||||
5. 速率不超过声明速率的 1.2 倍,也不超过本地硬上限。
|
||||
|
||||
通过后先**记录原始列**。收到 `EchoRequest` 时,立即回复一个长度**完全相同**的包,发往请求的**实际源地址**(这样可以跟随 NAT 映射变化),并记录回复时刻。
|
||||
通过后先记录原始列。收到 `EchoRequest` 时,立即回复一个长度相同的包,发往请求的实际源地址(这样可以跟随 NAT 映射变化),并记录回复时刻。
|
||||
|
||||
### 3.2 TCP/TLS 数据连接
|
||||
|
||||
@@ -107,11 +107,11 @@ Probe 只记录自己一侧的列,Agent 在数据面记录另一侧,由 Agen
|
||||
- Agent 和 Probe 会在 hello 与心跳中上报 IPv6 套接字、监听、全局地址、默认路由及网络命名空间状态。Podman/Docker 宿主网络部署里,采样反映宿主接口;桥接网络容器会分别报告容器网络和能读取到的宿主 IPv6。诊断只有布尔值和粗粒度状态,不包含地址和接口名称。
|
||||
- `ConnectedProbe`、`TestRunParams` 和 RawChunk 块头新增 `ip_version`,取值 `ipv4` / `ipv6`,缺省 `ipv4`。Agent 合并列块时以认证过的控制会话地址族为准,覆盖 Probe 上报的值。
|
||||
- IPv4 的线路 id `agent~probe`、序列 `line:probe` 保持不变;IPv6 使用 `agent~probe~ipv6`、`line:probe:ipv6`。指标、重算、SSE 和测试结果按路径隔离。
|
||||
- 本次是 JSON 字段扩展,二进制包、列编码和容器布局没有变化,协议与 `container_version` 保持原版本。双栈采集需要升级 Agent 和 Probe;旧节点继续提供 IPv4。
|
||||
- 双栈是 JSON 字段扩展,二进制包、列编码和容器布局不变,`protocol_version` 与 `container_version` 不升级。双栈采集需要升级 Agent 和 Probe,旧节点继续提供 IPv4。
|
||||
|
||||
### 通用规则
|
||||
|
||||
- Fleet 层的兼容规则见 [../backend/fleet/README.md](../../backend/fleet/README.md)。
|
||||
- Fleet 层的兼容规则见 [Fleet 文档](../../backend/fleet/README.md)。
|
||||
- `moshiro.*` 方法:新增方法或可选字段不需要升级版本,接收方忽略未知字段。
|
||||
- RawChunk:块头自描述,新增列或新增 Kind 不需要升级 `container_version`;只有容器格式变化时才升级。
|
||||
- QUIC Datagram 和 TCP 数据连接首帧的布局变化时,必须升级 `protocol_version`,并更新黄金字节测试。
|
||||
|
||||
+28
-25
@@ -4,11 +4,11 @@
|
||||
|
||||
## 1. 设计目标
|
||||
|
||||
1. **任意时间段**:支持 5 分钟、15 分钟这类相对时间段,也支持任意绝对区间。
|
||||
2. **短时间段足够精细**:近 5 分钟精确到 1 秒,近 1 小时精确到 5 秒。
|
||||
3. **汇总精确**:“这段时间内的丢包率、p95”由累加器精确合并得出,**不是各点的平均**。
|
||||
4. **快**:常用的短时间段直接从内存读取;较长的时间段读取预聚合层,现场合并。
|
||||
5. **新增指标时前端不用改**:指标按 id 选取,按分组(preset)展示,含义和单位来自目录。
|
||||
1. 任意时间段:支持 5 分钟、15 分钟这类相对时间段,也支持任意绝对区间。
|
||||
2. 短时间段足够精细:近 5 分钟精确到 1 秒,近 1 小时精确到 5 秒。
|
||||
3. 汇总精确:“这段时间内的丢包率、p95”由累加器精确合并得出,不是各点的平均。
|
||||
4. 快:常用的短时间段直接从内存读取;较长的时间段读取预聚合层,现场合并。
|
||||
5. 新增指标时前端不用改:指标按 id 选取,按分组(preset)展示,含义和单位来自目录。
|
||||
|
||||
## 2. 时间段与步长
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
原始段文件(30 天)── 超出热层时间范围的 1s~5s 精度查询 → 从原始块现算(区间 ≤ 15 分钟)
|
||||
```
|
||||
|
||||
**选择数据源的规则**:在“覆盖该时间段”并且“粒度能整除步长”的各层中,选择**最粗**的一层,然后把累加器按步长现场合并。
|
||||
选择数据源的规则:在“覆盖该时间段”并且“粒度能整除步长”的各层中,选择最粗的一层,然后把累加器按步长现场合并。
|
||||
|
||||
示例:
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
| 同上,但区间 > 15 分钟 | 降级为 10s 层或 1m 层,并标记 `degraded: true` |
|
||||
| 所需的层已经过期 | 降级为下一个更粗的可用层,并标记 `degraded: true` |
|
||||
|
||||
**热层内存估算**:15 条线路 × 3600 秒 × 约 300 B(稀疏直方图 + 计数)≈ 16 MB。热层也是 LiveHub 实时推送的数据来源。
|
||||
热层内存估算:15 条线路 × 3600 秒 × 约 300 B(稀疏直方图 + 计数)≈ 16 MB。热层也是 LiveHub 实时推送的数据来源。
|
||||
|
||||
## 4. 指标选择
|
||||
|
||||
@@ -86,14 +86,15 @@ id 的格式为 `指标.统计量`,详见 [analysis.md](analysis.md) 第 4 节
|
||||
|
||||
`preset=udp,tcp`
|
||||
|
||||
分组在 `Backend.Analysis` 的注册表中声明,并通过 `/api/public/catalog/presets` 下发给前端。新增指标后,只要把它加进某个分组,前端就会自动展示。
|
||||
分组在 `Backend.Analysis` 的 `MetricRegistry.Presets` 中声明,并通过 `/api/public/catalog/presets` 下发给前端。新增指标后,只要把它加进某个分组,前端就会自动展示。
|
||||
|
||||
| 分组 | 指标 |
|
||||
| --- | --- |
|
||||
| `udp` | `loss.up`、`loss.down`、`loss.total`、`rtt.p50`、`rtt.p95`、`rtt.p99`、`jitter.up.p95`、`jitter.down.p95`、`loss.run_max`、`reorder`、`dup` |
|
||||
| `quic` | `loss.up`、`loss.down`、`loss.total`、`rtt.p50`、`rtt.p95`、`rtt.p99`、`jitter.up.p95`、`jitter.down.p95`、`loss.run.up_max`、`loss.run.down_max`、`reorder`、`dup` |
|
||||
| `udp` | 与 `quic` 相同,保留为旧链接的别名;连续丢包使用 `loss.run_max` |
|
||||
| `tcp` | `tcp.rtt.p50`、`tcp.rtt.p95`、`tcp.rtt.p99`、`tcp.rtt.p999`、`tcp.rtt.max`、`tcp.rtt.tail`、`tcp.slow`、`tcp.stall.per_min`、`tcp.stall_max`、`tcp.retrans`、`tcp.srtt.p95` |
|
||||
| `connect` | `connect.success`、`connect.tcp_ms.p95`、`connect.tls_ms.p95`、`connect.fail.timeout`、`connect.fail.rst`、`connect.fail.tls` |
|
||||
| `session` | `hold.resets.per_h`、`hold.lifetime_max`、`probe.endpoint_changes` |
|
||||
| `session` | `hold.resets.per_h`、`hold.lifetime_max` |
|
||||
| `scores` | `score.base`、`score.mc`、`score.hy2`、`score.reality` |
|
||||
|
||||
目录中每个 id 都附带 `unit`、`label`、`description`、`better`(`lower` 或 `higher`,用于前端配色),以及 `thresholds`(优、良、差三档阈值,由模型给出)。
|
||||
@@ -102,7 +103,7 @@ id 的格式为 `指标.统计量`,详见 [analysis.md](analysis.md) 第 4 节
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_id": "agt_…",
|
||||
"line_id": "agt_…~prb_…", "agent_id": "agt_…", "probe_id": "prb_…", "ip_version": "ipv4",
|
||||
"from": "2026-09-28T10:00:00.000Z", "to": "2026-09-28T10:15:00.000Z",
|
||||
"step": "2s", "source": "hot", "live": true, "degraded": false,
|
||||
"t": [1759053600000, 1759053602000, 1759053604000],
|
||||
@@ -123,9 +124,9 @@ id 的格式为 `指标.统计量`,详见 [analysis.md](analysis.md) 第 4 节
|
||||
}
|
||||
```
|
||||
|
||||
- 时间轴 `t` 使用 Unix 毫秒,只出现一次;`values` 中每个数组的长度都与 `t` 相同,可以直接交给 uPlot 或 ECharts。**这是 [api.md](../api/api.md) 第 7.4 节“时间用 RFC 3339”规则的明确例外**,理由是减小体积、方便绘图;其余时间字段仍然使用 RFC 3339。
|
||||
- 时间轴 `t` 使用 Unix 毫秒,只出现一次;`values` 中每个数组的长度都与 `t` 相同,可以直接交给 uPlot 或 ECharts。这是 [api.md](../api/api.md) 第 7.4 节“时间用 RFC 3339”规则的明确例外,理由是减小体积、方便绘图;其余时间字段仍然使用 RFC 3339。
|
||||
- 某个桶没有数据时填 `null`(不填 0),前端据此绘制断线。
|
||||
- `summary` 是**整个时间段**的精确合并结果。`coverage` = 实际数据量 / 期望数据量,用来提示前端“数据是否完整”。
|
||||
- `summary` 是整个时间段的精确合并结果。`coverage` = 实际数据量 / 期望数据量,用来提示前端“数据是否完整”。
|
||||
- `gaps` 和 `events` 用于在曲线上做标注(断线、端点变化、时钟跳变等)。
|
||||
|
||||
## 6. 端点
|
||||
@@ -134,7 +135,7 @@ id 的格式为 `指标.统计量`,详见 [analysis.md](analysis.md) 第 4 节
|
||||
| --- | --- | --- |
|
||||
| `GET /api/public/overview` | 首页排行:每条线路在该时间段内的汇总,并支持排序 | `range`(默认 `5m`)、`preset` 或 `metrics`、`sort`、`order` |
|
||||
| `GET /api/public/lines/{line_id}/series` | 单条线路的时序 | `range` 或 `from/to`、`step`、`preset` 或 `metrics` |
|
||||
| `GET /api/public/compare` | 多条线路对比(时间轴对齐) | `agents`、`metric`(单个)、`range` 或 `from/to`、`step` |
|
||||
| `GET /api/public/compare` | 多条线路对比(时间轴对齐) | `lines`(线路 id,逗号分隔)、`metric`(单个)、`range` 或 `from/to`、`step` |
|
||||
| `GET /api/public/lines/{line_id}/distribution` | 该时间段内的分布(直方图、CDF) | `metric`(直方图类,如 `rtt`、`jitter.down`、`tcp.rtt`)、`range` 或 `from/to` |
|
||||
| `GET /api/public/lines/{line_id}/heatmap` | 时间 × 延迟桶的热力图(类似 smokeping) | `metric`、`range` 或 `from/to`、`step` |
|
||||
| `GET /api/public/catalog/presets` | 分组定义 | — |
|
||||
@@ -145,14 +146,14 @@ id 的格式为 `指标.统计量`,详见 [analysis.md](analysis.md) 第 4 节
|
||||
{
|
||||
"range": "15m", "from": "…", "to": "…",
|
||||
"lines": [
|
||||
{ "agent_id": "agt_…", "name": "jp-1", "online": true,
|
||||
{ "id": "agt_…~prb_…", "agent_id": "agt_…", "probe_id": "prb_…", "name": "jp-1", "online": true,
|
||||
"summary": { "loss.down": 0.1, "loss.up": 0.0, "rtt.p95": 48.0, "tcp.retrans": 0.1, "score.mc": 94 },
|
||||
"coverage": 1.0 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`compare` 的响应示例:`{ "t":[…], "values":{ "agt_a":[…], "agt_b":[…] }, "summary":{ "agt_a":0.1, "agt_b":0.4 } }`
|
||||
`compare` 的响应示例:`{ "t":[…], "values":{ "<line_a>":[…], "<line_b>":[…] }, "summary":{ "<line_a>":0.1, "<line_b>":0.4 } }`
|
||||
|
||||
`distribution` 的响应示例:`{ "metric":"rtt", "unit":"ms", "buckets":[{"le":40.0,"count":812}, …], "p50":44.2, "p95":61.0, "p99":140.2, "count":8990 }`
|
||||
|
||||
@@ -166,7 +167,7 @@ id 的格式为 `指标.统计量`,详见 [analysis.md](analysis.md) 第 4 节
|
||||
② SSE /api/public/live?topics=series:{id}:2s:udp,tcp → 每当原始块到达(约每 10 s),
|
||||
推送一批新的 2s 桶和最新的汇总
|
||||
event: series
|
||||
data: {"agent_id":"…","step":"2s","t":[…5 个],"values":{…},"summary":{…}}
|
||||
data: {"line_id":"…","step":"2s","t":[…5 个],"values":{…},"summary":{…}}
|
||||
③ 前端把新点追加到末尾,并丢弃超出窗口的旧点;用推送来的汇总替换原来的汇总
|
||||
|
||||
前端切换为“自定义 09:00–10:00”→ 只执行 ①(live=false)
|
||||
@@ -174,7 +175,7 @@ id 的格式为 `指标.统计量`,详见 [analysis.md](analysis.md) 第 4 节
|
||||
|
||||
- SSE 的订阅主题格式为 `series:{line_id}:{step}:{preset}`,只允许 ≤ 1m 的步长;更粗的步长由前端定时重新拉取即可。
|
||||
- `overview` 也可以跟随实时:订阅 `overview:{range}:{preset}`,每 10 s 推送一次最新汇总。
|
||||
- 实时延迟:取决于原始块的切块节奏(10 s)加上处理时间,**典型值为 10~12 s**。1s 步长的点会按批次到达,而不是每秒一个。
|
||||
- 实时延迟:取决于原始块的切块节奏(10 s)加上处理时间,典型值为 10~12 s。1s 步长的点会按批次到达,而不是每秒一个。
|
||||
|
||||
## 8. 缓存与限额
|
||||
|
||||
@@ -195,15 +196,17 @@ id 的格式为 `指标.统计量`,详见 [analysis.md](analysis.md) 第 4 节
|
||||
## 9. 实现位置
|
||||
|
||||
```text
|
||||
Backend.Api/Public/SeriesEndpoints.cs 参数解析、校验、缓存头(不含业务)
|
||||
Backend.Api/Public/QueryEndpoints.cs 参数解析、校验、缓存头(不含业务)
|
||||
Backend.Application/Query/
|
||||
├─ RangeResolver range 或 from/to 与 step → 对齐后的桶序列
|
||||
├─ Steps / Bucketing 常用步长与分桶
|
||||
├─ SourcePlanner 选择数据源(热层 / 10s / 1m / 5m / 1h / 原始块现算),判断是否降级
|
||||
├─ SeriesQuery 读取累加器 → 按步长合并 → 导出统计量 → 列式结果
|
||||
├─ SummaryQuery 整个时间段的精确合并(overview 和 series 共用)
|
||||
└─ DistributionQuery 合并直方图 → 分桶、CDF、热力图
|
||||
Backend.Application/Live/LiveHub 热层环形缓冲(1s,1 小时)+ SSE 主题
|
||||
Backend.Analysis 累加器的合并与导出(与持久化路径共用同一份代码)
|
||||
├─ RowLoader 读取累加器行
|
||||
├─ MetricSelection 按 id 或分组选取指标
|
||||
├─ QueryService series、overview、compare、distribution、heatmap
|
||||
└─ QueryResults 列式结果类型
|
||||
Backend.Application/Live/ HotLayer(1s 环形缓冲,1 小时)、HotLayerBackfill、LiveHub(SSE 主题)
|
||||
Backend.Analysis 累加器的合并与导出(与持久化路径共用同一份代码)
|
||||
```
|
||||
|
||||
新增指标时,这套查询代码不需要任何改动:`SeriesQuery` 只通过注册表按 id 读取累加器并导出值。
|
||||
新增指标时查询代码不需要改动:`QueryService` 通过注册表按 id 读取累加器并导出值。
|
||||
+23
-139
@@ -1,162 +1,53 @@
|
||||
# Backend 存储
|
||||
|
||||
实现位于 `MoShiro.Backend.Storage.Sqlite`,负责实现 `Backend.Application` 中定义的端口接口,**只做读写,不含业务逻辑**。降采样和分析由 Application 调用 Analysis 完成。
|
||||
实现位于 `MoShiro.Backend.Storage.Sqlite`,负责实现 `Backend.Application` 中定义的端口接口,只做读写,不含业务逻辑。降采样和分析由 Application 调用 Analysis 完成。
|
||||
|
||||
## 1. 目录布局
|
||||
|
||||
```text
|
||||
data/
|
||||
├─ moshiro.db # SQLite:配置、身份、原始块索引、指标、测试结果
|
||||
├─ backups/ # Backend 更新前的数据库备份
|
||||
└─ raw/
|
||||
└─ 2026-09-27/ # 按 UTC 日期分目录,保留期满整目录删除
|
||||
├─ agt_01J…/ # 每条线路或每个 Agent 一个目录
|
||||
├─ agt_01J…/ # 每个 Agent 一个目录
|
||||
│ ├─ udp_echo.seg # 段文件:同一天、同一 Kind 的块依次追加
|
||||
│ ├─ tcp_echo.seg
|
||||
│ └─ tsk_…_icmp_echo.seg # 目标任务
|
||||
└─ tests/tst_01J…/phase0.seg …
|
||||
```
|
||||
|
||||
- **段文件**:把收到的 RawChunk 原样追加写入(块本身已经带 crc,并且自描述),SQLite 只记录 `(segment, offset, length)`。每 10 秒一块也不会产生海量小文件。
|
||||
- **写入顺序**:追加写入 → `fsync`(每批合并执行一次)→ 插入索引 → 由收件箱确认(Fleet 回复 `fleet.outbox.ack`)。启动时截断没有索引记录的段文件尾部。
|
||||
- **去重**:`chunk_id` 已存在时,直接回复 ACK。
|
||||
- 段文件:把收到的 RawChunk 原样追加写入(块本身已经带 crc,并且自描述),SQLite 只记录 `(segment, offset, length)`。每 10 秒一块也不会产生海量小文件。
|
||||
- 写入顺序:追加写入 → `fsync`(每批合并执行一次)→ 插入索引 → 由收件箱确认(Fleet 回复 `fleet.outbox.ack`)。启动时截断没有索引记录的段文件尾部。
|
||||
- 去重:`chunk_id` 已存在时,直接回复 ACK。
|
||||
|
||||
## 2. SQLite 配置
|
||||
|
||||
- `journal_mode=WAL`、`synchronous=NORMAL`、`foreign_keys=ON`、`busy_timeout=5000`。
|
||||
- **单写入者**:所有写操作经过一个有界 `Channel`,由一个后台循环执行;读操作使用独立的只读连接池。
|
||||
- 单写入者:所有写操作经过一个有界 `Channel`,由一个后台循环执行;读操作使用独立的只读连接池。
|
||||
- 时间统一存储为 Unix 毫秒 `INTEGER`(UTC);表结构版本记录在 `PRAGMA user_version`,迁移脚本为内置 SQL。
|
||||
|
||||
## 3. 表结构
|
||||
|
||||
`agents`、`join_tokens` 以及公钥和状态版本字段,同时作为 Fleet 的 `IFleetStore` 存储(由 `MoShiro.Backend.Storage.Sqlite` 实现),和 MoShiro 的其他数据放在同一个 `moshiro.db` 中,由同一个单写入者写入。CI 签名的发布清单存在 `settings` 表里(`release.<组件>.manifest`、`release.<组件>.signature`),面板只原样保存和转发。
|
||||
完整定义以 `Schema.cs` 为准(当前版本 16,迁移为按版本号追加的内置 SQL)。`agents`、`join_tokens` 及公钥、状态版本字段同时作为 Fleet 的 `IFleetStore` 实现,与其他数据放在同一个 `moshiro.db`,由同一个单写入者写入。CI 签名的发布清单存在 `settings` 表中(`release.<组件>.manifest`、`release.<组件>.signature`),面板原样保存和转发。
|
||||
|
||||
```sql
|
||||
-- 身份与注册 -------------------------------------------------------------
|
||||
CREATE TABLE agents (
|
||||
id TEXT PRIMARY KEY, -- agt_<ULID>
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
status TEXT NOT NULL, -- pending | active | disabled
|
||||
pubkey BLOB NOT NULL, -- ECDSA P-256 身份公钥
|
||||
tls_spki BLOB NOT NULL, -- 面向 Probe 的证书指纹(由 tls_pubkey 算出)
|
||||
tls_pubkey BLOB, -- 面向 Probe 的证书公钥,Agent 在 hello 里上报;Probe 用它固定证书、派生敲门密钥
|
||||
public_host TEXT NOT NULL, -- 自动识别(Agent 上报的公网地址),手填后标记 public_host_manual,不再被覆盖
|
||||
probe_tcp_port INTEGER NOT NULL, probe_udp_ports TEXT NOT NULL,
|
||||
labels TEXT NOT NULL DEFAULT '[]',
|
||||
public_visible INTEGER NOT NULL DEFAULT 1, -- 是否在 public API 中展示
|
||||
kinds TEXT, agent_version TEXT, local_limits TEXT, -- 来自 fleet.hello
|
||||
task_version INTEGER NOT NULL DEFAULT 0, applied_task_version INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL, last_seen_at INTEGER, row_version INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
| 类别 | 表 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 身份与注册 | `agents`、`probes`、`join_tokens`、`node_meta` | 节点公钥、状态、地址(含手填标记)、IPv6 诊断、`task_version` / `applied_task_version`、`row_version`(ETag);`node_meta` 保存系统、构建信息与国家 / 运营商;`agents.name` 仅在非 pending 节点间唯一 |
|
||||
| 账户与信任 | `users`、`sessions`、`api_tokens`、`passkeys`、`operator_sets`、`audit_log`、`settings` | 会话与 CSRF、能力化 API 令牌、passkey、操作员集合链、审计日志、键值设置 |
|
||||
| 任务 | `tasks`、`tests`、`test_results` | `tasks.scope` 为 `line` 或 `target`,`agent_id` 为空表示全局模板,`base_task_id` 指向被覆盖的模板;`tests` 记录按需测试及其阶段、地址族、线路;`test_results` 保存指标与曲线 |
|
||||
| 原始数据索引 | `raw_segments`、`raw_chunks` | `raw_chunks` 记录 `(segment, offset, length)`、`series`(`line:<probe_id>[:ipv6]` 或 `target:<task_id>`)、Kind、任务、测试、起止时间 |
|
||||
| 指标 | `metric_10s`、`metric_1m`、`metric_5m`、`metric_1h`、`metric_versions` | 每个桶一行,`metrics` 为 `AccumulatorCodec` 编码的 `{metric_id: 累加器状态}`;主键 `(agent_id, series, window_start)`;`metric_versions` 保存指标指纹与重算进度 |
|
||||
| 分析调度 | `analysis_queue`、`rollup_dirty` | 待计算窗口(`seq` 递增,完成时只删除序号未变的项,处理期间到达的迟到块不丢更新);待降采样的桶(先低层 5 分钟,再高层 1 小时) |
|
||||
| 观测 | `events`、`host_samples` | 节点事件;每个节点每分钟一行的主机资源汇总 |
|
||||
|
||||
CREATE TABLE join_tokens (
|
||||
id TEXT PRIMARY KEY, token_sha256 BLOB NOT NULL UNIQUE, note TEXT,
|
||||
labels TEXT NOT NULL DEFAULT '[]', auto_approve INTEGER NOT NULL,
|
||||
max_uses INTEGER NOT NULL, uses INTEGER NOT NULL DEFAULT 0,
|
||||
expires_at INTEGER NOT NULL, revoked_at INTEGER, created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE host_samples ( -- 主机资源占用,每个 Agent 每分钟一行(schema v3)
|
||||
agent_id TEXT NOT NULL, at INTEGER NOT NULL,
|
||||
cpu REAL, mem_used INTEGER, mem_total INTEGER, disk_used INTEGER, disk_total INTEGER,
|
||||
net_rx_bps INTEGER, net_tx_bps INTEGER, uptime INTEGER,
|
||||
PRIMARY KEY (agent_id, at)
|
||||
) WITHOUT ROWID;
|
||||
|
||||
CREATE TABLE probes ( -- 每个 Probe 身份一行(吊销只设 revoked_at)
|
||||
id TEXT PRIMARY KEY, name TEXT NOT NULL, pubkey BLOB NOT NULL,
|
||||
created_at INTEGER NOT NULL, revoked_at INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE users (
|
||||
id TEXT PRIMARY KEY, username TEXT NOT NULL UNIQUE,
|
||||
password_hash BLOB NOT NULL, password_salt BLOB NOT NULL, iterations INTEGER NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE sessions (
|
||||
id_sha256 BLOB PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
csrf_token BLOB NOT NULL, created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE api_tokens (
|
||||
id TEXT PRIMARY KEY, name TEXT NOT NULL, scope TEXT NOT NULL, -- read | admin
|
||||
token_sha256 BLOB NOT NULL UNIQUE, created_at INTEGER NOT NULL, last_used_at INTEGER, revoked_at INTEGER
|
||||
);
|
||||
CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT NOT NULL); -- 如 site.public = true
|
||||
|
||||
-- 任务 ------------------------------------------------------------------
|
||||
CREATE TABLE tasks (
|
||||
id TEXT PRIMARY KEY, -- tsk_<ULID>
|
||||
scope TEXT NOT NULL, -- line | target
|
||||
agent_id TEXT REFERENCES agents(id) ON DELETE CASCADE, -- 为 NULL 时,是适用于所有 Agent 的 line 模板
|
||||
base_task_id TEXT REFERENCES tasks(id) ON DELETE CASCADE, -- 针对某个 Agent 覆盖模板时,指向模板
|
||||
kind TEXT NOT NULL,
|
||||
target TEXT, -- 仅 target 任务
|
||||
params TEXT NOT NULL DEFAULT '{}',
|
||||
schedule TEXT NOT NULL, -- continuous | every:<分钟>
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
updated_at INTEGER NOT NULL, row_version INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE tests ( -- 按需测试(once)
|
||||
id TEXT PRIMARY KEY, agent_id TEXT NOT NULL, plan TEXT NOT NULL, params TEXT NOT NULL,
|
||||
trigger TEXT NOT NULL, -- api | schedule:<task_id>
|
||||
status TEXT NOT NULL, -- queued | running | succeeded | failed | aborted
|
||||
error_code TEXT, requested_at INTEGER NOT NULL, started_at INTEGER, finished_at INTEGER
|
||||
);
|
||||
CREATE INDEX ix_tests_agent ON tests (agent_id, requested_at);
|
||||
|
||||
-- 原始数据索引 ------------------------------------------------------------
|
||||
CREATE TABLE raw_segments (
|
||||
id INTEGER PRIMARY KEY, day TEXT NOT NULL, path TEXT NOT NULL UNIQUE, bytes INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE raw_chunks (
|
||||
chunk_id TEXT PRIMARY KEY, segment_id INTEGER NOT NULL, offset INTEGER NOT NULL, length INTEGER NOT NULL,
|
||||
agent_id TEXT NOT NULL, series TEXT NOT NULL, -- line | target:<task_id>
|
||||
kind TEXT NOT NULL, task_id TEXT, test_id TEXT, phase INTEGER,
|
||||
start_ts INTEGER NOT NULL, duration_ms INTEGER NOT NULL, partial INTEGER NOT NULL
|
||||
) WITHOUT ROWID;
|
||||
CREATE INDEX ix_raw_series ON raw_chunks (agent_id, series, start_ts);
|
||||
CREATE INDEX ix_raw_test ON raw_chunks (test_id) WHERE test_id IS NOT NULL;
|
||||
|
||||
-- 指标(通用存储:每个桶一行,内容为 指标id → 累加器状态 的映射)----------------
|
||||
-- 分层:metric_10s(48 小时)、metric_1m(30 天)、metric_5m(365 天)、metric_1h(永久)
|
||||
-- 另有 1s 精度的内存热层(最近 1 小时,不落盘),见 query.md 第 3 节
|
||||
CREATE TABLE metric_1m ( -- 60 s
|
||||
agent_id TEXT NOT NULL, series TEXT NOT NULL, window_start INTEGER NOT NULL,
|
||||
partial INTEGER NOT NULL,
|
||||
metrics BLOB NOT NULL, -- AccumulatorCodec 编码的 {metric_id: state}
|
||||
PRIMARY KEY (agent_id, series, window_start)
|
||||
) WITHOUT ROWID;
|
||||
-- metric_10s 结构相同(由流式路径直接写入,不经过降采样)
|
||||
-- metric_5m / metric_1h 结构相同(window_start 换成 bucket_start,另加 windows 计数)
|
||||
|
||||
CREATE TABLE test_results (
|
||||
test_id TEXT PRIMARY KEY, metrics BLOB NOT NULL, series BLOB -- series 为逐秒或逐档的曲线
|
||||
) WITHOUT ROWID;
|
||||
|
||||
CREATE TABLE metric_versions (metric_id TEXT PRIMARY KEY, fingerprint TEXT NOT NULL,
|
||||
recompute_from INTEGER, recompute_cursor INTEGER);
|
||||
|
||||
CREATE TABLE analysis_queue (
|
||||
agent_id TEXT NOT NULL, series TEXT NOT NULL, window_start INTEGER NOT NULL,
|
||||
not_before INTEGER NOT NULL, reason TEXT NOT NULL, -- new | late_chunk | recompute
|
||||
PRIMARY KEY (agent_id, series, window_start)
|
||||
) WITHOUT ROWID;
|
||||
CREATE TABLE rollup_dirty (level INTEGER NOT NULL, agent_id TEXT NOT NULL, series TEXT NOT NULL,
|
||||
bucket_start INTEGER NOT NULL, PRIMARY KEY (level, agent_id, series, bucket_start)) WITHOUT ROWID;
|
||||
|
||||
-- 事件 ------------------------------------------------------------------
|
||||
CREATE TABLE events (id INTEGER PRIMARY KEY, agent_id TEXT, kind TEXT NOT NULL,
|
||||
at INTEGER NOT NULL, detail TEXT);
|
||||
CREATE INDEX ix_events_time ON events (at);
|
||||
```
|
||||
|
||||
**要点:新增 Kind 或指标都不需要改表结构。** 指标是 `metric_*.metrics` 里的一个键;Kind 只是 `raw_chunks.kind` 的一个取值。
|
||||
新增 Kind 或指标不需要改表结构:指标是 `metric_*.metrics` 中的一个键,Kind 只是 `raw_chunks.kind` 的一个取值。1 秒精度的热层在内存中,不落盘,见 [query.md](query.md) 第 3 节。
|
||||
|
||||
## 4. 保留
|
||||
|
||||
| 数据 | 默认保留 |
|
||||
| --- | --- |
|
||||
| 持续任务的原始段文件 | **30 天** |
|
||||
| 持续任务的原始段文件 | 30 天 |
|
||||
| 按需测试的原始段文件 | 90 天 |
|
||||
| 热层(1s,内存) | 最近 1 小时 |
|
||||
| `metric_10s` | 48 小时 |
|
||||
@@ -165,9 +56,9 @@ CREATE INDEX ix_events_time ON events (at);
|
||||
| `metric_1h`、`tests`、`test_results` | 永久 |
|
||||
| `events` | 365 天 |
|
||||
|
||||
保留期在管理端的设置里改,对应的键:`retention.raw_days`、`retention.test_raw_days`、`retention.metric_10s_hours`、`retention.metric_1m_days`、`retention.metric_5m_days`、`retention.events_days`。
|
||||
保留期在管理端设置中修改,键为 `retention.raw_days`、`retention.test_raw_days`、`retention.metric_10s_hours`、`retention.metric_1m_days`、`retention.metric_5m_days`、`retention.events_days`、`retention.host_days`(主机样本,默认 7 天)。
|
||||
|
||||
降采样由 Application 中的 `Downsampler` 负责:读取下一级的累加器状态,用 `AccumulatorCodec.Merge` 合并后写回。所有保留期都可以配置。
|
||||
降采样由 Application 的 `AnalysisScheduler` 驱动:读取下一级的累加器状态,合并后写回上一层,待处理的桶记录在 `rollup_dirty`。
|
||||
|
||||
## 5. 容量估算
|
||||
|
||||
@@ -175,19 +66,12 @@ CREATE INDEX ix_events_time ON events (at);
|
||||
|
||||
| 数据 | 估算 |
|
||||
| --- | --- |
|
||||
| 原始数据 | 每条线路约 7 MB/天(udp_echo)+ 14 MB/天(tcp_echo 及内核采样)≈ 21 MB/天;15 条线路约 0.3 GB/天,30 天约 **9.5 GB** |
|
||||
| 原始数据 | 每条线路约 7 MB/天(udp_echo)+ 14 MB/天(tcp_echo 及内核采样)≈ 21 MB/天;15 条线路约 0.3 GB/天,30 天约 9.5 GB |
|
||||
| 按需测试 | 一次 `hy2_ladder` 约 1.5 MB;每条线路每天 4 次,90 天约 8 GB |
|
||||
| SQLite | 指标窗口和降采样约 1~2 GB/年 |
|
||||
|
||||
写入量约每秒 3 块,单写入者绰绰有余。M5 阶段会用模拟数据验证这些估算。
|
||||
写入量约每秒 3 块,单写入者可以承载。以上为估算值。
|
||||
|
||||
## 6. 查询
|
||||
|
||||
时间段、自动步长、数据源选择、现场合并和降级规则见 [query.md](query.md)。
|
||||
|
||||
## 7. 与实现的差异(以 `Schema.cs` 为准)
|
||||
|
||||
- `agents`:`name` 不再 `UNIQUE`,改为“已建立(非 pending)节点之间唯一”的部分唯一索引,pending 节点可以重名;新增 `task_hash`(有效任务集的哈希,用来判断版本是否要递增)、`rejected`(节点上报的拒绝原因)、`meta`(注册元数据)。
|
||||
- `tests`:新增 `phase`、`phase_count`;`test_results` 的 `metrics` / `series` 保存 JSON。
|
||||
- `analysis_queue`:新增 `seq`,每次入队递增;完成时只删除序号没变的项,处理期间到达的迟到块不会丢更新。
|
||||
- `rollup_dirty`:先处理低层(5 分钟)再处理高层(1 小时)。
|
||||
Reference in new issue
Block a user