[i18n] print_printable_section [i18n] print_click_to_print.
开发手册
- 1: 扩展观测能力
- 2: 开发调试
- 3: 集成测试
- 4: BPF ABI 指南
- 5: 时间格式约定
1 - 扩展观测能力
HUATUO 支持 Metrics、Event 和 AutoTracing 三种观测扩展。三者共享注册机制, 但触发方式、运行开销和数据输出不同。
| 类型 | 触发方式 | 数据输出 | 适用场景 |
|---|---|---|---|
| Metrics | 周期采集 | Prometheus | 持续观测系统性能和长期趋势 |
| Event | 内核事件或阈值触发 | ES、本地文件,可选 Prometheus | 常态运行并捕获异常现场 |
| AutoTracing | 系统异常触发 | ES、本地文件,可选 Prometheus | 按需执行开销较高的上下文采集 |
模式说明
Metrics
Metrics 通过 procfs、sysfs 或 eBPF 周期采集系统状态,以 Prometheus 格式输出, 适合实时监控和长期趋势分析。内置采集能力包括:
- CPU:sys、usr、util、load、nr_running 等。
- 内存:vmstat、memory_stat、directreclaim、asyncreclaim 等。
- IO:d2c、q2c、freeze、flush 等。
- 网络:ARP、socket memory、qdisc、netstat、netdev、sockstat 等。
Event
Event 常态监听内核事件或阈值条件,在异常发生时保存内核运行上下文。该模式 面向需要持续开启的低开销观测,数据写入 Elasticsearch 和本地文件,也可以 同步生成 Prometheus 指标。内置事件包括:
- 软中断异常
softirq_tracing。 - 内存异常分配
oom。 - 软锁定
softlockup。 - D 状态进程
hungtask。 - 内存回收
memory_reclaim_events。 - 异常丢包
dropwatch。 - 网络接收延迟
net_rx_latency。
AutoTracing
AutoTracing 在检测到系统异常后自动调用诊断工具,采集火焰图或上下文快照等 现场信息。它适合采集成本较高、无法持续运行的诊断数据。结果写入 Elasticsearch 和本地文件,也可以转换为 Prometheus 指标。内置能力包括:
- CPU 空闲和系统态异常追踪
cpuidle、cpusys。 - D 状态负载追踪
dload。 - 内存突发分配追踪
memburst。 - 磁盘 IO 异常追踪
iotracing。
Event 和 AutoTracing 都属于 Tracing 模式,共享 ITracingEvent 接口。它们既能
保存异常上下文用于根因分析,也能通过实现 Collector 将统计结果暴露给
Prometheus。
添加 Metrics
自定义 Metrics 通过 /metrics 接口输出 Prometheus 指标。
实现 Collector
在 core/metrics 下创建实现 Collector 接口的类型:
type Collector interface {
Update() ([]*Data, error)
}
type exampleMetric struct{}
func (c *exampleMetric) Update() ([]*metric.Data, error) {
return []*metric.Data{
metric.NewGaugeData("example", value, "example value", nil),
}, nil
}
注册采集器
通过 FlagMetric 将实现注册为指标采集器:
func init() {
tracing.RegisterEventTracing("example", newExampleMetric)
}
func newExampleMetric() (*tracing.EventTracingAttr, error) {
return &tracing.EventTracingAttr{
TracingData: &exampleMetric{},
Flag: tracing.FlagMetric,
}, nil
}
管理 BPF 对象
同一个实现同时提供 Start 和 Update 时,两个方法可能并发执行。不要直接在
collector 字段中读写 bpf.BPF 接口;使用
internal/bpf/bpf_ref.go 提供的 Reference
和 Lease 管理对象生命周期:
type example struct {
object bpf.Reference
}
func (c *example) Start(ctx context.Context) (retErr error) {
object, err := bpf.LoadBpf(bpf.ThisBpfOBJ(), nil)
if err != nil {
return err
}
if err := object.Attach(); err != nil {
return errors.Join(err, object.Close())
}
if err := c.object.Publish(object); err != nil {
return errors.Join(err, object.Close())
}
defer func() {
retErr = errors.Join(retErr, c.object.UnPublish())
}()
<-ctx.Done()
return nil
}
func (c *example) Update() ([]*metric.Data, error) {
lease, ok := c.object.Acquire()
if !ok {
return nil, nil
}
defer lease.Release()
items, err := lease.DumpMapByName("example_map")
if err != nil {
return nil, fmt.Errorf("dump example_map: %w", err)
}
return buildMetrics(items), nil
}
接口约束:
Publish将对象所有权转移给Reference。成功后不要直接调用object.Close()。Acquire返回的Lease将 BPF 对象固定到本次Update结束;必须配对调用Release,且不要复制Lease。UnPublish先阻止新的Acquire,再等待所有 Lease 释放,最后关闭 BPF 对象并返回关闭错误。Publish和UnPublish必须串行调用。当前框架保证同一实例的Start不会并发执行。
因此,已经开始的 Update 可以完整使用原 BPF 对象;Start 退出或重启时,
对象只会在这些 Update 完成后关闭。
添加 Event
Event 需要实现 ITracingEvent:
type ITracingEvent interface {
Start(ctx context.Context) error
}
type exampleEvent struct{}
func (e *exampleEvent) Start(ctx context.Context) error {
// 检测事件并采集上下文。
// storage.Save 将数据写入 ES 和本地存储。
storage.Save("example", containerID, time.Now(), eventData)
return nil
}
注册时使用 FlagTracing:
func init() {
tracing.RegisterEventTracing("example", newExampleEvent)
}
func newExampleEvent() (*tracing.EventTracingAttr, error) {
return &tracing.EventTracingAttr{
TracingData: &exampleEvent{},
Interval: 10,
Flag: tracing.FlagTracing,
}, nil
}
如果事件还需要输出 Prometheus 指标,可以同时实现 Collector,并将
tracing.FlagMetric 合并到 Flag。
添加 AutoTracing
AutoTracing 与 Event 使用相同的 ITracingEvent 接口和注册框架:
type exampleAutoTracing struct{}
func (t *exampleAutoTracing) Start(ctx context.Context) error {
// 异常触发后采集上下文。
storage.Save("example", containerID, time.Now(), tracingData)
return nil
}
func init() {
tracing.RegisterEventTracing("example", newExampleAutoTracing)
}
func newExampleAutoTracing() (*tracing.EventTracingAttr, error) {
return &tracing.EventTracingAttr{
TracingData: &exampleAutoTracing{},
Interval: 10,
Flag: tracing.FlagTracing,
}, nil
}
项目中的 core/metrics、core/events 和 core/autotracing 提供了完整实现示例,
包括 BPF map 交互、容器信息获取、数据存储和 Prometheus 输出。
2 - 开发调试
全组件联调
开发 Compose 使用当前工作区源码构建开发镜像,并启动采集器、API Server、 Elasticsearch、Prometheus 和 Grafana。采集器需要访问宿主机内核和 cgroup, 因此应在 Linux 主机的项目根目录使用 root 权限运行:
sudo make compose-dev-up
Compose 在前台聚合所有组件日志。修改源码后,按 Ctrl+C 停止环境并重新执行
命令;Docker 会复用工具链和 Go 编译缓存。
调试结束后删除容器、数据卷和开发镜像:
sudo make compose-dev-down
该命令会删除 Elasticsearch 数据卷。需要保留联调数据时,不要执行该命令。
BPF 调试
BPF 代码可以使用 bpf_dbg() 和 bpf_dbg_msg() 宏在内核态输出调试信息。
宏定义位于 bpf/include/bpf_dbg.h。调试功能包含编译时和运行时两级开关,
默认完全关闭。
添加调试埋点
每个使用调试宏的 BPF 源文件都需要声明独立的调试 map:
#include "bpf_dbg.h"
BPF_DBG_MAP(native_cpu);
SEC("perf_event")
int prog(void *ctx)
{
bpf_dbg_msg(ctx, native_cpu, "enter prog");
bpf_dbg(ctx, native_cpu, "pid and addr", pid, addr, 0);
return 0;
}
bpf_dbg_msg() 只输出消息;bpf_dbg() 还可以携带最多三个 u64 参数。
编译调试对象
通过 BPF_DEBUG=1 将 -DDEBUG_BPF 传给 Clang:
make BPF_DEBUG=1
只重新编译 BPF 对象:
make BPF_DEBUG=1 bpf-build
BPF_DEBUG=0 是默认值。此时宏展开为空操作,调试 perf event array、事件结构、
bpf_ktime_get_ns 和 bpf_perf_event_output 都不会进入 BPF 对象。
启用运行时输出
编译调试对象后,启动 profiler 时还需要增加 --log-bpf-debug。当前只有 native
profiler 支持该开关:
./profiler --type cpu --language native --log-bpf-debug ...
加载 BPF 对象时,bpf.NewDbg(true) 会在 LoadBpf 前将
bpf_dbg_enabled 常量改写为 1。未启用时,verifier 会将对应分支作为死代码
消除。每个 BPF 对象维护独立开关,互不影响。
查看调试输出
用户态以 Debug 级别输出调试事件:
file:BPF 源文件名。line:源文件行号。ts:转换为 UTC 墙钟时间的事件时间戳。msg:调试消息。args:最多三个u64参数;全部为零时省略。
bpf_dbg: file=native_oncpu_profiler.c line=120 ts=2026-01-11T08:30:00.123456Z msg=enter prog args=[0x1f4 0xffff8881 0x0]
只有同时使用 BPF_DEBUG=1 编译并在运行时指定 --log-bpf-debug,才会产生
调试输出。
3 - 集成测试
集成测试用于验证 huatuo-bamai在使用模拟的 /proc 和 /sys 文件系统时,能够正确启动并对外暴露符合预期的Prometheus指标。
测试运行的是真实的可执行文件,并通过校验 /metrics 接口的输出结果,确保指标采集与暴露逻辑正确,而不依赖宿主机的内核或硬件环境。
脚本执行流程
该集成测试脚本主要包含以下步骤:
- 生成临时的
bamai.conf配置文件 - 使用模拟的
procfs和sysfs启动huatuo-bamai服务 - 等待
/metrics接口可访问 - 从
/metrics接口拉取所有指标数据 - 校验所有预期指标是否存在且内容匹配
- 停止服务并清理相关资源
- 若任意一个预期指标缺失或不匹配,测试将直接失败
运行方式
请在项目根目录下执行集成测试:
bash integration/run.sh
指定文件名可以只运行一个集成测试,第二个参数指定循环次数,默认为 1:
bash integration/run.sh test_metrics_exclude_filter.sh 10
或通过 Makefile 执行:
make integration
失败时的行为
huatuo-bamai服务指标和日志将直接输出到标准输出,便于问题定位- 临时工作目录将被保留,用于后续调试分析
成功时的行为
- 显示验证成功的
metrics列表
如何新增指标测试
第一步:新增或更新模拟数据
如果新增的指标依赖 /proc 或 /sys 文件内容,请在以下目录中新增或修改模拟数据:
integration/fixtures/
目录结构需与真实内核文件系统保持一致。
第二步:添加预期指标
在以下目录中新建一个文件:
integration/fixtures/expected_metrics/
├── cpu.txt
├── memory.txt
└── ...
每一行(非空、非注释行)表示一条期望的 Prometheus 指标,指标内容必须与 /metrics 接口返回结果完全一致,新增的*.txt 文件会被测试脚本自动加载并参与校验。
第三步:运行测试
bash integration/run.sh
当任意一个预期指标缺失或不匹配时,测试将失败。
4 - BPF ABI 指南
BPF 侧 C 结构体是 perf event ABI 的唯一来源。Go 类型由 BPF 对象中的 BTF 自动生成,禁止手工维护同构结构体。
约定
一个 ABI 域对应一个 C 头文件和一个 Go 生成文件:
| 项目 | 约定 |
|---|---|
| 域名 | <domain> |
| C 头文件 | bpf/include/abi/<domain>_types.h |
| C 结构体前缀 | <domain>_ |
| Go 生成文件 | internal/bpf/abi/<domain>_types_generated.go |
<domain> 必须以小写字母开头,只能包含小写字母、数字和下划线。避免使用
前缀重叠的域,例如 net 和 net_rx。
实现
以下示例创建 sample 域。
1. 定义 ABI 头文件
新建 bpf/include/abi/sample_types.h:
// Copyright 2026 The HuaTuo Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef __BPF_ABI_SAMPLE_H__
#define __BPF_ABI_SAMPLE_H__
#include "bpf_abi.h"
#define SAMPLE_DATA_LEN 4
struct sample_detail {
u32 code;
u8 data[SAMPLE_DATA_LEN];
};
struct sample_event {
u64 timestamp;
struct sample_detail detail;
u32 pid;
u8 kind;
u8 pad[3];
};
BPF_ABI_EXPORT(sample_detail);
BPF_ABI_EXPORT(sample_event);
#endif /* __BPF_ABI_SAMPLE_H__ */
要求:
- 结构体名称必须使用
<domain>_前缀。 - 每个需要生成 Go 类型的结构体都必须调用
BPF_ABI_EXPORT(<type>),包括嵌套结构体。 - 头文件只定义 ABI 类型和相关常量,不放置 BPF 程序、map 或业务逻辑。
- 数组长度必须是编译期常量,且在所有 BPF 编译单元中保持一致。
2. 在 BPF 程序中使用
ABI 头文件依赖 u8、u16 等基础类型,应在 vmlinux.h 和项目基础头文件
之后包含:
#include "vmlinux.h"
#include "bpf_common.h"
#include "abi/sample_types.h"
发送 perf event 时直接使用 ABI 结构体:
struct sample_event event = {};
event.timestamp = bpf_ktime_get_ns();
event.pid = bpf_get_current_pid_tgid() >> 32;
event.kind = kind;
bpf_perf_event_output(ctx, &events, COMPAT_BPF_F_CURRENT_CPU,
&event, sizeof(event));
必须零初始化事件,避免将未初始化的 padding 写入 perf buffer。传给
bpf_perf_event_output 的结构体必须定义在 ABI 头文件中。
3. 生成并使用 Go 类型
在项目根目录运行:
make gen-build
生成结果位于:
internal/bpf/abi/sample_types_generated.go
Go 代码直接引用生成类型:
import "huatuo-bamai/internal/bpf/abi"
var event abi.SampleEvent
if err := reader.ReadInto(&event); err != nil {
return fmt.Errorf("read sample event: %w", err)
}
生成文件包含结构体、SampleEventSize 大小常量,以及基于
unsafe.Sizeof 和 unsafe.Offsetof 的布局断言。生成文件只读,禁止手工
修改。
类型和布局
| 类型 | 支持情况 |
|---|---|
| 1、2、4、8 字节定宽整数 | 支持 |
| 非零长度的定长数组 | 支持 |
| 满足相同约束的嵌套结构体 | 支持 |
目标类型受支持的 typedef |
支持 |
指针、union、enum、浮点类型 |
不支持 |
位域、_Bool |
不支持 |
| 零长度数组、柔性数组 | 不支持 |
| 递归、字段重叠或非字节对齐结构体 | 不支持 |
布局要求:
- 使用
u8、s16、u32、s64等定宽整数。 - 按对齐要求排列字段,必要时使用
u8 pad[N]明确 padding。 - 不使用
long、unsigned long等随平台变化的类型。 - 不使用
__attribute__((packed))绕过布局校验。 - 同名结构体出现在多个 BPF 对象中时,字段、偏移和大小必须完全一致。
C 名称按下划线转换为 Go 导出名称,例如 sample_event 转换为
SampleEvent,pid_tgid 转换为 PIDTGID。避免使用会映射为相同 Go 名称
的 C 名称,例如 sample_id 和 sample_i_d。
验证
新增或修改 ABI 后执行:
make gen-build
go test ./build/bpfabi-tool
make check
新增 perf event 还应增加解码测试,至少覆盖:
- 整数边界值和本机字节序。
- 嵌套结构体和数组首尾元素。
- padding 前后的字段。
- 样本大小与生成的
<GoType>Size常量。
常见错误
| 错误 | 检查项 |
|---|---|
has no "<domain>_" btf anchors |
头文件是否被 BPF 源文件包含;类型是否调用 BPF_ABI_EXPORT |
without matching abi header |
头文件名与结构体前缀是否一致;域前缀是否重叠 |
differs between objects |
条件编译、数组长度宏和依赖头文件是否导致布局变化 |
go offset is ... btf offset is ... |
调整字段顺序或增加显式 padding,不要使用 packed |
go type name ... collides |
重命名映射为相同 Go 名称的 C 类型或字段 |
5 - 时间格式约定
时间格式约定
HUATUO 统一以 UTC 和固定九位纳秒精度序列化时间戳:
2006-01-02T15:04:05.000000000Z
例如,2026 年 7 月 22 日 00:30:00.123456789 的事件会写为:
2026-07-22T00:30:00.123456789Z
为什么使用固定 UTC 精度
- 所有主机都使用
Z时区,分布式查询无需处理不同的本地时区。 - 九位小数使字符串宽度固定,因此字典序与时间序一致,便于日志文件和关键字索引存储排序。
- 纳秒精度能够保留 Go
time.Time与事件追踪流水线提供的时间信息。
集成建议
向 HUATUO 写入数据时,请使用统一辅助函数:
timestamp := timeutil.FormatUTC(time.Now())
读取时间戳时使用 timeutil.Parse。它既支持统一格式,也支持包含 0 到 9 位小数的 RFC 3339 旧数据;返回值始终会规范化为 UTC。
只有在两侧均由 FormatUTC 生成字符串时,才应直接按字符串比较时间。第三方 RFC 3339 值可能使用可变小数精度或非 UTC 偏移。