docs: reorganize into design, security, api, ops and dev; drop stale designs; refresh the diagrams [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful
ci/woodpecker/push/main-amd64-build Pipeline was successful
- Group docs by category with an index (docs/README.md); fix every link, code comment and the protocol conformance test that read docs/protocol.md. - access.md and security.md rewritten to what is implemented: removed the unadopted member-proof chain, roles and recovery key that do not exist, the old implementation order and the budgets; gaps now carry a status. - enrollment, deployment, api: the install flow is the static, hash-checked script with environment variables; no default admin. - README and architecture: new topology and update-flow diagrams (operator passkey, CI signing, registry feed, auth and trust modules).
This commit is contained in:
1 parent
636a784043
commit
e014a19554
45 files changed
+592
-966
No files matched your search
@@ -0,0 +1,209 @@
|
||||
# 展示数据查询(前端取数)
|
||||
|
||||
本文定义前端从 Backend 拉取展示数据的方式:时间段、步长、指标选择、响应格式、实时跟随、缓存与限额。端点清单见 [api.md](../api/api.md) 第 2 节;指标与累加器见 [analysis.md](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. 数据分层
|
||||
|
||||
```text
|
||||
精细 ◀────────────────────────────────────────────────────▶ 粗略
|
||||
┌─────────────────┬──────────────┬──────────────┬─────────────┬─────────────┐
|
||||
│ 热层(内存) │ 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](analysis.md) 第 4 节;所有可用的 id 可以从 `/api/public/catalog/metrics` 获取。
|
||||
|
||||
### 4.2 按分组(preset)选取
|
||||
|
||||
`preset=udp,tcp`
|
||||
|
||||
分组在 `Backend.Analysis` 的注册表中声明,并通过 `/api/public/catalog/presets` 下发给前端。新增指标后,只要把它加进某个分组,前端就会自动展示。
|
||||
|
||||
| 分组 | 指标 |
|
||||
| --- | --- |
|
||||
| `udp` | `loss.up`、`loss.down`、`loss.total`、`rtt.p50`、`rtt.p95`、`rtt.p99`、`jitter.up.p95`、`jitter.down.p95`、`loss.run_max`、`reorder`、`dup` |
|
||||
| `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`、`probe.endpoint_changes` |
|
||||
| `scores` | `score.base`、`score.mc`、`score.hy2`、`score.reality` |
|
||||
|
||||
目录中每个 id 都附带 `unit`、`label`、`description`、`better`(`lower` 或 `higher`,用于前端配色),以及 `thresholds`(优、良、差三档阈值,由模型给出)。
|
||||
|
||||
## 5. 响应格式(列式)
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_id": "agt_…",
|
||||
"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": "probe_disconnected" }],
|
||||
"events": [{ "at": "…", "kind": "endpoint_changed", "detail": "…" }],
|
||||
"versions": { "loss.down": "a1f3…" }
|
||||
}
|
||||
```
|
||||
|
||||
- 时间轴 `t` 使用 Unix 毫秒,只出现一次;`values` 中每个数组的长度都与 `t` 相同,可以直接交给 uPlot 或 ECharts。**这是 [api-guidelines.md](../api/api-guidelines.md) 中“时间用 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` | 多条线路对比(时间轴对齐) | `agents`、`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` 的响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"range": "15m", "from": "…", "to": "…",
|
||||
"lines": [
|
||||
{ "agent_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":{ "agt_a":[…], "agt_b":[…] }, "summary":{ "agt_a":0.1, "agt_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. 实时跟随
|
||||
|
||||
```text
|
||||
前端选择“近 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: {"agent_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. 实现位置
|
||||
|
||||
```text
|
||||
Backend.Api/Public/SeriesEndpoints.cs 参数解析、校验、缓存头(不含业务)
|
||||
Backend.Application/Query/
|
||||
├─ RangeResolver range 或 from/to 与 step → 对齐后的桶序列
|
||||
├─ SourcePlanner 选择数据源(热层 / 10s / 1m / 5m / 1h / 原始块现算),判断是否降级
|
||||
├─ SeriesQuery 读取累加器 → 按步长合并 → 导出统计量 → 列式结果
|
||||
├─ SummaryQuery 整个时间段的精确合并(overview 和 series 共用)
|
||||
└─ DistributionQuery 合并直方图 → 分桶、CDF、热力图
|
||||
Backend.Application/Live/LiveHub 热层环形缓冲(1s,1 小时)+ SSE 主题
|
||||
Backend.Analysis 累加器的合并与导出(与持久化路径共用同一份代码)
|
||||
```
|
||||
|
||||
新增指标时,这套查询代码不需要任何改动:`SeriesQuery` 只通过注册表按 id 读取累加器并导出值。
|
||||
Reference in new issue
Block a user