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 空闲和系统态异常追踪 cpuidlecpusys
  • 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 对象

同一个实现同时提供 StartUpdate 时,两个方法可能并发执行。不要直接在 collector 字段中读写 bpf.BPF 接口;使用 internal/bpf/bpf_ref.go 提供的 ReferenceLease 管理对象生命周期:

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 对象并返回关闭错误。
  • PublishUnPublish 必须串行调用。当前框架保证同一实例的 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/metricscore/eventscore/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_nsbpf_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 接口的输出结果,确保指标采集与暴露逻辑正确,而不依赖宿主机的内核或硬件环境。

脚本执行流程

该集成测试脚本主要包含以下步骤:

  1. 生成临时的bamai.conf配置文件
  2. 使用模拟的 procfssysfs 启动 huatuo-bamai 服务
  3. 等待 /metrics 接口可访问
  4. /metrics 接口拉取所有指标数据
  5. 校验所有预期指标是否存在且内容匹配
  6. 停止服务并清理相关资源
  7. 若任意一个预期指标缺失或不匹配,测试将直接失败

运行方式

请在项目根目录下执行集成测试:

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> 必须以小写字母开头,只能包含小写字母、数字和下划线。避免使用 前缀重叠的域,例如 netnet_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 头文件依赖 u8u16 等基础类型,应在 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.Sizeofunsafe.Offsetof 的布局断言。生成文件只读,禁止手工 修改。

类型和布局

类型 支持情况
1、2、4、8 字节定宽整数 支持
非零长度的定长数组 支持
满足相同约束的嵌套结构体 支持
目标类型受支持的 typedef 支持
指针、unionenum、浮点类型 不支持
位域、_Bool 不支持
零长度数组、柔性数组 不支持
递归、字段重叠或非字节对齐结构体 不支持

布局要求:

  • 使用 u8s16u32s64 等定宽整数。
  • 按对齐要求排列字段,必要时使用 u8 pad[N] 明确 padding。
  • 不使用 longunsigned long 等随平台变化的类型。
  • 不使用 __attribute__((packed)) 绕过布局校验。
  • 同名结构体出现在多个 BPF 对象中时,字段、偏移和大小必须完全一致。

C 名称按下划线转换为 Go 导出名称,例如 sample_event 转换为 SampleEventpid_tgid 转换为 PIDTGID。避免使用会映射为相同 Go 名称 的 C 名称,例如 sample_idsample_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 偏移。