bpftrace: Фундаментальная трассировка ядра для разработчиков под Linux

Когда производительность приложения падает под нагрузкой, когда возникают непонятные лаги, или когда глубокие системные вызовы ведут себя непредсказуемо, как разработчики могут заглянуть под капот работающей системы без перезагрузки и перекомпиляции ядра? Ответ для современных Linux-систем лежит в использовании технологий на базе eBPF.

Почему именно bpftrace?

Давайте отложим историю экосистемы eBPF и сосредоточимся на практической стороне. bpftrace в экосистеме eBPF — это аналогия awk для трассировки ядра. Инструмент позволяет создавать мощные скрипты для трассировки событий ядра практически в реальном времени без чрезмерных накладных расходов.

Классические инструменты вроде strace или perf имеют ограничения:

  • strace существенно замедляет систему при интенсивном использовании
  • perf даёт данные в агрегированном виде, не подходит для детального исследования конкретных системных вызовов

bpftrace заполняет эту нишу, позволяя писать целенаправленные скрипты с минимальным влиянием на производительность за счёт использования виртуальной машины eBPF в ядре.

Установка и подготовка в Arch Linux

Установка проста, но требует дополнительного шага для эффективной работы:

bash
sudo pacman -S bpftrace linux-headers

Ключевой момент: Для трассировки функций ядра необходимы символы debug-информации. Установите debuginfod для автоматической загрузки отладочной информации:

bash
echo "export DEBUGINFOD_URLS='https://debuginfod.archlinux.org'" >> ~/.bashrc
source ~/.bashrc

Проверка установки базовым скриптом:

bash
sudo bpftrace -e 'kprobe:do_sys_openat2 { printf("PID %d opening file: %s\n", pid, str(uptr(arg2))); }'

Этот скрипт будет выводить все попытки открытия файлов системой в реальном времени.

Синтаксис bpftrace: от атомарных проб до комплексных сценариев

bpftrace скрипты состоят из зондов и действий. Простейшая структура:

text
probe_type:identifier [predicate] { action }

Разбор элементов:

  • probe_type: Где ставим точку останова (kprobe/uprobe/tracepoint и др.)
  • identifier: Конкретный следящий элемент (функция в ядре/пользовательском процессе)
  • predicate: Фильтрация событий (/pid == 123/)
  • action: Действие при срабатывании (логирование, статистика, агрегация)

Практический пример: анализ проблем с блокировками процессов

Допустим, мы подозреваем, что приложение зависает из-за глобальной блокировки. Мониторинг вызовов mutex_lock:

bash
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]);
}
'

Что происходит в этом скрипте:

  1. На входе в mutex_lock фиксируем время в словаре @start по идентификатору потока
  2. При выходе из функции вычисляем время блокировки
  3. Сохраняем время вместе со стеком вызовов в агрегирующий словарь @lock_times
  4. Перед завершением очищаем временные данные

После остановки скрипта (Ctrl+C) вы увидите распределение времени блокировок с привязкой к конкретным стекам вызовов.

Пробираемся глубже: асинхронные I/O операции

Для анализа операций типа io_uring — современного асинхронного интерфейса ввода/вывода ядра:

c
tracepoint:io_uring:io_uring_submit_sqe 
{
    @submit[args->opcode] = count();
}

Этот простой пробинг подсчитывает количество операций каждого типа в системном кольцевом буфере io_uring.

Элегантная визуализация данных

Bpftrace включает инструменты для построения гистограмм прямо в консоли:

bash
sudo bpftrace -e 'kprobe:vfs_read { @reads = hist(arg2 /* bytes */); }'

Результат:

text
@reads: 
[4K, 8K)         14 |@@@@@@@@@@@@@@                                      |
[8K, 16K)        22 |@@@@@@@@@@@@@@@@@@@@@@@@@@                          |
[16K, 32K)        5 |@@@@@@                                              |

Такая визуализация моментально показывает распределение размеров операций чтения.

Анатомия практического кейса: диагностика всплесков задержки

Сценарий: Веб-сервер испытывает периодические задержки в несколько сотен миллисекунд.

Наш план атаки:

  1. Сначала ищем процессы, дольше всего удерживающие планировщик CPU
bash
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; // начало для нового потока
}
'
  1. Если подозреваемый процесс найден: проанализировать системные вызовы, которые он выполняет
text
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_* /pid == <target_pid>/ { @calls[probe] = count(); }'
  1. Исследовать стек для медленных вызовов
bash
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); 
}
'

Работа с трудными случаями и ограничениями

  1. Трассировка внутри пространства имен: bpftrace видит только свой PID-неймспейс. Для контейнеров используйте опцию sudo bpftrace -p <container_pid>.

  2. Потеря событий: При интенсивной трассировке буферы могут переполняться. Увеличивайте размер буфера ключом -B:

bash
sudo bpftrace -B 2048 -e '...'
  1. Безопасность: Продвинутые зонды требуют прав root либо капсибильных возможностей (CAP_SYS_ADMIN и CAP_BPF). В продакшн-средах избегайте нефильтрованных зондов, влияющих на тысячи событий в секунду без ограничений.

  2. Отладка параметров функций: Если нужно точно определить сигнатуру трейссируемой функции (например, для определения номера аргумента):

bash
# Получение прототипа функции ядра:
grep mutex_lock /proc/kallsyms -C1

Переход на новый уровень: использование карт Беркли

В примерах выше мы использовали специальные переменные с символом @карты Беркли. Понимание их устройства усиливает ваши возможности:

bash
BEGIN 
{
  @global_counter = 0; // глобальная целочисленная карта
  @aggregation_map = hist(0); // предопределенная гистограмма 
}

kprobe:do_nanosleep 
{
  @local_per_thread[tid] = 1;  // карта хэширована по ключу tid
  @global_counter++
}

Карты могут быть разных типов: счетчики, гистограммы, стеки, временные метки. Они сохраняются между событиями.

Интеграция в разработческий цикл

Можно встраивать bpftrace в автоматизированные тесты. Пример проверки, что приложение не вызывает лишних блокировок:

bash
#!/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 — не просто инструмент трассировки; это фундаментальный сдвиг в том, как мы взаимодействуем с работающими системами. От простого подсчёта системных вызовов до глубокого анализа распределения времени задержек ядра — инструмент покрывает фантастическое количество сценариев.

Когда в следующий раз ваш вектор анализа попадет в ядро:

  1. Сформулируйте гипотезу ("хочу посмотреть на вызовы mmap дольше 2 мс")
  2. Постройте пробинг с фильтрацией (kretprobe:do_mmap)
  3. Анализируйте вывод в live или через агрегацию
  4. Уточняйте стеком, где именно происходит тормоз

Главное преимущество bpftrace — сохранение гибкости скриптового инструментария при непосредственной работе на предельно низком уровне с производительностью, приближенной к нативным системным вызовам. После нескольких месяцев использования вы начнёте задаваться вопросом: как мне раньше удавалось понимать систему без этих возможностей?