Files
justme c9beeee8e3 docs: describe the unified Agent topology and link controls
Align architecture, protocol, API, storage, security and deployment documentation with default mutual measurements and the Agent link board.
2026-10-21 11:55:55 +08:00

148 lines
10 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.
# 分析层(Backend.Analysis)
## 1. 结构:算子 → 指标 → 模型
```text
原始块(自描述列)
│ 解码为列视图(按列名读取)
▼
┌────────────┐ ┌──────────────────────┐ ┌───────────────────────┐
│ 算子 Op │──>│ 指标 MetricDef │──>│ 模型 Model(评分) │
│ 可复用计算 │ │ 一行声明 = 一个指标 │ │ 指标的评分函数 │
│ rtt/loss/… │ │ 输入 + 算子 + 累加器 │ │ base/mc/hy2/reality │
└────────────┘ └──────────┬───────────┘ └──────────┬────────────┘
│ 累加器状态(可合并) │ 查询时计算
▼ ▼
指标窗口(1m) → 5m → 1h API:metrics{} + scores{}
```
三层都采用注册制,由代码中的注册表驱动;API 的 `/catalog/metrics`、`/catalog/models` 也直接由注册表生成。
## 2. 累加器(固定的少数几种)
所有指标的中间状态只能是下面几种累加器之一。合并规则只写一次,存储、降采样、按时间范围聚合都通用处理:
| 累加器 | 状态 | 合并 | 可导出的值 |
| --- | --- | --- | --- |
| `Counter` | u64 | 相加 | 值;除以时长得到速率(例如每分钟) |
| `Ratio` | 分子、分母 | 分别相加 | 百分比 |
| `Sum` / `Min` / `Max` | f64 | 相加 / 取极值 | 值;平均值 = Sum / Counter |
| `Histogram` | 固定对数桶(100 µs~30 s,每 2 倍 8 个桶,相对误差 ≤ 4.5%)| 逐桶相加 | p50 / p90 / p95 / p99、均值、最值 |
| `RunLength` | 连续段长度的直方图 | 逐桶相加 | 平均或最大连续丢包长度 |
| `Series` | 固定分辨率的时间序列(例如每秒一个点) | 拼接 | 曲线(仅用于测试结果) |
状态的二进制编码由 `AccumulatorCodec` 统一实现,存储层只把它当作不透明的字节处理。
## 3. 算子(可复用的计算,新增时才需要写代码)
| 算子 | 输入列 | 作用 |
| --- | --- | --- |
| `rtt` | 回显流的 `t1..t4` | `(t4−t1)−(t3−t2)` |
| `loss(dir)` | 回显流或包列 | 按方向判断丢失 |
| `owd_var(dir)` | `t1,t2` 或 `t3,t4` | 去除偏移和线性漂移后的单向时延变化(抖动) |
| `loss_runs` | 丢失标记 | 连续丢失段的长度 |
| `reorder` / `dup` | `seq` | 乱序与重复 |
| `gap(expected, threshold)` | 到达时间 | 卡顿(到达间隔超过期望值 + 阈值) |
| `kernel_delta(field)` | `tcp_info` 流 | 两次采样之间的差值(重传、确认字节数) |
| `rate(dir)` | 字节数或包数 + 时间 | 每秒吞吐 |
| `phase_ms(a, b)` | `connect` 事件 | 阶段耗时(TCP 建连、TLS 握手) |
| `outcome` | 事件 | 按错误类别计数(超时、RST、TLS alert) |
| `change_point` | 逐秒序列 | 变点检测(QoS 开始限速的时刻) |
算子都是纯函数,每个都有单元测试。
## 4. 指标定义(一行声明)
```csharp
// Backend.Analysis/Metrics/LinkMetrics.cs
public static class LinkMetrics
{
public static readonly MetricDef Rtt =
Metric.Histogram("rtt", from: "*.echo", op: Ops.Rtt, unit: Unit.Ms,
exports: [Stat.P50, Stat.P95, Stat.P99, Stat.Mean]);
public static readonly MetricDef LossUp =
Metric.Ratio("loss.up", from: "udp_echo.msg", op: Ops.Loss(Dir.Up), unit: Unit.Pct);
public static readonly MetricDef JitterDown =
Metric.Histogram("jitter.down", from: "udp_echo.msg", op: Ops.OwdVar(Dir.Down), unit: Unit.Ms,
exports: [Stat.P95]);
public static readonly MetricDef TcpStall =
Metric.Counter("tcp.stall", from: "tcp_echo.msg", op: Ops.Gap(expectedFrom: "rate_hz", thresholdMs: 100),
unit: Unit.PerMin);
public static readonly MetricDef TcpRetrans =
Metric.Ratio("tcp.retrans", from: "tcp_echo.tcp_info", op: Ops.KernelDelta("retrans_total", over: "segs_out"),
unit: Unit.Pct);
}
```
- `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` 对照,可以区分"重传造成的尖峰"和"队头阻塞"。
- 导出值的名字为 `指标id.统计量`,例如 `rtt.p95`、`loss.up`、`tcp.stall.per_min`。
新增一个统计只需要新增一行 `MetricDef`,需要新算子时再加一个算子类。存储、降采样、API 输出、目录说明都不需要改。新指标上线后,会自动对保留期内(30 天)的原始数据回填(见第 7 节)。
## 5. 模型(服务体验评分)
模型是一组指标的评分函数,在查询时计算,不存储。改动评分公式后,立即对全部历史生效。
```csharp
public sealed class McModel : IServiceModel
{
public string Id => "mc";
public int Version => 1;
public IReadOnlyList<string> Needs => ["rtt.p95", "tcp.stall.per_min", "tcp.stall_max", "tcp.retrans"];
public ModelScore Score(MetricView m) => /* 分段打分 + 加权,返回 0..100 与各维度得分和说明 */;
}
```
| 模型 | 主要指标 | 说明 |
| --- | --- | --- |
| `base` | `rtt.*`、`loss.up/down`、`jitter.up/down`、`loss.run_max` | 线路基础质量 |
| `mc` | `tcp_echo` 的 `rtt.p95/p99`、`tcp.stall.per_min`、`tcp.stall_max`、`tcp.retrans` | TCP 上不会丢包,丢包表现为重传和卡顿(队头阻塞)。没有 `tcp_echo` 数据时,用 `udp_echo` 的丢包序列代入 TCP 恢复模型估算,并标记为 `estimated` |
| `hy2` | 测试中 `udp_train` 各档的 `loss`、`jitter`,以及变点检测得到的 `qos_onset`;持续的 `udp_echo` 作为趋势参考 | Brutal 固定码率:计算可持续码率(丢包 ≤ 5% 或 ≤ 10%)、有效吞吐、限速开始的时刻 |
| `reality` | `connect` 的成功率和阶段耗时、`hold` 的重置次数和存活时长、`tcp_bulk` 的吞吐(`tcp.bulk.peak.max`:最好的持续 1 秒窗口,评分用它;`tcp.bulk.mbps.*`:每个采样间隔)和重传。`hy2_ladder` 的阶梯里一步是一个阶段:拨号方 会把一个阶段切成几块(一块最多 50000 条记录,100 Mbit/s 的 10 秒要切 3~4 块),评估时同一阶段的块先合并,再算丢包、抖动和实际吞吐;实际吞吐的时长取发送时间戳(Agent 时钟,按每块的时钟锚点换算成 UTC)的跨度,不用块头里的时长(拨号方 切块时块头带的时长可能是整个阶段的)、`tcp_echo` 的 RTT | 握手、稳定性、首字节时间估算、吞吐 |
## 6. 两种处理路径
```text
chunk 入库 ─┬─> 流式路径:按 1s 桶计算累加器 → 热层环形缓冲(最近 1 小时)
│ → 每满 10 s 写入 metric_10s → 推送 SSE(series / overview)
└─> 持久路径:分钟窗口就绪(该分钟的块都已到达,或已过窗口结束时间 + 1 分钟)
→ 计算完整窗口 → 写入 metric_1m → 标记降采样脏桶(5m、1h)
按需测试:所有阶段的块到达后 → 计算测试结果(包含 Series 曲线)→ 写入 test_results
```
两条路径调用同一份 `MetricDef` 和算子代码,所以实时值与历史值使用的是同一套口径。迟到的块(例如 Agent 重连后补传)会同时更新热层(如果仍在 1 小时内)、`metric_10s`,并重新进入持久路径。
指标分组(preset)同样是注册制:`MetricRegistry.Presets` 中的 `PresetDef(id, label, metricIds)`,由 `/api/public/catalog/presets` 下发。前端按分组展示,新增指标只需要把它加进分组。查询规则见 [query.md](query.md)。
## 7. 版本与重算
- 每个 `MetricDef` 都有一个指纹(由声明内容和算子版本计算得出)。启动时与 `metric_versions` 表比对,发现新增或变化的指标时,就把保留期内的相关窗口加入重算队列,由后台以低优先级、限速的方式执行,进度可以通过 API 查询。
- 超出原始数据保留期的历史保留旧口径,查询结果中会标注指标的版本。
- 模型只在查询时计算,不参与重算。
## 8. 置信说明
每个模型的输出都附带:各指标的样本量、`estimated` 标记,以及局限说明,例如:
- 只模拟流量特征,不实现真实协议;
- 测量端口与真实服务端口不同(`port_differs_from_service`,默认端口 7443/7444 时总会出现)。
## 9. 项目结构
```text
backend/src/MoShiro.Backend.Analysis/ 依赖:MoShiro.Protocol、Fleet.Core(纯计算,没有 I/O)
├─ 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,可以脱离 Backend 单独引用。