1 - 存储服务

📖 概述

HUATUO(华佗)支持将采集到的 Linux 内核事件与 AutoTracing 数据持久化写入外部存储后端。当前支持 Elasticsearch 和 OpenSearch 两种存储系统。

采集到的事件在序列化为 JSON 后,同时写入节点本地目录(huatuo-local/)和配置的远端存储后端。本地目录保留事件的本地副本,远端存储提供持久化与结构化查询能力。

本文介绍 Elasticsearch 和 OpenSearch 的配置与验证方法。示例基于 Docker 部署,生产环境只需将地址替换为实际服务地址,配置方式一致。


🎯 应用场景

Kubernetes 云原生故障溯源

容器化环境中,Pod OOM、节点 Hung Task 等内核事件具有短暂性,日志往往在事件发生后被清理。将事件写入 Elasticsearch 或 OpenSearch 后,运维团队可按时间范围查询历史异常时间线,在事后复盘阶段精确定位间歇性故障的根因。

AI 计算集群稳定性审计

GPU 训练集群长期运行过程中,ras 硬件错误、iotracing I/O 延迟等事件的历史分布对容量规划和硬件健康评估至关重要。将采集数据持久化后,可通过聚合查询建立节点稳定性基线,为主动维护提供数据依据。

合规与事件留存

等保合规要求系统异常事件具备可追溯性。将 HUATUO 采集的内核事件写入 OpenSearch 并配置索引生命周期策略,可满足对事件留存周期和查询能力的合规要求。

可观测性平台集成

Elasticsearch 和 OpenSearch 均提供与 Grafana 的原生数据源对接能力。将 HUATUO 事件写入存储后,可在 Grafana 中构建内核事件趋势面板,与应用层指标叠加展示,实现历史数据分析与告警回顾。


💎 价值

维度 仅本地存储 接入外部存储后端
数据持久性 受节点磁盘容量限制,重启后可能丢失 数据持久化至分布式存储,支持长期保留
查询能力 无结构化查询,依赖文件搜索 支持全文检索、字段过滤、时间范围聚合
可视化集成 不支持 可直接对接 Grafana、Kibana 等可视化平台
多节点汇聚 数据分散在各节点本地 集中写入统一存储,支持跨节点查询
合规留存 难以满足留存周期要求 可配置索引生命周期策略,满足合规留存要求

🚀 使用

OpenSearch V2

1. 部署 OpenSearch

docker pull opensearchproject/opensearch:2.6.0
docker run -d --name opensearch --network host \
    -e "discovery.type=single-node" \
    opensearchproject/opensearch:2.6.0

2. 验证服务状态

curl -k -u admin:admin https://localhost:9200

返回示例:

{
  "name" : "22ca72df78c0",
  "cluster_name" : "docker-cluster",
  "cluster_uuid" : "yxb3foceQVKzXXO6bHpPHQ",
  "version" : {
    "distribution" : "opensearch",
    "number" : "2.6.0",
    "build_type" : "tar",
    "build_hash" : "7203a5af21a8a009aece1474446b437a3c674db6",
    "build_date" : "2023-02-24T18:57:04.388618985Z",
    "build_snapshot" : false,
    "lucene_version" : "9.5.0",
    "minimum_wire_compatibility_version" : "7.10.0",
    "minimum_index_compatibility_version" : "7.0.0"
  },
  "tagline" : "The OpenSearch Project: https://opensearch.org/"
}

若验证失败,可通过以下命令查看容器日志:

docker logs opensearch

3. 配置 huatuo-bamai

huatuo-bamai.conf 中添加以下配置。OpenSearch 容器镜像默认用户名和密码均为 admin。存储配置的详细说明请参见《配置指南》章节。

[Storage.ES]
    Address = "https://127.0.0.1:9200"
    Index = "huatuo_bamai"
    Username = "admin"
    Password = "admin"

4. 启动 huatuo-bamai

通过 --config-dir 指定配置文件所在目录:

./_output/bin/huatuo-bamai --region dev --config-dir .

当本地存储目录 huatuo-local/ 中生成文件(例如 net_rx_latency)时,说明已成功采集到内核事件。可使用以下命令从 OpenSearch 查询数据:

curl -k -u admin:admin \
    -X GET "https://localhost:9200/huatuo_bamai/_search?pretty" \
    -H "Content-Type: application/json" \
    -d '{"query": {"match_all": {}}}'

返回示例:

{
    "_index" : "huatuo_bamai",
    "_id" : "yjPG_50Bu_OF-hukxKR7",
    "_score" : 1.0,
    "_source" : {
      "hostname" : "hostname",
      "region" : "dev",
      "uploaded_time" : "2026-05-07T00:11:49.753166222Z",
      "time" : "2026-05-07 00:11:49.753 +0000",
      "tracer_name" : "net_rx_latency",
      "tracer_time" : "2026-05-07 00:11:49.753 +0000",
      "tracer_type" : "auto",
      "tracer_data" : {
        "comm" : "<nil>",
        "pid" : 0,
        "where" : "RX_STAGE_NETIF",
        "latency_ms" : 1776078133565,
        "saddr" : "127.0.0.1",
        "daddr" : "127.0.0.1",
        "sport" : 37736,
        "dport" : 9200,
        "seq" : 1080592402,
        "ack_seq" : 2465063876,
        "pkt_len" : 781
      }
    }
}

查看文档记录总数,不查看具体列表。

curl -k -u admin:admin -X GET "https://localhost:9200/huatuo_bamai/_count?pretty"

返回示例:其中 count 数字 = 写入记录的总数。

{
  "count" : 2680,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  }
}

Elasticsearch V8

1. 部署 Elasticsearch

docker pull docker.elastic.co/elasticsearch/elasticsearch:8.15.5
docker run -d --name elasticsearch --network host \
    -e "discovery.type=single-node" \
    -e "ES_JAVA_OPTS=-Xms1g -Xmx1g" \
    -e "ELASTIC_PASSWORD=123456" \
    docker.elastic.co/elasticsearch/elasticsearch:8.15.5

2. 验证服务状态

curl -k -u elastic:123456 https://localhost:9200

返回示例:

{
  "name" : "ab0b562f8dbd",
  "cluster_name" : "docker-cluster",
  "cluster_uuid" : "aVfOVgJTQXuhZ3HGotK3ww",
  "version" : {
    "number" : "8.15.5",
    "build_flavor" : "default",
    "build_type" : "docker",
    "build_hash" : "b10896bcfe167cce44a84ba2771d101fb596d40d",
    "build_date" : "2024-11-21T22:06:13.985834967Z",
    "build_snapshot" : false,
    "lucene_version" : "9.11.1",
    "minimum_wire_compatibility_version" : "7.17.0",
    "minimum_index_compatibility_version" : "7.0.0"
  },
  "tagline" : "You Know, for Search"
}

3. 配置 huatuo-bamai

huatuo-bamai.conf 中添加以下配置。Elasticsearch 容器镜像默认用户名为 elastic,密码通过环境变量 ELASTIC_PASSWORD 设置。存储配置的详细说明请参见《配置指南》章节。

[Storage.ES]
    Address = "https://127.0.0.1:9200"
    Index = "huatuo_bamai"
    Username = "elastic"
    Password = "123456"

4. 启动 huatuo-bamai

通过 --config-dir 指定配置文件所在目录:

./_output/bin/huatuo-bamai --region dev --config-dir .

当本地存储目录 huatuo-local/ 中生成文件(例如 net_rx_latency)时,说明已成功采集到内核事件。可使用以下命令从 Elasticsearch 查询数据:

curl -k -u elastic:123456 \
    -X GET "https://localhost:9200/huatuo_bamai/_search?pretty" \
    -H "Content-Type: application/json" \
    -d '{"query": {"match_all": {}}}'

返回示例:

{
    "_index" : "huatuo_bamai",
    "_id" : "WtNZAJ4BQ8x-thPHEY1i",
    "_score" : 1.0,
    "_source" : {
      "hostname" : "hostname",
      "region" : "dev",
      "uploaded_time" : "2026-05-07T02:51:37.696263325Z",
      "time" : "2026-05-07 02:51:37.696 +0000",
      "tracer_name" : "net_rx_latency",
      "tracer_time" : "2026-05-07 02:51:37.696 +0000",
      "tracer_type" : "auto",
      "tracer_data" : {
        "comm" : "<nil>",
        "pid" : 0,
        "where" : "RX_STAGE_NETIF",
        "latency_ms" : 1776078133565,
        "saddr" : "127.0.0.1",
        "daddr" : "127.0.0.1",
        "sport" : 2379,
        "dport" : 36706,
        "seq" : 950542706,
        "ack_seq" : 1960972383,
        "pkt_len" : 91
      }
    }
}

查看文档记录总数,不查看具体列表。

curl -k -u elastic:123456 -X GET "https://localhost:9200/huatuo_bamai/_count?pretty"

返回示例:其中 count 数字 = 写入记录的总数。

{
  "count" : 2680,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  }
}

Elasticsearch V7

V7 默认使用 HTTP,因此只需要在访问服务时替换为 HTTP 即可。

1. 部署 Elasticsearch

docker pull docker.elastic.co/elasticsearch/elasticsearch:7.10.1
docker run -d --name elasticsearch --network host \
    -e "discovery.type=single-node" \
    -e "ES_JAVA_OPTS=-Xms1g -Xmx1g" \
    -e "ELASTIC_PASSWORD=123456" \
    docker.elastic.co/elasticsearch/elasticsearch:7.10.1

2. 验证服务状态

curl -k -u elastic:123456 http://localhost:9200

返回示例:

{
  "name" : "d88c9e8df48b",
  "cluster_name" : "docker-cluster",
  "cluster_uuid" : "_ZZefWx4SniAc255t_lIVg",
  "version" : {
    "number" : "7.10.1",
    "build_flavor" : "default",
    "build_type" : "docker",
    "build_hash" : "1c34507e66d7db1211f66f3513706fdf548736aa",
    "build_date" : "2020-12-05T01:00:33.671820Z",
    "build_snapshot" : false,
    "lucene_version" : "8.7.0",
    "minimum_wire_compatibility_version" : "6.8.0",
    "minimum_index_compatibility_version" : "6.0.0-beta1"
  },
  "tagline" : "You Know, for Search"
}

3. 配置 huatuo-bamai

[Storage.ES]
    Address = "http://127.0.0.1:9200"
    Index = "huatuo_bamai"
    Username = "elastic"
    Password = "123456"

4. 启动 huatuo-bamai

通过 --config-dir 指定配置文件所在目录:

./_output/bin/huatuo-bamai --region dev --config-dir .

当本地存储目录 huatuo-local/ 中生成文件(例如 net_rx_latency)时,说明已成功采集到内核事件。可使用以下命令从 Elasticsearch 查询数据:

curl -k -u elastic:123456 \
    -X GET "http://localhost:9200/huatuo_bamai/_search?pretty" \
    -H "Content-Type: application/json" \
    -d '{"query": {"match_all": {}}}'

或者:
curl -k -u elastic:123456 \
    -X GET "http://localhost:9200/huatuo_bamai/_count?pretty"

⚙️ 原理

系统架构

HUATUO Storage 模块部署在节点上,将采集到的内核事件同时写入本地目录和 Elasticsearch 或 OpenSearch。两种存储后端共用同一套 [Storage.ES] 配置接口,通过地址区分。

写入远端时使用 ES/OpenSearch 的 Bulk API_bulk):事件先进入节点内的批量缓冲,由后台 worker 按"大小或时间"的阈值聚合后一次提交多条记录,并在传输层失败时按策略自动重试。

graph TB
    subgraph kernel["Linux 内核"]
        K1[内核事件]
        K2[AutoTracing]
    end

    subgraph huatuo["HUATUO Agent(节点级)"]
        T["采集层"]
        L["本地目录\nhuatuo-local/"]
        S["Storage 模块\nBulkIndexer 缓冲"]
    end

    subgraph backends["存储后端"]
        ES[Elasticsearch]
        OS[OpenSearch]
    end

    kernel --> T
    T --> L
    T --> S
    S -->|Bulk API + 自动重试| ES
    S -->|Bulk API + 自动重试| OS

数据写入流程

采集层调用 Save 后立即返回,事件落入 BulkIndexer 缓冲;后台 worker 在满足"字节阈值 / 时间阈值 / 进程退出"任一条件时将批次提交至远端。本地目录写入是同步落盘,与远端 Bulk 路径相互独立。

sequenceDiagram
    participant T as 采集层
    participant L as 本地目录(huatuo-local/)
    participant S as Storage 模块(BulkIndexer)
    participant B as ES / OpenSearch

    T->>S: 采集到内核事件,序列化为 JSON
    par 本地路径(同步)
        S->>L: 写入本地文件
    and 远端路径(异步批量)
        S->>S: 加入 Bulk 缓冲,立即返回
        Note over S: 满足 5 MB / 1 s / 退出 任一条件
        S->>B: POST /_bulk(多条记录)
        B-->>S: 200 OK + per-item 结果
        Note over S: 失败项通过 OnFailure 回调记录日志
    end

Bulk 批量写入机制

缓冲与刷新

参数 含义
FlushBytes 5 MB 缓冲累计达到该字节数立即刷新
FlushInterval 1 s 距上次刷新满 1 秒后强制刷新
NumWorkers 4 并发提交 Bulk 请求的后台 goroutine 数
进程退出 Close(ctx) SIGTERM/SIGINT 触发,限时 10 s 内排空缓冲

两级重试策略

Bulk 请求的失败语义分为两层,重试范围不同:

层级 触发条件 处理方式 是否重试
整批失败 传输错误(连接失败、超时、TLS)
HTTP 状态:429 / 502 / 503 / 504
客户端按指数退避自动重试:100 ms → 200 ms → 400 ms → 800 ms,最多 3 次 ✅ 自动
整批拒绝 HTTP 状态:400 / 401 / 403 / 404 / 413 不重试,整批所有记录全部丢弃,并通过 OnError 写错误日志 ❌ 丢弃
单条失败 200 OK 但 per-item 失败:版本冲突、字段映射错误、文档过大 不重试,仅该单条丢弃,通过 OnFailure 回调记录 index/id/status/type/reason ❌ 丢弃
单条成功 200 OK 且 per-item 成功 视为已落库

为什么这样设计:429/5xx 与传输错误是远端短暂不可用的信号,重试有效;4xx(除 429)与 per-item 错误是客户端语义问题(数据格式、权限),重试只会放大错误,应交给开发与运维侧排查日志后修正。

数据丢失场景

下列三种情况下,调用方调用 Save 时返回 nil,但事件最终未进入索引:

  1. 进程异常退出SIGKILL 或宿主机断电时,BulkIndexer 内存缓冲尚未刷新的部分直接丢失(仅本地目录保留副本)。
    • 缓解:SIGTERM/SIGINT 走优雅退出路径,shutdown 时调用 Close 强制 flush,最长等待 10 秒。
  2. 整批被永久拒绝:4xx(非 429)类错误一次性丢弃整批所有记录。常见诱因:索引被禁用、密码失效、单条文档超过集群 http.max_content_length
    • 排查:OnError 错误日志包含 ES 返回的 typereason
  3. 单条永久失败:mapping 冲突、版本冲突、文档语法错误。
    • 排查:OnFailure 错误日志按 index/id 定位失败记录。

本地目录始终保留副本:即使远端写入丢失,事件仍可从 huatuo-local/ 中找回,作为最终一致性的兜底。

解决的问题

将"逐事件 Index API"换成"BulkIndexer 批量 + 自动重试"主要解决以下四类问题:

问题 旧方案瓶颈 Bulk 方案的改进
TLS 握手 CPU 开销 每事件一次 HTTPS,握手在 FIPS / RSA-PSS 下占满 CPU 多条事件复用单连接 + 单次握手;TLS PSK ticket 缓存复用
远端 RTT 与吞吐 每事件一次往返,节点级写入受 RTT 限制 单次 Bulk 请求最多 5 MB,吞吐随批大小线性提升
远端短暂抖动 / 限流(429) 单次失败立即丢弃,无重试 客户端层面自动重试,吸收瞬态故障
采集层对存储后端解耦 远端慢会回压采集,影响内核事件采集时延 异步缓冲将采集与远端写入解耦,采集路径不被网络阻塞

🌟 结尾

2 - 内核事件订阅

📖 概述

/v1/events/watch 是华佗(HUATUO)提供的实时内核事件订阅接口。客户端通过一次 HTTP POST 长连接即可持续接收节点上发生的内核异常事件。事件以 CloudEvents 1.0 规范封装,通过 Server-Sent Events(SSE) 协议推送。


🎯 应用场景

内核事件订阅将操作系统层的异常信号直接暴露给上层系统,消除了传统轮询带来的延迟与开销。以下是典型的集成场景。

故障自愈系统

内核事件是自愈决策的第一手信号源。订阅 events/watch 后,自愈控制器可在事件发生的瞬间触发处置动作,而不必等待监控系统的告警流转:

  • OOM 自愈:收到 oom 事件后,立即对触发容器执行扩容、重启或流量摘除,将服务中断时间从分钟级压缩到秒级。
  • Hung Task 自愈:收到 hungtask 事件后,自动隔离节点并驱逐 Pod,防止级联阻塞蔓延至整个集群。
  • 网络故障自愈:收到 netdev_txqueue_timeoutnetdev_bonding_lacp 事件后,触发网卡重置或流量切换,实现分钟级网络链路自愈。
  • I/O 风暴自愈:收到 iotracing 事件后,结合 cgroup blkio 限速策略动态降低问题容器的磁盘 I/O 配额,保护同节点其他服务。

可观测性平台

将华佗内核事件接入可观测性平台,补齐应用指标和日志之外的内核视角:

  • 事件时间线关联:将 softlockupoom 等内核事件叠加到 Grafana 时间线上,与应用错误率、延迟曲线精确对齐,快速定位根因。
  • 异常驱动告警:以内核事件替代固定阈值告警,降低误报率。例如收到 ras 硬件错误事件时直接触发高优告警,而不依赖 CPU 错误率超阈值。
  • 容量与稳定性分析:长期订阅 memburstdload 等 AutoTracing 事件,建立节点稳定性基线,为容量规划提供内核级依据。
  • 多维下钻:事件中携带容器 ID、命名空间、地域等上下文,告警链接可直接下钻到对应的 Pod、Node、Region 视图。

安全审计与合规

  • 异常行为检测oomhungtasksoftlockup 等事件若在非业务高峰期集中出现,可能指示资源滥用或恶意负载,触发安全审查流程。
  • 事件留存与追溯:将 CloudEvents 事件流写入消息队列(Kafka、Pulsar)或对象存储,满足等保合规对系统异常事件留存的要求。

混沌工程与压测验证

  • 故障注入验证:混沌工程平台注入网络延迟、内存压力等故障后,实时订阅 net_rx_latencymemburst 事件验证故障是否生效,取代人工观察。
  • 压测基线建立:压测期间持续订阅全量事件,记录首个内核异常事件的出现时机,精确标定系统承压极限。

AIOps 智能运维

  • 事件驱动根因分析:将内核事件作为特征输入 AI/ML 模型,结合应用指标进行多维根因推断,减少人工排查时间。
  • 预测性维护:对 ras 硬件错误、netdev_bonding_lacp 等硬件层事件建模,在设备彻底失效前提前预警并触发迁移。
  • 智能抑制与聚合:对同一时间窗口内同类事件自动聚合,避免告警风暴,向 On-call 工程师呈现精简的根因摘要。

💎 价值

维度 传统方案 接入华佗 events/watch
时效性 告警触发延迟 1–5 分钟 内核事件实时推送,延迟 < 1 秒
信号准确性 基于指标阈值,误报率高 事件源自内核判定,误报率接近零
上下文丰富度 指标维度有限 携带容器、节点、地域等完整上下文
集成成本 需自建 eBPF 采集或依赖第三方 Agent 一次 HTTP POST 即可订阅,标准 CloudEvents 格式
协议兼容性 各厂商私有格式 遵循 CloudEvents 1.0 标准,可接入任意兼容平台

🚀 使用

1. CloudEvents 规范说明

1.1 CloudEvents 1.0 信封字段

每条推送事件均为一个符合 CloudEvents 1.0 规范的 JSON 对象:

字段 类型 说明
specversion string 固定值 "1.0"
id string 事件唯一标识符(UUID v4),每条事件独立生成
source string 事件来源路径,格式 /huatuo/{hostname}/{tracer_name}
type string 固定值 "tech.huatuo.kernel.event"
datacontenttype string 固定值 "application/json"
time string 事件采集时间(RFC 3339 纳秒精度,UTC)
data object 事件数据体,即 WatchEventData 结构体

1.2 华佗事件数据结构(WatchEventData)

data 字段包含华佗的标准事件记录:

{
  "specversion": "1.0",
  "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "source": "/huatuo/node-1/oom",
  "type": "tech.huatuo.kernel.event",
  "datacontenttype": "application/json",
  "time": "2026-05-18T10:23:45.123456789Z",
  "data": {
    "hostname": "node-1",
    "region": "cn-beijing",
    "observed_timestamp": "2026-05-18T10:23:45Z",
    "tracer_name": "oom",
    "tracer_id": "abc123",
    "tracer_run_type": "auto",
    "container_id": "d3f1a2b4c5e6",
    "container_hostname": "app-pod",
    "container_host_namespace": "prod",
    "container_type": "docker",
    "container_qos": "Guaranteed"
  }
}

WatchEventData 字段说明:

字段 类型 说明
hostname string 节点主机名
region string 节点所在地域
observed_timestamp string 内核事件发生时间(Tracer 采集时间)
tracer_name string 触发事件的采集器名称(见下文内核事件列表)
tracer_id string 事件实例唯一 ID
tracer_run_type string 采集模式,auto(自动触发)或 manual
container_id string 容器 ID(容器级事件时存在)
container_hostname string 容器主机名
container_host_namespace string 容器所在命名空间
container_type string 容器运行时类型(docker / containerd 等)
container_qos string 容器 QoS 等级

2. 支持的内核事件列表

tracer_name 说明
oom 内存不足(OOM Killer)触发事件
hungtask 内核任务长时间 D 状态(Hung Task)检测
softlockup CPU 软锁死(Soft Lockup)检测
ras 硬件可靠性(RAS)错误,如 ECC 内存错误
dropwatch 内核网络数据包丢弃(Drop Watch)事件
netdev_events 网络设备状态变更事件(Link Up/Down 等)
netdev_txqueue_timeout 网络设备发送队列超时事件
netdev_bonding_lacp Bond 设备 LACP 协议异常事件
net_rx_latency 网络接收延迟异常事件
softirq_tracing 软中断耗时异常追踪事件
memory_reclaim_events 内存回收异常事件
cpuidle CPU 空闲率异常(AutoTracing 自动触发)
cpusys CPU 系统态占用率异常(AutoTracing 自动触发)
dload 系统负载异常(AutoTracing 自动触发)
iotracing I/O 延迟异常(AutoTracing 自动触发)
memburst 内存突增异常(AutoTracing 自动触发)

3. POST 请求说明

3.1 接口地址

POST /v1/events/watch

3.2 请求头

Content-Type: application/json

3.3 请求体结构

{
  "filters": {
    "tracer_name": "<regex>",
    "hostname": "<regex>",
    "container_hostname": "<regex>",
    "container_host_namespace": "<regex>",
    "region": "<regex>"
  }
}

filters 字段说明:

字段 类型 是否必填 说明
tracer_name string 按采集器名称过滤,支持正则表达式
hostname string 按节点主机名过滤,支持正则表达式
container_hostname string 按容器主机名过滤,支持正则表达式
container_host_namespace string 按容器命名空间过滤,支持正则表达式
region string 按地域过滤,支持正则表达式
  • 所有过滤字段均为可选;省略或留空表示匹配所有值。
  • 多个字段同时指定时,所有条件须同时满足(AND 语义)。
  • 过滤器在服务端生效,仅匹配的事件才会推送到客户端。

3.4 响应格式(SSE 流)

连接建立后,服务端以 SSE 格式持续推送事件:

data: {"specversion":"1.0","id":"...","source":"/huatuo/node-1/oom",...}\n\n

服务端还会定期发送心跳注释行以保持连接:

: ping\n

4. EventsWatch 配置说明

在华佗配置文件(huatuo-bamai.conf)中通过 [EventsWatch] 段配置:

[EventsWatch]
    # 最大并发客户端连接数,超出后新连接返回 HTTP 429
    # Default: 100
    MaxClients = 100

    # SSE 心跳间隔(秒),防止代理/负载均衡因空闲而断开连接
    # 连续 3 次心跳写入失败则主动关闭该客户端连接
    # Default: 30
    KeepAliveInterval = 30
配置项 默认值 说明
MaxClients 100 同时允许的 /v1/events/watch 长连接上限,超出返回 HTTP 429
KeepAliveInterval 30 心跳间隔(秒),建议不超过上游代理的 idle timeout,推荐 15–60 秒

5. Curl 调用示例

5.1 订阅所有内核事件

curl -s -N -X POST http://<node-ip>:19704/v1/events/watch \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -H "Cache-Control: no-cache" \
  -H "Connection: keep-alive" \
  -d '{}'

5.2 只订阅 OOM 事件

curl -s -N -X POST http://<node-ip>:19704/v1/events/watch \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -H "Cache-Control: no-cache" \
  -H "Connection: keep-alive" \
  -d '{"filters": {"tracer_name": "^oom$"}}'

5.3 订阅指定节点的网络类事件

curl -s -N -X POST http://<node-ip>:19704/v1/events/watch \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -H "Cache-Control: no-cache" \
  -H "Connection: keep-alive" \
  -d '{
    "filters": {
      "hostname": "^node-1$",
      "tracer_name": "netdev|dropwatch|net_rx_latency"
    }
  }'

5.4 订阅 prod 命名空间的容器事件

curl -s -N -X POST http://<node-ip>:19704/v1/events/watch \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -H "Cache-Control: no-cache" \
  -H "Connection: keep-alive" \
  -d '{
    "filters": {
      "container_host_namespace": "^prod$"
    }
  }'

说明: -N 参数禁用 curl 缓冲,使 SSE 事件即时输出到终端。


6. Go 编程调用示例

以下示例展示如何在 Go 程序中订阅 events/watch 接口,实时消费 CloudEvents 事件。

package main

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"net/http"
	"os"
	"strings"
	"time"
)

// WatchRequest 是发送给 /v1/events/watch 的请求体。
type WatchRequest struct {
	Filters WatchFilters `json:"filters"`
}

type WatchFilters struct {
	TracerName             string `json:"tracer_name,omitempty"`
	Hostname               string `json:"hostname,omitempty"`
	ContainerHostname      string `json:"container_hostname,omitempty"`
	ContainerHostNamespace string `json:"container_host_namespace,omitempty"`
	Region                 string `json:"region,omitempty"`
}

// WatchEvent 是华佗推送的 CloudEvents 1.0 信封。
type WatchEvent struct {
	SpecVersion     string          `json:"specversion"`
	ID              string          `json:"id"`
	Source          string          `json:"source"`
	Type            string          `json:"type"`
	DataContentType string          `json:"datacontenttype"`
	Time            string          `json:"time"`
	Data            json.RawMessage `json:"data"`
}

func watchEvents(ctx context.Context, endpoint string, filters WatchFilters) error {
	reqBody, err := json.Marshal(WatchRequest{Filters: filters})
	if err != nil {
		return fmt.Errorf("marshal request: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(reqBody))
	if err != nil {
		return fmt.Errorf("create request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Accept", "text/event-stream")

	client := &http.Client{Timeout: 0} // SSE 长连接,不设超时
	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("connect: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("unexpected status: %d", resp.StatusCode)
	}

	scanner := bufio.NewScanner(resp.Body)
	for scanner.Scan() {
		line := scanner.Text()

		// 跳过心跳注释行和空行
		if line == "" || strings.HasPrefix(line, ":") {
			continue
		}

		// SSE data 行格式:`data: <json>`
		data, ok := strings.CutPrefix(line, "data: ")
		if !ok {
			continue
		}

		var event WatchEvent
		if err := json.Unmarshal([]byte(data), &event); err != nil {
			slog.Warn("parse event", "err", err)
			continue
		}

		fmt.Printf("[%s] source=%s id=%s\n", event.Time, event.Source, event.ID)
		fmt.Printf("  data: %s\n", event.Data)
	}

	return scanner.Err()
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
	defer cancel()

	err := watchEvents(ctx, "http://192.168.1.10:19704/v1/events/watch", WatchFilters{
		TracerName: "oom|hungtask|softlockup",
	})
	if err != nil {
		slog.Error("watch events", "err", err)
		os.Exit(1)
	}
}

6.1 使用 pkg/types 官方包(推荐)

如果你的项目与华佗在同一 Go module,可直接引用官方类型:

import pkgtypes "huatuo-bamai/pkg/types"

var event pkgtypes.WatchEvent
if err := json.Unmarshal([]byte(data), &event); err != nil { ... }

// WatchEvent.Data 是 json.RawMessage(延迟解析),需二次反序列化才能访问具体字段
dataBytes, err := json.Marshal(event.Data)
if err != nil {
    slog.Warn("marshal event data", "err", err)
    return
}
var payload pkgtypes.WatchEventData
if err := json.Unmarshal(dataBytes, &payload); err != nil {
    slog.Warn("unmarshal event data", "err", err)
    return
}
fmt.Println("tracer:", payload.TracerName)
fmt.Println("observed_timestamp:", payload.ObservedTimestamp)

6.2 重连机制建议

生产环境中,网络抖动或服务重启会导致连接断开,建议加入指数退避重连逻辑:

func watchWithRetry(ctx context.Context, endpoint string, filters WatchFilters) {
	backoff := time.Second
	for {
		if err := watchEvents(ctx, endpoint, filters); err != nil {
			if ctx.Err() != nil {
				return
			}
			slog.Warn("disconnected, retrying", "err", err, "backoff", backoff)
			// time.NewTimer + Stop 确保 context 取消时计时器资源立即释放
			timer := time.NewTimer(backoff)
			select {
			case <-ctx.Done():
				timer.Stop()
				return
			case <-timer.C:
			}
			if backoff < 30*time.Second {
				backoff *= 2
			}
		}
	}
}

⚙️ 原理

系统架构

HUATUO Agent 部署在每个节点上,通过 eBPF、Kprobe、Tracepoint 等机制挂钩内核关键路径,将内核异常事件采集后经过滤、封装,以 SSE 长连接推送给多个并发订阅客户端。

graph TB
    subgraph kernel["Linux 内核"]
        K1[OOM Killer]
        K2[Hung Task 检测]
        K3[Soft Lockup 检测]
        K4[RAS 硬件错误]
        K5[网络子系统]
        K6[AutoTracing]
    end

    subgraph huatuo["HUATUO Agent(节点级)"]
        T["Tracer 采集层\neBPF / Kprobe / Tracepoint"]
        F["过滤器\nhostname / tracer / namespace / region"]
        CE["CloudEvents 1.0 封装\nid / source / time / data"]
        EW["EventsWatch 分发层\nSSE 长连接管理"]
    end

    subgraph clients["订阅客户端"]
        C1[故障自愈系统]
        C2[可观测性平台]
        C3[AIOps 系统]
        C4[安全审计系统]
    end

    kernel --> T
    T --> F
    F --> CE
    CE --> EW
    EW -->|SSE 推送| C1
    EW -->|SSE 推送| C2
    EW -->|SSE 推送| C3
    EW -->|SSE 推送| C4

事件采集与推送原理

客户端发起 POST 请求后,连接保持打开状态。内核每次触发异常事件,HUATUO Agent 完成过滤和封装后立即将事件写入所有匹配的 SSE 流,无需客户端轮询。

sequenceDiagram
    participant C as 客户端
    participant EW as EventsWatch
    participant T as Tracer 采集层
    participant K as Linux 内核

    C->>EW: POST /v1/events/watch {"filters": {...}}
    EW-->>C: 200 OK (Content-Type: text/event-stream)

    loop SSE 长连接持续推送
        K->>T: 内核事件触发(oom / hungtask / softlockup ...)
        T->>EW: 上报原始事件
        EW->>EW: 过滤器匹配
        alt 匹配成功
            EW-->>C: data: {CloudEvents JSON}\n\n
        else 不匹配
            note over EW: 丢弃,不推送
        end
        EW-->>C: : ping(心跳保活,间隔 KeepAliveInterval 秒)
    end

事件处理流程

从内核事件产生到推送至客户端,经过采集、过滤、封装三个阶段,整体链路延迟小于 1 秒。

flowchart LR
    A([内核异常触发]) --> B["Tracer 采集\neBPF / Kprobe"]
    B --> C{过滤器匹配?}
    C -- 否 --> D([丢弃])
    C -- 是 --> E["封装 CloudEvents 1.0\nid / source / time / data"]
    E --> F[写入 SSE 流]
    F --> G([推送至订阅客户端])

🌟 结尾

3 - 性能剖析

火焰图格式

在性能剖析领域,collapsedflamegraph 是最常用的两种火焰图格式,分别对应"原始数据"与"可视化视图"两个层次。

Collapsed 格式

标准语法与格式

collapsed 格式(又称 folded stacks)由 Brendan Gregg 定义,是火焰图的原始文本输入格式。每行代表一条唯一的调用栈及其采样计数。

基本规则:

frame1;frame2;frame3;...;frameN COUNT
组成部分 说明
frame1 栈底(入口/根帧),如 mainstart_thread
; 帧分隔符(分号)
frameN 栈顶(当前执行帧,即采样命中点)
COUNT 采样次数(整数),与栈帧之间用空格分隔

格式要点:

  • 每行一条独立调用栈,相同栈路径的样本合并计数
  • 帧的排列顺序:从左到右为 根→叶(调用链方向)
  • 空行及 # 开头的行通常被视为注释,解析时忽略
  • COUNT 的语义取决于分析模式:CPU 采样时为采样次数,内存分配时为分配字节数,锁分析时为竞争时间(毫秒)

扩展规范:

部分剖析工具(如 async-profiler)在标准格式基础上引入了帧类型注解,用于标识帧的运行时类别:

frameName_{type} COUNT
注解 含义 说明
_[j] JIT compiled Java JIT 编译后的 Java 方法
_[i] Interpreted Java 解释执行的 Java 方法
_[k] Kernel 内核态帧
_[n] Native C/C++ 原生 C/C++ 帧
_[t] Thread 线程帧

此外,部分工具支持带权重的折叠格式(weighted collapsed),用于差分火焰图:

frame1;frame2;frameN WEIGHT

其中 WEIGHT 为浮点数,表示该栈的权重值而非简单计数。

样本示例

CPU 分析示例(以下数据源自 async-profiler 官方文档):

FileConverter.main;FileConverter.convertFile;FileConverter.saveResult 21
FileConverter.main;FileConverter.convertFile;FileConverter.saveResult;java/io/DataOutputStream.writeInt 1
FileConverter.main;FileConverter.convertFile;FileConverter.saveResult;java/io/DataOutputStream.writeInt;java/io/ByteArrayOutputStream.write 5
FileConverter.main;FileConverter.convertFile;FileConverter.saveResult;java/io/DataOutputStream.writeUTF;java/io/DataOutputStream.writeUTF 12
FileConverter.main;FileConverter.convertFile;FileConverter.saveResult;java/io/DataOutputStream.writeUTF;java/io/DataOutputStream.writeUTF;java/lang/String.length 3
FileConverter.main;FileConverter.convertFile;FileConverter.saveResult;java/io/DataOutputStream.writeUTF;java/io/DataOutputStream.writeUTF;java/io/DataOutputStream.write 6
start_thread;thread_native_entry;Thread::call_run;VMThread::run;VMThread::inner_execute;VMThread::evaluate_operation;VM_Operation::evaluate;VM_GenCollectForAllocation::doit;GenCollectedHeap::satisfy_failed_allocation;GenCollectedHeap::do_collection;GenCollectedHeap::collect_generation;DefNewGeneration::collect;DefNewGeneration::FastEvacuateFollowersClosure::do_void 12

带帧类型注解的示例(async-profiler 扩展):

Main.run_[j];Service.process_[j];DAO.query_[j];mysql_real_query_[n] 45
Main.run_[j];Service.process_[j];DAO.query_[j];recv_[k] 18

核心用途

用途 说明
火焰图生成 作为 flamegraph.plinferno 等可视化工具的标准输入格式
差分分析 对比两次 collapsed 文件,生成红蓝差分火焰图,定位性能回归
程序化处理 纯文本格式,便于用 awksed、Python 等工具做自定义聚合与过滤
跨工具互操作 Brendan Gregg 定义的通用标准,几乎所有火焰图工具链都支持此格式
长期存储 文本格式体积小,适合归档和版本对比
CI/CD 集成 可在流水线中自动采集、diff、判断性能回归阈值

生成命令示例:

# 以 async-profiler 为例
asprof -d 30 -f profile.collapsed -o collapsed <PID>

Flamegraph 格式

标准语法与格式

flamegraph 格式是一个自包含的 HTML 文件,内嵌 SVG 可视化与 JavaScript 交互逻辑,可直接在浏览器中打开。

结构组成:

flamegraph.html
├── HTML 骨架 + CSS 样式
├── SVG 火焰图主体
│   ├── <g> 每个帧对应的矩形块
│   │   ├── <title> 帧名称 + 采样数/占比
│   │   └── <rect> 位置、宽高、颜色
│   └── ...
├── JavaScript 交互逻辑
│   ├── 点击缩放(zoom into subtree)
│   ├── 搜索高亮(search & highlight)
│   ├── 悬浮提示(tooltip)
│   └── 重置视图(reset zoom)
└── 元数据(title、total samples 等)

视觉编码规则:

维度 编码含义
X 轴 调用栈帧按字母序排列(非时间线),宽度与采样数成正比
Y 轴 调用栈深度,底部为根帧,顶部为叶帧
帧宽度 该帧在栈中出现的采样比例,越宽表示消耗资源越多
帧颜色 标识帧类型(见下表)

帧颜色规范(以 async-profiler 为参考):

注意:火焰图的颜色方案并非跨工具统一标准。Brendan Gregg 原始 flamegraph.pl 使用随机暖色调,颜色无语义含义;perf/bpftrace 通常按 DSO 着色或使用随机色;async-profiler 则按帧类型语义着色。以下为 async-profiler 的配色规范:

颜色 帧类型 说明
🟢 绿色 Java (interpreted) 解释执行的 Java 方法
🟡 黄/橙色 Java (JIT compiled) JIT 编译后的 Java 方法
🔴 红色 C/C++ (native) 原生 C/C++ 代码
🔵 蓝色 Kernel 内核态代码
⬜ 灰色 Other/Unknown 其他类型或未知帧

扩展特性(以 async-profiler 为参考):

  • Icicle Graph(冰柱图):自顶向下展示调用链(根在顶部),更符合自上而下的阅读习惯,通过 --reverse 选项或浏览器内 Reverse 按钮切换
  • 多线程视图:不同线程的调用栈并列展示在根级别
  • 搜索高亮:输入关键词后,匹配帧高亮为紫色,不匹配帧变暗
  • 采样信息提示:悬浮显示帧名、采样数、占总采样百分比
  • Cutoff 帧:标记为 [...] 的帧表示栈截断(如因栈深度限制)

样本示例

生成命令示例:

# 以 async-profiler 为例
asprof -d 30 -f flamegraph.html <PID>

交互操作:

  • 点击帧:缩放至该帧为全宽,仅展示其子树
  • 搜索框:输入关键词,匹配帧高亮
  • 悬浮:显示帧名、采样数、百分比
  • Reset Zoom:恢复全局视图

核心用途

用途 说明
热点定位 直观识别最宽的帧块,快速找到 CPU/内存消耗最大的代码路径
根因分析 从叶帧向上追溯,理解资源消耗的调用链上下文
团队协作 HTML 文件可直接分享,无需安装额外工具,浏览器即可查看
性能优化验证 优化前后各生成一张火焰图,对比帧宽度变化验证优化效果
非专业友好 可视化形式对非性能工程师也更易理解,便于跨团队沟通

两种格式对比

对比维度 Collapsed Flamegraph
格式类型 纯文本 HTML + SVG
人可读性 中等(需理解栈帧语法) 高(可视化,直觉理解)
机器可读性 高(易解析、易 diff) 低(需解析 HTML/SVG)
交互性 支持缩放、搜索、悬浮提示
文件大小 极小(KB 级) 较大(百 KB~MB 级)
工具链依赖 无(纯文本) 浏览器
差分分析 原生支持(diff 两个文件) 需转换为 collapsed 后 diff
典型使用场景 程序化处理、CI 对比、存档 人工分析、团队分享、演示

典型工作流:

采集 ──► collapsed ──► flamegraph.html(人工分析)
                  ├──► 差分火焰图(性能回归检测)
                  ├──► 自定义聚合脚本
                  └──► 归档存储

4 - 网络丢包

📖 概述

dropwatch 是 HUATUO 提供的内核网络丢包观测工具。它通过挂载内核 Tracepoint tracepoint/skb/kfree_skb 实时采集网络丢包事件,输出完整的丢包上下文:协议类型、IP 五元组、进程名、PID、网络设备、MAC 地址,以及触发丢包的完整内核调用栈。

dropwatch 支持基于 tcpdump 风格过滤表达式的内核侧过滤,过滤逻辑由内置的纯 Go pcap 编译器 internal/pcapfilter 在加载时编译为 eBPF 字节码,过滤完全在内核态执行,只有匹配的数据包才会上报到用户空间,降低对宿主机的性能影响。

此外,dropwatch 支持设备白名单/黑名单过滤、全局上报限速,并可与 huatuo-bamai 集成,将丢包事件存储至 Elasticsearch 进行长期分析。


🎯 场景

1. Kubernetes 云原生网络丢包诊断

在容器漂移、Pod 频繁重启、Service 端口冲突等场景下,通过 dropwatch 实时捕获 kfree_skb 事件并关联到具体容器,快速定位丢包根因。结合 --filter "tcp and port <service-port>" 过滤特定业务流量,将平均故障定位时间从小时级降低至分钟级。

2. 网络性能毛刺分析

针对间歇性网络延迟突增、吞吐下降等问题,通过 dropwatch 采集丢包事件,结合内核调用栈定位丢包发生的具体内核函数(如 tcp_v4_rcvip_output 等),辅助区分是防火墙丢弃、路由失败还是缓冲区溢出等原因。

3. 多租户环境网络隔离故障排查

在共享网络命名空间或 veth 设备的容器环境中,通过 --device 过滤指定网络设备,结合 --filter 过滤特定协议,精确采集目标容器的丢包事件,避免其他租户流量干扰诊断结果。

4. 与可观测性平台集成

通过 --output-storage 将丢包事件发送给 huatuo-bamai,存储至 Elasticsearch 后与指标、日志进行多维关联分析。将丢包事件叠加到 Grafana 时间线上,与应用错误率、延迟曲线对齐,实现内核丢包与应用异常的精确关联。


🚀 使用

1. 过滤表达式

过滤表达式采用 tcpdump 语法,由内置的纯 Go pcap 编译器 internal/pcapfilter 在加载时编译为 eBPF 字节码,过滤完全在内核侧执行,降低对宿主机影响,只有匹配的数据包才会上报到用户空间。

1.1 支持的表达式

internal/pcapfilter 支持 tcpdump 标准语法的一个子集,下列原语可以可靠使用:

协议

ip   ip6   tcp   udp   icmp   icmp6   igmp   pim   esp   ah   vrrp   arp   rarp
ip proto tcp      ip6 proto udp        (仅协议名,不支持数字协议号)

主机地址

host 10.0.0.1
src host 10.0.0.1
dst host 10.0.0.1

端口

port 80
src port 443
dst port 8080

网段(CIDR)

net 10.0.0.0/8
src net 192.168.1.0/24
dst net 172.16.0.0/12

组播与以太地址

ip multicast    ip6 multicast    multicast    ether multicast
ether host 00:11:22:33:44:55

布尔运算与分组

tcp and port 80
tcp or udp
not arp
tcp and (port 80 or port 443)
ip and src net 192.168.1.0/24 and tcp dst port 3306

1.2 不支持的表达式

下列表达式不支持,使用后会导致编译失败或产生错误的匹配结果:

表达式 原因
tcp[tcpflags] & tcp-syn != 0ip[8]tcp[0:4] 字节偏移表达式(proto[offset:size])未实现
ip proto 6ip6 proto 17 不支持数字协议号,请改用协议名(如 ip proto tcp
ether proto 0x0800 不支持十六进制 EtherType,请改用名字(如 ether proto ip
sctp 关键字未识别
portrange 80-90tcp portrange 1-100 不支持端口范围
less Ngreater N 不支持按报文长度过滤
ip broadcastether broadcast 不支持广播匹配
vlanmplspppoes 不支持隧道/封装关键字
gateway 不支持

1.3 推荐写法示例

# 监控所有 TCP 丢包(默认值——L2 和 L3 上下文均可靠)
--filter "tcp"

# TCP 和 UDP
--filter "tcp or udp"

# 指定目标主机(TCP 和 UDP 均适用)
--filter "dst host 10.0.0.1"

# 指定端口
--filter "tcp and port 443"

# 排除噪声主机
--filter "tcp and not host 169.254.169.254"

# 指定子网 + 指定端口
--filter "src net 192.168.1.0/24 and tcp dst port 3306"

# 监控非 TCP 的丢包(仅 UDP 和 ICMP——不要用 "not tcp",会捕获到未知 L3 事件)
--filter "udp or icmp"

# 仅监控 ARP 丢包(仅 L2 上下文有效,L3 永远不匹配)
--filter "arp"

--filter "ip" / --filter "ip6" 现可正确匹配对应 IP 协议族(L2 按 EtherType、L3 按版本 nibble)。若只关心特定传输层或主机,仍建议用更精确的 tcpudphostip proto <name>


2. 运行 dropwatch

dropwatch [flags]
参数 默认值 说明
--bpf-path <path> 必填 dropwatch eBPF 对象文件路径
--filter <expr> (无) tcpdump 风格过滤表达式
--device <names> (无) 设备白名单:只采集这些设备的丢包,多个设备用逗号分隔(如 eth0,eth1
--device-excluded <names> (无) 设备黑名单:排除这些设备的丢包;与 --device 互斥
--duration <n> 0 运行 N 秒后退出(0 表示持续运行直至 Ctrl-C)
--output <json|text> text 输出格式;设置 --output-storage 时会被忽略
--output-storage <path> (无) 通过 Unix socket 将事件发送给 huatuo-bamai
--task-id <id> (无) 关联本次会话的任务 ID;通常与 --output-storage 一起使用
--max-events-per-second <n> 0 全局上报限速,0 表示不限速;在 --device / --filter 后生效

--filter 与设备过滤相互正交,同时指定时两者均生效(AND 语义)。不指定 --device / --device-excluded 时采集所有设备。--device--device-excluded 不能同时使用;白名单模式会丢弃没有 net_device 的 SKB,黑名单模式会放行没有 net_device 的 SKB。

常用命令

# 文本格式输出,监控所有设备的 TCP 丢包
sudo dropwatch --bpf-path bpf/dropwatch.o --filter "tcp"

# 只监控 eth0 上的丢包
sudo dropwatch --bpf-path bpf/dropwatch.o --device eth0 --output json

# 排除 loopback
sudo dropwatch --bpf-path bpf/dropwatch.o --device-excluded lo --output json

# 设备过滤与协议过滤组合
sudo dropwatch --bpf-path bpf/dropwatch.o --device eth0 --filter "tcp and port 443" --output json

# 抓取 60 秒后退出
sudo dropwatch --bpf-path bpf/dropwatch.o --filter "tcp and port 443" --duration 60 --output json

# 将事件转发给正在运行的 huatuo-bamai 实例
sudo dropwatch --bpf-path bpf/dropwatch.o --filter "tcp" --output-storage /var/run/huatuo/events.sock

# 通过 jq 过滤仅显示 RST 包
sudo dropwatch --bpf-path bpf/dropwatch.o --output json 2>/dev/null | jq 'select(.layers.tcp.flags == "RST")'

# 采集 10 秒 JSON 输出,并排除调用栈包含 ip_finish_output 的事件
sudo dropwatch --output json --duration 10 --bpf-path bpf/dropwatch.o | jq -c 'select(.stack | test("ip_finish_output") | not)'

# 采集 10 秒 JSON 输出,只打印除 stack 之外的字段
sudo dropwatch --output json --duration 10 --bpf-path bpf/dropwatch.o | jq -c 'del(.stack)'

jq -c 会把每条匹配事件压缩成单行 JSON,便于保存为 NDJSON 或继续用管道处理。test("ip_finish_output") 判断 stack 是否匹配该正则,not 会把结果取反,因此上面的命令会排除包含 ip_finish_output 的调用栈;去掉 | not 后,就是只保留包含 ip_finish_output 的事件。del(.stack) 只从 jq 输出中删除 stack 字段,适合只查看时间、设备、进程、packet_* 元数据和 layers 协议字段。如需在内核侧按调用栈过滤,可通过 huatuo-bamai 配置 EventTracing.IssuesList 实现(参见第 4 节)。


3. 事件数据结构

每条丢包事件以 JSON 对象(types.DropWatchTracing)表示。

字段 类型 说明
observed_timestamp string 采集到事件的 UTC 时间戳(RFC3339Nano)
type string 事件类型保留字段,当前为空字符串
drop_reason string 丢包原因保留字段,当前为空字符串
source string 事件来源,存在时标识 eventstools(omitempty)
comm string 丢包时的进程名
pid uint64 进程 TGID
container_id string 容器 ID(由 huatuo-bamai 解析填充,omitempty)
memory_cgroup_css_addr string 内存 cgroup CSS 地址,用于容器归属解析
net_namespace_cookie uint64 网络命名空间 cookie,用于容器归属解析
net_namespace_inode uint32 网络命名空间 inode,用于容器归属解析
netdev_name string 网络设备名(如 eth0
netdev_ifindex uint32 网络接口索引
netdev_queue_mapping uint32 TX 队列映射
netdev_linkstatus []string 网络设备链路标志
packet_skb_addr string SKB 地址(十六进制,omitempty)
packet_eth_proto string 原始 EtherType(十六进制,如 0x0800
packet_len uint32 数据包长度(字节)
layers object 分层协议解析结果,缺失的层会省略
stack string 内核调用栈(换行分隔)

layers 使用固定字段表达协议栈,不再依赖单独的协议枚举:

字段 说明
layers.label 协议组合标签,如 IPv4/TCPIPv6/UDPARPunknown
layers.ether 二层字段:srcdsttypelen(仅 802.3 帧存在)
layers.ipv4 IPv4 字段:versionihltoslenidflagsfrag_offsetttlprotocolchecksumsrcdst
layers.ipv6 IPv6 字段:versiontraffic_classflow_labellennext_headerhop_limitsrcdst
layers.tcp TCP 字段:sportdportseqackdata_offsetflagswindowchecksumurgentsk_state
layers.udp UDP 字段:sportdportlenchecksum
layers.icmp ICMP/ICMPv6 字段:typecodechecksumidseq
layers.arp ARP 字段:addr_typeprotocolhw_address_sizeprot_address_sizeoperationsender_macsender_iptarget_mactarget_ip

4. 与 huatuo-bamai 集成

huatuo-bamai 以子进程形式启动 dropwatch,并通过 --output-storage 将事件发送到内置处理流程,并最终存储到 Elasticsearch。典型参数如下:

dropwatch \
  --bpf-path <CoreBpfDir>/dropwatch.o \
  --output-storage /var/run/huatuo/events.sock \
  --filter "tcp"

4.1 配置项参考(huatuo-bamai.conf

[EventTracing]
    # 已知噪声调用栈过滤。dropwatch 会丢弃 stack 匹配这些正则的事件。
    # 默认示例覆盖邻居表清理和 bnxt TX 完成释放 SKB。
    IssuesList = [["neigh_invalidate", "neigh_invalidate"], ["bnxt_tx_int", "bnxt_tx_int"]]

[EventTracing.Dropwatch]
    # tcpdump 过滤表达式,转发给 dropwatch --filter。
    # 默认值: "tcp"
    Filter = "tcp"

    # 转发给 dropwatch --max-events-per-second。
    # 默认值: 100
    MaxEventsPerSecond = 100

4.2 噪声过滤

以下三类 kfree_skb 事件默认被过滤,因为它们不是真实的数据面丢包:

模式 调用栈帧前缀 原因
TCP CLOSE_WAIT + skb_rbtree_purge skb_rbtree_purge/ 正常的套接字关闭流程:内核在关闭 CLOSE_WAIT 状态的套接字时会释放飞行中的 SKB。
ARP/邻居表到期 neigh_invalidate/ 邻居表项到期清理,不影响任何活跃数据流。可从 EventTracing.IssuesList 移除对应规则以关闭过滤。
bnxt 网卡 TX 完成 bnxt_tx_int/__bnxt_tx_int/ Broadcom bnxt 网卡驱动在 DMA 发送完成后调用 kfree_skb 释放 SKB,此为正常行为,非丢包。

🌟 结尾