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

10 KiB
Raw Permalink Blame History

分析层(Backend.Analysis)

1. 结构:算子 → 指标 → 模型

 原始块(自描述列)
      │ 解码为列视图(按列名读取)
      ▼
 ┌────────────┐   ┌──────────────────────┐   ┌───────────────────────┐
 │ 算子 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. 指标定义(一行声明)

// 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. 模型(服务体验评分)

模型是一组指标的评分函数,在查询时计算,不存储。改动评分公式后,立即对全部历史生效。

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. 两种处理路径

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。

7. 版本与重算

  • 每个 MetricDef 都有一个指纹(由声明内容和算子版本计算得出)。启动时与 metric_versions 表比对,发现新增或变化的指标时,就把保留期内的相关窗口加入重算队列,由后台以低优先级、限速的方式执行,进度可以通过 API 查询。
  • 超出原始数据保留期的历史保留旧口径,查询结果中会标注指标的版本。
  • 模型只在查询时计算,不参与重算。

8. 置信说明

每个模型的输出都附带:各指标的样本量、estimated 标记,以及局限说明,例如:

  • 只模拟流量特征,不实现真实协议;
  • 测量端口与真实服务端口不同(port_differs_from_service,默认端口 7443/7444 时总会出现)。

9. 项目结构

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 单独引用。