Руководство пасечника или обзор инструментария 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