eBPF を使用してカーネル テレメトリーを収集する

このページでは、Android の Extended Berkeley Packet Filter(eBPF)カーネル テレメトリー フレームワークの概要について説明します。フレームワーク アーキテクチャ、組み込み指標、新しいカーネル指標を追加する方法について説明します。

Android には、オーバーヘッドの少ないカーネル指標を収集し、それらの指標を statsd などのユーザー空間サービスに公開するための、統合された eBPF カーネル テレメトリ フレームワークが含まれています。ポータブル eBPF プログラムをカーネル トレースポイントと kprobe に接続することで、フレームワークは Generic Kernel Image(GKI)を変更したり、カーネル モジュール インターフェース(KMI)を壊したりすることなく、高頻度のカーネル イベントをその場で集約し、コンパクトなヒストグラムまたはカウンタをエクスポートします。

カーネル テレメトリーの従来のアプローチでは、カスタム procfs ノードまたは sysfs ノード、ツリー外のカーネル パッチ、ベンダーフックに依存しており、カーネル、SELinux、Java Native Interface(JNI)、ユーザー空間サービス全体でカスタム配管が必要になります。eBPF カーネル テレメトリー フレームワークは、Android 共通カーネル(ACK)全体でこのパイプラインを標準化し、次の利点を提供します。

  • カーネル ABI と KMI のチャーンを回避する: カーネル ソースコードの変更、カーネル アプリケーション バイナリ インターフェース(ABI)の変更、TRACE_ANDROID_VH_* などの Android ベンダーフックの追加を行うことなく、eBPF プログラムを既存のカーネル トレースポイントまたは kprobe にアタッチします。デバイス メーカーとシステム オン チップ(SoC)パートナーは、新しいテレメトリー ベンダーフックを追加する代わりに、または既存のベンダーフックから移行するために、eBPF テレメトリーを使用できます。
  • カーネル バージョン間の移植性を維持: BPF Compile Once - Run Everywhere(CO-RE)と BPF Type Format(BTF)を使用することで、単一のコンパイル済み eBPF プログラムが、android14-6.1 以降の GKI バージョンにおける内部カーネル構造レイアウトの変更に自動的に適応します。
  • システムヘルスのオーバーヘッドを最小限に抑える: 高頻度のカーネル イベントをソースで直接フィルタリングして集約し、コンパクトな 32 バケットの 2 進対数(log2)ヒストグラムに変換します。通常、BPF マップあたり 1 KB 未満です。ユーザー空間は、1 つの BPF_MAP_LOOKUP_BATCH バッチ syscall を使用して、集計されたマップエントリを 1 日に 1 回取得します。
  • 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 Executable and Linkable Format(ELF)バイナリから BTF メタデータを実行時に解析し、構造体メンバーのオフセットをキャッシュに保存して、プリミティブ Java 配列へのバッチマップ読み取りを実行します。汎用
statsd のユーザースペース プラー累積ヒストグラム マップを毎日スケジュールに従って取得し、キャッシュに保存された履歴との差分を計算して、厳密に型指定された StatsEvent プルアトムを構築します。アトムごとに 1 つの case エントリを持つ汎用ヘルパー
テレメトリー スキーマ強く型付けされた statsd pull atom protobuf と、サーバーサイドのヒストグラムまたは値指標の集計を定義します。指標ごとにカスタム

ユーザー空間の使用量モデル

カーネル テレメトリ指標を設計する際は、指標が累積状態を表すか、順序付けられたイベント シーケンスを表すかに応じて、2 つのユーザー空間使用量モデルから選択します。次の表は、2 つのモデルを比較したものです。

プロパティアキュムレータストリーム
定義ステートフルで単調増加する累積カウンタまたはヒストグラムタイムスタンプ付きのカーネル イベントの順序付きシーケンス
アクセス パターンユーザースペースは、statsd を介して 24 時間に 1 回など、オンデマンドでポーリングします。ユーザー空間は、イベントのドロップを回避するために、リングバッファから継続的に消費します。
一般的な 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 には、GKI 6.1 以降のカーネル(android14-6.1、android15-6.6、android16-6.12、android17-6.18 など)で動作する eBPF カーネル テレメトリー プログラムが組み込まれています。各レイテンシ指標とサイズ指標では、24 時間のウィンドウで集計された 32 バケットの指数(log2)ヒストグラムが使用されます。次の表に、組み込みのカーネル テレメトリー指標を示します。

指標アトムサブシステム説明ディメンション キー
KswapdLatencyメモリ管理カーネル スワップ デーモン、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. 汎用 JNI BTF リーダーの BTF 構造体のレイアウトの互換性を検証します。
  3. statsd に pull atom を登録します。
  4. pull アトム 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++ コードを記述する必要はありません。BPF マップ エントリをコンパイル時にハードコードされた C 構造体にキャストする代わりに、汎用 JNI リーダーは実行時に 3 つのフェーズで構造体レイアウトを動的に解決します。

  • ELF からマップへの関連付け: JNI レイヤは、libbpf btf__parse 関数を使用して、/system/etc/bpf/kernelstats/directReclaim.bpf などのディスク上のコンパイル済み BPF ELF ファイルから BTF メタデータを直接解析します。
  • BTF 型の解決: 最初のアクセス時に、リーダーはマップのキーと値の C 構造体の構造表現を構築してキャッシュに保存し、構造体メンバー、整数と列挙型のバイト幅、配列要素のサイズをベースポインタから計算します。
  • バッチ ルックアップとシリアル化: statsd がマップをプルすると、JNI レイヤは sys_bpf syscall に単一の BPF_MAP_LOOKUP_BATCH コマンドを発行して、すべてのハッシュ エントリを一括で読み取り、解決されたキー ID とバケット配列を SetLongArrayRegion JNI 関数を使用してプリミティブ Java long[] 配列に直接コピーします。

将来のリリースで BPF C 構造体が変更された場合、再コンパイルされた BTF セクションが自動的に更新され、JNI リーダーが実行時に適応するため、JNI の再コンパイルは不要です。

pull アトムを statsd に登録

StatsPullAtomService.java では、汎用 pullKernelTelemetry ヘルパーが固定された BPF マップを読み取り、前のプルでキャッシュに保存されたカウントに対するバケットの差分を計算し、StatsEvent エントリを出力します。新しいカーネル テレメトリー指標を追加するには、次の 3 つの小さな追加のみが必要です。

// 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";

pull アトムの protobuf を定義する

BPF マップで生成されるディメンション キーと 32 バケットの log2 ヒストグラム配列に一致するように、frameworks/proto_logging/ で厳密に型指定されたプルアトム メッセージを定義します。

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;
}