透過 eBPF 收集核心遙測資料

本頁面概略說明 Android 的 Extended Berkeley Packet Filter (eBPF) 核心遙測架構,包括架構、內建指標,以及如何新增核心指標。

Android 包含統一的 eBPF 核心遙測架構,用於收集低負載核心指標,並將這些指標公開給使用者空間服務,例如 statsd。只要將可攜式 eBPF 程式附加至核心追蹤點和 kprobe,架構就能在適當位置匯總高頻率核心事件,並匯出精簡的直方圖或計數器,不必修改通用核心映像檔 (GKI) 或中斷核心模組介面 (KMI)。

傳統的核心遙測方法仰賴自訂 procfs 或 sysfs 節點、樹狀結構外核心修補程式,或需要跨核心、SELinux、Java 原生介面 (JNI) 和使用者空間服務自訂管道的供應商掛鉤。eBPF 核心遙測架構可將這個管道標準化,適用於 Android Common Kernels (ACK),並提供下列優點:

  • 避免核心 ABI 和 KMI 變動:將 eBPF 程式附加至現有核心追蹤點或 kprobe,不必修改核心原始碼、變更核心應用程式二進位檔介面 (ABI),或新增 Android 供應商掛鉤 (例如 TRACE_ANDROID_VH_*)。裝置製造商和系統單晶片 (SoC) 合作夥伴可以使用 eBPF 遙測技術,取代新增遙測供應商掛鉤,或從現有供應商掛鉤遷移。
  • 維持跨核心版本的可攜性:使用 BPF Compile Once - Run Everywhere (CO-RE) 和 BPF Type Format (BTF),讓單一編譯的 eBPF 程式自動適應跨 GKI 版本 (例如 android14-6.1 以上版本) 的內部核心結構版面配置變更。
  • 減少系統健康狀態的負擔:直接在來源將高頻率的 Kernel 事件篩選並匯總為精簡的 32 個 bucket base-2 對數 (log2) 直方圖,通常每個 BPF 對應的資料大小不到 1 KB。使用者空間每天會使用單一 BPF_MAP_LOOKUP_BATCH 批次系統呼叫,擷取匯總的地圖項目一次。
  • 簡化 SELinux 和 JNI 整合:將遙測對應固定在 /sys/fs/bpf/kernelstats/ 下,套用目錄層級 fs_bpf_kernelstats SELinux 類型繼承,並在執行階段使用 BTF 中繼資料解析 JNI 中的 struct 成員偏移,在新增指標時不需自訂 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。每個延遲時間和大小指標都會使用 32 個儲存區的指數 (log2) 直方圖,並在 24 小時內匯總。下表列出內建的核心遙測指標:

指標原子子系統說明維度鍵
KswapdLatency記憶體管理核心交換 Daemon kswapd 從喚醒到休眠的執行延遲時間 (以微秒為單位)非統一記憶體存取 (NUMA) node_id
DirectReclaimLatency記憶體管理同步直接記憶體回收延遲時間 (微秒)程序記憶體不足 (OOM) oom_score_adj 狀態
CompactionDuration記憶體管理全系統記憶體壓縮執行時間長度 (以微秒為單位),取代舊版壓縮供應商掛鉤系統通用
CmaAllocationLatency記憶體管理連續記憶體配置器 (CMA) 頁面配置延遲時間 (以微秒為單位)CMA 堆積名稱
DmabufRequestSizes記憶體管理DMA-BUF 分配要求大小 (以位元組為單位)匯出工具堆積名稱
SuspendResumeLatency電源管理核心暫停和恢復階段的延遲時間 (以微秒為單位)暫停動作階段

新增核心遙測指標

如要在 eBPF 核心遙測管道中新增累加器指標,請依序完成下列步驟:

  1. 在 system/bpfprogs/telemetry/ 下編寫 eBPF 程式,並將二進位檔建構至 /system/etc/bpf/kernelstats/。
  2. 驗證 BTF 結構體版面配置相容性,適用於一般 JNI BTF 讀取器。
  3. 在 statsd 中註冊提取原子。
  4. 定義提取原子 protobuf 結構定義。

以下各節會以 DirectReclaimLatency (directReclaim.c) 為例。

編寫 eBPF 程式

在 system/bpfprogs/telemetry/ 中,加入 <bpf_telemetry.h> 並定義地圖索引鍵和輸出累加器地圖。在 BPF 對應鍵中使用整數 ID 或列舉,而非原始字串,以盡量減少對應記憶體用量並維護隱私權:

#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 bucket,並使用 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 struct 的結構表示法,從基本指標計算 struct 成員的累計位元組偏移、整數和列舉位元組寬度,以及陣列元素大小。
  • 批次查詢和序列化:statsd 提取對應時,JNI 層會向 sys_bpf 系統呼叫發出單一 BPF_MAP_LOOKUP_BATCH 指令,大量讀取所有雜湊項目,並使用 SetLongArrayRegion JNI 函式,將解析後的金鑰 ID 和值區陣列直接複製到原始 Java long[] 陣列。

如果 BPF C 結構在未來版本中有所變更,重新編譯的 BTF 區段會自動更新,JNI 讀取器也會在執行階段調整,不需要重新編譯 JNI。

在 statsd 中註冊提取原子

在 StatsPullAtomService.java 中,一般 pullKernelTelemetry 輔助程式會讀取已釘選的 BPF 對應,根據先前提取的快取計數計算 bucket 差異,並發出 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 個 bucket 的直方圖陣列: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;
}