Align architecture, protocol, API, storage, security and deployment documentation with default mutual measurements and the Agent link board.
10 KiB
10 KiB
分析层(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 单独引用。