mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 14:06:36 +08:00
ee9d651c8a
仅上报 $upstream_cache_status,不上报回源地址;UI 由原始值推导 命中缓存 / 回源 / 未使用缓存。
780 lines
30 KiB
Markdown
780 lines
30 KiB
Markdown
# Agent 上报协议与观测落库数据模型
|
||
|
||
你会学到:重构后 Agent 心跳/WS 上报的 **数据结构**、Server **如何解析与写入**、ClickHouse / 关系库 **目标表结构**。
|
||
**无协议兼容层**:Agent 以销毁重建或二进制替换升级;旧字段不解析、旧缓冲整文件丢弃。
|
||
|
||
本设计是 [边缘可观测与业务流量统计重构](./observability-design.md) 的 **协议与存储专章**,实现时以本文字段与 DDL 为准。
|
||
|
||
**先读传输全景与示例:** [观测数据传输模型](./observability-transport-model.md)。
|
||
|
||
---
|
||
|
||
## 1. 设计目标
|
||
|
||
| 目标 | 说明 |
|
||
| --- | --- |
|
||
| Agent 只报事实 | 明细 + 主机读数 + 边缘健康瞬时态;无业务预聚合 |
|
||
| 一张业务明细表 | 访问日志是 L1 唯一写入路径 |
|
||
| 聚合在库内/控制面 | 小时汇总由 ClickHouse MV 或查询生成,Agent 不写汇总表 |
|
||
| 字段不重叠 | `bytes_sent` = 已提供数据;网卡 `network_*` = 宿主机;不再有业务 `openresty_tx` |
|
||
| 可演进 | 新字段可选;缺省数值填 0,不解析已删除的旧协议字段 |
|
||
|
||
---
|
||
|
||
## 2. 分层与写入总览
|
||
|
||
```text
|
||
Agent NodePayload (v2)
|
||
│
|
||
┌───────────────┼───────────────┐
|
||
▼ ▼ ▼
|
||
access_logs host_metrics edge_health
|
||
(L1 明细) (L3 读数) (L2 瞬时)
|
||
│ │ │
|
||
▼ ▼ ▼
|
||
of_node_access_logs of_node_metric_ of_node_edge_health
|
||
│ snapshots │
|
||
│ │ │
|
||
▼ ▼ │
|
||
of_access_log_hourly of_node_metric_ │
|
||
(MV, Server 侧) capacity_hourly (MV) │
|
||
│ │ │
|
||
└─────── 管理端聚合 API ───────────┘
|
||
|
||
关系库 (PostgreSQL/SQLite):节点最新状态、Profile、健康事件(非明细湖)
|
||
```
|
||
|
||
| 层 | 含义 | Agent 上报块 | ClickHouse 事实表 |
|
||
| --- | --- | --- | --- |
|
||
| L1 | 业务交付 | `access_logs` | `of_node_access_logs` |
|
||
| L2 | 边缘健康 | `edge_health` | `of_node_edge_health` |
|
||
| L3 | 宿主机资源 | `host_metrics` | `of_node_metric_snapshots` |
|
||
|
||
---
|
||
|
||
## 3. Agent 上报数据结构(协议 v2)
|
||
|
||
### 3.1 顶层 `NodePayload`
|
||
|
||
传输:HTTP 心跳 body 与 WebSocket `status` 消息共用同一结构。
|
||
|
||
```json
|
||
{
|
||
"schema_version": 2,
|
||
"node_id": "n_xxx",
|
||
"name": "edge-1",
|
||
"ip": "1.2.3.4",
|
||
"version": "3.3.0",
|
||
"ext_version": "",
|
||
"current_version": "cfg-checksum-or-version",
|
||
"last_error": "",
|
||
"profile": { },
|
||
"host_metrics": { },
|
||
"edge_health": { },
|
||
"access_logs": [ ],
|
||
"buffered": [ ],
|
||
"health_events": [ ],
|
||
"waf_ip_group_checksums": { "1": "md5..." }
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
| --- | --- | --- | --- |
|
||
| `schema_version` | int | 建议 | 固定为 `2`(本设计) |
|
||
| `node_id` | string | ✅ | 节点 ID |
|
||
| `name` | string | ✅ | 显示名 |
|
||
| `ip` | string | ✅ | 上报 IP |
|
||
| `version` / `ext_version` | string | ✅ | Agent 版本 |
|
||
| `current_version` | string | | 本地激活配置版本摘要 |
|
||
| `last_error` | string | | 最近同步/运行错误,可空 |
|
||
| `openresty_status` | string | ✅(有 OpenResty 时) | **最新健康态权威字段** → 写 PG 节点表 |
|
||
| `openresty_message` | string | | **最新健康说明权威字段** → 写 PG 节点表(**不进 CH**) |
|
||
| `profile` | object | | 主机概况,变化时上报(可节流) |
|
||
| `host_metrics` | object | 建议每拍 | L3 资源快照 |
|
||
| `edge_health` | object | 建议每拍 | L2 连接时序 + 与顶层一致的 status |
|
||
| `access_logs` | array | | 本拍增量访问明细 |
|
||
| `buffered` | array | | 离线补传的事实批次(见 §3.6) |
|
||
| `health_events` | array | | 边缘健康事件 |
|
||
| `waf_ip_group_checksums` | map | | 差分同步用,非观测湖 |
|
||
|
||
**已删除、Server 不再解析的字段(无兼容层):**
|
||
|
||
| 旧字段 | 处置 |
|
||
| --- | --- |
|
||
| `traffic_report` | 不存在于协议;不落库 |
|
||
| `openresty_observation` | 不存在;连接与状态走 `edge_health` |
|
||
| `snapshot` | 不存在;仅用 `host_metrics` |
|
||
| `buffered_observability` | 不存在;仅用 `buffered` |
|
||
|
||
### 3.2 `profile` — 主机概况(低频)
|
||
|
||
对应关系库 `of_node_system_profiles`(或现有等价表),**不进 ClickHouse 明细湖**。
|
||
|
||
```json
|
||
{
|
||
"hostname": "edge-1",
|
||
"os_name": "linux",
|
||
"os_version": "...",
|
||
"kernel_version": "...",
|
||
"architecture": "amd64",
|
||
"cpu_model": "...",
|
||
"cpu_cores": 8,
|
||
"total_memory_bytes": 16106127360,
|
||
"total_disk_bytes": 107374182400,
|
||
"uptime_seconds": 864000,
|
||
"reported_at_unix": 1720000000
|
||
}
|
||
```
|
||
|
||
| 字段 | 语义 |
|
||
| --- | --- |
|
||
| 硬件/OS 描述字段 | 事实读数 |
|
||
| `reported_at_unix` | Agent 采集时刻(UTC 秒) |
|
||
|
||
### 3.3 `host_metrics` — 宿主机资源(L3)
|
||
|
||
**全部为读数,不做 24h 业务总量。**
|
||
网卡/磁盘字节为 **内核累计计数器原值**(单调递增,重启可归零);CPU 为瞬时百分比;内存/磁盘占用为当前用量。
|
||
|
||
```json
|
||
{
|
||
"captured_at_unix": 1720000000,
|
||
"cpu_usage_percent": 12.5,
|
||
"memory_used_bytes": 4294967296,
|
||
"memory_total_bytes": 16106127360,
|
||
"storage_used_bytes": 50000000000,
|
||
"storage_total_bytes": 107374182400,
|
||
"disk_read_bytes": 9000000000,
|
||
"disk_write_bytes": 12000000000,
|
||
"network_rx_bytes": 500000000000,
|
||
"network_tx_bytes": 800000000000
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 语义 | Server 如何用 |
|
||
| --- | --- | --- | --- |
|
||
| `captured_at_unix` | int64 | 采样时刻 | `captured_at` |
|
||
| `cpu_usage_percent` | float | 瞬时 CPU% | 直接存;趋势取平均 |
|
||
| `memory_*` / `storage_*` | int64 | 当前用量/总量 | 直接存;算占用率 |
|
||
| `disk_read_bytes` / `disk_write_bytes` | int64 | **累计** IO 字节 | 存原值;查询时相邻差分 |
|
||
| `network_rx_bytes` / `network_tx_bytes` | int64 | **累计** 网卡字节 | 存原值;查询时相邻差分 →「宿主机网卡入/出站」 |
|
||
|
||
> Agent **禁止** 在上报前对网卡/磁盘做「本周期增量」替换累计值(否则 Server 差分会错)。
|
||
|
||
### 3.4 `edge_health` — OpenResty 边缘健康(L2)
|
||
|
||
**仅瞬时态,不包含业务吞吐。**
|
||
|
||
```json
|
||
{
|
||
"captured_at_unix": 1720000000,
|
||
"status": "healthy",
|
||
"message": "",
|
||
"connections": 42
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 语义 |
|
||
| --- | --- | --- |
|
||
| `status` | string | `healthy` / `unhealthy` / `unknown`(须与顶层 `openresty_status` 一致) |
|
||
| `message` | string | 状态说明(上报可带;**仅用于回填 PG 最新态,不进 CH**) |
|
||
| `connections` | int64 | stub_status Active connections |
|
||
|
||
#### 健康状态权威源(收敛)
|
||
|
||
| 数据 | 权威存储 | 说明 |
|
||
| --- | --- | --- |
|
||
| **当前** OpenResty 是否健康 + 说明文案 | **PG 节点表** `openresty_status` / `openresty_message` | UI 徽章、列表、告警以这里为准 |
|
||
| **时序** 健康 status + 连接数 | **CH** `of_node_edge_health`(`status`, `connections`) | 连接曲线 / 健康状态历史;**无 message 列** |
|
||
| Agent 上报 | 顶层 status/message + `edge_health` | Server 归一化后二者 status 对齐;message **只写 PG** |
|
||
|
||
因此:查「现在是否 unhealthy」→ 读 PG;查「过去 24h 连接数」→ 读 CH。
|
||
### 3.5 `access_logs[]` — 访问明细(L1,业务唯一事实)
|
||
|
||
Agent:tail access.log → 解析 JSON 行 → 原样字段上报(可截断 path)。
|
||
|
||
```json
|
||
{
|
||
"logged_at_unix": 1720000001,
|
||
"remote_addr": "203.0.113.10",
|
||
"host": "www.example.com",
|
||
"path": "/api/v1/ping",
|
||
"status_code": 200,
|
||
"bytes_sent": 1024,
|
||
"request_length": 128,
|
||
"request_time_ms": 15,
|
||
"user_agent": "Mozilla/5.0 ...",
|
||
"cache_status": "HIT"
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 来源(OpenResty) | 业务含义 |
|
||
| --- | --- | --- | --- | --- |
|
||
| `logged_at_unix` | int64 | ✅ | `$time_iso8601` 解析 | 请求完成时间 |
|
||
| `remote_addr` | string | ✅ | `$remote_addr` | 客户端 IP → UV |
|
||
| `host` | string | ✅ | `$host` | 域名 → Zone 归属 |
|
||
| `path` | string | ✅ | `$request_uri`,Agent 可截断 | 路径 |
|
||
| `status_code` | int | ✅ | `$status` | 状态码 |
|
||
| `bytes_sent` | int64 | ✅ | **`$body_bytes_sent`** | **已提供数据**(响应体) |
|
||
| `request_length` | int64 | 建议 | `$request_length` | **接收数据** |
|
||
| `request_time_ms` | int64 | 可选 | `$request_time * 1000` | 耗时;缺省 0 |
|
||
| `user_agent` | string | 建议 | `$http_user_agent` | UA;可截断入库 |
|
||
| `cache_status` | string | 建议 | **`$upstream_cache_status`** | 边缘缓存结果(见 §3.5.1) |
|
||
|
||
**明确不由 Agent 上报(由 Server 写入):**
|
||
|
||
* `region` / 国家:入库时 GeoIP 解析
|
||
* `id` / `created_at`:Server 生成
|
||
* `node_id`:取自 payload / 鉴权上下文
|
||
|
||
**明确不上报:**
|
||
|
||
* `upstream_addr` / 回源地址 / `origin_fetched`:不做回源端点追踪;「是否回源」仅由 `cache_status` 在控制面推导(§3.5.1)
|
||
|
||
### 3.5.1 `cache_status` — 缓存命中与回源(明细优先)
|
||
|
||
**目标(第一期):** 访问日志明细/详情能展示「是否命中缓存 / 是否回源 / 未使用缓存」。
|
||
**口径:** 只存 OpenResty `$upstream_cache_status` 原始值;**不上报** upstream 地址。
|
||
|
||
#### 原始值(入库)
|
||
|
||
| 值 | 含义(OpenResty) |
|
||
| --- | --- |
|
||
| `HIT` | 命中缓存 |
|
||
| `MISS` | 未命中,向 upstream 取内容 |
|
||
| `BYPASS` | 跳过缓存(如 method/cookie/策略导致 `$openflare_skip_cache`) |
|
||
| `EXPIRED` | 缓存过期后回源 |
|
||
| `STALE` | 提供陈旧缓存(stale) |
|
||
| `UPDATING` | 后台更新中,可能返回旧缓存 |
|
||
| `REVALIDATED` | 协商验证后仍用缓存 |
|
||
| `-` 或空 | 未经过 `proxy_cache`(如 Pages 本地静态、非代理 location) |
|
||
|
||
#### UI 三态推导(不落库)
|
||
|
||
控制面展示用派生枚举 `cache_outcome`,**不写 CH**:
|
||
|
||
| 三态 | 条件(`cache_status`) | 列表标签建议 |
|
||
| --- | --- | --- |
|
||
| **命中缓存** | `HIT` / `STALE` / `REVALIDATED` / `UPDATING` | 命中 |
|
||
| **回源** | `MISS` / `EXPIRED` | 回源 |
|
||
| **未使用缓存** | `BYPASS` / `-` / `""` | 未缓存 |
|
||
|
||
详情可同时显示三态 + 原始 `cache_status`。
|
||
|
||
#### 边界
|
||
|
||
* Pages 静态 / 无 `proxy_cache` 的 location:多为空或 `-` → **未使用缓存**,不得标成「命中」。
|
||
* 第一期只做明细可见;命中率看板、hourly 维度可后续用同一列聚合。
|
||
|
||
**单次心跳条数建议:**
|
||
|
||
* 软上限例如 2000 条/拍;超出进入 `buffered` 下一批,**禁止** 在 Agent 压成 TrafficReport。
|
||
|
||
### 3.6 `buffered[]` — 离线补传(只装事实)
|
||
|
||
```json
|
||
{
|
||
"captured_at_unix": 1719999900,
|
||
"host_metrics": { },
|
||
"edge_health": { },
|
||
"access_logs": [ ]
|
||
}
|
||
```
|
||
|
||
| 字段 | 说明 |
|
||
| --- | --- |
|
||
| `captured_at_unix` | 该批次采集/缓冲时刻,用于 ack 与去重窗口 |
|
||
| `host_metrics` / `edge_health` / `access_logs` | 与主 payload 同结构;可省略空块 |
|
||
|
||
**禁止** 在 buffered 中携带 `traffic_report` 或 rx/tx 吞吐。
|
||
|
||
### 3.7 `health_events[]`
|
||
|
||
```json
|
||
{
|
||
"event_type": "openresty_unhealthy",
|
||
"severity": "critical",
|
||
"message": "...",
|
||
"triggered_at_unix": 1720000000,
|
||
"metadata": { }
|
||
}
|
||
```
|
||
|
||
写入关系库健康事件表(现有模型即可),不进访问日志湖。
|
||
|
||
### 3.8 Go 协议草图(目标)
|
||
|
||
```go
|
||
// pkg/protocol/agent.go(目标形态,实现时替换旧类型)
|
||
|
||
type NodePayload struct {
|
||
SchemaVersion int `json:"schema_version,omitempty"`
|
||
NodeID string `json:"node_id"`
|
||
Name string `json:"name"`
|
||
IP string `json:"ip"`
|
||
Version string `json:"version"`
|
||
ExtVersion string `json:"ext_version"`
|
||
CurrentVersion string `json:"current_version"`
|
||
LastError string `json:"last_error"`
|
||
OpenrestyStatus string `json:"openresty_status"` // PG 最新态权威
|
||
OpenrestyMessage string `json:"openresty_message"` // PG 最新态权威;不进 CH
|
||
Profile *NodeSystemProfile `json:"profile,omitempty"`
|
||
HostMetrics *NodeHostMetrics `json:"host_metrics,omitempty"`
|
||
EdgeHealth *NodeEdgeHealth `json:"edge_health,omitempty"`
|
||
AccessLogs []NodeAccessLog `json:"access_logs,omitempty"`
|
||
Buffered []BufferedFacts `json:"buffered,omitempty"`
|
||
HealthEvents []NodeHealthEvent `json:"health_events"`
|
||
WAFIPGroupChecksums map[string]string `json:"waf_ip_group_checksums,omitempty"`
|
||
}
|
||
|
||
type NodeHostMetrics struct {
|
||
CapturedAtUnix int64 `json:"captured_at_unix"`
|
||
CPUUsagePercent float64 `json:"cpu_usage_percent"`
|
||
MemoryUsedBytes int64 `json:"memory_used_bytes"`
|
||
MemoryTotalBytes int64 `json:"memory_total_bytes"`
|
||
StorageUsedBytes int64 `json:"storage_used_bytes"`
|
||
StorageTotalBytes int64 `json:"storage_total_bytes"`
|
||
DiskReadBytes int64 `json:"disk_read_bytes"`
|
||
DiskWriteBytes int64 `json:"disk_write_bytes"`
|
||
NetworkRxBytes int64 `json:"network_rx_bytes"`
|
||
NetworkTxBytes int64 `json:"network_tx_bytes"`
|
||
}
|
||
|
||
type NodeEdgeHealth struct {
|
||
CapturedAtUnix int64 `json:"captured_at_unix"`
|
||
Status string `json:"status"`
|
||
Message string `json:"message"`
|
||
Connections int64 `json:"connections"`
|
||
}
|
||
|
||
type NodeAccessLog struct {
|
||
LoggedAtUnix int64 `json:"logged_at_unix"`
|
||
RemoteAddr string `json:"remote_addr"`
|
||
Host string `json:"host"`
|
||
Path string `json:"path"`
|
||
UserAgent string `json:"user_agent,omitempty"`
|
||
CacheStatus string `json:"cache_status,omitempty"` // $upstream_cache_status
|
||
StatusCode int `json:"status_code"`
|
||
BytesSent int64 `json:"bytes_sent"` // body_bytes_sent,已提供数据
|
||
RequestLength int64 `json:"request_length"` // 接收数据
|
||
RequestTimeMs int64 `json:"request_time_ms"` // 可选
|
||
}
|
||
|
||
type BufferedFacts struct {
|
||
CapturedAtUnix int64 `json:"captured_at_unix"`
|
||
HostMetrics *NodeHostMetrics `json:"host_metrics,omitempty"`
|
||
EdgeHealth *NodeEdgeHealth `json:"edge_health,omitempty"`
|
||
AccessLogs []NodeAccessLog `json:"access_logs,omitempty"`
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Server 解析与落库流程
|
||
|
||
### 4.1 入口
|
||
|
||
* HTTP:`POST /api/v1/agent/...` 心跳(现有路径)
|
||
* WebSocket:`type=status` payload = `NodePayload`
|
||
* 鉴权:`X-Agent-Token` → 绑定 `node_id`(payload.node_id 必须与 token 节点一致)
|
||
|
||
### 4.2 处理流水线(单次 payload)
|
||
|
||
```text
|
||
1. 反序列化 NodePayload
|
||
2. 归一化(normalize)
|
||
- schema_version < 2:
|
||
host_metrics ← snapshot
|
||
edge_health.status ← openresty_status
|
||
edge_health.connections ← openresty_observation.connections(若有)
|
||
traffic_report → drop
|
||
openresty_observation.rx/tx → drop
|
||
buffered ← buffered_observability
|
||
- path 再截断、status 范围钳制、负数字节 → 0
|
||
3. 关系库事务(节点最新态)
|
||
- 更新 node 在线时间、IP、版本、edge_health.status/message
|
||
- upsert profile(若有)
|
||
- insert health_events(若有)
|
||
4. ClickHouse 异步 batch(失败记日志,不阻断心跳响应的配置下发)
|
||
a. access_logs + buffered[].access_logs
|
||
→ 补 region(GeoIP)
|
||
→ 分配 snowflake id
|
||
→ BatchInsert of_node_access_logs
|
||
b. host_metrics + buffered[].host_metrics
|
||
→ of_node_metric_snapshots
|
||
c. edge_health + buffered[].edge_health
|
||
→ of_node_edge_health(仅 connections + status 快照可选)
|
||
5. 返回心跳响应(settings / active_config / waf 差分)
|
||
6. 若使用 buffer ack:按 buffered.captured_at_unix 列表确认
|
||
```
|
||
|
||
### 4.3 归一化规则(硬约束)
|
||
|
||
| 规则 | 行为 |
|
||
| --- | --- |
|
||
| `logged_at` 超前 now+5m | 钳制为 now 或丢弃该条(实现选定一种并单测) |
|
||
| `logged_at` 早于 now−TTL | 仍可写入,依赖表 TTL 清理 |
|
||
| 空 `host` | 允许,聚合进「未归属」 |
|
||
| `bytes_sent` / `request_length` < 0 | 置 0 |
|
||
| 单批 access_logs > N | 截断并打点监控(或只入 buffer 队列),不改为预聚合 |
|
||
| 重复补传 | CH 允许少量重复行;查询用 sum 近似(不强制精确去重) |
|
||
|
||
### 4.4 字段映射表(上报 → 表)
|
||
|
||
| 上报路径 | 目标存储 | 列 |
|
||
| --- | --- | --- |
|
||
| `access_logs[]` | CH `of_node_access_logs` | 见 §5.1 |
|
||
| `host_metrics` | CH `of_node_metric_snapshots` | 见 §5.2 |
|
||
| `edge_health` | CH `of_node_edge_health` + PG node 最新状态 | 见 §5.3 / §5.6 |
|
||
| `profile` | PG `of_node_system_profiles` | 现有列 |
|
||
| `health_events` | PG 健康事件表 | 现有模型 |
|
||
| `waf_ip_group_checksums` | 不落观测表 | 同步逻辑 |
|
||
| `traffic_report`(旧) | **不写** | — |
|
||
| `openresty_rx/tx`(旧) | **不写** | — |
|
||
|
||
### 4.5 查询侧(不落新「业务出站」列)
|
||
|
||
| 产品指标 | SQL 语义(示意) |
|
||
| --- | --- |
|
||
| 已提供数据 | `sum(bytes_sent)` |
|
||
| 接收数据 | `sum(request_length)` |
|
||
| 请求数 | `count()` |
|
||
| UV | `uniqExact(remote_addr)` |
|
||
| 5xx | `countIf(status_code >= 500)` |
|
||
| 按域名/状态码/地区 | `GROUP BY host / status_code / region` |
|
||
| 宿主机网卡出站 | 对 `network_tx_bytes` 按 node 时间序非负差分后 sum |
|
||
| OpenResty 连接 | `of_node_edge_health.connections` 最新或平均 |
|
||
|
||
---
|
||
|
||
## 5. 表结构(目标 DDL)
|
||
|
||
> 引擎与 TTL 与现网一致倾向:访问日志 90 天,指标 30 天。
|
||
> `id` 使用控制面 Snowflake/唯一 UInt64。
|
||
|
||
### 5.1 L1 事实表:`of_node_access_logs`
|
||
|
||
```sql
|
||
CREATE TABLE IF NOT EXISTS of_node_access_logs
|
||
(
|
||
id UInt64,
|
||
node_id String,
|
||
logged_at DateTime64(3, 'UTC'),
|
||
remote_addr String,
|
||
region String, -- Server GeoIP 写入,Agent 不传
|
||
host String,
|
||
path String,
|
||
user_agent String DEFAULT '', -- $http_user_agent
|
||
cache_status String DEFAULT '', -- $upstream_cache_status
|
||
status_code Int32,
|
||
bytes_sent UInt64, -- 已提供数据(body)
|
||
request_length UInt64 DEFAULT 0, -- 接收数据
|
||
request_time_ms UInt32 DEFAULT 0, -- 可选
|
||
created_at DateTime64(3, 'UTC')
|
||
)
|
||
ENGINE = MergeTree()
|
||
PARTITION BY toYYYYMM(logged_at)
|
||
ORDER BY (node_id, logged_at, host, status_code, remote_addr)
|
||
TTL toDateTime(logged_at) + INTERVAL 90 DAY
|
||
SETTINGS index_granularity = 8192;
|
||
```
|
||
|
||
| 列 | 类型 | 来源 |
|
||
| --- | --- | --- |
|
||
| `id` | UInt64 | Server |
|
||
| `node_id` | String | 鉴权/payload |
|
||
| `logged_at` | DateTime64(3) | `logged_at_unix` |
|
||
| `remote_addr` | String | 上报 |
|
||
| `region` | String | Server GeoIP |
|
||
| `host` | String | 上报 |
|
||
| `path` | String | 上报 |
|
||
| `user_agent` | String | 上报(可空) |
|
||
| `cache_status` | String | 上报(可空)→ **缓存状态** |
|
||
| `status_code` | Int32 | 上报 |
|
||
| `bytes_sent` | UInt64 | 上报 → **已提供数据** |
|
||
| `request_length` | UInt64 | 上报 → **接收数据** |
|
||
| `request_time_ms` | UInt32 | 上报可选 |
|
||
| `created_at` | DateTime64(3) | Server now |
|
||
|
||
**迁移:** 现表已有 `bytes_sent` / `request_length` / `request_time_ms` / `user_agent`;缓存状态新增:
|
||
|
||
```sql
|
||
ALTER TABLE of_node_access_logs
|
||
ADD COLUMN IF NOT EXISTS cache_status String DEFAULT '';
|
||
```
|
||
|
||
### 5.2 L1 小时汇总(Server 侧 MV)
|
||
|
||
**禁止 Agent 写入。** 供看板/节点 24h 快速查询请求数、错误数、字节量。
|
||
|
||
**已实现选型:`SummingMergeTree` + 不含 UV 列。**
|
||
|
||
```sql
|
||
CREATE TABLE IF NOT EXISTS of_access_log_hourly
|
||
(
|
||
node_id String,
|
||
hour DateTime('UTC'),
|
||
host String,
|
||
request_count UInt64,
|
||
error_count UInt64,
|
||
bytes_sent UInt64,
|
||
request_length UInt64
|
||
)
|
||
ENGINE = SummingMergeTree()
|
||
PARTITION BY toYYYYMM(hour)
|
||
ORDER BY (node_id, hour, host)
|
||
TTL hour + INTERVAL 90 DAY;
|
||
|
||
CREATE MATERIALIZED VIEW IF NOT EXISTS of_access_log_hourly_mv
|
||
TO of_access_log_hourly
|
||
AS
|
||
SELECT
|
||
node_id,
|
||
toStartOfHour(logged_at) AS hour,
|
||
host,
|
||
toUInt64(count()) AS request_count,
|
||
toUInt64(countIf(status_code >= 500)) AS error_count,
|
||
sum(bytes_sent) AS bytes_sent,
|
||
sum(request_length) AS request_length
|
||
FROM of_node_access_logs
|
||
GROUP BY node_id, hour, host;
|
||
```
|
||
|
||
历史小时(MV 创建前已入库的明细)需一次性回填,见迁移 `202607180003_backfill_access_log_hourly.sql`(ANTI JOIN 防重)。
|
||
|
||
#### UV 策略(必须遵守)
|
||
|
||
| 场景 | 数据源 | 算法 | 说明 |
|
||
| --- | --- | --- | --- |
|
||
| **窗口总 UV**(看板汇总、节点卡片、Zone 汇总) | `of_node_access_logs` 明细 | `uniqExact(remote_addr)`(`TrafficSummary` / 节点聚合) | **唯一权威**;不可用小时 UV 相加 |
|
||
| **24h 趋势折线请求/错误/字节** | `of_access_log_hourly` 优先,缺数据回落明细桶 | `sum(request_count)` 等 | 小时路径 **不填** `unique_visitor_count`(恒为 0) |
|
||
| **24h 趋势折线分时 UV** | 仅明细桶路径 | 桶内 `uniqExact` | 走 hourly 时 UI 应展示空/0 或隐藏 UV 序列,**禁止**对小时行做 `sum(UV)` |
|
||
|
||
**为何 hourly 不存 UV:**
|
||
|
||
1. `SummingMergeTree` 只能安全合并可加和计数;`uniqExact` 跨 part 合并需要 `AggregatingMergeTree` + state,实现与查询更重。
|
||
2. 即便存每小时 UV,对多小时窗口 **相加会严重高估**(同一 IP 跨小时重复计)。
|
||
3. 产品「24h 独立访客」只认整窗 `uniqExact`;趋势图主序列是请求量/错误/字节,分时 UV 非主指标。
|
||
|
||
可选未来:若需要分时 UV 曲线,再单独加 `AggregatingMergeTree` 状态表或查询时对明细做 `uniqExact` 按小时 group(成本更高,不阻塞当前看板)。
|
||
### 5.3 L3 事实表:`of_node_metric_snapshots`(保留,语义明确)
|
||
|
||
```sql
|
||
CREATE TABLE IF NOT EXISTS of_node_metric_snapshots
|
||
(
|
||
id UInt64,
|
||
node_id String,
|
||
captured_at DateTime64(3, 'UTC'),
|
||
cpu_usage_percent Float64,
|
||
memory_used_bytes Int64,
|
||
memory_total_bytes Int64,
|
||
storage_used_bytes Int64,
|
||
storage_total_bytes Int64,
|
||
disk_read_bytes Int64, -- 累计原值
|
||
disk_write_bytes Int64,
|
||
network_rx_bytes Int64, -- 累计原值 → 宿主机网卡入站
|
||
network_tx_bytes Int64, -- 累计原值 → 宿主机网卡出站
|
||
created_at DateTime64(3, 'UTC')
|
||
)
|
||
ENGINE = MergeTree()
|
||
PARTITION BY toYYYYMM(captured_at)
|
||
ORDER BY (node_id, captured_at, id)
|
||
TTL toDateTime(captured_at) + INTERVAL 30 DAY
|
||
SETTINGS index_granularity = 8192;
|
||
```
|
||
|
||
列与现网一致;**文档与 API 必须标注 network_* 为宿主机网卡累计值**。
|
||
|
||
### 5.4 L3 小时汇总:`of_node_metric_capacity_hourly`(保留)
|
||
|
||
现有 min/max 用于累计计数器小时增量近似 + CPU/内存平均。逻辑不变:
|
||
|
||
* `network_tx_max - network_tx_min` ≈ 该小时宿主机出站
|
||
* **不得** 用于「已提供数据」
|
||
|
||
### 5.5 L2 事实表:`of_node_edge_health`(新建,替换吞吐型 openresty 表)
|
||
|
||
```sql
|
||
CREATE TABLE IF NOT EXISTS of_node_edge_health
|
||
(
|
||
id UInt64,
|
||
node_id String,
|
||
captured_at DateTime64(3, 'UTC'),
|
||
status LowCardinality(String), -- healthy / unhealthy / unknown
|
||
connections Int64,
|
||
created_at DateTime64(3, 'UTC')
|
||
)
|
||
ENGINE = MergeTree()
|
||
PARTITION BY toYYYYMM(captured_at)
|
||
ORDER BY (node_id, captured_at, id)
|
||
TTL toDateTime(captured_at) + INTERVAL 30 DAY
|
||
SETTINGS index_granularity = 8192;
|
||
```
|
||
|
||
| 列 | 说明 |
|
||
| --- | --- |
|
||
| `status` | 瞬时健康(与 PG 当前态同源;用于时序,非唯一 UI 权威) |
|
||
| `connections` | 当前连接数 |
|
||
|
||
**无** `message` 列(说明文案仅 PG 最新态)。
|
||
**无** `openresty_rx_bytes` / `openresty_tx_bytes`。
|
||
### 5.6 关系库(节点最新态,非分析湖)
|
||
|
||
与观测湖分离,保持「最新一份」:
|
||
|
||
| 表(逻辑名) | 用途 | 关键列 |
|
||
| --- | --- | --- |
|
||
| `of_nodes`(或现节点表) | 在线、版本、IP | `last_seen_at`, `openresty_status`, `openresty_message`, `agent_version` |
|
||
| `of_node_system_profiles` | profile upsert | hostname, cpu_cores, total_memory_bytes, ... |
|
||
| 健康事件表 | `health_events` | event_type, severity, message, triggered_at |
|
||
|
||
> 具体物理表名以仓库现有 GORM 模型为准;本设计不强制改名,只强制 **不再把业务吞吐写进节点表**。
|
||
|
||
### 5.7 废弃表(停止写入 → TTL 后删除)
|
||
|
||
| 表 | 原因 | 替代 |
|
||
| --- | --- | --- |
|
||
| `of_node_request_reports` | Agent 预聚合 | `of_node_access_logs` + hourly |
|
||
| `of_node_traffic_hourly` + MV | 依赖 request_reports | `of_access_log_hourly` |
|
||
| `of_node_obs_openresty` | 含业务 rx/tx | `of_node_edge_health` |
|
||
| `of_node_openresty_hourly` + MV | 业务吞吐差分 | `of_access_log_hourly` 的 bytes_* |
|
||
|
||
Relay 专用 `of_node_obs_frps` / `of_node_obs_frpc` **保留**(非本 Agent 主路径,但同属 CH 观测)。
|
||
|
||
---
|
||
|
||
## 6. 表与协议对照总表
|
||
|
||
| 产品概念 | 协议字段 | 表.列 | 聚合 |
|
||
| --- | --- | --- | --- |
|
||
| 已提供数据 | `access_logs[].bytes_sent` | `of_node_access_logs.bytes_sent` | `sum` |
|
||
| 接收数据 | `access_logs[].request_length` | `...request_length` | `sum` |
|
||
| 请求数 | 行数 | — | `count` |
|
||
| UV(窗口总) | `remote_addr` | 同左明细 | `uniqExact`(**禁止** sum 小时 UV) |
|
||
| Top 域名 | `host` | 同左 | `group by` |
|
||
| 状态码分布 | `status_code` | 同左 | `group by` |
|
||
| 来源地区 | — | `region`(Server) | `group by` |
|
||
| 宿主机网卡出站 | `host_metrics.network_tx_bytes` | `of_node_metric_snapshots.network_tx_bytes` | 时间序差分 |
|
||
| 宿主机网卡入站 | `network_rx_bytes` | 同左 | 差分 |
|
||
| 磁盘读/写 | `disk_*_bytes` | 同左 | 差分 |
|
||
| CPU/内存 | 瞬时字段 | 同左 | avg |
|
||
| OpenResty 连接 | `edge_health.connections` | `of_node_edge_health.connections` | 最新/avg |
|
||
| OpenResty 健康 | `edge_health.status` | 节点表 + 可选 CH | 最新 |
|
||
|
||
**不再存在的映射:**
|
||
|
||
| 旧概念 | 旧字段 | 处置 |
|
||
| --- | --- | --- |
|
||
| OpenResty 出站 | `openresty_tx_bytes` | 删除;用已提供数据 |
|
||
| OpenResty 入站 | `openresty_rx_bytes` | 删除;用接收数据 |
|
||
| 窗口请求报告 | `traffic_report` | 删除 |
|
||
|
||
---
|
||
|
||
## 7. OpenResty 日志格式(与明细对齐)
|
||
|
||
目标 `log_format`(保证 `bytes_sent` 键 = body;含 UA 与缓存状态):
|
||
|
||
```nginx
|
||
log_format openflare_json escape=json
|
||
'{"ts":"$time_iso8601","host":"$host","path":"$request_uri",'
|
||
'"remote_addr":"$remote_addr","status":$status,'
|
||
'"request_time":$request_time,'
|
||
'"bytes_sent":$body_bytes_sent,"request_length":$request_length,'
|
||
'"user_agent":"$http_user_agent",'
|
||
'"cache_status":"$upstream_cache_status"}';
|
||
```
|
||
|
||
Agent 解析:
|
||
|
||
* `ts` → `logged_at_unix`
|
||
* `bytes_sent` → 协议 `bytes_sent`(已提供)
|
||
* `request_length` → 协议 `request_length`
|
||
* `request_time` → 可选 `request_time_ms = round(sec * 1000)`
|
||
* `user_agent` → 协议 `user_agent`
|
||
* `cache_status` → 协议 `cache_status`(原样透传,不做三态压缩)
|
||
|
||
---
|
||
|
||
## 8. 升级策略(无兼容层)
|
||
|
||
| 项 | 策略 |
|
||
| --- | --- |
|
||
| Agent 升级 | **销毁重建**优先;允许**二进制替换** |
|
||
| 协议 | 仅 schema v2 字段;旧 JSON 字段不解析 |
|
||
| 本地观测缓冲 | 若仍是旧格式(含 `snapshot` / `openresty_observation` / `traffic_report`)或损坏 → **整文件删除**,运行中重建 |
|
||
| 读路径 | 业务 API **只读** access_logs(及 hourly);健康当前态读 PG;连接时序读 CH edge_health |
|
||
| 旧 Agent | 必须升级;控制面不提供 v1 双读路径 |
|
||
|
||
---
|
||
|
||
## 9. 示例:一次心跳的落库结果
|
||
|
||
**Agent 上报(节选):**
|
||
|
||
```json
|
||
{
|
||
"schema_version": 2,
|
||
"node_id": "n1",
|
||
"host_metrics": {
|
||
"captured_at_unix": 1720000000,
|
||
"cpu_usage_percent": 10,
|
||
"memory_used_bytes": 1,
|
||
"memory_total_bytes": 2,
|
||
"storage_used_bytes": 3,
|
||
"storage_total_bytes": 4,
|
||
"disk_read_bytes": 100,
|
||
"disk_write_bytes": 200,
|
||
"network_rx_bytes": 1000,
|
||
"network_tx_bytes": 2000
|
||
},
|
||
"edge_health": {
|
||
"captured_at_unix": 1720000000,
|
||
"status": "healthy",
|
||
"message": "",
|
||
"connections": 5
|
||
},
|
||
"access_logs": [
|
||
{
|
||
"logged_at_unix": 1720000001,
|
||
"remote_addr": "1.1.1.1",
|
||
"host": "a.example.com",
|
||
"path": "/",
|
||
"status_code": 200,
|
||
"bytes_sent": 500,
|
||
"request_length": 80
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
**写入:**
|
||
|
||
1. PG 节点最新态:`openresty_status` / `openresty_message`(若上报)
|
||
2. `of_node_metric_snapshots` 1 行(network_tx=2000 累计)
|
||
3. `of_node_edge_health` 1 行(status + connections=5;**无 message**)
|
||
4. `of_node_access_logs` 1 行(bytes_sent=500, request_length=80, region=Server 填充)
|
||
5. MV 异步计入 `of_access_log_hourly`
|
||
|
||
**查询 24h 已提供数据:** `sum(bytes_sent)` → 至少 500(加历史)
|
||
**查询宿主机出站:** 对 snapshots 差分,与 500 **无强制相等关系**。
|
||
|
||
---
|
||
|
||
## 10. 实现检查清单
|
||
|
||
- [x] `pkg/protocol`:仅 v2 字段,无兼容别名
|
||
- [x] Agent:只组 `host_metrics` / `edge_health` / `access_logs` / `buffered`
|
||
- [x] Server:无 request_reports / openresty 吞吐;健康当前态 PG、时序 CH
|
||
- [x] CH migration:`request_length`、`request_time_ms`、`of_node_edge_health`、`of_access_log_hourly`、hourly 回填
|
||
- [x] 看板/Zone API 统一读 access log 聚合
|
||
- [x] UV:整窗 uniqExact;Zone 曲线标明分桶 UV;小时趋势不绘 UV
|
||
|
||
---
|
||
|
||
## 11. 修订记录
|
||
|
||
| 日期 | 说明 |
|
||
| --- | --- |
|
||
| 2026-07-17 | 初稿:协议 v2、Server 落库流水线、CH/关系库目标表结构与废弃表清单 |
|