Когда производительность приложения падает под нагрузкой, когда возникают непонятные лаги, или когда глубокие системные вызовы ведут себя непредсказуемо, как разработчики могут заглянуть под капот работающей системы без перезагрузки и перекомпиляции ядра? Ответ для современных Linux-систем лежит в использовании технологий на базе eBPF.
Почему именно bpftrace?
Давайте отложим историю экосистемы eBPF и сосредоточимся на практической стороне. bpftrace
в экосистеме eBPF — это аналогия awk
для трассировки ядра. Инструмент позволяет создавать мощные скрипты для трассировки событий ядра практически в реальном времени без чрезмерных накладных расходов.
Классические инструменты вроде strace
или perf
имеют ограничения:
strace
существенно замедляет систему при интенсивном использованииperf
даёт данные в агрегированном виде, не подходит для детального исследования конкретных системных вызовов
bpftrace заполняет эту нишу, позволяя писать целенаправленные скрипты с минимальным влиянием на производительность за счёт использования виртуальной машины eBPF в ядре.
Установка и подготовка в Arch Linux
Установка проста, но требует дополнительного шага для эффективной работы:
sudo pacman -S bpftrace linux-headers
Ключевой момент: Для трассировки функций ядра необходимы символы debug-информации. Установите debuginfod
для автоматической загрузки отладочной информации:
echo "export DEBUGINFOD_URLS='https://debuginfod.archlinux.org'" >> ~/.bashrc
source ~/.bashrc
Проверка установки базовым скриптом:
sudo bpftrace -e 'kprobe:do_sys_openat2 { printf("PID %d opening file: %s\n", pid, str(uptr(arg2))); }'
Этот скрипт будет выводить все попытки открытия файлов системой в реальном времени.
Синтаксис bpftrace: от атомарных проб до комплексных сценариев
bpftrace скрипты состоят из зондов и действий. Простейшая структура:
probe_type:identifier [predicate] { action }
Разбор элементов:
probe_type
: Где ставим точку останова (kprobe/uprobe/tracepoint и др.)identifier
: Конкретный следящий элемент (функция в ядре/пользовательском процессе)predicate
: Фильтрация событий (/pid == 123/
)action
: Действие при срабатывании (логирование, статистика, агрегация)
Практический пример: анализ проблем с блокировками процессов
Допустим, мы подозреваем, что приложение зависает из-за глобальной блокировки. Мониторинг вызовов mutex_lock
:
sudo bpftrace -e '
kprobe:mutex_lock
{
@start[tid] = nsecs;
}
kretprobe:mutex_lock
/tid/
{
$duration = nsecs - @start[tid];
@lock_times[tid, ustack(12)] = sum($duration);
delete(@start[tid]);
}
'
Что происходит в этом скрипте:
- На входе в
mutex_lock
фиксируем время в словаре@start
по идентификатору потока - При выходе из функции вычисляем время блокировки
- Сохраняем время вместе со стеком вызовов в агрегирующий словарь
@lock_times
- Перед завершением очищаем временные данные
После остановки скрипта (Ctrl+C) вы увидите распределение времени блокировок с привязкой к конкретным стекам вызовов.
Пробираемся глубже: асинхронные I/O операции
Для анализа операций типа io_uring
— современного асинхронного интерфейса ввода/вывода ядра:
tracepoint:io_uring:io_uring_submit_sqe
{
@submit[args->opcode] = count();
}
Этот простой пробинг подсчитывает количество операций каждого типа в системном кольцевом буфере io_uring.
Элегантная визуализация данных
Bpftrace включает инструменты для построения гистограмм прямо в консоли:
sudo bpftrace -e 'kprobe:vfs_read { @reads = hist(arg2 /* bytes */); }'
Результат:
@reads:
[4K, 8K) 14 |@@@@@@@@@@@@@@ |
[8K, 16K) 22 |@@@@@@@@@@@@@@@@@@@@@@@@@@ |
[16K, 32K) 5 |@@@@@@ |
Такая визуализация моментально показывает распределение размеров операций чтения.
Анатомия практического кейса: диагностика всплесков задержки
Сценарий: Веб-сервер испытывает периодические задержки в несколько сотен миллисекунд.
Наш план атаки:
- Сначала ищем процессы, дольше всего удерживающие планировщик CPU
sudo bpftrace -e '
kprobe:finish_task_switch
{
@times[tid] = nsecs;
}
kprobe:finish_task_switch
/@times[prev_pid]/
{
$delta = (nsecs - @times[prev_pid]) / 1000000; // ns -> ms
if ($delta > 100) { // фильтр по высоким задержкам
printf("PID %d blocked scheduler for %d ms, comm: %s\n",
prev_pid, $delta, comm);
}
delete(@times[prev_pid]);
@times[tid] = nsecs; // начало для нового потока
}
'
- Если подозреваемый процесс найден: проанализировать системные вызовы, которые он выполняет
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_* /pid == <target_pid>/ { @calls[probe] = count(); }'
- Исследовать стек для медленных вызовов
sudo bpftrace -e '
kprobe:vfs_read /pid == <target_pid>/
{
@start = nsecs;
}
kretprobe:vfs_read /pid == <target_pid>/
{
$dur = (nsecs - @start) / 1000;
@us_stacks[ustack(10)] = hist($dur);
}
'
Работа с трудными случаями и ограничениями
-
Трассировка внутри пространства имен: bpftrace видит только свой PID-неймспейс. Для контейнеров используйте опцию
sudo bpftrace -p <container_pid>
. -
Потеря событий: При интенсивной трассировке буферы могут переполняться. Увеличивайте размер буфера ключом
-B
:
sudo bpftrace -B 2048 -e '...'
-
Безопасность: Продвинутые зонды требуют прав root либо капсибильных возможностей (
CAP_SYS_ADMIN
иCAP_BPF
). В продакшн-средах избегайте нефильтрованных зондов, влияющих на тысячи событий в секунду без ограничений. -
Отладка параметров функций: Если нужно точно определить сигнатуру трейссируемой функции (например, для определения номера аргумента):
# Получение прототипа функции ядра:
grep mutex_lock /proc/kallsyms -C1
Переход на новый уровень: использование карт Беркли
В примерах выше мы использовали специальные переменные с символом @
— карты Беркли. Понимание их устройства усиливает ваши возможности:
BEGIN
{
@global_counter = 0; // глобальная целочисленная карта
@aggregation_map = hist(0); // предопределенная гистограмма
}
kprobe:do_nanosleep
{
@local_per_thread[tid] = 1; // карта хэширована по ключу tid
@global_counter++
}
Карты могут быть разных типов: счетчики, гистограммы, стеки, временные метки. Они сохраняются между событиями.
Интеграция в разработческий цикл
Можно встраивать bpftrace в автоматизированные тесты. Пример проверки, что приложение не вызывает лишних блокировок:
#!/bin/bash
bpftrace -e 'kprobe:mutex_lock /execname == "myapp"/ { @lock_count = count(); }' &
BT_PID=$!
# Запускаем тесты приложения
sleep 60
kill $BT_PID
[ $(< output awk '/@lock_count/ { print $NF }') -gt 100 ] && echo "FAIL: Too many locks"
Почему это меняет правила игры?
Bpftrace предоставляет возможность интерактивного изучения работы ядра без препятствия использованию симуляторных систем. Мы уходим от слепого профайлинга отдельных компонентов к целенаправленному исследованию гипотез о поведении системы. Многие проблемы ранее требовали сборки кастомных ядер с printk — теперь это делается через одну строчку в терминале.
Заключение для практиков
Bpftrace — не просто инструмент трассировки; это фундаментальный сдвиг в том, как мы взаимодействуем с работающими системами. От простого подсчёта системных вызовов до глубокого анализа распределения времени задержек ядра — инструмент покрывает фантастическое количество сценариев.
Когда в следующий раз ваш вектор анализа попадет в ядро:
- Сформулируйте гипотезу ("хочу посмотреть на вызовы mmap дольше 2 мс")
- Постройте пробинг с фильтрацией (
kretprobe:do_mmap
) - Анализируйте вывод в live или через агрегацию
- Уточняйте стеком, где именно происходит тормоз
Главное преимущество bpftrace — сохранение гибкости скриптового инструментария при непосредственной работе на предельно низком уровне с производительностью, приближенной к нативным системным вызовам. После нескольких месяцев использования вы начнёте задаваться вопросом: как мне раньше удавалось понимать систему без этих возможностей?