Coletar telemetria do kernel com eBPF

Esta página oferece uma visão geral da estrutura de telemetria do kernel do Extended Berkeley Packet Filter (eBPF) no Android, descrevendo a arquitetura da estrutura, as métricas integradas e como adicionar novas métricas do kernel.

O Android inclui uma estrutura unificada de telemetria do kernel do eBPF para coletar métricas de kernel de baixa sobrecarga e expor essas métricas a serviços do espaço do usuário, como statsd. Ao anexar programas portáteis do eBPF a tracepoints e kprobes do kernel, a estrutura agrega eventos de kernel de alta frequência no local e exporta histogramas ou contadores compactos sem modificar a imagem genérica do kernel (GKI, na sigla em inglês) ou interromper a interface do módulo do kernel (KMI, na sigla em inglês).

As abordagens tradicionais da telemetria do kernel dependem de nós procfs ou sysfs personalizados, patches de kernel fora da árvore ou hooks do fornecedor que exigem encanamento personalizado no kernel, SELinux, Java Native Interface (JNI) e serviços do espaço do usuário. A estrutura de telemetria do kernel do eBPF padroniza esse pipeline em kernels comuns do Android (ACKs, na sigla em inglês) e oferece os seguintes benefícios:

  • Evita a rotatividade da ABI e da KMI do kernel:anexa programas do eBPF a tracepoints ou kprobes do kernel sem modificar o código-fonte do kernel, mudar a interface binária do aplicativo do kernel (ABI, na sigla em inglês) ou adicionar hooks do fornecedor do Android, como TRACE_ANDROID_VH_*. Os fabricantes de dispositivos e os parceiros de sistema em chip (SoC, na sigla em inglês) podem usar a telemetria do eBPF em vez de adicionar novos hooks do fornecedor de telemetria ou migrar hooks do fornecedor atuais.
  • Mantém a portabilidade entre versões do kernel:usa o BPF Compile Once - Run Everywhere (CO-RE) e o BPF Type Format (BTF) para que um único programa eBPF compilado se adapte automaticamente às mudanças internas de layout de struct do kernel em versões da GKI, como android14-6.1 e mais recentes.
  • Minimiza a sobrecarga de integridade do sistema:filtra e agrega eventos de kernel de alta frequência diretamente na origem em histogramas logarítmicos de base 2 (log2) de 32 buckets compactos, normalmente com menos de 1 KB por mapa BPF. O espaço do usuário recupera entradas de mapa agregadas uma vez por dia usando uma única syscall de lote BPF_MAP_LOOKUP_BATCH.
  • Simplifica a integração do SELinux e da JNI:fixa mapas de telemetria em /sys/fs/bpf/kernelstats/ para aplicar a herança de tipo fs_bpf_kernelstats do SELinux no nível do diretório e resolve deslocamentos de membros de struct no tempo de execução na JNI usando metadados BTF para eliminar o código de análise C++ personalizado ao adicionar novas métricas.

Para mais informações sobre como o Android carrega e fixa programas e mapas do eBPF, consulte Estender o kernel com o eBPF.

Visão geral da arquitetura

O pipeline de telemetria do kernel do eBPF separa os componentes de infraestrutura genéricos das definições específicas da métrica para que a adição de uma nova métrica do kernel exija mudanças mínimas de código. A tabela a seguir descreve os componentes do pipeline:

ComponenteFunção no pipelineEscopo
Carregador de BPF, bpfloaderCarrega programas eBPF compilados de /system/etc/bpf/kernelstats/ na inicialização, se compatível com o kernel em execução, e fixa mapas em /sys/fs/bpf/kernelstats/. Ignora o carregamento de forma limpa quando desativado ou sem suporte.Genérico
Política do SELinux, fs_bpf_kernelstatsAplica rótulos do SELinux no nível do diretório a /sys/fs/bpf/kernelstats/ para que system_server possa ler mapas de telemetria sem atualizações de política do SELinux por métrica.Genérico
Programa eBPF em system/bpfprogs/telemetry/Anexa a tracepoints ou kprobes do kernel, filtra eventos e agrega métricas em mapas BPF.Personalizado por métrica
Leitor de BTF da JNIAnalisa metadados BTF do binário compilado do formato executável e vinculável (ELF, na sigla em inglês) do BPF no tempo de execução, armazena em cache os deslocamentos de membros de struct e realiza leituras de mapa em lote em matrizes primitivas Java.Genérico
Extrator do espaço do usuário em statsdExtrai mapas de histograma cumulativos em uma programação diária, calcula deltas em relação ao histórico armazenado em cache e cria átomos de extração StatsEvent fortemente tipados.Assistente genérico com uma única entrada case por átomo
Esquema de telemetriaDefine o protobuf de átomo de extração statsd fortemente tipado e agregações de métricas de histograma ou valor do lado do servidor.Personalizado por métrica

Modelos de consumo do espaço do usuário

Ao criar uma métrica de telemetria do kernel, escolha entre dois modelos de consumo do espaço do usuário, dependendo se a métrica representa um estado cumulativo ou uma sequência de eventos ordenada. A tabela a seguir compara os dois modelos:

PropriedadeAcumuladoresStreams
DefiniçãoContadores ou histogramas cumulativos com estado e monotonicamente crescentesSequência ordenada de eventos do kernel com carimbo de data/hora
Padrão de acessoO espaço do usuário pesquisa sob demanda, como uma vez a cada 24 horas pelo statsdO espaço do usuário consome continuamente de um buffer de anel para evitar eventos descartados
Tipos de mapa 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 simultaneidadeAs pesquisas de mapa do kernel são atômicas. Vários leitores podem pesquisar sem bloqueio do espaço do usuárioA semântica de leitura destrutiva exige um único encadeamento de leitor que envia para os consumidores
Exemplos de casos de usoHistogramas de latência de gerenciamento de memória, tamanhos de alocação de buffer de acesso direto à memória (DMA-BUF, na sigla em inglês), iteradores de origem de ativaçãoEventos de trace do agendador filtrados, traces de energia de subsistema de alta frequência

Métricas de telemetria do kernel integradas

O Android inclui programas de telemetria do kernel do eBPF integrados que funcionam em kernels GKI 6.1 e mais recentes, incluindo android14-6.1, android15-6.6, android16-6.12 e android17-6.18. Cada métrica de latência e tamanho usa um histograma exponencial (log2) de 32 buckets agregado em uma janela de 24 horas. A tabela a seguir lista as métricas de telemetria do kernel integradas:

Átomo métricoSubsistemaDescriçãoChave de dimensão
KswapdLatencyGerenciamento de memóriaLatência de execução de ativação para suspensão do daemon de troca de kernel, kswapd, em microssegundosnode_id de acesso à memória não uniforme (NUMA, na sigla em inglês)
DirectReclaimLatencyGerenciamento de memóriaLatência de recuperação de memória direta síncrona em microssegundosEstado oom_score_adj de falta de memória (OOM, na sigla em inglês) do processo
CompactionDurationGerenciamento de memóriaDuração da execução da compactação de memória em todo o sistema em microssegundos, substituindo hooks de fornecedor de compactação legadosEm todo o sistema
CmaAllocationLatencyGerenciamento de memóriaLatência de alocação de página do alocador de memória contígua (CMA, na sigla em inglês) em microssegundosNome do heap CMA
DmabufRequestSizesGerenciamento de memóriaTamanhos de solicitação de alocação de DMA-BUF em bytesNome do heap do exportador
SuspendResumeLatencyGerenciamento de energiaLatência nas fases de suspensão e retomada do kernel em microssegundosFase de ação de suspensão

Adicionar uma métrica de telemetria do kernel

Para adicionar uma nova métrica de acumulador ao pipeline de telemetria do kernel do eBPF, siga estas etapas em ordem:

  1. Grave o programa eBPF em system/bpfprogs/telemetry/ e crie o binário em /system/etc/bpf/kernelstats/.
  2. Verifique a compatibilidade do layout de struct BTF para o leitor de BTF da JNI genérico.
  3. Registre o átomo de extração em statsd.
  4. Defina o esquema de protobuf do átomo de extração.

As seções a seguir usam DirectReclaimLatency (directReclaim.c) como exemplo.

Gravar o programa eBPF

Em system/bpfprogs/telemetry/, inclua <bpf_telemetry.h> e defina a chave do mapa e o mapa do acumulador de saída. Use identificadores de números inteiros ou enums em chaves de mapa BPF em vez de strings brutas para minimizar o consumo de memória do mapa e preservar a privacidade:

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

Nos gerenciadores de tracepoint, registre o carimbo de data/hora de início na entrada, calcule o bucket de latência log2 na saída usando GET_LATENCY_BUCKET e incremente a entrada do mapa do acumulador com 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;
}

Verificar a compatibilidade do layout de struct BTF

Não é necessário gravar código C++ da JNI personalizado ao adicionar um novo mapa de histograma, desde que a chave do mapa e as structs de valor usem tipos de matriz de números inteiros, enum e fixos padrão. Em vez de converter entradas de mapa BPF em structs C fixadas no código no tempo de compilação, o leitor de JNI genérico resolve layouts de struct dinamicamente no tempo de execução em três fases:

  • Associação de ELF a mapa:a camada JNI analisa metadados BTF diretamente do arquivo ELF do BPF compilado no disco, como /system/etc/bpf/kernelstats/directReclaim.bpf, usando a função btf__parse libbpf.
  • Resolução de tipo BTF:no primeiro acesso, o leitor cria e armazena em cache uma representação estrutural das structs C de chave e valor do mapa, calculando deslocamentos de bytes cumulativos para membros de struct, larguras de bytes de números inteiros e enum e tamanhos de elementos de matriz do ponteiro base.
  • Pesquisa e serialização em lote:quando statsd extrai o mapa, a camada JNI emite um único comando BPF_MAP_LOOKUP_BATCH para a syscall sys_bpf para ler todas as entradas de hash em massa e copia os IDs de chave resolvidos e as matrizes de bucket diretamente para matrizes long[] Java primitivas usando a função SetLongArrayRegion da JNI.

Se uma struct C do BPF mudar em uma versão futura, a seção BTF recompilada será atualizada automaticamente e o leitor de JNI será adaptado no tempo de execução sem exigir a recompilação da JNI.

Registrar o átomo de extração no statsd

Em StatsPullAtomService.java, o auxiliar genérico pullKernelTelemetry lê o mapa BPF fixado, calcula deltas de bucket em relação às contagens armazenadas em cache da extração anterior e emite entradas StatsEvent. A adição de uma nova métrica de telemetria do kernel exige apenas três pequenas adições:

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

Definir o protobuf do átomo de extração

Defina a mensagem de átomo de extração fortemente tipada em frameworks/proto_logging/ para corresponder à chave de dimensão e à matriz de histograma log2 de 32 buckets produzida pelo mapa 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;
}