Perforator: новая система непрерывного профилирования теперь в опенсорсе
- пятница, 31 января 2025 г. в 00:00:14
Привет! Сегодня мы выложили в опенсорс Perforator — систему непрерывного профилирования (continuous profiling), которую используем внутри Яндекса для анализа производительности большинства сервисов.
В Github‑репозитории доступен исходный код системы и инфраструктура для развёртывания своей инсталляции Perforator на кластере Kubernetes. Кроме того, Perforator можно использовать на своём компьютере как более простую замену perf record: профили получаются точнее, а оверхед меньше. Исходный код доступен под лицензией MIT (и GPL для eBPF‑программ) и запускается под x86–64 Linux.
При помощи Perforator и прошлых подходов к задаче профилирования мы регулярно оптимизируем самые крупные сервисы в Яндексе, например Баннерную крутилку или Поиск, на десятки процентов. Кроме того, Perforator реализует недостающий в опенсорсе компонент профилирования для простой автоматической оптимизации программ с использованием profile‑guided optimization. Наши тесты показывают, что использование PGO даёт ускорение около 10% в разных сценариях.
В этой статье
поговорим про профилирование под Linux;
опишем вызовы и сложности, возникающие при профилировании;
изучим, как устроен Perforator внутри;
обсудим, как можно использовать полученную систему.
Часто разработчикам приходится выяснять, почему их программа тормозит или использует слишком много ресурсов. Причины могут быть самые разные. Например, заказ от бизнеса на оптимизацию железа или необходимость ускорения программы для увеличения отзывчивости.
Практика показывает, что разработчики не очень хорошо угадывают, где тормозит код. Компьютеры слишком сложны, чтобы простая ментальная модель хорошо предсказывала все детали. На помощь приходят профилировщики — специальные программы, показывающие понятным образом, что происходит внутри программы. С их помощью разработчики находят и оптимизируют узкие места в коде, зачастую добиваясь кратного ускорения. Сайты быстрее загружаются, приложения меньше тормозят, а компании экономят значительные суммы денег на серверах.
Профилировщики настолько полезны, что мэтры программирования не рекомендуют оптимизировать программы вообще, не изучив профиль исполнения. Очень часто при первом запуске программы под профилировщиком находят значительное количество низко висящих фруктов — тривиальных оптимизаций, ускоряющих исполнение на десятки процентов.
Современный мир разработки справляется с профилированием очень хорошо. Наверное, сколько лет человечество пишет код, столько его и оптимизирует. Существует огромное количество инструментов, позволяющих понять практически всё про исполнение программы, вплоть до анализа потока исполняемых инструкций (Intel PT).
Можно выделить две основных категории профилировщиков:
Инструментирующие профилировщики. Эти инструменты особым образом меняют программу так, чтобы можно было легко собирать статистику о её исполнении. Мы хотим профилировать очень много самых разных программ в разных уголках большого кластера, так что такой подход, скорее всего, не подойдёт: инструментирование зачастую вносит заметный оверхед и, что ещё хуже, требует модификации процесса сборки программ.
Сэмплирующие профилировщики. Можно регулярно останавливать исполнение программы и изучать её состояние. Если повторить процедуру достаточное количество раз, мы получим вполне репрезентативную картину выполнения программы.
Ещё есть смешанные подходы. Например, замечательный tracy одновременно позволяет и инструментировать код, и сэмплировать. Но проблемы инструментирования так или иначе остаются, поэтому дальше будем больше смотреть на сэмплирование.
Одна из самых простых и гениальных в своей простоте идей — профайлер бедного человека (ПБЧ). Суть проста: давайте иногда подключаться к интересующей нас программе через gdb, получать стеки всех потоков и продолжать исполнение. Если повторим процедуру несколько раз, получим взвешенный набор стеков. Чем больше времени проведём в конкретном месте программы, тем больше таких стеков соберём. Чем больше стеков соберём, тем выше точность полученного профиля.
Такая идея позволяет получить хорошие результаты за минимальное количество усилий. Например, до внедрения Perforator в Баннерную крутилку — один из самых крупных сервисов Яндекса — долгое время мы пользовались именно ПБЧ. Это очень простой сервис, всего несколько сотен строк кода на Python, который сэкономил огромное количество ресурсов компании. Кстати, про ПБЧ очень интересно рассказывал Антон Полднев.
Однако у ПБЧ есть и минусы. Он достаточно инвазивный и хорошо работает только на кластерах значительного размера, иначе достаточное для репрезентативного профиля число сэмплов собирать сложно. Кроме того, ПБЧ по построению профилирует только wall‑time — реальное время. Например, если ваша программа 50% времени спит, а 50% времени тратит CPU, ПБЧ покажет на профиле и ту, и другую нагрузку. Для анализа CPU time или других событий (например, числа инструкций или промахов мимо кеша процессора) используют альтернативные подходы.
Де‑факто стандартный инструмент для профилирования под Linux — Linux perf. По своей сути это комбайн, вариантов использования perf крайне много, и в них достаточно сложно разобраться. Одним из самых популярных является вариант perf record — сэмплирующий профилировщик. Через подсистему perf_event он подписывается на различные события в системе и собирает релевантную профилю информацию (стеки, имена процессов и потоков и так далее) каждые несколько событий.
Например, команда perf record ‑e instructions ‑c 1 000 000 ‑p 1234
будет сохранять снимок состояния потока каждый миллион инструкций, исполненных в рамках процесса с pid 1234.
Perf позволяет профилировать совсем не только использование времени CPU. Например, можно построить профиль по количеству page faults или доступов в кеши конкретного уровня. Единственное ограничение — поддержка конкретного события со стороны системы (для софтварных событий вроде числа page faults или переключений контекста) или CPU. Занятно, что получить аналогичный ПБЧ профиль по wall‑time через perf нетривиально: нужно как‑то клеить on‑cpu и off‑cpu профили.
Для отрисовки профиля обычно используют Flame Graphs от евангелиста профилирования Брендана Грега. Flame Graph — красивый и функциональный интерактивный формат, позволяющий легко находить узкие места и исследовать внутренности программ.
В своё время Google выпустил статью про Google Wide Profiler. Это непрерывный профилировщик, который исполняется на всех серверах в нескольких дата‑центрах и позволяет понимать, как работают программы на масштабе всего кластера. Хотя GWP никогда не выкладывался в открытый доступ, из публикаций и рассказов выросла целая отрасль профилирования. Под капотом распределённых профилировщиков обычно находятся классические инструменты, выполняемые на одной машине, вроде того же perf.
Подобные системы собирают огромное количество информации об исполнении сервисов и позволяют отвечать на вопросы в духе «сколько денег сэкономит оптимизация этой функции» или автоматически оптимизировать программы. В частности, Google и Meta* за последние годы выложили в открытый доступ большое количество инструментов и наработок для автоматической post‑link‑оптимизации на основе профиля от сэмплирующего профилировщика (BOLT, AutoFDO). Использование этих подходов позволяет достичь ускорения порядка 10–20% даже относительно сборок с LTO.
Перечисленные технологии достаточно зрелые, используются много лет в самых больших кодовых базах. Однако для удобного использования этих инструментов в CI/CD не хватает одной маленькой детальки: собственно распределённого профилировщика.
Поэтому мы разработали Perforator — распределённый непрерывный профилировщик для дата‑центров.
Издалека задача выглядит несложной. Надо взять проверенный временем perf, запустить его на всём кластере, тривиальным кодом собрать со всех серверов профили и как‑нибудь проагрегировать. Наверное, этой статьи бы не было, если бы всё прошло по плану. Но, как обычно, дьявол кроется в деталях.
Как работает perf? Когда мы делаем perf record -e instructions -c 1000000 -p 1234
, perf через ядро Linux настраивает CPU так, что он начинает считать количество выполненных инструкций через PMU (Performance Monitoring Unit). Это выделенные регистры на процессоре, которые инкрементятся, скажем, каждую инструкцию. Когда счётчик числа событий в PMU переполняется, процессор вызывает особое прерывание. Это прерывание обрабатывается ядром Linux, которое и собирает снимок состояния потока в момент, когда была выполнена миллионная инструкция.
Данная архитектура позволяет очень точно анализировать состояние потока, однако требует реализации значительной части perf прямо внутри ядра Linux (!), так как прерывание необходимо обработать в kernel space. Например, Linux умеет раскручивать стек потока, используя знание про организацию стековых кадров на конкретной архитектуре.
Чтобы perf record начал собирать стеки, нужно добавить флажок --call-graph
. И если поиграться с этим флажком, легко заметить, что часто профили получаются не очень понятные. Типичный битый профиль выглядит как‑то так (в качестве примера выбрана релизная сборка ClickHouse):
Так происходит потому, что современные компиляторы по умолчанию не генерируют указатели стековых кадров (frame pointers). Это позволяет сэкономить пару инструкций на функцию и освободить один регистр, однако лишает нас возможности легко профилировать. У Брендана Грега есть отличный обзор проблемы. Популярным решением стало возвращение frame pointers в сборку. В среднем просадка производительности небольшая, около 1–2%. Такой подход часто используют большие компании и дистрибутивы Linux.
Однако пересобрать все программы и библиотеки с -fno-omit-frame-pointer
сложно. Даже если собрать так основной бинарь, всё равно возникнут системные библиотеки, которые собраны с -fomit-frame-pointer
. И стеки, которые проходят, например, через glibc, получаются битые. Кроме того, точные цифры замедления сильно зависят от конкретной нагрузки. В некоторых сценариях просадка намного заметнее, вплоть до десятков процентов.
Есть альтернативное решение — DWARF. Через него работают отладчики и исключения. Компиляторы достаточно давно начали генерировать специальную секцию, в которой закодировано, как из любой исполняемой инструкции в программе вычислить состояние родительского стекового кадра. Эта секция (.eh_frame
) генерируется даже тогда, когда в программе отключены исключения или их вообще нет (например, в C). Можно отключить генерацию .eh_frame
через флажок -fno-asynchronous-unwind-tables
, однако на практике его использование экономит единицы процентов размера исполняемого файла в обмен на сложности с отладкой и профилированием.
Perf умеет использовать DWARF для раскрутки стека: нужно указать флажок --call-graph=dwarf
. Однако здесь есть очень важный нюанс: DWARF — Тьюринг-полный. Как мы помним, раскрутка стека происходит в прерывании внутри ядра Linux, и поддержка DWARF там практически невозможна: мало того, что его нужно прочитать с диска в прерывании, так и сам код раскрутки получается безумно сложным, потенциально содержащим огромное количество багов. Линус Торвальдс когда-то крайне красноречиво ответил на предложение добавить раскрутку через DWARF в ядро:
I never ever want to see this code ever again.
...
Dwarf unwinder had bugs itself, or our dwarf information had bugs,and in either case it actually turned several "trivial" bugs into a total undebuggable hell.
...
...dwarf is a complex mess ...
...
An unwinder that is several hundred lines long is simply not even remotely interesting to me.
...
just follow the damn chain on the stack without the "smarts" of an inevitably buggy piece of crap.
Поэтому perf делает иначе. Он просто копирует верхушку стека потока в user‑space, а уже потом раскручивает стек в user space, где можно писать произвольно сложный код.
Конечно, весь стек копировать дорого: типично это несколько мегабайт на каждый сэмпл. Поэтому perf вынужден терять часть данных для программ, которые сколько‑нибудь используют стек. Максимум поддерживается 65 528 последних байт стека, при этом размер стека потока под Linux по умолчанию — 8 МБ. Обычно данный подход работает, и получаются неплохие профили. Профиль того же самого инстанса ClickHouse выглядит вот так:
Однако даже на нём видно проблемы. Если приглядеться, то видно стеки, которые раскрутились криво. Один из них выглядит как‑то так:
ZSTD, судя по всему, активно использует стек, и лимита от perf в 65 528 байт не хватило.
Более того, даже без учёта проблем с лимитом на размер стека профили с --call-graph=dwarf
получаются на пару десятичных порядков крупнее, чем аналогичные с --call-graph=fp
. Нужно сохранять вместо небольшого списка адресов возврата весь стек. В итоге массовое использование --call-graph=dwarf
оказывается нецелесообразно дорогим. Профиль за несколько десятков секунд может занимать гигабайты. Если попытаемся уменьшить лимит на размер стека, то профиль начнёт деградировать. Давайте попробуем это проделать:
Perf не подходит. Нужно как‑то раскрутить стек, и обязательно внутри ядра Linux. Какие есть варианты?
Пропатчить ядро. Довольно безумно по сложности: патчи сложно выкатывать и ещё сложнее поддерживать, при этом остаются потенциальные проблемы со стабильностью и безопасностью.
Модуль ядра. Более гибкий механизм для нашего сценария, но стабильность всё равно под вопросом. Так, кстати, была устроена первая версия Google Wide Profiler: использовался OProfile, который до появления Linux perf реализовывал модуль ядра для профилирования. Очень хрупко и ненадёжно.
eBPF. К счастью, относительно недавно появилась крайне мощная технология по запуску небольших не Тьюринг‑полных верифицируемых программ внутри ядра Linux. Сейчас eBPF достаточно популярен для решения задачи профилирования.
Linux позволяет запускать eBPF‑программу при срабатывании прерывания от PMU, а также набор вспомогательных функций для чтения памяти в пространстве пользователя или ядра. Таким образом, можно построить аналог perf.
Осталось раскрутить стек, используя DWARF. Вспоминаем, что DWARF сложный и, вообще говоря, Тьюринг‑полный (sic!). К счастью, в конце 2010-х годов мир начал приходить к тому, что эта сложность в DWARF не нужна и можно выразить всё то же самое намного более простыми механизмами (например, 1, 2 и 3). Фундаментальным стало наблюдение из статьи Reliable and Fast DWARF‑Based Stack Unwinding, где авторы обнаружили, что, несмотря на сложность DWARF, компиляторы генерируют более‑менее тривиальные правила раскрутки.
Строго говоря, DWARF — это семейство форматов. Для нас представляет интерес DWARF CFI: Call Frame Information. В CFI для каждой инструкции закодировано правило раскрутки: как найти позицию родительского стекового кадра, то есть значения регистров в родительской функции. Вот эти правила и бывают произвольно сложными в терминах программ для виртуальной машины DWARF. Однако современные компиляторы генерируют такую структуру машинного кода, что правило раскрутки всегда тривиально: позиция стека на момент вызова текущей функции вычисляется как смещение от регистра rsp
или rbp
на x86–64. Аналогичные эвристики есть и для других архитектур.
Это значит, что DWARF CFI можно упростить до более‑менее тривиальной структуры, которая теряет всю сложность и легко анализируется eBPF. Дальше дело техники. Полученная схема работает на удивление хорошо. В итоге профилировщик получает из eBPF сырые стеки: набор адресов в бинарном файле.
Полученные адреса надо символизировать: преобразовать в имена функций и позиции в исходном коде. Эта задача, как ни странно, тоже не из простых: под Linux необходимое знание обычно лежит также в DWARF, и его тоже крайне сложно парсить. Парсинг требует значительных объёмов памяти и времени CPU. Поэтому мы делаем символизацию в самый последний момент, когда пользователь запросил профиль.
Кроме того, мы почти всегда используем более интересный формат GSYM, который намного компактнее и эффективнее предоставляет структуры для символизации. Информации про GSYM в Интернете немного, его поддерживают разработчики из Meta*, видимо, для аналогичной задачи внутреннего профилировщика.
Perforator конвертирует DWARF для всех бинарных файлов в GSYM, и дальше оперирует только GSYM. Часто это оказывается быстрее, чем символизировать через DWARF, но и такая опция есть.
Полученная конструкция достаточно эффективна. Прямо сейчас мы профилируем такты CPU на значительной части нашего флота через Perforator непрерывно, собирая по 100 сэмплов в секунду с ядра. При этом измеренное замедление пользовательских процессов незначительно — порядка 0,1%. Благодаря этому мы можем включать Perforator сразу на всех. Более того, за месяцы использования системы нам ни разу не пришлось исключать сервисы из‑под профилирования. Поэтому мы верим, что подход достаточно надёжный и стабильный.
Агент использует некоторое количество ресурсов на хосте. Точные цифры сильно зависят от размера хоста, нагрузки, вида бинарных файлов и частоты профилирования, но мы наблюдаем на больших хостах цифры порядка процента CPU хоста и единиц гигабайт памяти.
На каждом хосте профилируемого кластера мы запускаем хостовый агент Perforator. Его задача — анализировать все процессы на хосте, профилировать интересные (например, контейнеры) и раз в минуту отправлять профили в хранилище.
Кроме того, агент отправляет бинарные файлы вместе с профилями. Это необходимо для офлайн‑символизации: если один и тот же бинарный файл исполняется на тысяче серверов, сильно эффективнее провести символизацию этого файла не на каждой из машинок, а один раз на бэкенде.
Собранные профили нужно где‑то хранить. Для обеспечения масштабируемости конструкции мы собрали хранилище из хорошо известных масштабируемых запчастей. Все бинарные файлы и минутные профили мы храним в S3, а метаинформацию про профили — в ClickHouse. Это позволяет хранить петабайты профилей и за сотни миллисекунд находить интересные профили по сложным селекторам.
Поверх хранилища находится бэкенд, который реализует удобный gRPC‑интерфейс как для пользователей, так и для агентов. Бэкенд разбит на пачку микросервисов, что позволяет масштабировать нужные компоненты независимо.
Мы опенсорсим инструмент, активно используемый внутри нашего монорепозитория, поэтому в настоящее время используем в качестве системы сборки yamake. Лицензия двойная: почти весь код MIT, за исключением программ на eBPF — это GPL.
Perforator написан на смеси Go (почти весь бэкенд и агент), C (eBPF) и C++ (самые горячие места: агрегация профилей, анализ исполняемых файлов через llvm и так далее). Такая не очень обычная конфигурация стала возможна благодаря гибкости системы сборки. В дальнейшем мы планируем упростить сборку и перейти по возможности на cmake и go build.
Perforator хорошо поддерживает нативные языки, в первую очередь мы оптимизировали этот сценарий. Точно проверили C++, C, Go, Rust. С большой вероятностью хорошо работают и остальные не слишком экзотические варианты.
Perforator всё ещё иногда не справляется с раскруткой стека. Есть две основных причины:
Раскрутка стека споткнулась о рукописный ассемблерный код. Компиляторы дают возможность разработчикам разметить ассемблерный код для генерации DWARF CFI из него, однако разработчики редко пользуются этой возможностью, а когда пользуются, генерируют достаточно сложные правила CFI.
Некоторые библиотеки и исполняемые файлы отключают генерацию .eh_frame
через флажок -fno-asynchronous-unwind-tables
. В таком режиме справляется мало какой инструмент.
Однако у нас есть планы, как починить обе проблемы и научиться раскручивать стеки практически идеально.
Кроме нативных языков на масштабе всего кластера крайне интересно одним и тем же инструментом смотреть и на интерпретируемые или JIT‑компилируемые языки. Использование общей системы вместо хороших уже существующих языков позволяет получить кумулятивный эффект от технологии: многие фичи обобщаются и не особо зависят от конкретного языка. Например, можно отобразить профиль из Perforator на исходный код.
Perforator сейчас поддерживает несколько языков. К сожалению, почти каждый новый язык требует довольно неприятных хаков вроде «сохранить смещения внутренних структур для всех версий рантайма Python». Нас вдохновил пример из тестов подсистемы eBPF в Linux, который собирает стеки Python через eBPF, и похожих проектов вроде py‑spy. Сейчас мы умеем раскручивать свежие версии Python (после 3.12). Активно работаем над расширением множества поддерживаемых языков и рантаймов. В первую очередь поддержим профилирование Java без необходимости модифицировать запуск JVM.
Кроме того, Perforator умеет читать де‑факто стандартный механизм для JIT‑компилируемых языков — perf-pid.map
. Этот формат поддерживает значительное число современных рантаймов, однако почти везде нужно включать дополнительные флажки при запуске VM. Так умеют делать Python после 3.12, Java и NodeJS.
Одной из ключевых возможностей Perforator мы считаем поддержку генерации профилей для FDO: feedback‑driven optimization. Это относительно свежий механизм оптимизации программ на основе профиля предыдущих версий той же самой программы. Предыдущая альтернатива — PGO — более громоздкая и из‑за этого сильно менее распространена. Нужно собирать бинарник два раза и в промежутке, как часть пайплайна сборки, поднимать бинарь и воспроизводить реальную нагрузку для снятия профиля. Использование Perforator позволяет сильно упростить процесс. Достаточно просто перед сборкой собрать профиль через API Perforator и сделать его доступным компилятору флажком -fprofile-sample-use
.
Наши бенчмарки показывают ускорение до 10% на самых горячих и оптимизированных программах. Детали можно прочитать в документации.
Изначально для отрисовки профилей мы использовали SVG от оригинального flamegraph.pl. Однако в процессе работы над Perforator мы столкнулись с существенными ограничениями этого формата. По большей части это следствие того, что Perforator собирает значительное количество данных профилирования, заметно больше, чем можно быстро собрать с одного сервера, и профили получаются подробными, но тяжёлыми.
flamegraph.pl достаточно медленный. Даже небольшие профили рендерятся несколько секунд, а типичный результат профилирования в Perforator — минуты.
flamegraph.pl агрессивно удаляет функции с небольшими весами. Это приводит к тому, что профиль сильно теряет в точности и за пару кликов по интересным функциям сильно деградирует.
Если понижать порог, по которому flamegraph.pl удаляет функции, профиль становится крайне большим (вплоть до единиц гигабайт) и неинтерактивным. Отрисовка и клики даже на Apple Silicon занимают десятки секунд из‑за большого количества элементов в DOM: каждая функция в SVG — это несколько нод в DOM, а функций — десятки и сотни тысяч.
С учётом огромного количества интересной информации от профилировщика у нас хотелось научиться хорошо рисовать большие профили. После изучения существующих технологий мы взяли за основу формат flamegraph из замечательного async‑profiler и творчески обработали его: оптимизировали так, что отрисовка флеймграфа на миллион функций занимает меньше 100 мс на современном железе.
Этот формат кардинально меняет восприятие флеймграфов: по ним приятно кликать и исследовать свою программу. На больших программах флеймграфы ощущаются фрактальными: за несколько кликов по редко исполняющимся, но интересным функциям структура профиля всё равно остаётся достаточно подробной.
Данное свойство позволяет использовать флеймграфы не только для оптимизации, но и для чтения и изучения кода. Как ни странно, часто хороший способ познакомиться с объёмной программой — изучить её профиль, а не сразу погружаться в чтение сотен тысяч строк кода. Подробные флеймграфы как раз и упрощают процесс знакомства с кодом.
Perforator можно запускать в локальном режиме в качестве замены perf record. Это удобный способ получить без какой‑либо модификации программ профиль процесса или всей системы.
Дополнительно Perforator умеет символизировать бинарные файлы без отладочной информации через debuginfod. Достаточно выставить переменную окружения DEBUGINFOD_URLS
в подходящую для вашего дистрибутива. Например, для Ubuntu это будет https://debuginfod.ubuntu.com/
. Подробнее описано в документации.
Perforator умеет профилировать не только такты CPU, но и множество других событий по аналогии с perf. Однако, как мы обсудили выше, wall‑time профилировать сложно. Это бывает нужно, когда приходится оптимизировать время ответа программы пользователю. Если она делает хоть что‑то, что отличается от CPU‑bound‑вычислений, то на профиле по CPU cycles данную активность легко пропустить: например, когда процесс читает с диска или ждёт пакетов из сети, CPU он не использует.
Для этого мы научились на уровне агента Perforator объединять время потока на CPU и в ожидании IO. Тут есть нюансы: потоки, которые спят, например, часами, мы не сможем обнаружить. Но для большей части приложений данный метод применим. В итоге получается профиль, где каждый поток занимает одинаковую долю и в котором видно и использование CPU, и ожидание ввода‑вывода.
Ещё одной интересной возможностью Perforator стала аннотация собранных стеков различными тегами из кода программы на C++. Как это может пригодиться? Изменения поведения программ часто выкатывают через A/B‑тест. Новую фичу включают на небольшую долю запросов и следят за поведением пользователей или программ на этих запросах. Например, через A/B можно выкатить использование новой тяжёлой ML‑модели или сложного алгоритма. В таком случае новая фича может потреблять сильно больше ресурсов, но на A/B сложно понять, где именно случилось замедление.
Для решения этой проблемы в программах на C++ можно разметить стеки thread‑local‑тегами: строчками или числами, которые дальше будут считаны eBPF‑программой и записаны в профиль. Поэтому можно построить профиль по одному срезу пользователей или A/B‑тесту: записать уникальный идентификатор запроса в теги, после чего отфильтровать полученный профиль по логам системы и отбросить запросы, не попавшие в нужную выборку. Такой механикой мы строим внутри Яндекса профили по некоторым A/B‑тестам.
Из нюансов — данная фича достаточно сильно увеличивает размер профиля, так как каждый сэмпл получает уникальный ключ. Например, раньше по каждому стеку мы получали много сэмплов, которые склеивали в один, а с уникальными тегами так не получится. Изначально для хранения профиля мы использовали стандартный формат из pprof, однако столкнулись с рядом проблем с размером и скоростью обработки профиля. Сейчас же мы сильно дедуплицировали все сущности в профиле, и использование тегов растит размер профиля незначительно.
Забавной случайной возможностью системы стал сбор стеков при фатальных сигналах. Благодаря гибкости eBPF можно подцепиться на ядерную функцию доставки сигнала потоку, после чего научиться дёшево доставать стеки потоков при получении любых фатальных сигналов, включая SIGKILL.
Это позволяет организовывать легковесный аналог сбора кордампов. На больших кластерах часто ограничивают вероятность откладывания кордампа, чтобы при массовом падении быстрее восстанавливаться. Через Perforator можно с минимальным оверхедом собирать 100% падений.
Более того, сочетая сбор стеков при сигналах с возможностью читать thread‑local переменные, можно понимать, какой именно запрос обрабатывался, что особенно полезно в рантайм‑сервисах.
Важно отметить несколько нюансов, потенциально ограничивающих использование системы.
Perforator требует CAP_SYS_ADMIN
. Через eBPF он может читать произвольную память, включая ядерную, а также ему необходимо уметь читать произвольные бинарные файлы в системе для построения таблиц раскрутки.
Perforator требует достаточно свежего ядра Linux, не старше 5.4. В прекоммитных проверках мы проверяем свойства агента на всех LTS‑ядрах после 5.4 включительно. Почему так? eBPF крайне активно развивается, и до 5.4 возможностей eBPF сильно не хватает для написания достаточно сложной программы. В теории можно реализовать бо́льшую часть логики на 4.19, однако это достаточно тяжёлый процесс.
Perforator пока работает под x86–64 Linux. Хоть поддержка ARM и реализуется, мы ещё не готовы выкладывать её. Надеемся, что в будущем она появится.
Раскрутка через DWARF, хоть и хорошо работает почти всегда, иногда всё же ломается. На масштабах нашего флота мы видим единичные проблемы, не связанные с описанными выше сложностями с кодом без .eh_frame
и рукописным ассемблером, но всё же вероятность ненулевая. В таком случае помогает точечно включать -fno-omit-frame-pointer
.
Агент Perforator может требовать заметное количество анонимной памяти для хранения таблиц раскрутки. Это числа порядка единиц гигабайт на больших хостах. У нас есть понятные идеи по оптимизации, хотим целиться в число меньше гигабайта почти всегда.
Perforator можно попробовать двумя способами.
Простой — запустить локально. Нужно скачать или собрать бинарный файл Perforator, после чего запустить sudo perforator record -a --duration=60s
. Через минуту Perforator откроет в браузере полученный профиль вашей системы.
Сложный — запустить на кластере Kubernetes. Это более трудоёмкий процесс, но мы постарались максимально упростить его. За несколько команд можно развернуть рабочую инсталляцию. Подробнее — в документации.
А ещё можно посмотреть, как в Perforator выглядит результат профилирования.
Мы верим, что такие фундаментальные системные технологии, как операционные системы, компиляторы, системные библиотеки, отладчики и профилировщики, должны быть открыты и развиваться вместе с коммьюнити. А ещё мы верим, что наша разработка будет полезной миру и может принести свою ценность как разработчикам, так и бизнесам. При этом открытость технологии позволяет принимать решения по развитию инфраструктуры профилирования вместе с сообществом.
Например, мы верим, что нужно развивать новые общие форматы: формат хранения правил раскрутки стека вместо DWARF CFI, формат отладочной информации для символизации (GSYM), формат хранения профиля. Мы планируем донести свой опыт до процесса разработки универсального формата профиля в OpenTelemetry.
Perforator теперь есть в открытом доступе на GitHub. Документация и инструкции по установке находятся на perforator.tech.
Мы продолжаем активную разработку, поэтому на старте возможны некоторые шероховатости — их обязательно починим. Будет здорово, если вы заинтересуетесь и попробуете Perforator. Приносите фидбэк в GitHub Issues, а лучше — сразу в Pull Requests!
* Компания Meta признана экстремистской организацией, а её продукты, Facebook и Instagram, запрещены на территории РФ.