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

12 KiB
Raw Permalink Blame History

展示数据查询(前端取数)

本文定义前端从 Backend 拉取展示数据的方式:时间段、步长、指标选择、响应格式、实时跟随、缓存与限额。端点清单见 api.md 第 2 节;指标与累加器见 analysis.md。

1. 设计目标

  1. 任意时间段:支持 5 分钟、15 分钟这类相对时间段,也支持任意绝对区间。
  2. 短时间段足够精细:近 5 分钟精确到 1 秒,近 1 小时精确到 5 秒。
  3. 汇总精确:“这段时间内的丢包率、p95”由累加器精确合并得出,不是各点的平均。
  4. 快:常用的短时间段直接从内存读取;较长的时间段读取预聚合层,现场合并。
  5. 新增指标时前端不用改:指标按 id 选取,按分组(preset)展示,含义和单位来自目录。

2. 时间段与步长

2.1 参数

参数 取值 说明
range 5m、15m、30m、1h、3h、6h、12h、24h、7d、30d、90d、365d,或任意 <数字><s|m|h|d> 相对时间段,to = now,可以跟随实时数据
from + to RFC 3339 UTC 绝对区间,左闭右开 [from, to);与 range 互斥
step auto(默认),或下列常用步长之一 指定的步长比数据源所能提供的更细时,自动降级,并在响应中标记 degraded
anchor now(默认)或 latest 只用于序列接口的相对时间段(range=):latest 以这条线路最后一个样本为终点(最后样本在最近 5 秒内就仍以当前时间为终点)。离线的线路因此显示它最后一段数据,而不是空图;响应里的 from/to 是实际使用的窗口,live 为 false
tz IANA 时区,默认 UTC 只影响 ≥ 1d 的分桶对齐,比如“按天”统计时从本地零点切分

常用步长:1s 2s 5s 10s 15s 30s 1m 2m 5m 10m 15m 30m 1h 2h 3h 6h 12h 1d

2.2 自动步长

step = 常用步长中 ≥ 区间长度 / 720 的最小值,即每条曲线最多约 720 个点。

时间段 步长 点数 数据源
5m 1s 300 内存热层
15m 2s 450 内存热层
30m 5s 360 内存热层
1h 5s 720 内存热层
6h 30s 720 10s 层
24h 2m 720 1m 层
7d 15m 672 5m 层
30d 1h 720 1h 层
90d 3h 720 1h 层
  • 桶对齐到 UTC 的整数倍步长。
  • 起点不对齐的绝对区间:第一个桶和最后一个桶可能不完整,会在响应的 edge_partial 中标出。

3. 数据分层

            精细 ◀────────────────────────────────────────────────────▶ 粗略
 ┌─────────────────┬──────────────┬──────────────┬─────────────┬─────────────┐
 │ 热层(内存)      │ metric_10s   │ metric_1m    │ metric_5m   │ metric_1h   │
 │ 1s 累加器         │ 保留 48 小时   │ 保留 30 天    │ 保留 365 天  │ 永久         │
 │ 最近 1 小时(环形缓冲)│ SQLite     │ SQLite       │ SQLite      │ SQLite      │
 └────────┬────────┴──────────────┴──────────────┴─────────────┴─────────────┘
          │ 重启后热层为空 → 按需从原始块回填(最近 1 小时,后台以低优先级执行)
 原始段文件(30 天)── 超出热层时间范围的 1s~5s 精度查询 → 从原始块现算(区间 ≤ 15 分钟)

选择数据源的规则:在“覆盖该时间段”并且“粒度能整除步长”的各层中,选择最粗的一层,然后把累加器按步长现场合并。

示例:

  • step = 30s 时,取 10s 层,每 3 个桶合并一次;
  • step = 2m 时,取 1m 层,每 2 个桶合并一次。

合并方式是计数相加、直方图逐桶相加,所以结果精确。

情况 处理方式
时间段在最近 1 小时内,步长 ≥ 1s 热层
时间段在 1 小时之前、30 天之内,步长 < 10s,区间 ≤ 15 分钟 从原始块现算(全局最多同时 2 个查询)
同上,但区间 > 15 分钟 降级为 10s 层或 1m 层,并标记 degraded: true
所需的层已经过期 降级为下一个更粗的可用层,并标记 degraded: true

热层内存估算:15 条线路 × 3600 秒 × 约 300 B(稀疏直方图 + 计数)≈ 16 MB。热层也是 LiveHub 实时推送的数据来源。

4. 指标选择

4.1 按 id 选取

metrics=loss.down,tcp.retrans,rtt.p95

id 的格式为 指标.统计量,详见 analysis.md 第 4 节;所有可用的 id 可以从 /api/public/catalog/metrics 获取。

4.2 按分组(preset)选取

preset=udp,tcp

分组在 Backend.Analysis 的 MetricRegistry.Presets 中声明,并通过 /api/public/catalog/presets 下发给前端。新增指标后,只要把它加进某个分组,前端就会自动展示。

分组 指标
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
scores score.base、score.mc、score.hy2、score.reality

目录中每个 id 都附带 unit、label、description、better(lower 或 higher,用于前端配色),以及 thresholds(优、良、差三档阈值,由模型给出)。

5. 响应格式(列式)

{
  "line_id": "agt_…~agt_…", "agent_id": "agt_…", "dialer_id": "agt_…", "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],
  "values": {
    "loss.down":   [0.0, 0.0, 10.0],
    "rtt.p95":     [48.1, 49.0, 61.2],
    "tcp.retrans": [0.0, 0.1, null]
  },
  "summary": {
    "metrics": { "loss.down": 0.42, "rtt.p95": 61.3, "tcp.retrans": 0.08 },
    "samples": { "udp_echo": 8990, "tcp_echo": 17980 },
    "coverage": 0.998
  },
  "edge_partial": { "first": false, "last": true },
  "gaps":   [{ "from": "…", "to": "…", "reason": "dialer_disconnected" }],
  "events": [{ "at": "…", "kind": "endpoint_changed", "detail": "…" }],
  "versions": { "loss.down": "a1f3…" }
}
  • 时间轴 t 使用 Unix 毫秒,只出现一次;values 中每个数组的长度都与 t 相同,可以直接交给 uPlot 或 ECharts。这是 api.md 第 7.4 节“时间用 RFC 3339”规则的明确例外,理由是减小体积、方便绘图;其余时间字段仍然使用 RFC 3339。
  • 某个桶没有数据时填 null(不填 0),前端据此绘制断线。
  • summary 是整个时间段的精确合并结果。coverage = 实际数据量 / 期望数据量,用来提示前端“数据是否完整”。
  • gaps 和 events 用于在曲线上做标注(断线、端点变化、时钟跳变等)。

6. 端点

端点 用途 主要参数
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 多条线路对比(时间轴对齐) 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 分组定义 —

overview 的响应示例:

{
  "range": "15m", "from": "…", "to": "…",
  "lines": [
    { "id": "agt_…~agt_…", "agent_id": "agt_…", "dialer_id": "agt_…", "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":{ "<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 }

heatmap 的响应示例:{ "t":[…], "edges":[…], "counts":[[…],[…]] },其中 counts[i] 是时间桶 t[i] 在各个延迟桶中的计数。

7. 实时跟随

前端选择“近 15 分钟”
  ① GET /lines/{id}/series?range=15m&preset=udp,tcp      → 一次性获取 450 个点和汇总
  ② SSE /api/public/live?topics=series:{id}:2s:udp,tcp    → 每当原始块到达(约每 10 s),
                                                            推送一批新的 2s 桶和最新的汇总
     event: series
     data: {"line_id":"…","step":"2s","t":[…5 个],"values":{…},"summary":{…}}
  ③ 前端把新点追加到末尾,并丢弃超出窗口的旧点;用推送来的汇总替换原来的汇总

前端切换为“自定义 09:00–10:00”→ 只执行 ①(live=false)
  • SSE 的订阅主题格式为 series:{line_id}:{step}:{preset},只允许 ≤ 1m 的步长;更粗的步长由前端定时重新拉取即可。
  • overview 也可以跟随实时:订阅 overview:{range}:{preset},每 10 s 推送一次最新汇总。
  • 实时延迟:取决于原始块的切块节奏(10 s)加上处理时间,典型值为 10~12 s。1s 步长的点会按批次到达,而不是每秒一个。

8. 缓存与限额

请求类型 缓存策略
已经结束的绝对区间(to 早于当前时间 − 2 分钟) Cache-Control: public, max-age=31536000, immutable,并带 ETag(由参数和指标版本计算)
相对时间段(to = now) Cache-Control: no-store
目录(catalog) max-age=300,并带 ETag
限额 默认值 超出时
单次响应的点数 每条曲线 ≤ 2000 422 range_too_large,并给出建议的步长
单次请求的指标数 ≤ 32 422 too_many_metrics
compare 的线路数 ≤ 20 422
从原始块现算的查询 全局最多同时 2 个,区间 ≤ 15 分钟 429 too_many_requests(带 Retry-After)或者降级
查询超时 10 s 503

9. 实现位置

Backend.Api/Public/QueryEndpoints.cs         参数解析、校验、缓存头(不含业务)
Backend.Application/Query/
  ├─ RangeResolver       range 或 from/to 与 step → 对齐后的桶序列
  ├─ Steps / Bucketing   常用步长与分桶
  ├─ SourcePlanner       选择数据源(热层 / 10s / 1m / 5m / 1h / 原始块现算),判断是否降级
  ├─ RowLoader           读取累加器行
  ├─ MetricSelection     按 id 或分组选取指标
  ├─ QueryService        series、overview、compare、distribution、heatmap
  └─ QueryResults        列式结果类型
Backend.Application/Live/                    HotLayer(1s 环形缓冲,1 小时)、HotLayerBackfill、LiveHub(SSE 主题)
Backend.Analysis                             累加器的合并与导出(与持久化路径共用同一份代码)

新增指标时查询代码不需要改动:QueryService 通过注册表按 id 读取累加器并导出值。