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_timestamp" : "2026-05-07T00:11:49.753166222Z",
"tracer_name" : "net_rx_latency",
"observed_timestamp" : "2026-05-07T00:11:49.753166222Z",
"tracer_type" : "event",
"tracer_data" : {
"comm" : "<nil>",
"pid" : 0,
"latency_stage" : "RX_STAGE_NETIF",
"latency_ms" : 1776078133565,
"tcp_saddr" : "127.0.0.1",
"tcp_daddr" : "127.0.0.1",
"tcp_sport" : 37736,
"tcp_dport" : 9200,
"tcp_seq" : 1080592402,
"tcp_ack_seq" : 2465063876,
"packet_len_bytes" : 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_timestamp" : "2026-05-07T02:51:37.696263325Z",
"tracer_name" : "net_rx_latency",
"observed_timestamp" : "2026-05-07T02:51:37.696263325Z",
"tracer_type" : "event",
"tracer_data" : {
"comm" : "<nil>",
"pid" : 0,
"latency_stage" : "RX_STAGE_NETIF",
"latency_ms" : 1776078133565,
"tcp_saddr" : "127.0.0.1",
"tcp_daddr" : "127.0.0.1",
"tcp_sport" : 2379,
"tcp_dport" : 36706,
"tcp_seq" : 950542706,
"tcp_ack_seq" : 1960972383,
"packet_len_bytes" : 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
过滤条件语义
内部存储查询 API 使用 AND 组合过滤条件。SQLite 和 Elasticsearch 对非 null 标量值采用以下约定:
| 操作符 | 含义 |
|---|---|
OpEq、OpNe |
相等/不等;缺失和 null 值均不匹配 |
OpGt、OpGte、OpLt、OpLte |
严格/包含边界的范围比较 |
OpIn |
属于非空切片或数组中的标量值 |
OpExists、OpNotExists |
存在/不存在非 null 的可查询值;不设置 Value |
空字符串是字面值:OpEq("") 匹配空字符串;OpNe("") 要求字符串存在、非 null
且非空。查询缺失字段应使用 OpNotExists,OpEq("") 不再表示字段缺失。
主机 profiling 查询改用 container_hostname 的 OpNotExists,保留原有筛选行为。
比较值支持 Go 内建字符串、布尔、整数及有限浮点数,范围比较不接受布尔值。
time.Time 和 timeutil.Timestamp 统一转为保留九位小数的 UTC 字符串,
OpIn 元素也采用相同规则。nil、指针、对象、嵌套集合及非有限数值返回
driver.ErrInvalidQuery。整数范围边界保留原始 JSON 精度,实际支持的数值范围
仍受后端和字段映射约束。
Elasticsearch 调用方必须指定实际用于精确比较的索引字段:keyword 映射使用
原字段;text 映射使用明确配置的 keyword 子字段,例如 hostname.keyword。
后端不会猜测子字段,也不会将 text 与 keyword 查询进行 OR 组合。
OpNe 对同一字段执行 exists 和 must_not term。全文匹配和数组整体相等
不属于这里的标量比较约定。
Elasticsearch 的存在性判断针对索引值,而不是 _source 中是否包含键。
ignore_above、null_value 和 normalizer 等映射设置可能改变存在性或相等行为。
要求与 SQLite 一致时,应使用保留目标标量语义的映射。日期字段不能与空字符串
比较;表达“已有日期值”时应使用存在性操作符。
数据写入流程
采集层调用 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 成功 | 主分片写入成功;副本失败时记录包含索引、文档 ID 和失败分片数的警告日志 | — |
为什么这样设计:429/5xx 与传输错误是远端短暂不可用的信号,重试有效;4xx(除 429)与 per-item 错误是客户端语义问题(数据格式、权限),重试只会放大错误,应交给开发与运维侧排查日志后修正。
分批读取
Query(ctx, query, consume) 按顺序同步调用回调,每批至少一条记录。Limit 是跳过
Offset 条匹配记录后交给回调的总条数上限。storage.Store.Query 统一校验 Limit
必须大于零、Offset 和 BatchSize 非负;后端只消费已校验的参数。
BatchSize 控制每批条数,零使用统一默认值 100,每批不超过剩余总限额。
需要数据的调用方必须明确传入正数总上限。Count 和 Values 只使用查询中的
Filters,忽略 Sorts、Limit、Offset 和 BatchSize,包括零值及负数分页参数。
Count 统计全部匹配记录;Values 使用独立的 size 参数,由 storage.Store.Values
在调用后端前校验其非负约束,后端不再重复校验这两个操作的分页参数或 size。
本地文件存储仍不支持查询。
SQLite 使用一条带 LIMIT/OFFSET 的 SELECT 逐行读取。回调期间查询占用唯一数据库
连接,不能同步再次访问同一后端。任务恢复按 ID 升序每批读取 1000 个 Job,读取关闭后
再启动监督任务,通过 ID 游标继续查询,避免任务状态变化导致偏移分页漏读。
Elasticsearch 7/8 和 OpenSearch 统一使用 Scroll 快照,支持跨越结果窗口的读取和偏移。
深偏移仍需读取并跳过前面的记录,不同 API 请求不共享快照。
各批次独立持有数据。回调、解码、后端错误或取消都会立即终止查询,已执行回调的效果 不会回滚。Scroll 使用独立的有界上下文清理,即使原查询已取消也会尝试释放资源。 查询不自动重试,因为丢失响应时 Scroll 游标可能已经前进;写入重试策略保持不变。
API 仅在未传 limit 时填默认值:Job 列表默认 100、最大 1000;原始 Profile 每页默认
20、最大 100,并保留 64 MiB 响应限制。显式零值、负数或超过上限的 limit 返回参数
错误。每页额外读取一条判断 has_more。
火焰图边读取边合并 Profile,每次查询总上限为 100000 个窗口。额外读取一条检测超限, 超限时返回错误,提示缩小时间范围或过滤条件;读取或合并失败都不返回部分火焰图。 输入批次可随处理进度释放,但合并结果的内存仍随不同调用栈数量增长。
响应错误
HTTP 成功不代表结果完整。Query 和 Values 在搜索超时或分片失败时返回错误,
Query 不交付失败批次,但此前已消费的批次不会回滚;Values 不返回部分结果。
Count 在分片失败时返回零和错误。搜索超时优先于分片失败返回。
分片错误包含失败分片总数,以及首条失败的类型、原因及嵌套原因(如果存在)。
收到成功 HTTP 响应后,DeleteByQuery 按超时、执行失败的顺序检查,
遇到首个错误即返回,同时保留响应中的已删除数量。
索引不存在时返回零且不报错,其他 HTTP 错误返回零和错误。
已完成的删除不会回滚。
直接写入和删除在副本失败时保留主分片操作成功的语义,
通过警告日志记录可用的失败详情。Bulk 写入也会报告副本失败,不重试已经成功的主分片写入。
HTTP 错误包含响应状态码和可用的 Elasticsearch 错误详情。无法识别或过大的错误响应体 以原文报告,限制为 4 KiB,并在截断时追加标记。
数据丢失场景
下列三种情况下,调用方调用 Save 时返回 nil,但事件最终未进入索引:
- 进程异常退出:
SIGKILL或宿主机断电时,BulkIndexer 内存缓冲尚未刷新的部分直接丢失(仅本地目录保留副本)。- 缓解:SIGTERM/SIGINT 走优雅退出路径,shutdown 时调用
Close强制 flush,最长等待 10 秒。
- 缓解:SIGTERM/SIGINT 走优雅退出路径,shutdown 时调用
- 整批被永久拒绝:4xx(非 429)类错误一次性丢弃整批所有记录。常见诱因:索引被禁用、密码失效、单条文档超过集群
http.max_content_length。- 排查:
OnError错误日志包含 ES 返回的type与reason。
- 排查:
- 单条永久失败: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) | 单次失败立即丢弃,无重试 | 客户端层面自动重试,吸收瞬态故障 |
| 采集层对存储后端解耦 | 远端慢会回压采集,影响内核事件采集时延 | 异步缓冲将采集与远端写入解耦,采集路径不被网络阻塞 |
🌟 结尾
内核观测时间
事件文档可包含顶层 kernel_observed_timestamp,其值为 UTC 日期时间。
observed_timestamp 是用户态观测时刻,uploaded_timestamp 是存储写入时刻。
三者不互相补填。旧文档及没有内核时间的事件省略新增字段;历史数据不自动迁移。
原始 kernel_observed_ns 仅用于进程内事件关联,不写入新文档。
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_timestamp - UID:
huatuo-bamai-es
Provisioning 配置示例见 build/docker/grafana/datasources/。
导入仪表盘
Docker Compose 会自动加载仓库内置仪表盘。外部 Grafana 按以下步骤导入:
- 打开 Grafana 的 Dashboards -> Import
- 上传或粘贴
build/docker/grafana/dashboards/*.json - 点击 Load
- 选择
huatuo-bamai-prom和huatuo-bamai-es数据源 - 点击 Import
也可以从 HUATUO 控制台导出仪表盘:
- 访问
http://console.huatuo.tech/dashboards - 登录并选择仪表盘
- 点击 Export -> Export as JSON
- 勾选 “Export the dashboard to use in another instance”
- 复制 JSON 并导入目标 Grafana
控制台登录凭证由部署管理员提供,不应保存在公开文档中。
3 - 内核事件订阅
📖 概述
/v1/events/watch 是华佗(HUATUO)提供的实时内核事件订阅接口。客户端通过一次 HTTP POST 长连接即可持续接收节点上发生的内核异常事件。事件以 CloudEvents 1.0 规范封装,通过 Server-Sent Events(SSE) 协议推送。
🎯 应用场景
内核事件订阅将操作系统层的异常信号直接暴露给上层系统,消除了传统轮询带来的延迟与开销。以下是典型的集成场景。
故障自愈系统
内核事件是自愈决策的第一手信号源。订阅 events/watch 后,自愈控制器可在事件发生的瞬间触发处置动作,而不必等待监控系统的告警流转:
- OOM 自愈:收到
memory_oom_kill事件后,立即对触发容器执行扩容、重启或流量摘除,将服务中断时间从分钟级压缩到秒级。 - Hung Task 自愈:收到
hungtask事件后,自动隔离节点并驱逐 Pod,防止级联阻塞蔓延至整个集群。 - 网络故障自愈:收到
netdev_txqueue_timeout或netdev_bonding_lacp事件后,触发网卡重置或流量切换,实现分钟级网络链路自愈。 - I/O 风暴自愈:收到
iotracing事件后,结合 cgroup blkio 限速策略动态降低问题容器的磁盘 I/O 配额,保护同节点其他服务。
可观测性平台
将华佗内核事件接入可观测性平台,补齐应用指标和日志之外的内核视角:
- 事件时间线关联:将
softlockup、memory_oom_kill等内核事件叠加到 Grafana 时间线上,与应用错误率、延迟曲线精确对齐,快速定位根因。 - 异常驱动告警:以内核事件替代固定阈值告警,降低误报率。例如收到
ras硬件错误事件时直接触发高优告警,而不依赖 CPU 错误率超阈值。 - 容量与稳定性分析:长期订阅
memburst、dload等 AutoTracing 事件,建立节点稳定性基线,为容量规划提供内核级依据。 - 多维下钻:事件中携带容器 ID、命名空间、地域等上下文,告警链接可直接下钻到对应的 Pod、Node、Region 视图。
安全审计与合规
- 异常行为检测:
memory_oom_kill、hungtask、softlockup等事件若在非业务高峰期集中出现,可能指示资源滥用或恶意负载,触发安全审查流程。 - 事件留存与追溯:将 CloudEvents 事件流写入消息队列(Kafka、Pulsar)或对象存储,满足等保合规对系统异常事件留存的要求。
混沌工程与压测验证
- 故障注入验证:混沌工程平台注入网络延迟、内存压力等故障后,实时订阅
net_rx_latency、memburst事件验证故障是否生效,取代人工观察。 - 压测基线建立:压测期间持续订阅全量事件,记录首个内核异常事件的出现时机,精确标定系统承压极限。
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/memory_oom_kill",
"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": "memory_oom_kill",
"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 | 事件生产者在用户态观测事件的 UTC 时间 |
kernel_observed_timestamp |
string | 可选;内核观测事件的 UTC 时间 |
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 |
说明 |
|---|---|
memory_oom_kill |
内存不足(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 |
网络接收延迟异常事件 |
sched_tick |
调度 tick 间隔异常追踪事件 |
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 请求头
Authorization: Bearer <node-token>
Content-Type: application/json
3.3 请求体结构
{
"filters": {
"tracer_name": "<regex>",
"hostname": "<regex>",
"container_hostname": "<regex>",
"container_host_namespace": "<regex>",
"container_qos": "<regex>",
"region": "<regex>"
}
}
filters 字段说明:
| 字段 | 类型 | 是否必填 | 说明 |
|---|---|---|---|
tracer_name |
string | 否 | 按采集器名称过滤,支持正则表达式 |
hostname |
string | 否 | 按节点主机名过滤,支持正则表达式 |
container_hostname |
string | 否 | 按容器主机名过滤,支持正则表达式 |
container_host_namespace |
string | 否 | 按容器命名空间过滤,支持正则表达式 |
container_qos |
string | 否 | 按容器 QoS 过滤,支持正则表达式 |
region |
string | 否 | 按地域过滤,支持正则表达式 |
- 所有过滤字段均为可选;省略或留空表示匹配所有值。
- 多个字段同时指定时,所有条件须同时满足(AND 语义)。
- 过滤器在服务端生效,仅匹配的事件才会推送到客户端。
3.4 响应格式(SSE 流)
连接建立后,服务端以 SSE 格式持续推送事件:
data: {"specversion":"1.0","id":"...","source":"/huatuo/node-1/memory_oom_kill",...}\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 "Authorization: Bearer <node-token>" \
-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 "Authorization: Bearer <node-token>" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-H "Cache-Control: no-cache" \
-H "Connection: keep-alive" \
-d '{"filters": {"tracer_name": "^memory_oom_kill$"}}'
5.3 订阅指定节点的网络类事件
curl -s -N -X POST http://<node-ip>:19704/v1/events/watch \
-H "Authorization: Bearer <node-token>" \
-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 "Authorization: Bearer <node-token>" \
-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 客户端示例
POST /v1/events/watch 已纳入 Node OpenAPI 契约。以下示例直接使用生成的
请求类型、事件类型和客户端。
package main
import (
"bufio"
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"os"
"strings"
"time"
nodeapi "github.com/ccfos/huatuo/apis/v1/node"
)
func watchEvents(
ctx context.Context,
baseURL string,
token string,
filters nodeapi.WatchEventFilters,
) error {
client, err := nodeapi.NewClient(
baseURL,
nodeapi.WithHTTPClient(&http.Client{}),
nodeapi.WithRequestEditorFn(func(_ context.Context, request *http.Request) error {
request.Header.Set("Authorization", "Bearer "+token)
request.Header.Set("Accept", "text/event-stream")
return nil
}),
)
if err != nil {
return fmt.Errorf("create Node API client: %w", err)
}
resp, err := client.WatchEvents(ctx, nodeapi.WatchEventsJSONRequestBody{
Filters: &filters,
})
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 nodeapi.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.Format(time.RFC3339Nano),
event.Source,
event.ID.String(),
)
fmt.Printf(" data: %+v\n", event.Data)
}
return scanner.Err()
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
tracerName := "memory_oom_kill|hungtask|softlockup"
err := watchEvents(ctx, "http://192.168.1.10:19704", "node-token", nodeapi.WatchEventFilters{
TracerName: &tracerName,
})
if err != nil {
slog.Error("watch events", "err", err)
os.Exit(1)
}
}
6.1 流式客户端选择
应使用生成的 Client.WatchEvents 方法直接获取 http.Response。不要对该接口使用
ClientWithResponses.WatchEventsWithResponse:后者会读取响应体直到 EOF,而 SSE
连接通常会持续到调用方取消 context。
6.2 重连机制建议
生产环境中,网络抖动或服务重启会导致连接断开,建议加入指数退避重连逻辑:
func watchWithRetry(
ctx context.Context,
baseURL string,
token string,
filters nodeapi.WatchEventFilters,
) {
backoff := time.Second
for {
if err := watchEvents(ctx, baseURL, token, 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: 内核事件触发(memory_oom_kill / 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 - 性能剖析
火焰图格式
在性能剖析领域,collapsed 和 flamegraph 是最常用的两种火焰图格式,分别对应"原始数据"与"可视化视图"两个层次。
Collapsed 格式
标准语法与格式
collapsed 格式(又称 folded stacks)由 Brendan Gregg 定义,是火焰图的原始文本输入格式。每行代表一条唯一的调用栈及其采样计数。
基本规则:
frame1;frame2;frame3;...;frameN COUNT
| 组成部分 | 说明 |
|---|---|
frame1 |
栈底(入口/根帧),如 main、start_thread |
; |
帧分隔符(分号) |
frameN |
栈顶(当前执行帧,即采样命中点) |
COUNT |
采样次数(整数),与栈帧之间用空格分隔 |
格式要点:
- 每行一条独立调用栈,相同栈路径的样本合并计数
- 帧的排列顺序:从左到右为 根→叶(调用链方向)
- 空行及
#开头的行通常被视为注释,解析时忽略 - COUNT 的语义取决于分析模式:CPU 采样时为采样次数,内存分配时为分配字节数,锁分析时为竞争时间(毫秒)
帧名称规范化
collapsed/folded 使用 ; 分隔栈帧,且没有统一的转义规则。为避免符号名称
被错误拆分,HUATUO 输出该格式时进行以下处理:
- 将帧名称中的
;替换为: - 将回车和换行替换为空格,确保每条调用栈只占一行
- 仅修改 collapsed/folded 输出;内部符号及结构化格式保留原始名称
例如,generic::<[u8; 7]> 输出为 generic::<[u8: 7]>。
该处理有损:只在 ; 和 : 上不同的符号会合并。需要保留完整符号名称时,
应使用 pprof 等结构化格式。
扩展规范:
部分剖析工具(如 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.pl、inferno 等可视化工具的标准输入格式 |
| 差分分析 | 对比两次 collapsed 文件,生成红蓝差分火焰图,定位性能回归 |
| 程序化处理 | 纯文本格式,便于用 awk、sed、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_rcv、ip_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 != 0、ip[8]、tcp[0:4] |
字节偏移表达式(proto[offset:size])未实现 |
ip proto 6、ip6 proto 17 |
不支持数字协议号,请改用协议名(如 ip proto tcp) |
ether proto 0x0800 |
不支持十六进制 EtherType,请改用名字(如 ether proto ip) |
sctp |
关键字未识别 |
portrange 80-90、tcp portrange 1-100 |
不支持端口范围 |
less N、greater N |
不支持按报文长度过滤 |
ip broadcast、ether broadcast |
不支持广播匹配 |
vlan、mpls、pppoes |
不支持隧道/封装关键字 |
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)。若只关心特定传输层或主机,仍建议用更精确的tcp、udp、host或ip 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 无法获取报文。
devlink 硬件丢包检测
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/net_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 并上报时才可见;exception 和 control 类型的 trap 不会上报为丢包事件。
--filter、--device、--device-excluded 和 --max-events-per-second 同时作用于软件与硬件事件。文本输出将硬件原因表示为 reason=<group>/<trap> drop_source=hardware;JSON 输出使用独立的 drop_reason_group、drop_reason 和 drop_source 字段。
tcpshark --with-dropwatch 复用相同的来源分类和 reason 解析,也自动启用可用的硬件采集;成功关联时保留相同的 drop_source、drop_reason 和 drop_reason_group。参见 重传关联说明。
常用命令
# 文本格式输出,监控所有设备的 TCP 丢包
sudo dropwatch --bpf-path bpf/net_dropwatch.o --filter "tcp"
# 只监控 eth0 上的丢包
sudo dropwatch --bpf-path bpf/net_dropwatch.o --device eth0 --output json
# 排除 loopback
sudo dropwatch --bpf-path bpf/net_dropwatch.o --device-excluded lo --output json
# 设备过滤与协议过滤组合
sudo dropwatch --bpf-path bpf/net_dropwatch.o --device eth0 --filter "tcp and port 443" --output json
# 抓取 60 秒后退出
sudo dropwatch --bpf-path bpf/net_dropwatch.o --filter "tcp and port 443" --duration 60 --output json
# 将事件转发给正在运行的 huatuo-bamai 实例
sudo dropwatch --bpf-path bpf/net_dropwatch.o --filter "tcp" --output-storage /var/run/huatuo-toolstream.sock
# 采集 10 秒 JSON 输出,并排除调用栈包含 ip_finish_output 的事件
sudo dropwatch --output json --duration 10 --bpf-path bpf/net_dropwatch.o | jq -c 'select(.stack | test("ip_finish_output") | not)'
# 采集 10 秒 JSON 输出,只打印除 stack 之外的字段
sudo dropwatch --output json --duration 10 --bpf-path bpf/net_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 时间 |
kernel_observed_timestamp |
string | 内核观测事件的 UTC 时间(RFC3339Nano),由原始单调时钟转换。 |
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_bytes |
uint32 | 内核 skb->len 快照;表示 SKB 逻辑长度,可能与线上帧长度不同 |
layers |
object | 分层协议解析结果,缺失的层会省略 |
stack |
string | 内核调用栈(换行分隔) |
硬件事件的 stack 表示驱动调用 devlink trap 上报接口时的内核调用栈,不代表 ASIC 内部的实际丢弃位置。定位硬件原因时应以 drop_reason_group、drop_reason、设备信息和驱动文档为主。
layers 使用固定字段表达协议栈,不再依赖单独的协议枚举:
| 字段 | 说明 |
|---|---|
layers.label |
协议组合标签,如 IPv4/TCP、IPv6/UDP、ARP、unknown |
layers.ether |
存在真实 Ethernet header 时输出二层字段:saddr、daddr、type、len;仅 IEEE 802.3 framing 的 len 非零 |
layers.ipv4 |
IPv4 字段:version、ihl、tos、len、id、flags、frag_offset、ttl、protocol、checksum、saddr、daddr |
layers.ipv6 |
IPv6 字段:version、traffic_class、flow_label、len、next_header、hop_limit、saddr、daddr |
layers.tcp |
TCP 字段:sport、dport、seq、ack_seq、data_offset、window、checksum、urgent、sk_state |
layers.udp |
UDP 字段:sport、dport、len、checksum |
layers.icmp |
ICMP/ICMPv6 字段:type、code、checksum、id、seq;ICMPv6 Echo 请求/应答包含标识符和序号,其他 ICMPv6 消息省略这两个字段 |
layers.arp |
ARP 字段:addr_type、protocol、hw_address_size、prot_address_size、operation、sender_mac、sender_ip、target_mac、target_ip |
ICMPv6 Echo Request/Reply(类型 128/129)仅在捕获完整四字节 Echo 字段时
填充 id、seq。Echo 字段截断时保留通用 ICMPv6 字段,ID/Seq 为零;
通用头也被截断时仍保留已解码的 IPv6 层,layers.icmp 不存在。
其他 ICMPv6 消息类型不填充 Echo 字段。
JSON 分别省略值为零的 id、seq;文本仅在两者都为零时省略它们。
因此,输出字段缺失无法区分未包含或截断的 Echo 字段与合法的零值。
4. 与 huatuo-bamai 集成
huatuo-bamai 以子进程形式启动 dropwatch,并通过 --output-storage 将事件发送到内置处理流程,并最终存储到 Elasticsearch。典型参数如下:
dropwatch \
--bpf-path <CoreBpfDir>/net_dropwatch.o \
--output-storage /var/run/huatuo-toolstream.sock \
--filter "tcp"
4.1 配置项参考(huatuo-bamai.conf)
[EventTracing]
# 可选调用栈过滤。dropwatch 会丢弃 stack 匹配已配置正则的事件。
# 默认值: []
IssuesList = []
[EventTracing.Dropwatch]
# standalone dropwatch 使用的 tcpdump filter。
# 默认值: "tcp"
Filter = "tcp"
# 转发给 dropwatch --max-events-per-second。
# 默认值: 100
MaxEventsPerSecond = 100
[EventTracing.TCPRetransmit]
# 使用 tcpshark 私有的 embedded dropwatch source。
EnableDropwatch = false
standalone dropwatch 始终输出 raw DropWatchTracing。TCP 重传 local 关联会加载另一份 net_dropwatch.o,两个输入统一使用 EventTracing.TCPRetransmit.Filter,并且只输出定型后的 TCPRetransmitTracing 结果。两种模式可以并行;embedded drop 不会重复保存成 raw event。
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_skb 和 tcp/tcp_retransmit_synack 观测 TCP 重传相关活动。显式开启 TLP 后,还会观测 tcp_send_loss_probe kprobe。根据事件类型,每条事件可携带 IP 四元组、TCP 状态、拥塞控制状态、重传计数器、序列号信息,以及用于解析容器归属的 socket 元数据。
用户态分类器根据事件类型、sk_state、ca_state 和乱序计数器生成连接阶段与原因标签。这些标签是用于运维分析的启发式分类,不是丢包根因的确定性证据。
过滤表达式由 internal/pcapfilter 在加载时编译并在内核中执行。无论是否开启 local 关联,SKB、SYN-ACK 和 TLP 三个 hook 都对同一种合成 L3 TCP 报文执行过滤。支持协议、地址、网段和端口条件;不提供以太网地址、payload、真实包长、IP/TCP options 或原始 byte-offset 语义。ether proto ip 等安全的 ethertype 判断会转换为 L3 判断。一对 IPv4-mapped IPv6 socket 地址按 IPv4 执行过滤;原始 perf record 仍保留 AF_INET6,用户态在匹配前规范化地址。local 关联模式还会把完全相同的表达式用于 embedded dropwatch。需要保留反向 ACK 或 SYN-ACK 证据时,应使用方向对称的表达式。
🎯 场景
1. TCP 网络质量与重传诊断
通过持续观测 RTO、快速重传、乱序倾向重传和 TLP 事件,识别连接建立、数据传输及连接关闭阶段的异常重传,辅助判断网络丢包、拥塞、乱序或对端不可达等问题。
2. Kubernetes 容器网络故障排查
结合容器 ID、网络命名空间和 socket cgroup 元数据定位发生重传的工作负载,并使用 --filter "tcp and port <service-port>" 聚焦特定服务流量,减少宿主机上其他连接的干扰。
3. 应用延迟与吞吐毛刺分析
将 TCP 重传事件与应用延迟、错误率和吞吐曲线对齐,分析 RTO 或连续重传是否与服务性能下降同时发生,辅助区分应用处理变慢与底层网络异常。
4. 与 dropwatch 关联定位丢包位置
使用 tcpshark local 模式在同一进程内关联重传与丢包。匹配会检查 network namespace、四元组方向、TCP sequence 或 ACK 证据及内核单调时间顺序。严格匹配表示找到了满足关联条件的丢包证据,软件或硬件来源以记录为准。no-match 保持 unknown,因为 source ready 不能证明更早的因果历史已经被观测。
🚀 使用
1. 运行 tcpshark
tcpshark --mode retransmit [flags]
| 参数 | 默认值 | 说明 |
|---|---|---|
--mode retransmit |
必填 | 选择 TCP 重传追踪模式。 |
--enable-tlp、--tlp |
关闭 | 同时挂载 tcp_send_loss_probe 并输出 TLP 事件。 |
--bpf-path <path> |
非关联模式必填 | 单个 tcp_retransmit.o 文件路径。 |
--bpf-path-dir <dir> |
关联模式必填 | 同时包含 tcp_retransmit.o 和 net_dropwatch.o 的目录。 |
--with-dropwatch |
关闭 | 加载 embedded dropwatch 并与重传关联。 |
--filter <expr> |
(无) | 三个重传 hook 共用的 L3 兼容 tcpdump 风格过滤器;local 模式下也与 embedded dropwatch 共用,见 §2。 |
--device <names> |
(无) | 与 dropwatch 相同的网卡白名单,逗号分隔;只过滤 embedded dropwatch,要求 --with-dropwatch。 |
--device-excluded <names> |
(无) | 网卡黑名单,与 --device 互斥;要求 --with-dropwatch。 |
--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"
# 本地关联;两个 BPF 输入使用同一个方向对称 filter
sudo tcpshark --mode retransmit --with-dropwatch --bpf-path-dir bpf \
--filter "tcp and 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 = ""
# 设置为 true 时传入 tcpshark --enable-tlp;默认 false。
EnableTLP = false
# 使用 embedded dropwatch;默认 false。
EnableDropwatch = false
# 传给 tcpshark --max-events-per-second;默认 100,0 表示不限速。
MaxEventsPerSecond = 100
EventTracing.TCPRetransmit.Filter 在两种模式下都控制重传采集。关闭 local
关联时,空值不传 --filter。开启 local 关联时,规范化后的表达式同时传给
tcpshark 的两个输入,空值规范化为 tcp。EventTracing.Dropwatch.Filter
保持独立,只控制 standalone dropwatch。
tcp_retransmit tracer 默认位于全局 BlackList 中。需要启用时,应将其移除并
重启 huatuo-bamai。local 关联使用私有 dropwatch source,因此 standalone
dropwatch 可以继续位于黑名单中。
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)"
local 模式下,同一个表达式必须覆盖两个流量方向。方向性 selector 可能排除反向 ACK 或 SYN-ACK drop 证据,降低结果可信度。
local 模式拒绝
ether host 02:00:00:00:00:01等依赖以太网地址的 primitive。ether proto ip和ether proto ip6可转换为 raw-IP version 判断,因此受支持。
3. 事件数据结构
每条事件以 NDJSON 对象(types.TCPRetransmitTracing)表示。带 omitempty 标签的字段在值为空或零时不会输出。
| 字段 | 类型 | 说明 |
|---|---|---|
observed_timestamp |
string | 用户态接收/格式化事件时生成的 UTC 时间(RFC3339Nano),不是内核 hook 时间。 |
kernel_observed_timestamp |
string | 内核观测事件的 UTC 时间(RFC3339Nano),由原始单调时钟转换。 |
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 状态,如 ESTABLISHED、SYN_SENT 或 NEW_SYN_RECV。 |
phase |
string | 分类结果:connect、data 或 close。 |
tcp_reason |
string | 分类结果:RTO、fast_retransmit、TLP 或 unknown。 |
event_type |
string | tcp_retransmit_skb、tcp_retransmit_synack 或 tcp_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 使用 request snt_isn。 |
tcp_ack_seq |
uint32 | SKB 事件使用 tcp_sk(sk)->rcv_nxt;TLP 使用 snd_una;字段可用时 SYN-ACK 使用 request rcv_nxt。 |
tcp_end_seq |
uint32 | SKB 事件使用 TCP_SKB_CB(skb)->end_seq;字段可用时 SYN-ACK 使用 request snt_isn + 1;TLP 中省略。 |
tcp_flags |
string | 渲染后的 TCP flag 集合,如 `SYN |
skb_addr |
string | 十六进制重传队列 SKB 指针;SYN-ACK 和 TLP 事件中不存在。 |
drop_location |
string | local 关联分类:software、hardware 或 unknown;匹配时与 drop_source 相同,与 dropwatch 的内核地址字段语义不同。 |
drop_source |
string | 匹配丢包的来源:software 或 hardware;无法识别 ABI 来源时为 unknown,未匹配时省略。 |
drop_reason |
string | 与 dropwatch 一致:软件丢包为 BTF 解析的 SKB_DROP_REASON_*,不可解析时为十进制数字,旧内核不支持时为 NOT_SUPPORTED;硬件丢包为 devlink trap 名称。 |
drop_reason_group |
string | devlink trap 分组,如 l2_drops,用于聚合硬件丢包原因;软件丢包和未匹配事件省略。 |
correlation_reason |
string | 唯一终态:matched、unsupported、warmup、wait_timeout、queue_full 或 interrupted;未启用关联时省略。 |
matched_net_namespace |
bool | 同一 TCP flow 下观测到相同 namespace 的 drop,独立于报文和时间检查。严格匹配时必为 true;false 时省略。 |
drop_perf_status |
object | 未匹配事件的 embedded dropwatch 累计计数;map_counters_available 标记 map 计数是否可用,读取失败时仍保留有效的 reader lost_samples。 |
drop_stack |
string | 匹配到的 drop 调用栈;未匹配的栈不做符号化。 |
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 相同的事件变量。可选变量仅在非零或非空时显示,字符串值不添加 JSON 引号或转义。为兼容原文本格式,state、skb、seq、end、ack、flags、ca、retrans 和 reason 分别对应 JSON 中的 tcp_state、skb_addr、tcp_seq、tcp_end_seq、tcp_ack_seq、tcp_flags、ca_state、icsk_retransmits 和 correlation_reason。
<timestamp> [<phase>/<tcp_reason>] <saddr>:<sport> > <daddr>:<dport> state=<STATE> event_type=<TYPE> [kernel_observed_timestamp=<UTC>] [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>] [drop_source=<SOURCE>] [drop_reason=<REASON>] [drop_reason_group=<GROUP>] [reason=<REASON>] [matched_net_namespace=true] [dropwatch_map_counters_available=<true|false> dropwatch_perf_lost=<N> dropwatch_lost_samples=<N> dropwatch_rate_limited=<N>] [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 kernel_observed_timestamp=2026-07-23T02:14:40.304Z 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
示例中的 pid 和 comm 表示 hook 运行时的执行上下文;工作负载归属应使用 container_id 和 socket 元数据判断。
非空的 drop_stack 会作为事件行之后的缩进调用栈行输出,不使用行内 drop_stack= token。
3.2 容器 ID 解析
tcpshark 自身无法访问 Pod 管理器。独立输出时通常没有 container_id,但可用时仍会输出 socket memcg 和网络命名空间元数据。通过 huatuo-bamai 运行时,空的 container_id 按 memory_cgroup_css_addr、net_namespace_cookie、net_namespace_inum 的顺序解析。
全部解析未命中时,事件仍会存储,但 container_id 保持为空。pid 和 comm 描述的是 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_skb,ca_state=4(Loss) |
RTO |
socket 当前处于 TCP_CA_Loss。 |
tcp_retransmit_skb,ca_state=3(Recovery) |
fast_retransmit |
Recovery 路径重传。 |
tcp_retransmit_skb,ca_state=0..2,connect/close 阶段 |
RTO |
当前分类器使用的阶段回退结果。 |
tcp_retransmit_skb,ca_state=0..2,data 阶段 |
unknown |
当前快照不足以生成其他标签。 |
分类器只观察 hook 时刻的 socket 状态,无法重建完整的 ACK/丢包历史。因此应把 tcp_reason 视为聚合标签,而不是经过验证的根因。
4.4 乱序启发式判断
4.5 运维解读
没有任何事件类型可以无条件丢弃。相比只按 event_type 或 tcp_reason 过滤,更推荐按速率、比例和服务影响设置阈值。通用的 huatuo-bamai 噪声过滤机制请参考 dropwatch 文档。
| 模式 | 通常优先级 | 建议 |
|---|---|---|
tcp_reason=RTO |
高 | 排查持续增长或与服务异常相关的 RTO;它通常比 Recovery 路径重传带来更大延迟影响。 |
tcp_reason=fast_retransmit |
中 | 结合丢包、拥塞及 SACK/RACK 行为分析。 |
tcp_reason=TLP |
视上下文而定 | 这是可选信号;用于告警前应确认已主动开启 TLP 采集。 |
event_type=tcp_retransmit_synack |
单次通常较低 | 重复出现可能意味着握手可达性、主机出口、防火墙、客户端或网络问题。 |
配置告警时,应按服务或连接聚合并结合流量规模判断。繁忙主机上的少量绝对计数可能无害,而低流量关键服务上的突发事件可能具有较大影响。
5. 与 dropwatch 关联
指定 --with-dropwatch 后,一个 tcpshark 进程持有两条 perf 输入。重传最多等待 100ms,让较晚送达用户态的 dropwatch 事件参与匹配;候选 drop 的内核单调时间必须早于重传且相差不超过 1s。embedded source 不输出 raw drop 文档;独立启用的 standalone dropwatch 仍是另一条 raw event stream。
关于双 perf stream 的读取乱序、100ms 到达窗口、1s 因果窗口及 negative evidence 的限制,参见 TCP retransmit 与 dropwatch 关联的难点。
与独立 dropwatch 一样,embedded source 自动检测并启用 devlink DROP trap 采集(HardwareAuto),无需额外硬件开关;tracepoint 不可用时输出 warning 并继续采集软件丢包。硬件可见性取决于驱动是否上报目标 DROP trap,条件见 dropwatch 硬件丢包说明。
--device、--device-excluded、--filter、--max-events-per-second 使用与 dropwatch 相同的参数名。网卡白名单/黑名单仅限制 embedded dropwatch,同时作用于其软件和硬件事件;白名单拒绝没有网卡信息的记录,黑名单允许这类记录。重传输入仍按共同的 L3 filter 采集,设备过滤可能减少可匹配的 drop 证据。
匹配后保留 drop_source、drop_reason 和硬件的 drop_reason_group。drop_reason 是观测到的丢包原因,tcp_reason 是重传触发分类,correlation_reason 是本次关联的唯一结束原因,独立诊断字段描述观测限制。BTF reason 表每次会话加载一次;加载失败时记录 warning,软件 reason 回退为数字,硬件 trap 解析不受影响。
# 使用与 dropwatch 相同的网卡和流量参数,查看匹配到的硬件 drop
sudo tcpshark --mode retransmit --with-dropwatch --bpf-path-dir bpf \
--device eth0 --filter "tcp and port 443" --output json \
| jq -c 'select(.drop_source == "hardware")'
硬件 drop 也必须满足相同的 namespace、TCP 和时间约束。缺少 namespace 或 TCP 匹配字段时保持未匹配;未匹配不能证明发生了硬件丢包。硬件 drop_stack 是驱动上报 trap 时的内核栈,不是 ASIC 内部的丢弃位置。
来源枚举未知时,匹配记录的 drop_source 和 drop_location 均为 unknown;不会根据 reason 或调用栈猜测来源;该记录仍输出 correlation_reason=matched。
5.1 关联结果
| 结果 | 必须满足的证据 | 输出 |
|---|---|---|
| 出方向 segment 匹配 | network namespace、地址族、方向、四元组、单调时间顺序相同,且 SYN/data/FIN sequence range 重叠。 | 输出 correlation_reason=matched,以及丢包来源、原因和 drop_stack。 |
| 反方向 ACK 匹配 | 相同 namespace 中的反向四元组、ACK flag、单调时间顺序,且 ACK 覆盖重传 sequence end。 | 输出 correlation_reason=matched,以及丢包来源、原因和 drop_stack。 |
| 无严格匹配 | 缺少本地证据不能证明实际丢包位置。 | drop_location=unknown、唯一的 correlation_reason 和独立诊断信息。 |
不存在仅四元组、仅 SKB pointer、跨 namespace 或 ambiguous 的正向匹配。matched_net_namespace 在报文和时间检查前记录同一 flow(任一方向)下相同 namespace 的 drop。该值为 true 不代表严格匹配成功,完整关联仍以 correlation_reason=matched 为准。false 或字段缺省表示尚未观测到 namespace 匹配,不代表已确认 namespace 不同。匹配到的 drop 只消费一次,同一连接的后续 drop 仍可继续匹配。只有成功匹配后才做调用栈符号化。
5.2 关联终态
每条已定型事件只有一个 correlation_reason:
| 值 | 含义 |
|---|---|
matched |
找到严格匹配的 drop,包括来源未知的已匹配记录。 |
unsupported |
事件缺少匹配规则要求的类型、namespace、时间或 sequence 证据。 |
warmup |
100ms 等待到期仍未匹配,且重传发生时间早于 embedded source ready。 |
wait_timeout |
100ms 等待到期仍未匹配,且重传发生时间不早于 embedded source ready;这里是关联等待超时,与 TCP 的 RTO 无关。 |
queue_full |
有界重传等待队列已满,该事件在到期前被淘汰。 |
interrupted |
关联循环退出,该事件尚未到期的等待被中断,包括正常停止和异常退出。 |
匹配和容量检查前先处理到期事件。退出时使用同一个时间点:已到期项为 warmup 或 wait_timeout,剩余等待项为 interrupted。每条事件只定型一次,随后移出队列。无法解码或从 drop 缓存淘汰的记录不会直接结束某条重传的关联。
单值字段替代原原因数组,tcpshark 与接收端需要配套更新。文本输出使用 reason=<值>;未启用关联时省略该字段。
warmup 仅用于等待到期且重传内核时间早于 embedded source ready 的事件。重传时间等于或晚于 ready 时,等待到期统一使用 wait_timeout,包括 ready 后不足 1s 的情况。drop 到重传的 1s 上限只约束候选匹配,不定义预热期。处理延迟不会改变分类。严格匹配、不支持、队列淘汰和提前中断保留各自原因。该终态表达观测时序,不证明未匹配的具体根因。
matched_net_namespace 保留为独立布尔诊断字段,同时用于已匹配和未匹配结果;可以与丢失计数同时存在,不改变终态原因。
退出时不再读取 dropwatch perf ring 中尚未交给关联器的记录;未读取的尾部 drop 原本仍可能与被中断的重传匹配。
5.3 Dropwatch Perf Status
每个非空输出批次在写出前读取一次状态,包括全部匹配的批次。未匹配事件保留状态快照:
| 字段 | 含义 |
|---|---|
map_counters_available |
两个 map 计数是否均可用;false 区分计数不可用与观测值为零。 |
perf_lost |
embedded dropwatch 未能写入 perf stream 的累计事件数;仅在 map 计数可用时有效。 |
lost_samples |
用户态 reader 从 PERF_RECORD_LOST 累计的丢失样本数;map 读取失败时仍有效,为零时省略。 |
rate_limited |
embedded dropwatch 被限速拒绝的累计事件数;仅在 map 计数可用时有效。 |
状态读取失败时,map_counters_available=false,perf_lost、rate_limited 的零值表示不可用,快照仍保留 lost_samples。先尝试输出本批事件,再返回状态错误;写出失败则停止该批次,并保留两个错误。已匹配事件保留 matched 与丢包元数据,不附加未匹配状态快照。
计数绑定本次 dropwatch 实例,重新加载时归零;map 与 reader 分别采样,不构成原子快照。它们不能证明某条重传未匹配的根因,丢失和限流也不会追加或替换关联原因。
5.4 使用条件与排查方式
| 观测结果 | 检查项 |
|---|---|
software |
结合 drop_reason、tuple、方向、sequence、namespace 检查匹配栈。 |
hardware |
按 drop_reason_group、trap 名称和驱动文档检查硬件丢包;匹配仍是基于报文证据的关联。 |
unknown 且 loss counter 非零 |
收紧共同 filter、增大 perf 容量或调整 embedded dropwatch 限速后重新采集。 |
correlation_reason=wait_timeout |
100ms 内没有严格候选;结合诊断标记和计数排查。 |
correlation_reason=queue_full |
等待队列已满,收紧采集范围。 |
correlation_reason=interrupted |
检查采集停止或错误,关联等待被提前结束。 |
matched_net_namespace=true,但关联未匹配 |
同一 flow 下已观测到相同 namespace 的 drop,继续检查报文和时间条件;该标记本身不构成完整关联。 |
correlation_reason=warmup |
等待到期,且重传发生时间早于 source ready;继续采集后续重传,本次结果无法排除 source ready 之前的丢包。 |
drop_location 不存在 |
off 模式。 |
huatuo-bamai 会向 local 关联的两个输入传入同一个规范化 EventTracing.TCPRetransmit.Filter。采集范围一致可以避免两个 source 观察不同流量,但在缺少可靠因果起点边界时,no-match 仍不能成为确定结论。
🌟 结尾
7 - IRQ 追踪
独立命令采集系统全部 CPU 或指定 CPU 的 irq/softirq 栈,并生成火焰图:其中 sources 表示发起 softirq 的来源,victims 表示被 softirq 直接抢占的任务。由 ksoftirqd 处理 softirq 时,ksoftirqd 是执行者而不是 victim,因此不会记录为 victim。huatuo-bamai 守护进程的 AutoTracing.IRQTracing tracer 正是通过 shell 调用这一工具;直接运行它即可得到相同的结果,无需任何存储后端或配置文件。
构建
常规项目构建会自动发现 cmd/irqtracing 并编译 bpf/irqtracing.c:
make build
命令输出到 _output/bin/irqtracing,BPF 对象输出到 _output/bpf/irqtracing.o;运行时需要通过 --bpf-path 指定该对象。
运行
sudo ./_output/bin/irqtracing \
--bpf-path ./_output/bpf/irqtracing.o \
--duration 5 > irqtracing.folded
--target-cpu 可选,默认值为 -1,表示跟踪系统中的全部 CPU。工具只挂载 irq/softirq_raise / irq/softirq_entry tracepoint;全量模式聚合所有 CPU 的 source 和直接被抢占的 victim,指定非负 CPU 时只保留该 CPU 的数据。两种模式都采集 --duration 秒(默认 3)。
--max-events-per-second-per-cpu N 可选,用于限制每个被跟踪 CPU 每秒采集的 source 和 victim 栈样本总数。省略该参数或设置为 0 时不限速;启用时 N 必须在 2 到 8589934590 之间。每 CPU 额度在 softirq_raise 和 softirq_entry 之间尽量均分;奇数额度多出的一个样本分给 raise。全 CPU 模式下,系统总额度随被跟踪 CPU 数量增长。
本地结果写入 stdout。--output text 是默认值,也是 collapsed 的兼容别名,
输出可供火焰图工具使用的 folded stack:
source;source[raiser,NET_RX];raise_softirq_[k] 12
victim;victim[worker(123),NET_RX];work 4
使用 --output flamegraph 或 --output svg 可直接输出 SVG 火焰图:
sudo ./_output/bin/irqtracing \
--bpf-path ./_output/bpf/irqtracing.o \
--duration 5 \
--output flamegraph > irqtracing.svg
需要平台 profile 数据和丢样计数时使用 --output json:
sudo ./_output/bin/irqtracing \
--bpf-path ./_output/bpf/irqtracing.o \
--duration 5 \
--output json > irqtracing.json
JSON 内容:
{
"flamedata": { "...": "profile 树,未采集到任何数据时为 null" },
"nmissed": 1234
}
flamedata 与平台消费的 profile 格式一致(ProfileType 为 irqtracing:irq:count:irq:count)。每条栈以 source 或 victim 为根,后接 source[comm,VEC] 或 victim[comm(pid),VEC] 标签帧,再依次是用户态栈帧和带 _[k] 后缀的内核态栈帧。VEC 取值为 HI、TIMER、NET_TX、NET_RX、BLOCK、IRQ_POLL、TASKLET、SCHED、HRTIMER、RCU 之一,未知向量显示为 VEC<n>。
nmissed 是采集窗口内丢弃的样本总数。样本丢弃要么来自 --max-events-per-second-per-cpu 启用的 first-N 预算,要么来自 counts 映射已满——两者都意味着火焰图不完整。当丢弃计数无法读取时,工具会直接失败而不是写出结果。非零的 nmissed 同时会在 stderr 输出一条告警;JSON 字段是守护进程消费的持久化契约。
--output-storage <socket> 会通过 Toolstream 发送 JSON,而不在本地输出,且
必须同时指定 --task-id;此时显式指定的 --output 会被忽略。运行日志被
丢弃,本地结果写入 stdout,失败信息和丢样告警写入 stderr。
进程在以下情况退出:采集时长到达、调用方取消、或收到 SIGHUP、SIGQUIT、SIGINT、SIGTERM 信号。
由 huatuo-bamai 调用
守护进程的 AutoTracing.IRQTracing tracer 在其规则检测到某个 CPU 出现 irq/softirq 突增或持续高利用率时,会调用守护进程可执行文件旁的 irqtracing 二进制(CoreBinDir 由守护进程自身路径推导;源码构建树中即 _output/bin/irqtracing),传入 --bpf-path <CoreBpfDir>/irqtracing.o、--target-cpu <cpu>、--duration <RunTracingToolTimeout> 和 --max-events-per-second-per-cpu <MaxEventsPerSecond>。MaxEventsPerSecond 默认是 1000,因此 raise 和 entry 默认各限制为 500/s。CLI 通过 Toolstream 返回结果并合并进保存的 tracing 数据;守护进程另外记录 rule、trigger_cpu、trace_duration 和 hit_cpus。