Мониторинг подсказывает, что серверу плохо, но почти никогда не говорит, кто именно и на каком этапе. Средняя загрузка процессора выглядит скромной, а пользователи жалуются на тормоза, потому что отдельные операции записи на диск зависают на сотни миллисекунд. Встроиться в работающее ядро раньше означало писать модуль, собирать его под конкретную версию и рассчитывать, что система переживёт нагрузочный тест. Технология eBPF перевернула ситуацию: короткие программы с жёсткой проверкой загружаются в живое ядро на лету, собирают статистику сетевых соединений и задержек диска, а оверхед на сервере часто не выходит за доли процента. Ниже разобраны внутренняя механика eBPF и команды, которые можно сразу применить на боевом сервере.

Программа для ядра, которая проходит строгую проверку перед запуском

Аббревиатура BPF появилась в девяностых как фильтр пакетов для tcpdump. В 2014 году ядро 3.18 получило расширенную версию под названием eBPF, и с тех пор слово "расширенная" перестало быть скромной приставкой. Внутри это 64-битная виртуальная машина с одиннадцатью регистрами от r0 до r10, где r10 указывает на стек кадра. Программу компилируют в машинный код ещё при загрузке, поэтому срабатывание по стоимости близко к обычному вызову функции. С версии ядра 4.15 доступна опция CONFIG_BPF_JIT_ALWAYS_ON, которой JIT фиксируют для всей системы.

Точек крепления десятки. Зонд kprobe позволяет поймать почти любую функцию ядра, сам механизм существует со времён версии 2.6.9. Статические tracepoints дают стабильный формат аргументов между версиями. XDP обрабатывает входящий пакет на самом раннем этапе, до выделения сетевого буфера. Отдельные хуки висят на cgroup и на подсистеме безопасности LSM. Программа снимается мгновенно и без перезагрузки, поэтому наблюдаемость перестала требовать планового окна обслуживания.

Зрелость технологии подтверждает расстановка сил на крупных площадках. Netflix превратил скрипты на bcc в рабочую лошадку производственной диагностики, Cloudflare держит на XDP фильтрацию пакетов на границе сети, облачные провайдеры встраивают eBPF в собственные агенты мониторинга. Возможности и правда продолжают расти: ядро 4.16 разрешило вложенные вызовы одних программ из других, 5.10 сделало программы sleepable, способными спать в ожидании данных, 5.15 добавило таймеры. Каждое такое расширение открывает новый класс инструментов, и старые ядра его просто не знают.

Верификатор не пускает в ядро небезопасный код

Перед загрузкой каждую программу прогоняет верификатор. Он моделирует все ветки исполнения и требует, чтобы ни одна из них не читала неинициализированные регистры, не выходила за границы карт и не работала с указателем неясного типа. Для непривилегированных программ действует лимит в 4096 инструкций, константа BPF_MAXINSNS. При анализе привилегированных программ верификатор способен исследовать до миллиона инструкций. Ограниченные циклы разрешены только с ядра 5.3, до этого программы писали с развёрнутыми проверками.

Побочный эффект такой строгости заметен сразу. Ошибка в классическом модуле ядра обычно заканчивается паникой, а ошибка в eBPF заканчивается сообщением об отказе в загрузке с подробным логом, где видна конкретная строка и причина. Привыкать к стилю программирования непросто, зато упавший код не кладёт сервер. По сути, верификатор делает для ядра то же, что компилятор для программиста: не пропускает небрежность дальше в рантайм.

Зонды kprobe и tracepoint плюс карты для хранения метрик

Зонд отвечает на вопрос "где", карта отвечает на вопрос "куда складывать". Разница между kprobe и kretprobe простая: первый срабатывает на входе в функцию и видит её аргументы, второй ловит выход и возвращаемое значение. Адреса функций ядра перечислены в /proc/kallsyms, а список доступных зондов bpftrace показывает командой -l. Карта BPF_MAP_TYPE_HASH подходит под произвольные ключи, BPF_MAP_TYPE_LRU_HASH сама вытесняет старые записи, BPF_MAP_TYPE_ARRAY служит быстрым счётчиком по индексу. С ядра 5.8 появился BPF_MAP_TYPE_RINGBUF, экономный кольцевой буфер для потоковой передачи событий. Данные накапливаются прямо в ядре, а пользовательский процесс вычитывает их раз в секунду или по нажатию Ctrl-C. Именно на этом держится низкий оверхед: наверх уходит готовая сводка, а не поток сырых событий.

У ядра больше двухсот вспомогательных функций: bpf_get_current_pid_tgid() отдаёт идентификатор процесса, bpf_ktime_get_ns() точное время в наносекундах, bpf_get_current_comm() имя процесса. С опцией CONFIG_DEBUG_INFO_BTF=y в ядро вшиты описания структур, поэтому скомпилированная однажды программа работает на разных версиях ядра без перекомпиляции, этот подход называется CO-RE.

Простой пример с картой-счётчиком. В терминале сервера запускается подсчёт системных вызовов по именам процессов:

sudo bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'

После Ctrl-C на экране появляется сводка наподобие такой:

@[bpftrace]: 6
@[systemd]: 24
@[sshd]: 125

Одна строка кода, а ответ на вопрос "кто забил ядро вызовами" получен за минуту.

Установка bcc и bpftrace вместе с первыми однострочниками

Инструменты живут в двух слоях. Набор bcc поставляет готовые скрипты: opensnoop показывает открытия файлов, biolatency строит гистограмму диска, tcpconnlat меряет задержку соединений. Утилита bpftrace отвечает на короткие вопросы однострочниками. В Debian и Ubuntu пакеты называются bpfcc-tools и bpftrace, скрипты получают суффикс -bpfcc. В Fedora и RHEL пакет называется bcc-tools, а скрипты лежат в каталоге /usr/share/bcc/tools.

Подготовка сервера сводится к четырём шагам:

  1. сверить версию ядра, для bpftrace нужно не меньше 4.9, а для BTF желательно 5.2 и новее;
  2. проверить поддержку BPF в текущем ядре;
  3. установить пакеты bpfcc-tools и bpftrace;
  4. загрузить первую тестовую программу и убедиться, что верификатор её пропустил.

В терминале сервера это выглядит так:

uname -r
grep -E "CONFIG_BPF_SYSCALL|CONFIG_DEBUG_INFO_BTF" /boot/config-$(uname -r)
sudo apt install -y bpfcc-tools bpftrace
sudo bpftrace -e 'BEGIN { printf("hello world\n"); }'

Последняя строка печатает приветствие и завершается по Ctrl-C, что подтверждает работоспособность всей цепочки. Список доступных точек выводит команда с шаблоном:

sudo bpftrace -l 'tracepoint:syscalls:sys_enter_*'

Шаблон поддерживает подстановку * и ?, поэтому легко сузить поиск до одного вызова. С выпуска bpftrace 0.19 аргументы зондов читаются в форме args.filename, раньше писали args->filename. Если в ядре нет BTF, утилита попросит заголовки, в Ubuntu пакет называется linux-headers-$(uname -r); на трассировках по tracepoint это почти не сказывается, а вот кастомные программы для kprobe без описаний структур писать тяжелее.

Задержки диска в гистограмме biolatency

Средняя задержка диска в системах мониторинга спокойна почти всегда, а хвосты она прячет безупречно. Гистограмма показывает правду: сколько операций легло в каждый диапазон времени. Инструмент biolatency измеряет путь запроса от отправки устройству до завершения. Запуск на сервере от root:

sudo biolatency-bpfcc 10 1

Числа означают: 10 задаёт длительность измерения в секундах, 1 ограничивает вывод одним отчётом. Флаг -m переключает единицы на миллисекунды, флаг -Q добавляет в измерение время ожидания в очереди ядра, флаг -D разделяет статистику по отдельным устройствам. Типичный отчёт:

     usecs           : count     distribution
     128 -> 255      : 12       |********                              |
     256 -> 511      : 15       |**********                            |
     512 -> 1023     : 43       |*******************************       |
    1024 -> 2047     : 52       |**************************************|
    2048 -> 4095     : 47       |**********************************    |
    4096 -> 8191     : 52       |**************************************|
    8192 -> 16383    : 36       |**************************            |
   32768 -> 65535    : 2        |*                                     |

Для вращающихся дисков мода в диапазоне от одной до восьми миллисекунд считается нормой. Хвост из единичных операций за 65 миллисекунд повод проверить очередь запросов и нагрузку соседей по хосту. Кто именно открыл файл, показывает opensnoop:

sudo opensnoop-bpfcc -n postgres

Ключ -n фильтрует вывод по имени процесса, в отчёте видны номер дескриптора, код ошибки и путь.

Задержку можно измерить и на уровне отдельной функции ядра. Инструмент funclatency строит гистограмму по времени работы выбранной функции:

sudo funclatency-bpfcc -m vfs_read

Флаг -m переключает единицы на миллисекунды, снятие статистики привычное, по Ctrl-C. Такой замер удобен для проверки гипотез: после правки конфигурации сразу видно, ускорилась ли функция, а после обновления системы, не замедлилась ли она.

Сетевые соединения и их задержки в tcpconnect и tcpconnlat

Для сети у набора bcc есть своя линейка. tcpconnect фиксирует новые соединения, tcpconnlat меряет время от отправки SYN до ответного пакета, tcptop показывает трафик по процессам, tcpretrans ловит повторные передачи. Запуск измерителя задержки соединений:

sudo tcpconnlat-bpfcc

Отчёт выглядит так:

PID    COMM         IP SADDR            DADDR            DPORT LAT(ms)
1201   wget         4  10.153.223.157   23.23.100.231    80    1.65
2852   curl         4  10.153.223.157   23.101.17.61     80    250.86

Значение 1.65 мс укладывается в обычный сетевой раунд. Строка с 250.86 мс сигнал, что конкретный процесс ждал ответ конкретного адреса четверть секунды, и дальше уже решать, потеря пакетов это или медленный удалённый сервер. Сквозную картину трафика даёт tcptop:

sudo tcptop-bpfcc -C

Флаг -C отключает очистку экрана, по умолчанию инструмент работает в полноэкранном режиме в духе atop. В колонках видны адреса и порты обеих сторон плюс килобайты в секунду в каждую сторону, причём только по реальным соединениям ядра. Повторные передачи показывает tcpretrans вместе с флагами TCP и состоянием соединения, каждая такая строка готовая улика для сетевика. Каждая запись tcpretrans по-настоящему дорога: повторная передача значит, что где-то потерян сегмент, а значит растёт задержка приложения. Отдельный плюс измерений на eBPF в том, что фильтрация идёт внутри ядра и пакеты не копируются в пользовательское пространство, в отличие от полного захвата трафика tcpdump.

Реальный оверхед трассировки и настройки безопасности

Цена вопроса измерена практикой. Программа на eBPF компилируется в машинный код, поэтому одно срабатывание стоит единицы наносекунд. Инструменты вроде biolatency держат таймстампы и гистограммы в картах ядра и вычитывают их порциями, поэтому на загруженном сервере суммарный оверхед обычно укладывается в единицы процентов, а на кратковременных замерах часто вообще неразличим. Для сравнения, классический strace останавливает процесс на каждый системный вызов и на нагруженном сервисе способен заметно изменить его поведение.

Права доступа настраиваются честнее, чем кажется. Системный вызов bpf() доступен только root или процессу с нужными возможностями, с ядра 5.8 для наблюдаемости достаточно CAP_BPF и CAP_PERFMON без полного root. Отдельный переключатель kernel.unprivileged_bpf_disabled запрещает загрузку программ без привилегий: значение 1 закрывает дверь после первой загрузки, значение 2 закрывает её насовсем. С ядра 5.16 опция сборки CONFIG_BPF_UNPRIV_DEFAULT_OFF даёт по умолчанию именно 2, и на сервере это разумная отправная точка. Проверка текущего состояния одной командой:

sysctl kernel.unprivileged_bpf_disabled

Есть и инженерная граница. Карты занимают реальную память, и на старых ядрах квота привязывалась к лимиту memlock, поэтому сложные программы отказывались грузиться до правки ulimit -l. С ядра 5.11 учёт памяти eBPF перевели в cgroup, и бухгалтерия стала честнее. Остаётся помнить, что программа держит свои карты, пока загружена: забытый на сервере замер продолжает считать события неделями.

Осторожность всё равно нужна. Зонд на самой горячей функции с высокой частотой срабатываний способен дать заметную нагрузку, поэтому сначала смотрят на частоту события однострочником, а уже потом вешают тяжёлую программу. Готовые скрипты из bcc годами вылизаны на реальных серверах и остаются самым безопасным первым шагом. Когда понадобится метрика, которой нет ни в одном готовом инструменте, eBPF позволит добавить её в живое ядро за вечер, и это ощутимо меняет отношение к слову "невозможно" в графиках мониторинга.