Align architecture, protocol, API, storage, security and deployment documentation with default mutual measurements and the Agent link board.
12 KiB
展示数据查询(前端取数)
本文定义前端从 Backend 拉取展示数据的方式:时间段、步长、指标选择、响应格式、实时跟随、缓存与限额。端点清单见 api.md 第 2 节;指标与累加器见 analysis.md。
1. 设计目标
- 任意时间段:支持 5 分钟、15 分钟这类相对时间段,也支持任意绝对区间。
- 短时间段足够精细:近 5 分钟精确到 1 秒,近 1 小时精确到 5 秒。
- 汇总精确:“这段时间内的丢包率、p95”由累加器精确合并得出,不是各点的平均。
- 快:常用的短时间段直接从内存读取;较长的时间段读取预聚合层,现场合并。
- 新增指标时前端不用改:指标按 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 读取累加器并导出值。