Raccogliere la telemetria del kernel con eBPF

Questa pagina fornisce una panoramica del framework di telemetria del kernel Extended Berkeley Packet Filter (eBPF) in Android, descrivendo l'architettura del framework, le metriche integrate e come aggiungere nuove metriche del kernel.

Android include un framework di telemetria del kernel eBPF unificato per la raccolta di metriche del kernel a basso overhead e l'esposizione di queste metriche ai servizi dello spazio utente come statsd. Collegando i programmi eBPF portatili ai tracepoint e ai kprobe del kernel, il framework aggrega gli eventi del kernel ad alta frequenza in loco ed esporta istogrammi o contatori compatti senza modificare l'immagine del kernel generico (GKI) o interrompere l'interfaccia del modulo kernel (KMI).

Gli approcci tradizionali alla telemetria del kernel si basano su nodi procfs o sysfs personalizzati, patch del kernel out-of-tree o hook del fornitore che richiedono un plumbing personalizzato tra kernel, SELinux, Java Native Interface (JNI) e servizi dello spazio utente. Il framework di telemetria del kernel eBPF standardizza questa pipeline su tutti i kernel comuni di Android (ACK) e offre i seguenti vantaggi:

  • Evita il churn dell'ABI del kernel e del KMI: collega i programmi eBPF ai tracepoint o ai kprobe del kernel esistenti senza modificare il codice sorgente del kernel, cambiare l'interfaccia binaria dell'applicazione del kernel (ABI) o aggiungere hook del fornitore Android come TRACE_ANDROID_VH_*. I produttori di dispositivi e i partner di system-on-chip (SoC) possono utilizzare la telemetria eBPF anziché aggiungere nuovi hook del fornitore di telemetria o per eseguire la migrazione dagli hook del fornitore esistenti.
  • Mantiene la portabilità tra le versioni kernel: utilizza BPF Compile Once - Run Everywhere (CO-RE) e BPF Type Format (BTF) in modo che un singolo programma eBPF compilato si adatti automaticamente alle modifiche del layout della struttura interna del kernel tra le versioni GKI, come android14-6.1 e versioni successive.
  • Riduce al minimo l'overhead di integrità del sistema: filtra e aggrega gli eventi del kernel ad alta frequenza direttamente all'origine in istogrammi logaritmici (log2) base 2 compatti a 32 bucket, in genere inferiori a 1 KB per mappa BPF. Lo spazio utente recupera le voci della mappa aggregate una volta al giorno utilizzando una singola chiamata di sistema batch BPF_MAP_LOOKUP_BATCH.
  • Semplifica l'integrazione di SELinux e JNI: fissa le mappe di telemetria in /sys/fs/bpf/kernelstats/ per applicare l'ereditarietà del tipo SELinux fs_bpf_kernelstats a livello di directory e risolve gli offset dei membri della struttura in fase di runtime in JNI utilizzando i metadati BTF per eliminare il codice di analisi C++ personalizzato quando si aggiungono nuove metriche.

Per saperne di più su come Android carica e fissa i programmi eBPF e le mappe, consulta Estendere il kernel con eBPF.

Panoramica dell'architettura

La pipeline di telemetria del kernel eBPF separa i componenti dell'infrastruttura generica dalle definizioni specifiche delle metriche, in modo che l'aggiunta di una nuova metrica del kernel richieda modifiche minime al codice. La seguente tabella descrive i componenti della pipeline:

ComponenteRuolo nella pipelineAmbito
Caricatore BPF, bpfloaderCarica i programmi eBPF compilati da /system/etc/bpf/kernelstats/ all'avvio se compatibili con il kernel in esecuzione e fissa le mappe a /sys/fs/bpf/kernelstats/. Salta il caricamento in modo pulito quando è disabilitato o non supportato.Generica
Policy SELinux, fs_bpf_kernelstatsApplica le etichette SELinux a livello di directory a /sys/fs/bpf/kernelstats/ in modo che system_server possa leggere le mappe di telemetria senza aggiornamenti delle policy SELinux per metrica.Generica
Programma eBPF in system/bpfprogs/telemetry/Si collega ai tracepoint o ai kprobe del kernel, filtra gli eventi e aggrega le metriche nelle mappe BPF.Personalizzata per metrica
Lettore BTF JNIAnalizza i metadati BTF dal file binario ELF (Executable and Linkable Format) BPF compilato in fase di runtime, memorizza nella cache gli offset dei membri della struttura ed esegue letture batch della mappa in array Java primitivi.Generica
Puller dello spazio utente in statsdEstrae le mappe degli istogrammi cumulativi in base a una pianificazione giornaliera, calcola le differenze rispetto alla cronologia memorizzata nella cache e crea atomi di pull StatsEvent con tipo di dati definito.Helper generico con una singola voce case per atomo
Schema di telemetriaDefinisce il protobuf dell'atomo di pull statsd con tipo di dati definito e le aggregazioni di istogrammi o metriche di valore lato server.Personalizzata per metrica

Modelli di consumo dello spazio utente

Quando progetti una metrica di telemetria del kernel, scegli tra due modelli di consumo dello spazio utente a seconda che la metrica rappresenti uno stato cumulativo o una sequenza di eventi ordinata. La seguente tabella confronta i due modelli:

ProprietàAccumulatoriStream
DefinizioneContatori o istogrammi cumulativi con stato e aumento monotonoSequenza ordinata di eventi del kernel con timestamp
Pattern di accessoLo spazio utente esegue il polling on demand, ad esempio una volta ogni 24 ore tramite statsdLo spazio utente utilizza continuamente un buffer circolare per evitare eventi eliminati
Tipi di mappe BPF tipiciBPF_MAP_TYPE_HASH, BPF_MAP_TYPE_LRU_HASH, BPF_MAP_TYPE_PERCPU_HASH, BPF_MAP_TYPE_LRU_PERCPU_HASHBPF_MAP_TYPE_RINGBUF
Modello di concorrenzaLe ricerche di mappe del kernel sono atomiche; più lettori possono eseguire il polling senza bloccare lo spazio utenteLa semantica di lettura distruttiva richiede un singolo thread di lettura che invia ai consumatori
Esempi di casi d'usoIstogrammi di latenza di gestione della memoria, dimensioni di allocazione del buffer di accesso diretto alla memoria (DMA-BUF), iteratori di origine di riattivazioneEventi di traccia dello scheduler filtrati, tracce di alimentazione del sottosistema ad alta frequenza

Metriche di telemetria del kernel integrate

Android include programmi di telemetria del kernel eBPF integrati che funzionano su kernel GKI 6.1 e versioni successive, tra cui android14-6.1, android15-6.6, android16-6.12 e android17-6.18. Ogni metrica di latenza e dimensione utilizza un istogramma esponenziale (log2) a 32 bucket aggregato su una finestra di 24 ore. La seguente tabella elenca le metriche di telemetria del kernel integrate:

Atomo di metricaOOBDescrizioneChiave di dimensione
KswapdLatencyGestione della memoriaLatenza di esecuzione di wake-to-sleep del daemon di swap del kernel, kswapd, in microsecondinode_id di accesso alla memoria non uniforme (NUMA)
DirectReclaimLatencyGestione della memoriaLatenza di recupero della memoria diretta sincrona in microsecondiStato oom_score_adj di esaurimento della memoria (OOM) del processo
CompactionDurationGestione della memoriaDurata di esecuzione della compattazione della memoria a livello di sistema in microsecondi, in sostituzione degli hook del fornitore di compattazione legacyA livello di sistema
CmaAllocationLatencyGestione della memoriaLatenza di allocazione delle pagine di Contiguous Memory Allocator (CMA) in microsecondiNome heap CMA
DmabufRequestSizesGestione della memoriaDimensioni delle richieste di allocazione DMA-BUF in byteNome heap esportatore
SuspendResumeLatencyGestione alimentazioneLatenza tra le fasi di sospensione e ripresa del kernel in microsecondiFase di azione di sospensione

Aggiungere una metrica di telemetria del kernel

Per aggiungere una nuova metrica di accumulatore alla pipeline di telemetria del kernel eBPF, completa i seguenti passaggi in ordine:

  1. Scrivi il programma eBPF in system/bpfprogs/telemetry/ e crea il file binario in /system/etc/bpf/kernelstats/.
  2. Verifica la compatibilità del layout della struttura BTF per il lettore BTF JNI generico.
  3. Registra l'atomo di pull in statsd.
  4. Definisci lo schema protobuf dell'atomo di pull.

Le seguenti sezioni utilizzano DirectReclaimLatency (directReclaim.c) come esempio.

Scrivere il programma eBPF

In system/bpfprogs/telemetry/, includi <bpf_telemetry.h> e definisci la chiave della mappa e la mappa dell'accumulatore di output. Utilizza identificatori interi o enum nelle chiavi della mappa BPF anziché stringhe non elaborate per ridurre al minimo il footprint della memoria della mappa e preservare la privacy:

#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);

Nei gestori dei tracepoint, registra il timestamp di inizio all'ingresso, calcola il bucket di latenza log2 all'uscita utilizzando GET_LATENCY_BUCKET e incrementa la voce della mappa dell'accumulatore con 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;
}

Verificare la compatibilità del layout della struttura BTF

Non è necessario scrivere codice JNI C++ personalizzato quando aggiungi una nuova mappa di istogrammi, a condizione che le strutture della chiave e del valore della mappa utilizzino tipi di array interi, enum e fissi standard. Anziché eseguire il cast delle voci della mappa BPF a strutture C hardcoded in fase di compilazione, il lettore JNI generico risolve i layout delle strutture in modo dinamico in fase di runtime in tre fasi:

  • Associazione ELF-mappa: il livello JNI analizza i metadati BTF direttamente dal file ELF BPF compilato su disco, ad esempio /system/etc/bpf/kernelstats/directReclaim.bpf, utilizzando la funzione btf__parse di libbpf.
  • Risoluzione del tipo BTF: al primo accesso, il lettore crea e memorizza nella cache una rappresentazione strutturale delle strutture C della chiave e del valore della mappa, calcolando gli offset di byte cumulativi per i membri della struttura, le larghezze di byte interi ed enum e le dimensioni degli elementi dell'array dal puntatore di base.
  • Ricerca e serializzazione batch: quando statsd estrae la mappa, il livello JNI emette un singolo comando BPF_MAP_LOOKUP_BATCH alla chiamata di sistema sys_bpf per leggere tutte le voci hash in blocco e copia gli ID delle chiavi risolte e gli array di bucket direttamente negli array long[] Java primitivi utilizzando la funzione JNI SetLongArrayRegion.

Se una struttura BPF C cambia in una release futura, la sezione BTF ricompilata si aggiorna automaticamente e il lettore JNI si adatta in fase di runtime senza richiedere la ricompilazione JNI.

Registrare l'atomo di pull in statsd

In StatsPullAtomService.java, l'helper generico pullKernelTelemetry legge la mappa BPF fissa, calcola le differenze dei bucket rispetto ai conteggi memorizzati nella cache dell'estrazione precedente ed emette voci StatsEvent. L'aggiunta di una nuova metrica di telemetria del kernel richiede solo tre piccole aggiunte:

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

Definire il protobuf dell'atomo di pull

Definisci il messaggio dell'atomo di pull con tipo di dati definito in frameworks/proto_logging/ in modo che corrisponda alla chiave di dimensione e all'array di istogrammi log2 a 32 bucket prodotto dalla mappa BPF:

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