使用 eBPF 收集内核遥测数据

本页面简要介绍了 Android 中的扩展伯克利数据包过滤器 (eBPF) 内核遥测框架,并介绍了框架架构、内置指标以及如何添加新的内核指标。

Android 包含一个统一的 eBPF 内核遥测框架,用于收集低开销内核指标,并将这些指标公开给 statsd 等用户空间服务。通过将可移植的 eBPF 程序挂接到内核跟踪点和 kprobe,该框架可以在本地聚合高频内核事件,并 导出紧凑的直方图或计数器,而无需修改 通用内核映像 (GKI) 或破坏内核模块接口 (KMI)。

传统的内核遥测方法依赖于自定义 procfs 或 sysfs 节点、树外内核补丁或供应商钩子,这些方法需要在内核、SELinux、Java 原生接口 (JNI) 和用户空间服务之间进行自定义管道。 eBPF 内核遥测框架在 Android 通用内核 (ACK) 中标准化了此管道,并提供了以下优势:

  • 避免内核 ABI 和 KMI 变动: 将 eBPF 程序挂接到现有内核跟踪点或 kprobe,而无需修改内核源代码、更改内核应用二进制接口 (ABI) 或添加 TRACE_ANDROID_VH_* 等 Android 供应商钩子。设备制造商和系统芯片 (SoC) 合作伙伴可以使用 eBPF 遥测来代替添加新的遥测供应商钩子,或迁移掉现有供应商钩子。
  • 保持跨内核版本的可移植性: 使用 BPF Compile Once - Run Everywhere (CO-RE) 和 BPF Type Format (BTF),以便单个编译的 eBPF 程序自动适应 GKI 版本(例如 android14-6.1 及更高版本)中内部内核结构布局的变化。
  • 最大限度地减少系统健康状况开销: 直接在源中过滤和聚合高频内核事件,将其转换为紧凑的 32 存储桶以 2 为底的对数 (log2) 直方图,通常每个 BPF 映射小于 1 KB。 用户空间每天使用单个 BPF_MAP_LOOKUP_BATCH 批量系统调用检索聚合的映射条目。
  • 简化 SELinux 和 JNI 集成: 将遥测映射固定在 /sys/fs/bpf/kernelstats/ 下,以应用目录级 fs_bpf_kernelstats SELinux 类型继承,并使用 BTF 元数据在 JNI 中运行时解析结构成员偏移量,从而在添加新指标时消除自定义 C++ 解析代码。

如需详细了解 Android 如何加载和固定 eBPF 程序和映射, 请参阅 使用 eBPF 扩展内核。

架构概览

eBPF 内核遥测管道将通用基础架构组件与特定于指标的定义分开,以便添加新的内核指标只需进行最少的代码更改。下表介绍了管道的组件:

组件在管道中的角色范围
BPF 加载器,bpfloader如果与正在运行的内核兼容,则在启动时从 /system/etc/bpf/kernelstats/ 加载已编译的 eBPF 程序,并将映射固定到 /sys/fs/bpf/kernelstats/。如果已停用或不受支持,则会干净利落地跳过加载。通用
SELinux 政策,fs_bpf_kernelstats将目录级 SELinux 标签应用于 /sys/fs/bpf/kernelstats/,以便 system_server 可以读取遥测映射,而无需针对每个指标更新 SELinux 政策。通用
system/bpfprogs/telemetry/ 中的 eBPF 程序挂接到内核跟踪点或 kprobe,过滤事件,并将指标聚合到 BPF 映射中。每个指标的自定义
JNI BTF 读取器在运行时从已编译的 BPF 可执行文件和可链接格式 (ELF) 二进制文件解析 BTF 元数据,缓存结构成员偏移量,并批量将映射读取到原始 Java 数组中。通用
statsd 中的用户空间提取器按每日计划提取累积直方图映射,根据缓存的历史记录计算增量,并构建强类型 StatsEvent 提取原子。通用帮助程序,每个原子都有一个 case 条目
遥测架构定义强类型 statsd 提取原子 protobuf 和服务器端直方图或值指标聚合。每个指标的自定义

用户空间使用模式

在设计内核遥测指标时,请根据指标是表示累积状态还是有序事件序列,在两种用户空间使用模式之间进行选择。下表比较了这两种模式:

属性累加器数据流
定义有状态、单调递增的累积计数器或直方图带时间戳的内核事件的有序序列
访问模式用户空间按需轮询,例如通过 statsd 每 24 小时轮询一次用户空间从环形缓冲区持续使用,以避免事件丢失
典型的 BPF 映射类型BPF_MAP_TYPE_HASH、BPF_MAP_TYPE_LRU_HASH、BPF_MAP_TYPE_PERCPU_HASH、BPF_MAP_TYPE_LRU_PERCPU_HASHBPF_MAP_TYPE_RINGBUF
并发模型内核映射查找是原子的;多个读取器可以轮询,而无需用户空间锁定破坏性读取语义需要单个读取器线程调度到使用者
应用场景示例内存管理延迟直方图、直接内存访问缓冲区 (DMA-BUF) 分配大小、唤醒源迭代器经过过滤的调度程序跟踪事件、高频子系统电源跟踪

内置内核遥测指标

Android 包含内置的 eBPF 内核遥测程序,这些程序适用于 GKI 6.1 及更高版本的内核,包括 android14-6.1、android15-6.6、android16-6.12 和 android17-6.18。每个延迟和大小指标都使用在 24 小时窗口内聚合的 32 存储桶指数 (log2) 直方图。下表列出了内置内核遥测指标:

指标原子子系统说明维度键
KswapdLatency内存管理内核交换守护程序 kswapd 的唤醒到休眠执行延迟时间(以微秒为单位)非统一内存访问 (NUMA) node_id
DirectReclaimLatency内存管理同步直接内存回收延迟时间(以微秒为单位)进程内存不足 (OOM) oom_score_adj 状态
CompactionDuration内存管理系统级内存压缩执行时长(以微秒为单位),取代了旧版压缩供应商钩子系统级
CmaAllocationLatency内存管理连续内存分配器 (CMA) 页面分配延迟时间(以微秒为单位)CMA 堆名称
DmabufRequestSizes内存管理DMA-BUF 分配请求大小(以字节为单位)导出器堆名称
SuspendResumeLatency电源管理内核暂停和恢复阶段的延迟时间(以微秒为单位)暂停操作阶段

添加内核遥测指标

如需向 eBPF 内核遥测管道添加新的累加器指标,请按顺序完成以下步骤:

  1. 编写 eBPF 程序,位置在 system/bpfprogs/telemetry/ 下,并将二进制文件构建到 /system/etc/bpf/kernelstats/ 中。
  2. 验证 BTF 结构体布局兼容性,适用于 通用 JNI BTF 读取器。
  3. 在 statsd 中注册提取原子。
  4. 定义提取原子 protobuf 架构。

以下部分以 DirectReclaimLatency (directReclaim.c) 为例。

编写 eBPF 程序

在 system/bpfprogs/telemetry/ 中,添加 <bpf_telemetry.h> 并定义映射 键和输出累加器映射。在 BPF 映射键中使用整数标识符或枚举,而不是原始字符串,以最大限度地减少映射内存占用并保护隐私:

#include <android_bpf_defs.h>
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_helpers.h>
#include <bpf_telemetry.h>
#include <stdint.h>

struct direct_reclaim_key {
    uint32_t process_state;
};

#define DIRECT_RECLAIM_START_MAP_ENTRIES 128
#define DIRECT_RECLAIM_OUTPUT_MAP_ENTRIES 8

// Temporary map to store start time. Key: pid (thread_id), Value: start_time_ns
DEFINE_BPF_MAP_GRO(direct_reclaim_start_map, HASH, uint32_t, uint64_t,
                   DIRECT_RECLAIM_START_MAP_ENTRIES, AID_SYSTEM);

// Output map. Key: direct_reclaim_key, Value: accumulator_value (32 log2 buckets)
DEFINE_BPF_MAP_GRO(direct_reclaim_latency_map, HASH, struct direct_reclaim_key,
                   struct accumulator_value, DIRECT_RECLAIM_OUTPUT_MAP_ENTRIES,
                   AID_SYSTEM);

在跟踪点处理程序中,记录条目中的开始时间戳,使用 GET_LATENCY_BUCKET 计算退出时的 log2 延迟时间存储桶,并使用 INCREMENT_ACCUMULATOR 递增累加器映射条目:

DEFINE_BPF_PROG("tp_btf/mm_vmscan_direct_reclaim_begin", AID_ROOT, AID_SYSTEM,
                tracepoint_vmscan_mm_vmscan_direct_reclaim_begin)(
        uint64_t* args) {
    (void)args;
    uint32_t pid = (uint32_t)bpf_get_current_pid_tgid();
    SAVE_FIRST_START_TIME(direct_reclaim_start_map, &pid);
    return 0;
}

DEFINE_BPF_PROG("tp_btf/mm_vmscan_direct_reclaim_end", AID_ROOT, AID_SYSTEM,
                tracepoint_vmscan_mm_vmscan_direct_reclaim_end)(
        uint64_t* args) {
    (void)args;
    uint32_t pid = (uint32_t)bpf_get_current_pid_tgid();
    uint32_t bucket = GET_LATENCY_BUCKET(direct_reclaim_start_map, &pid);
    if (bucket == BPF_TELEMETRY_INVALID_BUCKET) return 0;

    struct task_struct* task = (struct task_struct*)bpf_get_current_task();
    int oom_score_adj = BPF_CORE_READ(task, signal, oom_score_adj);
    enum process_state state = get_process_state(oom_score_adj);

    struct direct_reclaim_key key = {.process_state = (uint32_t)state};
    INCREMENT_ACCUMULATOR(direct_reclaim_latency_map, &key, bucket);
    return 0;
}

验证 BTF 结构布局兼容性

添加新的直方图映射时,只要您的映射键和值结构使用标准整数、枚举和固定数组类型,就不需要编写自定义 JNI C++ 代码。通用 JNI 读取器不会在编译时将 BPF 映射条目转换为硬编码的 C 结构,而是在运行时分三个阶段动态解析结构布局:

  • ELF 到映射关联: JNI 层使用 libbpf btf__parse 函数直接从磁盘上已编译的 BPF ELF 文件(例如 /system/etc/bpf/kernelstats/directReclaim.bpf)解析 BTF 元数据。
  • BTF 类型解析: 首次访问时,读取器会构建并缓存映射的键和值 C 结构体的结构表示法,从基指针计算结构体成员的累积字节偏移量、整数和枚举字节宽度以及数组元素大小。
  • 批量查找和序列化: 当 statsd 提取映射时,JNI 层会向 sys_bpf 系统调用发出单个 BPF_MAP_LOOKUP_BATCH 命令,以批量读取所有哈希条目,并使用 SetLongArrayRegion JNI 函数将解析的键 ID 和存储桶数组直接复制到原始 Java long[] 数组中。

如果 BPF C 结构体在未来的版本中发生变化,重新编译的 BTF 部分会自动更新,并且 JNI 读取器会在运行时进行调整,而无需重新编译 JNI。

在 statsd 中注册提取原子

在 StatsPullAtomService.java 中,通用 pullKernelTelemetry 帮助程序会读取固定的 BPF 映射,根据上一次提取的缓存计数计算存储桶增量,并发出 StatsEvent 条目。添加新的内核遥测指标只需要进行三项小改动:

// 1. Dispatch the atom tag to pullKernelTelemetry() in onPullAtomLocked():
case FrameworkStatsLog.DIRECT_RECLAIM_LATENCY:
    return pullKernelTelemetry(atomTag, data);

// 2. Add the atom tag to the registered atoms list in registerKernelTelemetry():
int[] atoms = {
    FrameworkStatsLog.DIRECT_RECLAIM_LATENCY,
    // ...
};

// 3. Map the atom tag to its pinned BPF map path:
case FrameworkStatsLog.DIRECT_RECLAIM_LATENCY ->
    "/sys/fs/bpf/kernelstats/map_directReclaim_direct_reclaim_latency_map";

定义提取原子 protobuf

在 frameworks/proto_logging/ 中定义强类型提取原子消息,以匹配 BPF 映射生成的维度键和 32 存储桶 log2 直方图数组:

message DirectReclaimLatency {
    enum ProcessState {
        PROCESS_STATE_UNSPECIFIED = 0;
        STATE_NATIVE = 1;
        STATE_SYSTEM = 2;
        STATE_PERSISTENT = 3;
        STATE_FOREGROUND = 4;
        STATE_VISIBLE = 5;
        STATE_PERCEPTIBLE = 6;
        STATE_SERVICE_BACKGROUND = 7;
        STATE_CACHED = 8;
    }
    optional ProcessState process_state = 1;
    repeated int32 duration_log2_buckets_micros = 2;
}