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.Elasticsearch]
    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.Elasticsearch]
    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.Elasticsearch]
    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.Elasticsearch] 配置接口,通过地址区分。

写入远端时使用 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 - 数据源配置

HUATUO 通过 Prometheus 采集指标,并将事件数据写入 Elasticsearch。本文介绍 Docker Compose 和 Kubernetes 环境下的数据源与仪表盘配置方法。

不要在同一节点同时运行 Docker Compose、systemd 和 Kubernetes 版本的 huatuo-bamai。它们会争用 19704 端口和 /var/run/huatuo-toolstream.sock

Docker Compose

build/docker/ 提供已配置完成的 Prometheus、Elasticsearch、Grafana 和 HUATUO 组件。

export ELASTIC_PASSWORD=huatuo-bamai
docker compose --project-directory ./build/docker up -d

服务使用宿主机网络:

服务 地址 用途
Elasticsearch http://localhost:9200 事件存储
Prometheus http://localhost:9090 指标采集
Grafana http://localhost:3000 可视化
huatuo-bamai http://localhost:19704 指标和事件采集

Grafana 默认账号为 admin/admin。首次登录后应立即修改密码。

已预配置的数据源

数据源由 build/docker/grafana/datasources/ 自动加载:

名称 类型 UID
huatuo-bamai-prom Prometheus huatuo-bamai-prom
huatuo-bamai-es Elasticsearch huatuo-bamai-es
huatuo-bamai-infinity Infinity huatuo-bamai-infinity-auto-flamegraph

已预配置的仪表盘

仪表盘由 build/docker/grafana/dashboards/ 自动加载:

  • Metric 大盘(宿主机)
  • Metric 大盘(容器)
  • HuaTuo 根因定位 AutoTracing
  • Continuous Profiling(宿主机)
  • Continuous Profiling(容器)
  • AutoTracing Flame Redirect

使用 Docker Compose 时不需要手动配置数据源或导入仪表盘。

Kubernetes

Prometheus 可以通过 Pod 注解或 ServiceMonitor 采集 HUATUO 指标。两种方式选择一种即可,避免重复采集。

Pod 注解

build/huatuo-daemonset.minimal.yaml 默认包含以下注解:

template:
  metadata:
    annotations:
      prometheus.io/scrape: "true"
      prometheus.io/port: "19704"
      prometheus.io/path: "/metrics"

Helm Chart 根据 charts/huatuo/values.yaml 中的以下配置生成注解:

metrics:
  enabled: true
  port: 19704
  path: /metrics

Prometheus 需要启用 Kubernetes Pod 服务发现,并通过 relabel 读取注解:

scrape_configs:
  - job_name: k8s-pods
    kubernetes_sd_configs:
      - role: pod
    relabel_configs:
      - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
        action: keep
        regex: "true"
      - source_labels:
          [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
        action: replace
        regex: ([^:]+)(?::\d+)?;(\d+)
        replacement: $1:$2
        target_label: __address__
      - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
        action: replace
        regex: (.+)
        target_label: __metrics_path__
      - source_labels: [__meta_kubernetes_namespace]
        action: replace
        target_label: kubernetes_namespace
      - source_labels: [__meta_kubernetes_pod_name]
        action: replace
        target_label: kubernetes_pod_name

ServiceMonitor

ServiceMonitor 仅适用于安装了 Prometheus Operator 和 ServiceMonitor CRD 的集群。以下示例假设 Helm release 名和命名空间均为 huatuo。如果实际名称不同,需要同步修改命名空间和 app.kubernetes.io/instance 标签。

创建 huatuo-service.yaml

apiVersion: v1
kind: Service
metadata:
  name: huatuo
  namespace: huatuo
  labels:
    app.kubernetes.io/name: huatuo
    app.kubernetes.io/instance: huatuo
spec:
  clusterIP: None
  selector:
    app.kubernetes.io/name: huatuo
    app.kubernetes.io/instance: huatuo
  ports:
    - name: metrics
      port: 19704
      targetPort: 19704
      protocol: TCP

创建 huatuo-servicemonitor.yaml

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: huatuo
  namespace: huatuo
  labels:
    release: prometheus
spec:
  namespaceSelector:
    matchNames:
      - huatuo
  selector:
    matchLabels:
      app.kubernetes.io/name: huatuo
      app.kubernetes.io/instance: huatuo
  endpoints:
    - port: metrics
      path: /metrics
      interval: 30s
      scrapeTimeout: 10s

release: prometheus 必须匹配 Prometheus 实例的 spec.serviceMonitorSelector。不同 Prometheus Operator 安装方式可能使用不同标签。

如果使用 build/huatuo-daemonset.minimal.yaml,Pod 标签是 app: huatuo,需要将 Service 的 spec.selector 改为:

selector:
  app: huatuo

ServiceMonitor 选择的是 Service 的 metadata.labels,不是 Pod 标签。

应用配置:

kubectl apply -f huatuo-service.yaml
kubectl apply -f huatuo-servicemonitor.yaml

Grafana 数据源

不使用 Docker Compose 时,在 Grafana 中手动添加数据源。

Prometheus

  • URL:http://<prometheus-host>:9090
  • Access:Server (proxy)
  • UID:huatuo-bamai-prom

Elasticsearch

  • URL:http://<elasticsearch-host>:9200
  • Authentication:Basic Authentication
  • Username:elastic
  • Password:<password>
  • Index name:huatuo_bamai*
  • Time field name:uploaded_time
  • UID:huatuo-bamai-es

Provisioning 配置示例见 build/docker/grafana/datasources/

导入仪表盘

Docker Compose 会自动加载仓库内置仪表盘。外部 Grafana 按以下步骤导入:

  1. 打开 Grafana 的 Dashboards -> Import
  2. 上传或粘贴 build/docker/grafana/dashboards/*.json
  3. 点击 Load
  4. 选择 huatuo-bamai-promhuatuo-bamai-es 数据源
  5. 点击 Import

也可以从 HUATUO 控制台导出仪表盘:

  1. 访问 http://console.huatuo.tech/dashboards
  2. 登录并选择仪表盘
  3. 点击 Export -> Export as JSON
  4. 勾选 “Export the dashboard to use in another instance”
  5. 复制 JSON 并导入目标 Grafana

控制台登录凭证由部署管理员提供,不应保存在公开文档中。

3 - 内核事件订阅

📖 概述

/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. HTTP 服务事件流配置

在华佗配置文件的 [HTTPServer] 段配置事件流参数:

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

    # SSE 心跳间隔(秒),防止代理/负载均衡因空闲而断开连接
    # 连续 3 次心跳写入失败则主动关闭该客户端连接
    # Default: 30
    EventStreamKeepAliveIntervalSeconds = 30
配置项 默认值 说明
MaxEventStreamClients 100 /v1/events/watch 长连接上限,超出返回 HTTP 429
EventStreamKeepAliveIntervalSeconds 30 心跳间隔,应小于上游代理的 idle timeout

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(按配置间隔发送心跳)
    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([推送至订阅客户端])

🌟 结尾

4 - 性能剖析

火焰图格式

在性能剖析领域,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(人工分析)
                  ├──► 差分火焰图(性能回归检测)
                  ├──► 自定义聚合脚本
                  └──► 归档存储

5 - 网络丢包

📖 概述

dropwatch 是 HUATUO 提供的网络丢包观测工具。它通过 tracepoint/skb/kfree_skb 采集软件丢包,并通过 raw_tracepoint/devlink_trap_report 采集支持 devlink trap 的硬件丢包,输出协议类型、IP 五元组、网络设备、丢包原因和内核调用栈。

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。

dropwatch 启动时自动检测 devlink:devlink_trap_report。内核支持时同时加载软硬件丢包探针;不支持时记录 warning 并仅加载软件丢包探针。硬件丢包采集还要求网卡驱动注册 devlink drop trap,并将目标 trap action 配置为 trap。action 为 drop 时硬件不会向 CPU 提供报文副本,dropwatch 无法获取报文。

dropwatch 无需额外启动参数。使用前确认内核、驱动和目标 trap 均满足条件:

# 1. 确认内核提供 devlink trap tracepoint
test -e /sys/kernel/tracing/events/devlink/devlink_trap_report/id || \
  test -e /sys/kernel/debug/tracing/events/devlink/devlink_trap_report/id

# 2. 查看 devlink 设备及驱动注册的 trap
sudo devlink dev show
sudo devlink trap show <bus/device>

# 3. 为目标 DROP trap 启用报文上报
sudo devlink trap set <bus/device> trap <trap-name> action trap

# 4. 启动 dropwatch,并只查看硬件丢包
sudo dropwatch --bpf-path bpf/dropwatch.o --output json 2>/dev/null | \
  jq -c 'select(.drop_source == "hardware")'

<bus/device> 使用 devlink dev show 返回的设备标识,例如 pci/0000:03:00.0。诊断结束后应将 trap 恢复为变更前的 action。

该能力只采集驱动通过 DEVLINK_TRAP_TYPE_DROP 上报的报文,不会采集所有硬件报文,也不等同于网卡硬件丢包计数器。硬件队列满等丢包只有在驱动将其实现为 devlink drop trap 并上报时才可见;exceptioncontrol 类型的 trap 不会上报为丢包事件。

--filter--device--device-excluded--max-events-per-second 同时作用于软件与硬件事件。文本输出将硬件原因表示为 reason=<group>/<trap> drop_source=hardware;JSON 输出使用独立的 drop_reason_groupdrop_reasondrop_source 字段。

常用命令

# 文本格式输出,监控所有设备的 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-toolstream.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. 事件数据结构

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

字段 类型 说明
observed_timestamp string 用户态接收/格式化事件时生成的 UTC 时间(RFC3339Nano),不是内核 hook 时间
type string 预留 TCP 事件类型,当前未设置(1 普通丢包、2 SYN flood、3/4 listen overflow)
drop_source string 丢包来源:software 表示内核协议栈,hardware 表示 devlink DROP trap
drop_reason string 软件丢包为 SKB_DROP_REASON_*;无法从内核 BTF 解析时记录 warning 并回退为数字。硬件丢包为 devlink trap 名称
drop_reason_group string devlink trap 分组名称,用于归类硬件丢包;软件丢包不输出该字段
drop_location string 软件丢包的 kfree_skb 调用地址(十六进制);硬件丢包不输出该字段
source string 事件来源;独立运行 dropwatch 时为 tools,由 huatuo-bamai 启动时为 events
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_inum uint32 网络命名空间 inum,用于容器归属解析
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 内核调用栈(换行分隔)

硬件事件的 stack 表示驱动调用 devlink trap 上报接口时的内核调用栈,不代表 ASIC 内部的实际丢弃位置。定位硬件原因时应以 drop_reason_groupdrop_reason、设备信息和驱动文档为主。

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

字段 说明
layers.label 协议组合标签,如 IPv4/TCPIPv6/UDPARPunknown
layers.ether 存在真实 Ethernet header 时输出二层字段:saddrdaddrtypelen;仅 IEEE 802.3 framing 的 len 非零
layers.ipv4 IPv4 字段:versionihltoslenidflagsfrag_offsetttlprotocolchecksumsaddrdaddr
layers.ipv6 IPv6 字段:versiontraffic_classflow_labellennext_headerhop_limitsaddrdaddr
layers.tcp TCP 字段:sportdportseqack_seqdata_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-toolstream.sock \
  --filter "tcp"

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

[EventTracing]
    # 可选调用栈过滤。dropwatch 会丢弃 stack 匹配已配置正则的事件。
    # 默认值: []
    IssuesList = []

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

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

4.2 噪声过滤

默认不启用任何调用栈噪声过滤。配置 EventTracing.IssuesList 后,huatuo-bamai 才会丢弃匹配事件。下表是可由运维人员配置的候选模式;启用前应结合本机内核和工作负载验证:

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

🌟 结尾

6 - 重传追踪

📖 概述

tcpshark --mode retransmit 通过内核跟踪点 tcp/tcp_retransmit_skbtcp/tcp_retransmit_synack 观测 TCP 重传相关活动。显式开启 TLP 后,还会观测 tcp_send_loss_probe kprobe。根据事件类型,每条事件可携带 IP 四元组、TCP 状态、拥塞控制状态、重传计数器、序列号信息,以及用于解析容器归属的 socket 元数据。

用户态分类器根据事件类型、sk_stateca_state 和乱序计数器生成连接阶段与原因标签。这些标签是用于运维分析的启发式分类,不是丢包根因的确定性证据。

过滤表达式由 internal/pcapfilter 在加载时编译并在内核中执行。过滤器只对携带 SKB 的 tcp_retransmit_skb 事件生效;SYN-ACK 和 TLP 事件会绕过 pcap 过滤器。


🎯 场景

1. TCP 网络质量与重传诊断

通过持续观测 RTO、快速重传、乱序倾向重传和 TLP 事件,识别连接建立、数据传输及连接关闭阶段的异常重传,辅助判断网络丢包、拥塞、乱序或对端不可达等问题。

2. Kubernetes 容器网络故障排查

结合容器 ID、网络命名空间和 socket cgroup 元数据定位发生重传的工作负载,并使用 --filter "tcp and port <service-port>" 聚焦特定服务流量,减少宿主机上其他连接的干扰。

3. 应用延迟与吞吐毛刺分析

将 TCP 重传事件与应用延迟、错误率和吞吐曲线对齐,分析 RTO 或连续重传是否与服务性能下降同时发生,辅助区分应用处理变慢与底层网络异常。

4. 与 dropwatch 关联定位丢包位置

在同一 huatuo-bamai 进程中同时运行 dropwatch 和 tcp_retransmit,通过 SKB 指针或连接四元组关联丢包与重传事件,辅助判断问题更可能发生在主机协议栈还是外部网络;关联结果属于启发式证据,仍需结合调用栈和网络指标确认。


🚀 使用

1. 运行 tcpshark

tcpshark --mode retransmit [flags]
参数 默认值 说明
--mode retransmit 必填 选择 TCP 重传追踪模式。
--enable-tlp--tlp 关闭 同时挂载 tcp_send_loss_probe 并输出 TLP 事件。
--bpf-path <path> 必填 tcp_retransmit.o eBPF 对象文件路径。
--filter <expr> (无) 仅用于 tcp_retransmit_skb 事件的 tcpdump 风格过滤器,见 §2。
--duration <n> 0 运行 N 秒后退出(0 表示持续运行直至 Ctrl-C)。
--max-events-per-second <n> 0 BPF 侧事件限速,0 表示不限速。
--output <json|text> text 输出格式;设置 --output-storage 时会被忽略。
--output-storage <path> (无) 通过 Unix socket 将事件发送给 huatuo-bamai。
--task-id <id> (无) toolstream 会话关联的任务 ID;必须与 --output-storage 一起使用。

显式同时指定 --output--output-storage 时,--output 会被忽略并打印警告。

1.1 常用命令

# 文本格式输出全部重传相关事件
sudo tcpshark --mode retransmit --bpf-path bpf/tcp_retransmit.o

# NDJSON 格式输出
sudo tcpshark --mode retransmit --bpf-path bpf/tcp_retransmit.o --output json

# 在 BPF 侧过滤指定目标主机和端口的常规重传 SKB
sudo tcpshark --mode retransmit --bpf-path bpf/tcp_retransmit.o --filter "dst host 10.0.0.1 and dst port 443"

# 包含 Tail Loss Probe 事件(默认关闭)
sudo tcpshark --mode retransmit --enable-tlp --bpf-path bpf/tcp_retransmit.o

# 最多输出 100 条事件/秒;超限时打印 rate limit hit 日志
sudo tcpshark --mode retransmit --bpf-path bpf/tcp_retransmit.o \
  --max-events-per-second 100

# 在用户态过滤全部格式化事件类型,只保留目标端口 443
sudo tcpshark --mode retransmit --bpf-path bpf/tcp_retransmit.o --output json \
  | jq -c 'select(.tcp_dport == 443)'

# 运行 60 秒,仅保留分类为 RTO 的事件
sudo tcpshark --mode retransmit --bpf-path bpf/tcp_retransmit.o --duration 60 --output json \
  | jq -c 'select(.tcp_reason == "RTO")'

# 将事件转发给正在运行的 huatuo-bamai 实例
sudo tcpshark --mode retransmit --bpf-path bpf/tcp_retransmit.o \
  --output-storage /var/run/huatuo-toolstream.sock

jq -c 将每条结果压缩成单行 JSON,便于保存为 NDJSON 或继续通过管道处理。

1.2 与 huatuo-bamai 集成

tcpshark 与 dropwatch 使用相同的 --output-storage 和 toolstream 流程。通用存储方式请参考 dropwatch 文档。TCP 重传追踪增加以下配置:

[EventTracing.TCPRetransmit]
    # 转发给 tcpshark --filter;仅过滤 tcp_retransmit_skb。
    # 默认值: ""
    Filter = ""

    # 设置为 true 时传入 tcpshark --enable-tlp;默认 false。
    EnableTLP = false

    # 传给 tcpshark --max-events-per-second;默认 100,0 表示不限速。
    MaxEventsPerSecond = 100

tcp_retransmit tracer 默认位于全局 BlackList 中。需要启用时,从名单中移除 tcp_retransmit 并重启 huatuo-bamai。丢包关联缓存仅在 tracer 运行期间启用,tracer 停止时会关闭并清空。启用后可通过 HTTP API 启停追踪:

curl -X PUT http://localhost:19704/tracers/tcp_retransmit/start
curl -X PUT http://localhost:19704/tracers/tcp_retransmit/stop

2. 过滤表达式

tcpshark 使用与 dropwatch 相同的 tcpdump 风格过滤表达式。完整语法、限制和更多示例请参考 dropwatch 文档

# 指定目标主机和端口
--filter "dst host 10.0.0.1 and dst port 443"

# 观察两个网段之间的双向流量
--filter "(src net 10.10.0.0/16 and dst net 10.20.0.0/16) or (src net 10.20.0.0/16 and dst net 10.10.0.0/16)"

--filter 只作用于 tcp_retransmit_skbtcp_retransmit_synack 和启用后的 tcp_send_loss_probe 不携带 SKB,因此不会应用该过滤器。


3. 事件数据结构

每条事件以 NDJSON 对象(types.TCPRetransmitTracing)表示。带 omitempty 标签的字段在值为空或零时不会输出。

字段 类型 说明
observed_timestamp string 用户态接收/格式化事件时生成的 UTC 时间(RFC3339Nano),不是内核 hook 时间。
comm string 当前内核执行上下文的进程名,不一定是 socket 所属进程。
pid uint64 当前执行上下文的 TGID,不一定是 socket 所属进程的 TGID。
container_id string huatuo-bamai 解析出的容器 ID,见 §3.2。
memory_cgroup_css_addr string 用于解析容器归属的 socket 内存 cgroup CSS 十六进制地址。
net_namespace_cookie uint64 用于解析容器归属的 socket 网络命名空间 cookie。
net_namespace_inum uint32 用于解析容器归属的 socket 网络命名空间 inum。
tcp_saddr string 源 IP 地址。
tcp_daddr string 目的 IP 地址。
tcp_sport uint16 源端口。
tcp_dport uint16 目的端口。
tcp_state string TCP socket 状态,如 ESTABLISHEDSYN_SENTNEW_SYN_RECV
phase string 分类结果:connectdataclose
tcp_reason string 分类结果:RTOfast_retransmitreorder_prone_fastTLPunknown
event_type string tcp_retransmit_skbtcp_retransmit_synacktcp_send_loss_probe
ca_state uint8 拥塞控制状态:0=Open、1=Disorder、2=CWR、3=Recovery、4=Loss。
icsk_retransmits uint8 当前重传计数器快照。
icsk_pending uint8 inet_connection_sock 中原始的待处理定时器状态,取值见下表。
reord_seen uint32 连接累计乱序计数器。
dsack_dups uint32 累计 DSACK 重复计数器。
tcp_seq uint32 SKB 事件使用 TCP_SKB_CB(skb)->seq;TLP 事件使用 snd_nxt;SYN-ACK 事件中为零。
tcp_ack_seq uint32 SKB 事件使用 tcp_sk(sk)->rcv_nxt;TLP 事件使用 snd_una;SYN-ACK 事件中为零。
tcp_end_seq uint32 SKB 事件使用 TCP_SKB_CB(skb)->end_seq;SYN-ACK 和 TLP 事件中省略。
tcp_flags string 渲染后的 TCP flag 集合,如 `SYN
skb_addr string 十六进制重传队列 SKB 指针;SYN-ACK 和 TLP 事件中不存在。
drop_location string huatuo-bamai 生成的丢包关联启发式结果,见 §5。
source string 事件来源。独立运行 tcpshark 时为 tools,由 huatuo-bamai 启动时为 events

icsk_pending 是 hook 时刻的定时器状态快照,不是重传原因的稳定枚举。TLP 分类以明确的 event_type=tcp_send_loss_probe 为准,不依赖 icsk_pending=5

内核状态 含义
0 None 当前没有待处理的发送定时器事件。
1 ICSK_TIME_RETRANS 重传超时定时器(RTO)。
2 ICSK_TIME_DACK 延迟 ACK;现代内核将该状态保存在 icsk_ack.pending 并使用独立的 delayed-ACK timer,因此通常不会出现在 icsk_pending 中。
3 ICSK_TIME_PROBE0 零窗口探测定时器。
4 版本相关 当前主线内核不再定义该值;旧内核曾将其用于 Early Retransmit,更早的内核曾用于 Keepalive。
5 ICSK_TIME_LOSS_PROBE Tail Loss Probe(TLP)定时器。
6 ICSK_TIME_REO_TIMEOUT 乱序超时定时器,主要用于 RACK 丢包判断。

3.1 文本输出格式

文本输出保留面向终端的可读布局,同时覆盖与 JSON 相同的事件变量。带 omitempty 的变量仅在非零或非空时显示,字符串值不添加 JSON 引号或转义。为兼容原文本格式,stateskbseqendackflagscaretrans 分别对应 JSON 中的 tcp_stateskb_addrtcp_seqtcp_end_seqtcp_ack_seqtcp_flagsca_stateicsk_retransmits

<timestamp> [<phase>/<tcp_reason>] <saddr>:<sport> > <daddr>:<dport> state=<STATE> event_type=<TYPE> [SYNACK] [skb=<ADDR>] seq=<N> [end=<N>] ack=<N> [flags=<FLAGS>] pid=<N> comm=<COMM> ca=<N> retrans=<N> icsk_pending=<N> [reord_seen=<N>] [dsack_dups=<N>] [container_id=<ID>] [memory_cgroup_css_addr=<ADDR>] [net_namespace_cookie=<N>] [net_namespace_inum=<N>] [drop_location=<LOCATION>] [source=<SOURCE>]

示例:

2026-07-23T02:14:40.304775546Z [data/RTO] 127.0.0.1:19996 > 127.0.0.1:42128 state=ESTABLISHED event_type=tcp_retransmit_skb skb=0xffff931c14fdf800 seq=3154974646 end=3154991030 ack=948393597 flags=ACK|PSH pid=1420 comm=kube-apiserver ca=4 retrans=4 icsk_pending=0 net_namespace_inum=4026531992

示例中的 pidcomm 表示 hook 运行时的执行上下文;工作负载归属应使用 container_id 和 socket 元数据判断。

3.2 容器 ID 解析

tcpshark 自身无法访问 Pod 管理器。独立输出时通常没有 container_id,但可用时仍会输出 socket memcg 和网络命名空间元数据。通过 huatuo-bamai 运行时,空的 container_idmemory_cgroup_css_addrnet_namespace_cookienet_namespace_inum 的顺序解析。

全部解析未命中时,事件仍会存储,但 container_id 保持为空。pidcomm 描述的是 hook 执行上下文,不能作为判断 socket 归属的回退依据。


4. 内核事件与分类

4.1 内核挂载点

挂载点 内核位置 事件含义 可用数据
tracepoint tcp/tcp_retransmit_skb __tcp_retransmit_skb() 对一个重传队列 SKB 发起了重传尝试;tcpshark 事件不保留内核发送结果。该 SKB 是 headerless 的,因此序列号来自 TCP_SKB_CB(skb),ACK 来自 tcp_sk(sk)->rcv_nxt SKB 指针、TCP seq/end_seq/ack/flags、socket 状态、CA 状态、定时器和乱序计数器。
tracepoint tcp/tcp_retransmit_synack tcp_rtx_synack() tcp_rtx_synack() 成功提交了一次被动建连 SYN-ACK 重传。 request socket 地址和端口;没有重传 SKB 指针及 TCP seq/ack。
kprobe tcp_send_loss_probe tcp_send_loss_probe() 正在准备 Tail Loss Probe;仅在指定 --enable-tlp 时采集。 socket 元数据及 snd_nxt/snd_una;没有 SKB 指针或渲染后的 TCP flags。

BPF 程序通过 BPF_CORE_READ 等辅助方法执行 CO-RE 字段读取,因此在支持的内核布局上无需为每个内核版本重新编译 C 源码。

4.2 连接阶段

常规 SKB 事件的阶段由 sk_state 决定;SYN-ACK 事件在用户态使用固定阶段。

下面以 TCP 三次握手说明 connect 阶段及对应的重传观测点:

sequenceDiagram
    participant C as 客户端
    participant S as 服务端
    Note over C,S: 初始状态:CLOSED / LISTEN
    C->>S: ① SYN
    Note left of C: SYN_SENT(2)<br/>phase=connect
    opt SYN 未被确认
        C-->>S: SYN 重传<br/>tcp_retransmit_skb
    end
    Note right of S: SYN_RECV(3) 或 NEW_SYN_RECV(12)<br/>phase=connect
    S->>C: ② SYN + ACK
    opt 最终 ACK 未到达
        S-->>C: SYN-ACK 重传<br/>tcp_retransmit_synack
    end
    C->>S: ③ ACK
    Note over C,S: ESTABLISHED(1)<br/>后续常规数据 SKB 事件 phase=data

图中的三条实线表示首次握手报文,不会产生 tcpshark 事件;只有可选框中的重传路径会被观测。主动端 SYN 重传通过 tcp_retransmit_skb 上报,被动端 SYN-ACK 重传通过 tcp_retransmit_synack 上报,两者都归类为 connect

完整阶段映射如下:

阶段 来源状态或事件 说明
connect SYN_SENT(2)、SYN_RECV(3)、NEW_SYN_RECV(12) 或 tcp_retransmit_synack 连接建立。
data ESTABLISHED(1) 或无法识别/默认状态 数据传输或默认分类。
close FIN_WAIT1(4)、FIN_WAIT2(5)、TIME_WAIT(6)、CLOSE_WAIT(8)、LAST_ACK(9)、CLOSING(11) 连接关闭。

4.3 原因分类

事件或条件 原因 含义
tcp_retransmit_synack RTO SYN-ACK 重试定时器路径的固定用户态标签。
tcp_send_loss_probe TLP 可选 Tail Loss Probe hook 的固定用户态标签。
tcp_retransmit_skbca_state=4(Loss) RTO socket 当前处于 TCP_CA_Loss。
tcp_retransmit_skbca_state=3(Recovery) fast_retransmitreorder_prone_fast Recovery 路径重传;存在累计乱序历史时使用 reorder-prone 标签。
tcp_retransmit_skbca_state=0..2,connect/close 阶段 RTO 当前分类器使用的阶段回退结果。
tcp_retransmit_skbca_state=0..2,data 阶段 unknown 当前快照不足以生成其他标签。

分类器只观察 hook 时刻的 socket 状态,无法重建完整的 ACK/丢包历史。因此应把 tcp_reason 视为聚合标签,而不是经过验证的根因。

4.4 乱序启发式判断

reord_seendsack_dups 任一累计计数器非零时,分类器会选择乱序倾向标签。连接一旦出现过乱序历史,后续 Recovery 状态的 SKB 事件就可能标记为 reorder_prone_fast。这是连接级启发式判断,不能证明当前重传由乱序触发。

4.5 运维解读

没有任何事件类型可以无条件丢弃。相比只按 event_typetcp_reason 过滤,更推荐按速率、比例和服务影响设置阈值。通用的 huatuo-bamai 噪声过滤机制请参考 dropwatch 文档

模式 通常优先级 建议
tcp_reason=RTO 排查持续增长或与服务异常相关的 RTO;它通常比 Recovery 路径重传带来更大延迟影响。
tcp_reason=fast_retransmit 结合丢包、拥塞及 SACK/RACK 行为分析。
tcp_reason=reorder_prone_fast 视上下文而定 连接存在乱序历史,但不能证明当前事件是伪重传;应检查延迟和计数器增长。
tcp_reason=TLP 视上下文而定 这是可选信号;用于告警前应确认已主动开启 TLP 采集。
event_type=tcp_retransmit_synack 单次通常较低 重复出现可能意味着握手可达性、主机出口、防火墙、客户端或网络问题。

配置告警时,应按服务或连接聚合并结合流量规模判断。繁忙主机上的少量绝对计数可能无害,而低流量关键服务上的突发事件可能具有较大影响。


5. 与 dropwatch 关联

dropwatch 和 tcpshark 向同一个 huatuo-bamai 进程发送事件时,dropwatch 事件会从到达用户态的时刻起在缓存中保留两秒。tcpshark 事件会立即按与方向无关的连接 key 查询此前已收到且尚未过期的 drop 事件。当前实现不会等待之后才到达的 drop 事件,也不会在事件存储后更新关联结果。

5.1 关联结果

内部结果 匹配条件 drop_location 安全解读方式
TCPRetransmitDropDirect 在同一连接缓存桶内,非空的 dropwatch.packet_skb_addrtcpshark.skb_addr 相等。 host_software 有较强证据表明观测到的主机丢包与重传指向同一 SKB 指针。
TCPRetransmitDrop4Tuple 缓存中的 TCP drop 与重传事件的地址和端口正向或反向匹配。 host_software 重传附近在同一连接上观测到了主机丢包,不能证明因果关系。
TCPRetransmitNoDrop 没有匹配且仍有效的缓存项。 network_or_host_hardware 只是当前实现的回退标签,不能证明发生了网络或硬件丢包。

dropwatch 未启用、过滤器未覆盖该连接、事件被抑制或丢失、投递乱序、相关 drop 超出缓存保留窗口时,同样会得到 network_or_host_hardware。四元组匹配也可能把繁忙连接上的无关报文关联到一起。缓存 key 不包含网络命名空间或容器标识,因此不同网络命名空间中地址和端口完全相同的连接也可能发生串联。

5.2 使用条件与排查方式

观测结果 检查项
host_software 且直接匹配 检查对应 dropwatch 事件的调用栈、设备和 drop 元数据。
host_software 且仅连接匹配 在判断因果前核对方向、TCP seq/ack 上下文和时间关系。
network_or_host_hardware 先确认 dropwatch 与 tcpshark 位于同一 huatuo-bamai 进程且过滤器覆盖该连接,再检查网卡和网络计数器。
drop_location 不存在 独立输出中的预期行为;关联由 huatuo-bamai 而不是 CLI 执行。

要让“未观测到主机丢包”具备较可靠的负向证据,dropwatch 必须处于运行状态,并且过滤范围至少覆盖待分析的 tcpshark 流量。当前 schema 没有单独的 unknowndropwatch_not_observed 值,因此消费者应把 network_or_host_hardware 视为排查提示,而不是事实。


🌟 结尾