Recopila datos de telemetría del kernel con eBPF

En esta página, se proporciona una descripción general del framework de telemetría del kernel de Extended Berkeley Packet Filter (eBPF) en Android, en la que se describe la arquitectura del framework, las métricas integradas y cómo agregar nuevas métricas del kernel.

Android incluye un framework de telemetría del kernel de eBPF unificado para recopilar métricas del kernel de baja sobrecarga y exponerlas a servicios del espacio de usuario, como statsd. Al adjuntar programas de eBPF portátiles a los puntos de seguimiento del kernel y a los kprobes, el framework agrega eventos del kernel de alta frecuencia en su lugar y exporta histogramas o contadores compactos sin modificar la imagen genérica del kernel (GKI) ni interrumpir la interfaz del módulo del kernel (KMI).

Los enfoques tradicionales de la telemetría del kernel se basan en nodos procfs o sysfs personalizados, parches del kernel fuera del árbol o hooks del proveedor que requieren una canalización personalizada en el kernel, SELinux, la interfaz nativa de Java (JNI) y los servicios del espacio de usuario. El framework de telemetría del kernel de eBPF estandariza esta canalización en los kernels comunes de Android (ACKs) y proporciona los siguientes beneficios:

  • Evita la rotación de ABI y KMI del kernel: Adjunta programas de eBPF a los puntos de seguimiento o kprobes del kernel existentes sin modificar el código fuente del kernel, cambiar la interfaz binaria de la aplicación del kernel (ABI) ni agregar hooks del proveedor de Android, como TRACE_ANDROID_VH_*. Los fabricantes de dispositivos y los socios de sistema en chip (SoC) pueden usar la telemetría de eBPF en lugar de agregar nuevos hooks del proveedor de telemetría o para migrar de los hooks del proveedor existentes.
  • Mantiene la portabilidad en las versiones del kernel: Usa BPF Compile Once - Run Everywhere (CO-RE) y BPF Type Format (BTF) para que un solo programa de eBPF compilado se adapte automáticamente a los cambios internos del diseño de la estructura del kernel en las versiones de GKI, como android14-6.1 y versiones posteriores.
  • Minimiza la sobrecarga de estado del sistema: Filtra y agrega eventos del kernel de alta frecuencia directamente en la fuente en histogramas logarítmicos base 2 (log2) compactos de 32 buckets, por lo general, menos de 1 KB por mapa de BPF. El espacio de usuario recupera las entradas de mapa agregadas una vez al día con una sola llamada al sistema BPF_MAP_LOOKUP_BATCH por lotes.
  • Simplifica la integración de SELinux y JNI: Fija los mapas de telemetría en /sys/fs/bpf/kernelstats/ para aplicar la herencia de tipo SELinux fs_bpf_kernelstats a nivel de directorio y resuelve los desplazamientos de miembros de la estructura en el tiempo de ejecución en JNI con metadatos de BTF para eliminar el código de análisis de C++ personalizado cuando se agregan métricas nuevas.

Para obtener más información sobre cómo Android carga y fija programas y mapas de eBPF, consulta Extiende el kernel con eBPF.

Descripción general de la arquitectura

La canalización de telemetría del kernel de eBPF separa los componentes de infraestructura genéricos de las definiciones específicas de las métricas, de modo que agregar una nueva métrica del kernel requiere cambios mínimos en el código. En la siguiente tabla, se describen los componentes de la canalización:

ComponenteRol en la canalizaciónAlcance
Cargador de BPF, bpfloaderCarga programas de eBPF compilados desde /system/etc/bpf/kernelstats/ durante el inicio si es compatible con el kernel en ejecución y fija mapas en /sys/fs/bpf/kernelstats/. Omite la carga de forma limpia cuando está inhabilitado o no es compatible.Genérico
Política de SELinux, fs_bpf_kernelstatsAplica etiquetas de SELinux a nivel de directorio a /sys/fs/bpf/kernelstats/ para que system_server pueda leer mapas de telemetría sin actualizaciones de la política de SELinux por métrica.Genérico
Programa de eBPF en system/bpfprogs/telemetry/Se adjunta a los puntos de seguimiento o kprobes del kernel, filtra eventos y agrega métricas en mapas de BPF.Personalizado por métrica
Lector de BTF de JNIAnaliza los metadatos de BTF del objeto binario compilado de BPF Executable and Linkable Format (ELF) en el tiempo de ejecución, almacena en caché los desplazamientos de miembros de la estructura y realiza lecturas de mapas por lotes en arrays primitivos de Java.Genérico
Extractor del espacio de usuario en statsdExtrae mapas de histogramas acumulativos según una programación diaria, calcula deltas en el historial almacenado en caché y compila átomos de extracción StatsEvent con tipo seguro.Auxiliar genérico con una sola entrada case por átomo
Esquema de telemetríaDefine el protobuf de átomo de extracción statsd con tipo seguro y las agregaciones de histogramas o métricas de valores del servidor.Personalizado por métrica

Modelos de consumo del espacio de usuario

Cuando diseñes una métrica de telemetría del kernel, elige entre dos modelos de consumo del espacio de usuario según si la métrica representa un estado acumulativo o una secuencia de eventos ordenada. En la siguiente tabla, se comparan los dos modelos:

PropiedadAcumuladoresTransmisiones
DefiniciónContadores o histogramas acumulativos con estado y que aumentan de forma monotónicaSecuencia ordenada de eventos del kernel con marca de tiempo
Patrón de accesoEl espacio de usuario sondea a pedido, por ejemplo, una vez cada 24 horas a través de statsd.El espacio de usuario consume de forma continua desde un búfer de anillo para evitar eventos descartados.
Tipos de mapas de BPF típicosBPF_MAP_TYPE_HASH, BPF_MAP_TYPE_LRU_HASH, BPF_MAP_TYPE_PERCPU_HASH, BPF_MAP_TYPE_LRU_PERCPU_HASHBPF_MAP_TYPE_RINGBUF
Modelo de simultaneidadLas búsquedas de mapas del kernel son atómicas; varios lectores pueden sondear sin bloqueo del espacio de usuario.La semántica de lectura destructiva requiere un solo subproceso de lector que se envíe a los consumidores.
Ejemplos de casos de usoHistogramas de latencia de administración de la memoria, tamaños de asignación de búfer de acceso directo a la memoria (DMA-BUF), iteradores de fuente de activaciónEventos de seguimiento del programador filtrados, seguimientos de energía del subsistema de alta frecuencia

Métricas de telemetría del kernel integradas

Android incluye programas de telemetría del kernel de eBPF integrados que funcionan en kernels de GKI 6.1 y versiones posteriores, incluidos android14-6.1, android15-6.6, android16-6.12 y android17-6.18. Cada métrica de latencia y tamaño usa un histograma exponencial (log2) de 32 buckets agregado en un período de 24 horas. En la siguiente tabla, se enumeran las métricas de telemetría del kernel integradas:

Átomo de métricaSubsistemaDescripciónClave de dimensión
KswapdLatencyAdministración de la memoriaDaemon de intercambio de kernel, kswapd, latencia de ejecución de activación a suspensión en microsegundosAcceso a memoria no uniforme (NUMA) node_id
DirectReclaimLatencyAdministración de la memoriaLatencia de recuperación de memoria directa síncrona en microsegundosEstado oom_score_adj de proceso sin memoria (OOM)
CompactionDurationAdministración de la memoriaDuración de la ejecución de la compactación de memoria en todo el sistema en microsegundos, que reemplaza los hooks del proveedor de compactación heredadosEn todo el sistema
CmaAllocationLatencyAdministración de la memoriaLatencia de asignación de página del asignador de memoria contigua (CMA) en microsegundosNombre del montón de CMA
DmabufRequestSizesAdministración de la memoriaTamaños de solicitud de asignación de DMA-BUF en bytesNombre del montón del exportador
SuspendResumeLatencyAdministración de energíaLatencia en las etapas de suspensión y reanudación del kernel en microsegundosEtapa de acción de suspensión

Agrega una métrica de telemetría del kernel

Para agregar una nueva métrica de acumulador a la canalización de telemetría del kernel de eBPF, completa los siguientes pasos en orden:

  1. Escribe el programa de eBPF en system/bpfprogs/telemetry/ y compila el objeto binario en /system/etc/bpf/kernelstats/.
  2. Verifica la compatibilidad del diseño de la estructura de BTF para el lector de BTF de JNI genérico.
  3. Registra el átomo de extracción en statsd.
  4. Define el esquema de protobuf del átomo de extracción.

En las siguientes secciones, se usa DirectReclaimLatency (directReclaim.c) como ejemplo.

Escribe el programa de eBPF

En system/bpfprogs/telemetry/, incluye <bpf_telemetry.h> y define la clave del mapa y el mapa del acumulador de salida. Usa identificadores de números enteros o enumeraciones en las claves de mapa de BPF en lugar de cadenas sin procesar para minimizar el espacio en memoria del mapa y preservar la privacidad:

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

En los controladores de puntos de seguimiento, registra la marca de tiempo de inicio en la entrada, calcula el bucket de latencia log2 en la salida con GET_LATENCY_BUCKET y aumenta la entrada del mapa del acumulador 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;
}

Verifica la compatibilidad del diseño de la estructura de BTF

No es necesario que escribas código C++ de JNI personalizado cuando agregues un nuevo mapa de histogramas, siempre que las estructuras de clave y valor del mapa usen tipos de números enteros, enumeraciones y arrays fijos estándares. En lugar de convertir las entradas de mapa de BPF en estructuras C codificadas de forma rígida en el tiempo de compilación, el lector de JNI genérico resuelve los diseños de estructura de forma dinámica en el tiempo de ejecución en tres fases:

  • Asociación de ELF a mapa: La capa de JNI analiza los metadatos de BTF directamente desde el archivo ELF de BPF compilado en el disco, como /system/etc/bpf/kernelstats/directReclaim.bpf, con la función btf__parse de libbpf.
  • Resolución de tipo BTF: En el primer acceso, el lector compila y almacena en caché una representación estructural de las estructuras C de clave y valor del mapa, y calcula los desplazamientos de bytes acumulativos para los miembros de la estructura, los anchos de bytes de números enteros y enumeraciones, y los tamaños de elementos de array desde el puntero base.
  • Búsqueda y serialización por lotes: Cuando statsd extrae el mapa, la capa de JNI emite un solo comando BPF_MAP_LOOKUP_BATCH a la llamada al sistema sys_bpf para leer todas las entradas hash de forma masiva y copia los IDs de clave resueltos y los arrays de buckets directamente en arrays long[] primitivos de Java con la función SetLongArrayRegion de JNI.

Si una estructura C de BPF cambia en una versión futura, la sección de BTF recompilada se actualiza automáticamente y el lector de JNI se adapta en el tiempo de ejecución sin requerir la recompilación de JNI.

Registra el átomo de extracción en statsd

En StatsPullAtomService.java, el auxiliar genérico pullKernelTelemetry lee el mapa de BPF fijado, calcula las deltas de buckets en función de los recuentos almacenados en caché de la extracción anterior y emite entradas StatsEvent. Agregar una nueva métrica de telemetría del kernel requiere solo tres pequeñas adiciones:

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

Define el protobuf del átomo de extracción

Define el mensaje de átomo de extracción con tipo seguro en frameworks/proto_logging/ para que coincida con la clave de dimensión y el array de histogramas log2 de 32 buckets que produce el mapa de 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;
}