Руководство пасечника или обзор инструментария eBPF
Я во время съемок фильма Пчеловод 3 раза запустил bpftrace и ни разу об это не пожалел. Джейсон Стейтем
Ранее мы уже рассказывали кратко про то, что такое eBPF. В этой статье мы посмотрим на экосистему вокруг eBPF и на инструментарий, который используется для эксплуатации и разработки программ eBPF. Вкратце постараемся привести некоторые примеры использования данных инструментов.
bpftrace
Начнем с самой простой утилиты, которая используется для того, чтобы взаимодействовать с eBPF. Брендан Грегг в своем блоге ставит эту утилиту на первое место по легкости освоения. Поставить bpftrace, ни у кого думаю, не составит труда. После установки в /usr/sbin появятся небольшие файлы примеров с расширением .bt, с которыми можно поиграться. Исходный код и небольшие описания есть на github проекта.
Также хороший cheat sheet есть на сайте Брендана Грегга:
bpftrace предоставляет простой синтаксис для написания однострочных команд или более сложных скриптов, позволяющих отслеживать и анализировать поведение ядра и приложений в режиме реального времени.
- Мониторить системные вызовов
- Анализировать задержки ввода-вывода
- Профилировать использование CPU и памяти
- Отслеживать сетевую активность
- Диагностировать проблемы производительности
Давайте теперь взглянем на bpftrace в действии.
Этот скрипт в реальном времени мониторит создание новых процессов. Команда печатает текущее время, имя процесса, PID и родительский PID, str(args->filename) конвертирует указатель на файл в строку
bpftrace -e 'tracepoint:sched:sched_process_exec {time("%H:%M:%S "); printf("Process exec: %s (PID: %d, PPID: %d)\n", str(args->filename), pid, curtask->parent->pid); }'
Профилирование системных вызовов по процессу.
Этот скрипт профилирует системный вызов sys_enter и с интервалом каждые 10 секунд выводит кол-во системных вызовов sys_enter, а также PID процесса и имя процесса.
bpftrace -e 'tracepoint:raw_syscalls:sys_enter {@syscalls[pid, comm, args->id] = count();} interval:s:10 { print(@syscalls); clear(@syscalls); }'
Этот скрипт мониторит кол-во переключений контекста между 2-мя процессами prev_comm - процесс, который запущен перед переключением контекста, а next_comm - процесс, что будет запущен после переключения контекста.
bpftrace -e 'tracepoint:sched:sched_switch {@task_switches[args->prev_comm, args->next_comm] = count();}'
- У bpftrace есть флаг -d для запуска в режиме dry-run.
- Для сложных скриптов лучше создавать отдельные файлы .bt. Загляните как это сделано в файлах /usr/sbin/*.bt
- Для длинных сессий сбора данных используйте interval
- быстрый обзор системы по проблеме производительности
- однострочные и простые скрипты
- когда достаточно будет простых агрегаций, которые может предоставить bpftrace: подсчет, гистограммы и простая статистика
- с bpftrace не нужно иметь опыт программирования
Кроме того у bpftrace есть свой сайт, где есть страничка с однострочниками, туториалом по однострочникам, страничка с практическими занятиями
bcc
BCC (BPF Compiler Collection) - это набор инструментов и библиотек для создания эффективных программ eBPF. BCC предоставляет Python и Lua интерфейсы для написания программ, которые компилируются в eBPF-код "на лету" при запуске.
Инструкцию по установке для различных дистрибутивов можно найти по ссылке.
Также как и для bpftrace на основе BCC написано множество полезных утилит с исходным кодом, ознакомиться можно по ссылке. А огромное кол-во примеров для этих утилит можно найти здесь.
Также есть полезный туториал для bcc.
Отдельно для Python разработчиков.
Ниже на скрине представлены области применения готовых BCC инструментов:
execsnoop
инструмент из набора BCC, который отслеживает все новые системные вызовы exec(). Он показывает в реальном времени, какие программы запускаются, кем они запускаются (PID, PPID) и с какими аргументами. Это полезно для понимания того, что происходит в системе, особенно когда программы запускаются автоматически или через скрипты.
biolatency -D 5
biolatency измеряет время выполнения блочных операций ввода-вывода и отображает их в виде гистограммы. Опция -D 5 указывает выводить обновленную гистограмму каждые 5 секунд. Это помогает выявить проблемы с дисковой подсистемой, такие как медленные диски или неэффективные шаблоны доступа к данным.
Если у Вас debian, то возможно придется воспользоваться подсказкой, иначе будете получать ошибку:
Traceback (most recent call last): File "/path/of/bitehist.py", line 17, in <module> ImportError: cannot import name 'BPF' from 'bcc' (/opt/python/3.9.6/lib/python3.9/site-packages/bcc/__init__.py)
Далее напишем, что-нибудь свое:
from bcc import BPF from time import sleep # Определяем BPF программу # Импортируем необходимые заголовочные файлы bpf_text = """ #include <uapi/linux/ptrace.h> #include <linux/sched.h> // Определяем структуру для хранения PID и имени процесса struct key_t { u32 pid; char comm[TASK_COMM_LEN]; }; // Создаем хэш мапу для хранения последнего timestamp для каждого PID BPF_HASH(last_time, u32); // Создаем хэш мапу для хранения времени выполнения BPF_HASH(data, struct key_t, u64); // Аттачимся к точке трассировки sched_switch в планировщике ядра, // которая срабатывает, когда происходит переключение планировщика между // процессами TRACEPOINT_PROBE(sched, sched_switch) { // получаем PID следующего для запуска процесса // получаем текущий timestamp в наносекундах // получаем последнюю временную метку для этого PID u32 pid = args->next_pid; u64 ts = bpf_ktime_get_ns(); u64 *last = last_time.lookup(&pid); // проверяем если есть предыдущий timestamp для PID, то вычисляем дельту // между текущим timestamp и последним запуском, создаем key c PID и // именем процесса, сохраняем дельту в data if (last) { u64 delta = ts - *last; struct key_t key = {}; key.pid = pid; bpf_get_current_comm(&key.comm, sizeof(key.comm)); data.update(&key, &delta); } // Обновляем последний timestamp для этого PID last_time.update(&pid, &ts); return 0; } """ # Загружаем BPF программу, тут ранее написанная BPF программа компилируется # и загружается в ядро. b = BPF(text=bpf_text) print("Отслеживание времени выполнения процессов... Нажмите Ctrl+C для завершения.") # Запускаем в бесконечном цикле и выводим PID, COMMAND и RUNTIME (в миллисекундах). Итерируемся по data хэш мапе. try: while True: sleep(1) print("\n%-6s %-16s %-16s" % ("PID", "COMM", "RUNTIME (ms)")) for k, v in b["data"].items(): print("%-6d %-16s %-16.2f" % (k.pid, k.comm.decode('utf-8', 'replace'), v.value / 1000000)) b["data"].clear() except KeyboardInterrupt: print("Выходим...")
Этот скрипт отслеживает время выполнения процессов, используя точку трассировки shed_switch, которая вызывается при переключении задач. Для каждого процесса скрипт сохраняет время последнего запуска и вычисляет разницу между текущим и предыдущим запуском. Результаты выводятся каждую секунду, показывая PID процесса, имя процесса и время его выполнения в миллисекундах.
- Используйте готовые инструменты BCC для стандартных задач трассировки
- Используйте агрегацию данных (карты, гистограммы) вместо трассировки каждого события
- Учитывайте, что BCC требует установленных заголовков ядра и компилятора LLVM/Clang
- Когда нужен более сложный анализ, чем могут дать однострочники bpftrace
- Когда нужны долгосрочные решения для мониторинга, в т.ч. утилиты для многократного использования
- Если нужно интегрировать eBPF инструменты с другими системами, в т.ч. с системами мониторинга
libbpf
libbpf - это C библиотека для работы с eBPF программами, которая позволяет загружать, верифицировать и управлять eBPF программами из пользовательского пространства. В отличие от BCC, libbpf не требует компилятора во время выполнения, что делает ее более подходящей для производственных сред. libbpf нужен, когда нужно выжать максимум из тех возможностей, которые предоставляет eBPF
- Высокопроизводительная обработка сетевого трафика
- Долгосрочный мониторинг системы
- Обнаружение аномалий и угроз безопасности
- Сбор метрик производительности с минимальными накладными расходами
- Расширение возможностей ядра без модификации исходного кода
Для примера libbpf возьмем простейший пример из репозитория
Нужно установить сперва пакеты, которые нужны для компиляции
apt-get install -y build-essential clang llvm libelf-dev libcap-dev
git clone https://github.com/libbpf/libbpf-bootstrap.git
cd libbpf-bootstrap/examples/c/
Рассмотрим более подробно исходный код нашего примера
// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause /* Copyright (c) 2020 Facebook */ // Импортируем заголовочные файлы #include <linux/bpf.h> #include <bpf/bpf_helpers.h> // Это строка необходима для загрузки eBPF программы. Ядро загружает // программы только с совместимой лицензией. char LICENSE[] SEC("license") = "Dual BSD/GPL"; // Переменная, которая нам потребуется в пользовательском пространстве int my_pid = 0; // Это прикрепляет программу к точки трассировки, которая // срабатывает, когда процесс вызывает системный вызов write() SEC("tp/syscalls/sys_enter_write") // Функция, которая будет вызываться при каждом достижении точки трассировки. // Функция извлекает PID текущего процесса, проверяет, что PID соответствует целевому PID my_pid, // если они совпадают, то логирует сообщение, используя bpf_printk, которая пишет в trace pipe int handle_tp(void *ctx) { int pid = bpf_get_current_pid_tgid() >> 32; if (pid != my_pid) return 0; bpf_printk("BPF triggered from PID %d.\n", pid); return 0; }
minimal.c - это программа пользовательского пространства ядра, которая будет загружать и управлять eBPF программой.
Как видно мы импортируем minimal.skel.h это файл, который во время компиляции автоматически генерируется BPFTool, его мы не будем рассматривать, он после компиляции находится в каталоге рядом .output
// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) /* Copyright (c) 2020 Facebook */ // Импортируем заголовочные файлы #include <stdio.h> #include <unistd.h> #include <sys/resource.h> #include <bpf/libbpf.h> #include "minimal.skel.h" /* Функция callback, которая принимает libbpf log level, строку format и переменные аргументы. Перенаправляет libbpf логи в stderr */ static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args) { return vfprintf(stderr, format, args); } int main(int argc, char **argv) { /* Определяем указатель на skeleton структуру и целое число для хранения кода ошибки. */ struct minimal_bpf *skel; int err; /* Регистрируем нашу функцию callback для обработки сообщений libbpf */ libbpf_set_print(libbpf_print_fn); /* Открываем BPF приложение */ skel = minimal_bpf__open(); if (!skel) { fprintf(stderr, "Failed to open BPF skeleton\n"); return 1; } /* Убеждаемся, что BPF программа обрабатывает только системные вызовы write() */ skel->bss->my_pid = getpid(); /* Загружаем программу (на этом этапе также проходит проверка верификатором ядра, чтобы гарантировать безопасность BPF программы */ err = minimal_bpf__load(skel); if (err) { fprintf(stderr, "Failed to load and verify BPF skeleton\n"); goto cleanup; } /* Прикрепляем загруженную BPF программу к точки трассировки в ядре */ err = minimal_bpf__attach(skel); if (err) { fprintf(stderr, "Failed to attach BPF skeleton\n"); goto cleanup; } printf("Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` " "to see output of the BPF programs.\n"); for (;;) { /* Пишем в файл из нашей BPF программы */ fprintf(stderr, "."); sleep(1); } /* cleanup блок, в который переходим в случае ошибки или прерывания программы */ cleanup: minimal_bpf__destroy(skel); return -err; }
Исходный код рассмотрели, теперь компилируем
make
./minimal
cat /sys/kernel/debug/tracing/trace_pipe
minimal-1064528 [002] d..31 1761398.028307: bpf_trace_printk: BPF triggered from PID 1064528. minimal-1064528 [002] d..31 1761399.028377: bpf_trace_printk: BPF triggered from PID 1064528. minimal-1064528 [002] d..31 1761400.028446: bpf_trace_printk: BPF triggered from PID 1064528. minimal-1064528 [002] d..31 1761401.028530: bpf_trace_printk: BPF triggered from PID 1064528. minimal-1064528 [002] d..31 1761402.028585: bpf_trace_printk: BPF triggered from PID 1064528. minimal-1064528 [002] d..31 1761403.028679: bpf_trace_printk: BPF triggered from PID 1064528. minimal-1064528 [002] d..31 1761404.028765: bpf_trace_printk: BPF triggered from PID 1064528. minimal-1064528 [002] d..31 1761405.028851: bpf_trace_printk: BPF triggered from PID 1064528. minimal-1064528 [002] d..31 1761406.028941: bpf_trace_printk: BPF triggered from PID 1064528. minimal-1064528 [002] d..31 1761407.029029: bpf_trace_printk: BPF triggered from PID 1064528. minimal-1064528 [002] d..31 1761408.029124: bpf_trace_printk: BPF triggered from PID 1064528. minimal-1064528 [002] d..31 1761409.029218: bpf_trace_printk: BPF triggered from PID 1064528. minimal-1064528 [002] d..31 1761410.029309: bpf_trace_printk: BPF triggered from PID 1064528.
Заключение
bpftrace — идеальный инструмент для быстрой диагностики проблем и создания ad-hoc инструментов трассировки. Его простой синтаксис и мощные возможности делают его незаменимым для системных администраторов и разработчиков, которым необходимо оперативно выяснить причину проблем производительности. Однако, bpftrace не лучший выбор для долгосрочных решений мониторинга или продакшен-окружений из-за потенциальных накладных расходов.
BCC Tools предоставляет богатый набор готовых инструментов и Python API для создания собственных eBPF программ. Это делает его популярным среди разработчиков, которым нужна гибкость и возможность быстрого создания прототипов. BCC Tools лучше всего подходит для глубокой трассировки и исследования работы системы. Главным недостатком является зависимость от LLVM/Clang во время выполнения, что может быть проблематично в некоторых производственных средах.
libbpf — наиболее эффективное и производительное решение для работы с eBPF. Отсутствие зависимостей от компилятора во время выполнения и поддержка CO-RE делают его идеальным выбором для продакшен-систем и встраиваемых устройств. libbpf предлагает самый низкоуровневый API, что требует более глубокого понимания eBPF и C, но взамен даёт максимальный контроль и производительность. Это лучший выбор для долгосрочных решений мониторинга и наблюдаемости.
Дополнительные материалы
- https://github.com/cloudflare/ebpf_exporter - ebpf exporter
- https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main - огромное кол-во уроков по ebpf
- https://habr.com/ru/articles/683566/ - хорошая статья про bcc tools и мониторинг dns запросов
- https://eunomia.dev/tutorials/ - очень много туториалов по libbpf
- https://nakryiko.com/posts/libbpf-bootstrap/ - статья про libbpf-bootstrap от разработчика ядра BPF
- https://nakryiko.com/posts/bpf-core-reference-guide/ - Руководство по BPF CO-RE от разработчика ядра BPF
- https://www.piter.com/collection/linux/product/proizvoditelnost-sistem - легендарная книга System Performance от Брендана Грегга
- https://cilium.isovalent.com/hubfs/Learning-eBPF%20-%20Full%20book.pdf - бесплатная книга Learning eBPF автор Liz Rice
- https://www.sobyte.net/post/2022-07/c-ebpf/ - хорошая статья про libbpf
- Также у Брендана Грегга есть еще одна книга BPF Performance Tools Linux System and Application Observability
- https://github.com/eunomia-bpf/GPTtrace - интересный проект, который совмещает LLM и eBPF, трассировка и исследование linux с использованием естественного языка
- https://tetragon.io/ - интересный проект для observability, безопасности и трассировки на основе eBPF от создателей Cilium CNI для Kubernetes
- https://www.brendangregg.com/ebpf.html - блог Брендана Грегга по теме eBPF
- https://github.com/cloudflare/ebpf_exporter - ebpf_exporter для создания кастомных метрик на базе eBPF
- https://bpfman.io/main/ - свежий проект под покровительством CNCF, ПО для запуска и управления eBPF программ
- https://nvd.codes/post/monitor-any-command-typed-at-a-shell-with-ebpf/ - статья, как мониторить все команды, вводимые в shell
- https://medium.com/all-things-ebpf - блог исключительно про eBPF
- https://www.trackawesomelist.com/zoidbergwill/awesome-ebpf/readme/ - большой список дополнительных материалов по eBPF