April 29

Руководство пасечника или обзор инструментария eBPF

Я во время съемок фильма Пчеловод 3 раза запустил bpftrace и ни разу об это не пожалел. Джейсон Стейтем

Введение

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

bpftrace

Начнем с самой простой утилиты, которая используется для того, чтобы взаимодействовать с eBPF. Брендан Грегг в своем блоге ставит эту утилиту на первое место по легкости освоения. Поставить bpftrace, ни у кого думаю, не составит труда. После установки в /usr/sbin появятся небольшие файлы примеров с расширением .bt, с которыми можно поиграться. Исходный код и небольшие описания есть на github проекта.

Также хороший cheat sheet есть на сайте Брендана Грегга:

Картинка взята из репозитория bpftrace https://github.com/bpftrace/bpftrace/blob/master/images/bpftrace_probes_2018.png

Картинка взята из книги BPF Performance Tools Brendan Gregg 2019

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

С помощью 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 не нужно иметь опыт программирования

Кроме того у bpftrace есть свой сайт, где есть страничка с однострочниками, туториалом по однострочникам, страничка с практическими занятиями

bcc

BCC (BPF Compiler Collection) - это набор инструментов и библиотек для создания эффективных программ eBPF. BCC предоставляет Python и Lua интерфейсы для написания программ, которые компилируются в eBPF-код "на лету" при запуске.

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

Также как и для bpftrace на основе BCC написано множество полезных утилит с исходным кодом, ознакомиться можно по ссылке. А огромное кол-во примеров для этих утилит можно найти здесь.

Также есть полезный туториал для bcc.

Отдельно для Python разработчиков.

Ниже на скрине представлены области применения готовых BCC инструментов:

Картинка взята из репозитория bcc https://github.com/iovisor/bcc/blob/master/images/bcc_tracing_tools_2019.png

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

Когда использовать bcc:

  • Когда нужен более сложный анализ, чем могут дать однострочники 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/

Рассмотрим более подробно исходный код нашего примера

minimal.bpf.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

minimal.c

// 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

bpftrace — идеальный инструмент для быстрой диагностики проблем и создания ad-hoc инструментов трассировки. Его простой синтаксис и мощные возможности делают его незаменимым для системных администраторов и разработчиков, которым необходимо оперативно выяснить причину проблем производительности. Однако, bpftrace не лучший выбор для долгосрочных решений мониторинга или продакшен-окружений из-за потенциальных накладных расходов.

BCC Tools

BCC Tools предоставляет богатый набор готовых инструментов и Python API для создания собственных eBPF программ. Это делает его популярным среди разработчиков, которым нужна гибкость и возможность быстрого создания прототипов. BCC Tools лучше всего подходит для глубокой трассировки и исследования работы системы. Главным недостатком является зависимость от LLVM/Clang во время выполнения, что может быть проблематично в некоторых производственных средах.

libbpf

libbpf — наиболее эффективное и производительное решение для работы с eBPF. Отсутствие зависимостей от компилятора во время выполнения и поддержка CO-RE делают его идеальным выбором для продакшен-систем и встраиваемых устройств. libbpf предлагает самый низкоуровневый API, что требует более глубокого понимания eBPF и C, но взамен даёт максимальный контроль и производительность. Это лучший выбор для долгосрочных решений мониторинга и наблюдаемости.

Дополнительные материалы