跳转到内容

Agent 上报协议与观测落库数据模型

你会学到:重构后 Agent 心跳/WS 上报的 数据结构、Server 如何解析与写入、ClickHouse / 关系库 目标表结构
无协议兼容层:Agent 以销毁重建或二进制替换升级;旧字段不解析、旧缓冲整文件丢弃。

本设计是 边缘可观测与业务流量统计重构协议与存储专章,实现时以本文字段与 DDL 为准。

先读传输全景与示例: 观测数据传输模型


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_logsof_node_access_logs
L2边缘健康edge_healthof_node_edge_health
L3宿主机资源host_metricsof_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_versionint建议固定为 2(本设计)
node_idstring节点 ID
namestring显示名
ipstring上报 IP
version / ext_versionstringAgent 版本
current_versionstring本地激活配置版本摘要
last_errorstring最近同步/运行错误,可空
openresty_statusstring✅(有 OpenResty 时)最新健康态权威字段 → 写 PG 节点表
openresty_messagestring最新健康说明权威字段 → 写 PG 节点表(不进 CH
profileobject主机概况,变化时上报(可节流)
host_metricsobject建议每拍L3 资源快照
edge_healthobject建议每拍L2 连接时序 + 与顶层一致的 status
access_logsarray本拍增量访问明细
bufferedarray离线补传的事实批次(见 §3.6)
health_eventsarray边缘健康事件
waf_ip_group_checksumsmap差分同步用,非观测湖

已删除、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_unixAgent 采集时刻(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_unixint64采样时刻captured_at
cpu_usage_percentfloat瞬时 CPU%直接存;趋势取平均
memory_* / storage_*int64当前用量/总量直接存;算占用率
disk_read_bytes / disk_write_bytesint64累计 IO 字节存原值;查询时相邻差分
network_rx_bytes / network_tx_bytesint64累计 网卡字节存原值;查询时相邻差分 →「宿主机网卡入/出站」

Agent 禁止 在上报前对网卡/磁盘做「本周期增量」替换累计值(否则 Server 差分会错)。

3.4 edge_health — OpenResty 边缘健康(L2)

仅瞬时态,不包含业务吞吐。

json
{
  "captured_at_unix": 1720000000,
  "status": "healthy",
  "message": "",
  "connections": 42
}
字段类型语义
statusstringhealthy / unhealthy / unknown(须与顶层 openresty_status 一致)
messagestring状态说明(上报可带;仅用于回填 PG 最新态,不进 CH
connectionsint64stub_status Active connections

健康状态权威源(收敛)

数据权威存储说明
当前 OpenResty 是否健康 + 说明文案PG 节点表 openresty_status / openresty_messageUI 徽章、列表、告警以这里为准
时序 健康 status + 连接数CH of_node_edge_healthstatus, connections连接曲线 / 健康状态历史;无 message 列
Agent 上报顶层 status/message + edge_healthServer 归一化后二者 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_unixint64$time_iso8601 解析请求完成时间
remote_addrstring$remote_addr客户端 IP → UV
hoststring$host域名 → Zone 归属
pathstring$request_uri,Agent 可截断路径
status_codeint$status状态码
bytes_sentint64$body_bytes_sent已提供数据(响应体)
request_lengthint64建议$request_length接收数据
request_time_msint64可选$request_time * 1000耗时;缺省 0
user_agentstring建议$http_user_agentUA;可截断入库
cache_statusstring建议$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_metricsCH of_node_metric_snapshots见 §5.2
edge_healthCH of_node_edge_health + PG node 最新状态见 §5.3 / §5.6
profilePG of_node_system_profiles现有列
health_eventsPG 健康事件表现有模型
waf_ip_group_checksums不落观测表同步逻辑
traffic_report(旧)不写
openresty_rx/tx(旧)不写

4.5 查询侧(不落新「业务出站」列)

产品指标SQL 语义(示意)
已提供数据sum(bytes_sent)
接收数据sum(request_length)
请求数count()
UVuniqExact(remote_addr)
5xxcountIf(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;
类型来源
idUInt64Server
node_idString鉴权/payload
logged_atDateTime64(3)logged_at_unix
remote_addrString上报
regionStringServer GeoIP
hostString上报
pathString上报
user_agentString上报(可空)
cache_statusString上报(可空)→ 缓存状态
status_codeInt32上报
bytes_sentUInt64上报 → 已提供数据
request_lengthUInt64上报 → 接收数据
request_time_msUInt32上报可选
created_atDateTime64(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 非主指标。

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(或现节点表)在线、版本、IPlast_seen_at, openresty_status, openresty_message, agent_version
of_node_system_profilesprofile upserthostname, cpu_cores, total_memory_bytes, ...
健康事件表health_eventsevent_type, severity, message, triggered_at

具体物理表名以仓库现有 GORM 模型为准;本设计不强制改名,只强制 不再把业务吞吐写进节点表

5.7 废弃表(停止写入 → TTL 后删除)

原因替代
of_node_request_reportsAgent 预聚合of_node_access_logs + hourly
of_node_traffic_hourly + MV依赖 request_reportsof_access_log_hourly
of_node_obs_openresty含业务 rx/txof_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_sentof_node_access_logs.bytes_sentsum
接收数据access_logs[].request_length...request_lengthsum
请求数行数count
UV(窗口总)remote_addr同左明细uniqExact禁止 sum 小时 UV)
Top 域名host同左group by
状态码分布status_code同左group by
来源地区region(Server)group by
宿主机网卡出站host_metrics.network_tx_bytesof_node_metric_snapshots.network_tx_bytes时间序差分
宿主机网卡入站network_rx_bytes同左差分
磁盘读/写disk_*_bytes同左差分
CPU/内存瞬时字段同左avg
OpenResty 连接edge_health.connectionsof_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 解析:

  • tslogged_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. 修订记录

日期说明
2026-07-17初稿:协议 v2、Server 落库流水线、CH/关系库目标表结构与废弃表清单

基于 Apache License 2.0 发布