Самые коварные неполадки те, что не воспроизводятся в тестовой среде. Микросервис под боевой нагрузкой даёт всплески задержек, в спокойной обстановке всё гладко, а обычные инструменты разводят руками: процессор не загружен, память стабильна, в журналах чисто. Хочется заглянуть внутрь ядра и посмотреть, что система делает в момент всплеска, но традиционные средства либо слишком грубы, либо требуют остановить приложение, чего в продакшене позволить нельзя. Здесь и выходит на сцену технология, изменившая диагностику Linux, - встроенный в ядро механизм безопасного исполнения трассирующих программ.
Речь о технологии расширенных программ ядра и удобном языке трассировки поверх неё. Они позволяют динамически наблюдать за системными вызовами, функциями ядра и приложениями - без перекомпиляции, без правки кода, без перезапуска и с минимальными накладными расходами. Программа трассировки исполняется прямо в ядре в ответ на события, что делает её безопасной для боевых систем. Разберём, как устроена эта трассировка, как писать простые наблюдения, и почему она пригодна там, где грубые инструменты опасны.
Безопасное исполнение в ядре отличает технологию от грубых средств
Главное достоинство подхода - сочетание глубины и безопасности. Программа трассировки исполняется внутри ядра, реагируя на события, но прежде чем её запустить, особый проверяющий механизм ядра убеждается, что она безопасна: не обращается к недозволенной памяти и не содержит бесконечных циклов. Это делает подход надёжнее обычных модулей ядра, которые при ошибке способны уронить всю систему.
Накладные расходы малы по двум причинам. Во-первых, исполнение событийное: код срабатывает только при наступлении события, а не крутится постоянной выборкой. Во-вторых, всё происходит в ядре, без дорогого перекладывания данных между ядром и пространством пользователя на каждом шаге. Поэтому такую трассировку считают пригодной для продакшена, в отличие от тяжёлых инструментов.
Удобно и то, что вмешательства в приложение не требуется. Не нужно перекомпилировать программу, добавлять отладочную печать в её исходный код или перезапускать её. Трассировщик прицепляется к работающей системе со стороны и наблюдает. Именно это делает подход незаменимым для боевых систем и случаев, когда пересборка невозможна.
Список доступных проб и простейшие однострочники
Точки, к которым можно прицепиться, называются пробами - это места в системе, где можно перехватить событие. Их множество: системные вызовы, функции ядра, пользовательские функции. Список доступных проб выводит особый флаг трассировщика, и его удобно сужать поиском по образцу.
bpftrace -l # список всех доступных проб
bpftrace -l 'tracepoint:syscalls:*' # только пробы системных вызовов
Простейшая трассировка задаётся одной строкой через флаг исполнения программы. Программа описывает, к какому событию прицепиться и что делать при его наступлении. Классический пример - перехват запуска программ: при каждом системном вызове запуска печатать, какой процесс что запустил.
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%s запустил %s\n", comm, str(args.filename)); }'
Разберём строение. Указание пробы говорит, к какому событию прицепиться - здесь к входу в системный вызов запуска. В фигурных скобках - действие при событии. Встроенная переменная имени процесса даёт имя того, кто вызвал, а особая функция превращает указатель в строку, чтобы достать имя запускаемого файла из аргументов вызова. Аргументы события доступны через автоматически сформированную структуру, и какие в ней поля, можно узнать из описания пробы.
Трассировка требует прав администратора, потому что лезет в ядро. Это общее правило для всей технологии.
Подсчёт и распределение вскрывают то, что прячут счётчики
Сила подхода не только в печати событий, но и в их обобщении прямо в ядре. Самое полезное - подсчёт событий по группам и построение распределений. Подсчёт системных вызовов по процессам сразу показывает, кто и сколько их делает.
sudo bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'
Здесь особая карта накапливает счётчик по имени процесса, а по нажатию завершения трассировщик выводит итог - сколько системных вызовов сделал каждый процесс за время наблюдения. Так находят программу, неэффективно штурмующую ядро лавиной вызовов.
Ещё ценнее распределения. Обычные счётчики дают среднее, но среднее скрывает выбросы и многомодальность - а именно в них часто и кроется причина всплесков задержек. Технология умеет строить гистограмму, раскладывающую значения по диапазонам, и так вскрывает то, что усреднённая метрика прячет.
sudo bpftrace -e 'tracepoint:syscalls:sys_exit_read /args.ret > 0/ { @bytes = hist(args.ret); }'
Эта программа строит распределение размеров успешных чтений. Косые черты задают фильтр - условие, при котором действие выполняется, здесь только для чтений с положительным результатом. Распределение покажет, мелкими или крупными порциями идёт чтение, чего средняя цифра никогда бы не выдала. Именно эта способность раскладывать метрику на распределение и закрывает слепые зоны, недоступные счётчикам.
Измерение задержек через парные пробы
Развитый приём - измерять задержку операции, прицепившись к её началу и концу. Программа запоминает время на входе в операцию, а на выходе вычитает и строит распределение длительностей. Так измеряют задержку дисковых операций или сетевых вызовов на уровне ядра.
sudo bpftrace -e '
kprobe:blk_account_io_start { @start[arg0] = nsecs; }
kprobe:blk_account_io_done /@start[arg0]/ { @usecs = hist((nsecs - @start[arg0]) / 1000); delete(@start[arg0]); }'
Логика читается по шагам. На входе в дисковую операцию программа запоминает текущее время, привязав его к идентификатору операции. На выходе, если начало было зафиксировано, она вычисляет разницу, переводит в микросекунды и складывает в распределение, после чего убирает запомненное начало. Результат - гистограмма задержек дисковых операций, показывающая не среднюю задержку, а её полную картину с хвостами и выбросами.
Пробы тут двух родов: к стабильным точкам ядра цепляются через имена событий, а к произвольным функциям ядра - через перехват входа в функцию. Перехват функций даёт доступ к внутренностям ядра, но имена и аргументы функций могут меняться между версиями, тогда как стабильные точки событий надёжнее.
Главная ловушка и осторожность в продакшене
Несмотря на низкие накладные расходы, есть ловушка, способная навредить. Прицепиться к очень частому событию без ограничения вывода - значит затопить терминал потоком и нагрузить систему. Вход в системные вызовы или их выход срабатывают тысячи раз в секунду, и печать каждого без фильтра обрушит и читаемость, и производительность.
Лекарство - обобщать в ядре вместо печати каждого события, фильтровать по процессу или условию, выбирать выборкой. Подсчёт и распределение, как в примерах выше, безопаснее печати, потому что наружу выдают лишь итог, а не каждое событие. При работе на боевой системе разумный порядок - начать с малого на тестовом узле, убедиться в низких накладных расходах, и лишь затем выкатывать на продакшен с фильтрами по конкретному процессу или выборкой, чтобы ограничить нагрузку.
Стоит учесть и совместимость версий. Трассировщик, собранный под старое ядро, может не запуститься на новом, поэтому инструментарий держат в актуальном состоянии под текущее ядро. Современные ядра и дистрибутивы обычно несут нужные пакеты из коробки. Технология выросла из идей более старого средства полносистемной трассировки и унаследовала его подход событийных программ, отвечающих на события системы.
Что складывается в практику
Картина выстраивается ясно. Технология расширенных программ ядра даёт глубокую трассировку с проверкой безопасности и низкими накладными расходами, пригодную для продакшена, - без перекомпиляции, правки кода и перезапуска приложения. Доступные точки наблюдения перечисляет особый флаг, а простейшая трассировка задаётся одной строкой с указанием пробы и действия.
Сила подхода - в обобщении прямо в ядре: подсчёт по группам находит штурмующих ядро, а распределения вскрывают выбросы и многомодальность, которые скрывает усреднённая метрика. Парные пробы на входе и выходе измеряют задержку операций полной картиной, а не средним. Главная осторожность - не печатать каждое из очень частых событий, а обобщать, фильтровать и выбирать, и выкатывать на продакшен постепенно.
Главная мысль: эта трассировка позволяет заглянуть в ядро работающей системы, не трогая её, - там, где грубые инструменты либо слепы, либо опасны. Невоспроизводимые в тесте всплески задержек перестают быть загадкой, когда метрику можно разложить на распределение прямо в бою. Стоит освоить простые однострочники с подсчётом и распределением и помнить про осторожность с частыми событиями, и продакшен перестаёт быть чёрным ящиком - его внутренности становятся видны с почти нулевым риском для работы.