Как собирать телеметрию ядра с помощью eBPF

На этой странице представлен обзор фреймворка телеметрии ядра Extended Berkeley Packet Filter (eBPF) в Android, описана архитектура фреймворка, встроенные показатели и способы добавления новых показателей ядра.

В Android есть унифицированная платформа телеметрии ядра eBPF, которая собирает показатели ядра с низкими накладными расходами и предоставляет их сервисам пользовательского пространства, таким как statsd. Прикрепляя переносимые программы eBPF к точкам трассировки ядра и kprobes, фреймворк агрегирует высокочастотные события ядра на месте и экспортирует компактные гистограммы или счетчики, не изменяя GKI и не нарушая работу интерфейса модуля ядра (KMI).

Традиционные подходы к телеметрии ядра основаны на специальных узлах procfs или sysfs, исправлениях ядра вне дерева или хуках поставщиков, которые требуют специальной настройки ядра, SELinux, Java Native Interface (JNI) и сервисов пользовательского пространства. Фреймворк телеметрии ядра eBPF стандартизирует этот конвейер для общих ядер Android (ACK) и дает следующие преимущества:

  • Предотвращает отток ABI и KMI ядра. Программы eBPF подключаются к существующим точкам трассировки ядра или kprobes без изменения исходного кода ядра, изменения двоичного интерфейса приложения ядра (ABI) или добавления хуков поставщиков Android, таких как TRACE_ANDROID_VH_*. Производители устройств и партнеры, выпускающие системы на кристалле, могут использовать телеметрию eBPF вместо добавления новых хуков поставщиков телеметрии или для перехода с существующих хуков.
  • Сохраняет переносимость между версиями ядра. Использует BPF Compile Once – Run Everywhere (CO-RE) и BPF Type Format (BTF), чтобы одна скомпилированная программа eBPF автоматически адаптировалась к изменениям внутренней структуры ядра в разных версиях GKI, например android14-6.1 и выше.
  • Минимизация нагрузки на систему. Фильтрует и агрегирует высокочастотные события ядра непосредственно в источнике в компактные логарифмические (log2) гистограммы с основанием 2 и 32 интервалами, обычно менее 1 КБ на карту BPF. Пространство пользователя получает агрегированные записи карты один раз в день, используя один пакетный системный вызов BPF_MAP_LOOKUP_BATCH.
  • Упрощает интеграцию SELinux и JNI. Закрепляет карты телеметрии в каталоге /sys/fs/bpf/kernelstats/, чтобы применить наследование типа SELinux на уровне каталога fs_bpf_kernelstats, и разрешает смещения членов структуры во время выполнения в JNI с помощью метаданных BTF, чтобы устранить пользовательский код синтаксического анализа C++ при добавлении новых показателей.

Подробнее о том, как Android загружает и закрепляет программы и карты eBPF, можно узнать в статье Расширение ядра с помощью eBPF.

Обзор архитектуры

Конвейер телеметрии ядра eBPF отделяет общие компоненты инфраструктуры от определений, относящихся к определенным показателям, поэтому добавление нового показателя ядра требует минимальных изменений в коде. В таблице ниже описаны компоненты конвейера.

КомпонентРоль в конвейереОбласть действия
Загрузчик BPF, bpfloaderЗагружает скомпилированные программы eBPF из /system/etc/bpf/kernelstats/ при загрузке, если они совместимы с запущенным ядром, и закрепляет карты в /sys/fs/bpf/kernelstats/. Если функция отключена или не поддерживается, загрузка пропускается.Generic
Правило SELinux, fs_bpf_kernelstatsПрименяет метки SELinux на уровне каталога к /sys/fs/bpf/kernelstats/, чтобы system_server мог считывать карты телеметрии без обновления политики SELinux для каждого показателя.Generic
Программа eBPF в system/bpfprogs/telemetry/Прикрепляется к точкам трассировки ядра или kprobes, фильтрует события и агрегирует показатели в карты BPF.Настраиваемый для каждого показателя
JNI BTF readerВыполняет синтаксический анализ метаданных BTF из скомпилированного исполняемого и связываемого формата BPF (ELF) во время выполнения, кэширует смещения членов структуры и выполняет пакетное чтение карт в примитивные массивы Java.Generic
Утилита для извлечения данных из пользовательского пространства в statsdЕжедневно извлекает накопительные гистограммы, вычисляет дельты по сравнению с сохраненными данными и создает атомы извлечения StatsEvent со строгой типизацией.Универсальный помощник с одной записью case на атом
Схема телеметрииОпределяет строго типизированный атомарный protobuf-файл statsd и агрегации гистограмм или показателей ценности на стороне сервера.Настраиваемый для каждого показателя

Модели потребления в пространстве пользователя

При разработке метрики телеметрии ядра выберите одну из двух моделей потребления пространства пользователя в зависимости от того, представляет ли метрика совокупное состояние или упорядоченную последовательность событий. В таблице ниже сравниваются две модели.

СвойствоАккумуляторыПотоки
ОпределениеСчетчики или гистограммы с сохранением состояния и монотонным увеличениемУпорядоченная последовательность событий ядра с временными метками.
Шаблон доступаОпрос пространства пользователя по запросу, например раз в 24 часа с помощью statsd.Пространство пользователя постоянно считывает данные из кольцевого буфера, чтобы избежать потери событий.
Типичные типы карт BPFBPF_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. Для каждого показателя задержки и размера используется экспоненциальная (log2) гистограмма с 32 сегментами, агрегированная за 24-часовой период. В таблице ниже перечислены встроенные показатели телеметрии ядра.

Атом показателяПодсистемаОписаниеКлюч параметра
KswapdLatencyУправление памятьюДемон подкачки ядра, kswapd, задержка выполнения при переходе из режима ожидания в спящий режим в микросекундахНеоднородный доступ к памяти (NUMA) node_id
DirectReclaimLatencyУправление памятьюЗадержка синхронного прямого освобождения памяти в микросекундахСостояние процесса oom_score_adj при нехватке памяти
CompactionDurationУправление памятьюДлительность выполнения системной дефрагментации памяти в микросекундах, заменяющая устаревшие хуки поставщика дефрагментацииНа уровне системы
CmaAllocationLatencyУправление памятьюЗадержка выделения страниц в непрерывном распределителе памяти (CMA) в микросекундахНазвание кучи CMA
DmabufRequestSizesУправление памятьюРазмеры запросов на выделение буфера DMA-BUF в байтахНазвание кучи экспортера
SuspendResumeLatencyУправление питаниемЗадержка при переходе между этапами приостановки и возобновления работы ядра (в микросекундах)Приостановить этап действия

Добавить показатель телеметрии ядра

Чтобы добавить новый показатель аккумулятора в конвейер телеметрии ядра eBPF, выполните следующие действия по порядку:

  1. Напишите программу eBPF в system/bpfprogs/telemetry/ и соберите двоичный файл в /system/etc/bpf/kernelstats/.
  2. Проверьте совместимость макета структуры BTF для универсального средства чтения BTF JNI.
  3. Зарегистрируйте pull-атом в statsd.
  4. Определите схему protobuf для атома pull.

В следующих разделах в качестве примера используется 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);

В обработчиках точек трассировки запишите временную метку начала при входе, вычислите сегмент задержки log2 при выходе с помощью GET_LATENCY_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++. Вместо того чтобы приводить записи карты BPF к жестко заданным структурам C во время компиляции, универсальный считыватель JNI динамически разрешает макеты структур во время выполнения в три этапа:

  • Сопоставление ELF с картой. Уровень JNI анализирует метаданные BTF непосредственно из скомпилированного файла ELF BPF на диске, например /system/etc/bpf/kernelstats/directReclaim.bpf, с помощью функции libbpf btf__parse.
  • Разрешение типа BTF. При первом доступе программа чтения создает и кеширует структурное представление ключа и значения C-структур карты, вычисляя кумулятивные смещения байтов для членов структуры, ширину байтов для целых чисел и перечислений, а также размеры элементов массива на основе базового указателя.
  • Пакетный поиск и сериализация. Когда statsd извлекает карту, уровень JNI отправляет одну команду BPF_MAP_LOOKUP_BATCH системному вызову sys_bpf, чтобы массово прочитать все записи хеша и скопировать разрешенные идентификаторы ключей и массивы сегментов непосредственно в примитивные массивы long[] Java с помощью функции JNI SetLongArrayRegion.

Если в будущем выпуске структура C BPF изменится, перекомпилированный раздел BTF обновится автоматически, а средство чтения JNI адаптируется во время выполнения без необходимости перекомпиляции JNI.

Как зарегистрировать атом pull в statsd

В StatsPullAtomService.java общий вспомогательный инструмент pullKernelTelemetry считывает закрепленную карту BPF, вычисляет дельты сегментов по сравнению с кэшированными счетчиками предыдущего извлечения и генерирует записи 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";

Определите протокол буфера атома pull

Определите сообщение атома с сильной типизацией в frameworks/proto_logging/, чтобы оно соответствовало ключу параметра и массиву гистограммы log2 с 32 сегментами, созданному картой 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;
}