Перейти к содержимому

«Сервис тормозит, надо оптимизировать» — самая частая фраза, после которой инженер начинает менять код наугад. Ни одного измерения. Performance & Profiling — это дисциплина измерения перед оптимизацией: снять профиль через pprof, perf или flame graph, увидеть, где горит на самом деле, а не где казалось, и только потом трогать код. Я регулярно вижу циклы «оптимизировали неделю, сделали хуже» именно потому, что горячая точка была не там. Знаменитое «преждевременная оптимизация — корень всех зол» у Кнута ровно об этом: оптимизация без измерения почти всегда трата времени. Flame graph, придуманный Бренданом Греггом в 2011 году, и pprof, выросший из gperftools в Google, — два самых заметных инструмента; метод USE (Utilization, Saturation, Errors) — самая практичная методика.

Граница: Capacity Planning — «хватит ли ресурсов»; здесь — «правильно ли используем те, что есть». SLO Engineeringчто мерить, SLI как обещание пользователю; профилирование — как именно копать, когда этот SLI просел. Programming Languages — инструменты конкретного языка (pprof для Go, py-spy для Python); здесь — методика поверх любого из них.

Главный навык на уровне L4 — мерить до того, как трогать код. По моим наблюдениям, разница между начинающим и опытным в задачах на производительность — не в знании инструментов, их найдёт поиск, а в дисциплине «сначала flame graph, потом гипотеза». Первый читает код, придумывает гипотезу «вот тут квадрат по количеству элементов», переписывает — и оказывается, что горело в другом месте. Второй снимает профиль, смотрит на реальное распределение и уже потом формулирует гипотезу. Разница в одной привычке, которая экономит часы каждую неделю.

L3

  • Знает, что вообще профилируют (процессор, память, диск, сеть); понимает разницу между сэмплирующими профайлерами (perf, async-profiler) и инструментирующими (pprof в среде исполнения Go); умеет запустить базовый pprof, py-spy или async-profiler.
  • Читает flame graph: что означает ширина полосы (доля выборок), что — глубина стека, какие функции наверху.

L4

  • Применяет метод USE: для каждого ресурса — загрузка (какую долю времени он занят), насыщение (длина очереди, время ожидания), ошибки (потери, повторы). Чеклист Грегга — стандартная отправная точка.
  • Профилирует до правки кода, а не после. Формулирует гипотезу на измеренных данных и проверяет её повторным профилем.
  • Знает классические антипаттерны: запрос в цикле вместо одного, конкуренция за горячую блокировку, неограниченный рост мусора, аллокации в горячем цикле, утечка памяти против нормального роста, ложное разделение кеш-линий.

L5

  • Внедряет непрерывное профилирование в проде (Pyroscope, Parca, Grafana Cloud Profiles, Datadog Continuous Profiler): регрессии ловятся автоматически, детали доступны по требованию, а сервис не страдает.
  • Использует сравнительное профилирование: снимок до релиза и после, сравнение через pprof -base. Каждый релиз — потенциальная просадка, и без сравнения они копятся незаметно.
  • Профилирует прод, а не «локально на ноутбуке». Микробенчмарки врут регулярно: расположение данных в памяти, давление на сборщик мусора и характер операций ввода-вывода на синтетике совсем другие.

L6+

  • Строит бюджет задержки: у каждого сервиса он расписан по составляющим (сеть, процессор, диск, вызовы соседей), отслеживается во времени, а отклонение поднимает алерт.
  • Связывает работу над производительностью с программой SLO: оптимизация нацелена на изменение конкретного SLI, а не на «общую скорость», а эффективность использования ресурсов считается явно, через стоимость запроса (см. Cloud Cost Control).
  • Brendan Gregg — Systems Performance: Enterprise and the Cloud (Addison-Wesley, 2-е изд., 2020). Каноническая книга. Если выбирать одну — эту: метод USE, методики, конкретные инструменты Linux. Восемьсот с лишним страниц, которые читают главами, а не подряд.
  • Brendan Gregg — BPF Performance Tools (Addison-Wesley, 2019). Что eBPF изменил в профилировании — следующий шаг после классического perf. Практическая книга с готовыми инструментами (bcc-tools).
  • Дональд Кнут, «преждевременная оптимизация — корень всех зол» (Computing Surveys, 1974). Цитату знают все, продолжение — почти никто: Кнут там же говорит, что оставшиеся три процента кода оптимизировать необходимо, и что решать, какие именно, должны измерения. Ровно эта половина фразы и есть содержание профилирования.
  • Andrei Alexandrescu — Modern C++ Design (Addison-Wesley, 2001). Про C++, но раздел об оптимизации на шаблонах — ровно про дисциплину «сначала измерь».
  • Brendan Gregg — Flame Graphs (с 2011 года и по сей день). Главный практический инструмент, описанный целиком на одной странице. Если выбирать одну статью — эту. Главный публичный кейс — см. ниже.
  • Brendan Gregg — The USE Method. Канонический системный подход. «Solves 80% of server issues with 5% of the effort». Применим к любой системе.
  • Google — pprof README. Документация инструмента, которая заодно остаётся лучшим введением в чтение профилей вообще.
  • Netflix Tech Blog — Java in Flames (2015). Показывает, как flame graph меняет саму манеру отладки; уже классика.
  • Charity Majors — Observability Is a Many Splendored Definition. Не про производительность напрямую, но задаёт контекст: непрерывное профилирование — часть наблюдаемости, а не отдельный остров.
  • pprof (родной для Go, есть библиотеки под большинство языков) — стандарт де-факто для профилей процессора, памяти, блокировок и мьютексов. По моим наблюдениям, ручка /debug/pprof/ — первое, что подключают к новому сервису на Go.
  • perf (родной для Linux) — профилирование на уровне ядра: perf record, perf report. Работает с любым языком через сэмплирование. Для серьёзной работы с производительностью его обязательно стоит освоить.
  • eBPF / bcc-tools / bpftrace — наблюдение за ядром без инструментирования. Готовые скрипты: execsnoop, biolatency, tcplife, runqlat. Когда вопрос звучит как «что ядро делает прямо сейчас», ответ здесь.
  • async-profiler — сэмплирующий профайлер для JVM, стандарт де-факто в Java. Умеет процессор, аллокации и конкуренцию за блокировки.
  • py-spy — сэмплирующий профайлер для Python, работает извне процесса, целевой сервис не трогает.
  • Pyroscope / Parca / Grafana Cloud Profiles — платформы непрерывного профилирования. Pyroscope (теперь часть Grafana) — open-source, Datadog Continuous Profiler — коммерческий. По моим наблюдениям, Pyroscope чаще берут те, кто разворачивает у себя, а Datadog или New Relic — те, кто уже там живёт.
  • FlameGraph — генератор flame graph из вывода perf. Командная строка, ничего лишнего.
  • Анти-инструмент: «оптимизация по интуиции без профиля». Это не инструмент, а антипаттерн; самый частый источник историй «оптимизировали и сделали хуже».

Главный публичный кейс — работа Брендана Грегга и Мартина Спира над профилированием Java в Netflix, статья «Java in Flames» (2015). Сам flame graph Грегг придумал раньше, в 2011 году, но Java долго оставалась для него слепой зоной: системные профайлеры вроде perf видели системные стеки и не видели методов Java (JIT не отдаёт таблицу символов), а профайлеры JVM видели ровно наоборот. Пока картина была разорвана пополам, спорить о том, где на самом деле горит процессор, можно было бесконечно. Решение оказалось прозаичным до обидного: флаг -XX:+PreserveFramePointer, появившийся в JDK 8u60, — и стек снова стал целым, а профиль — сквозным, от ядра до прикладного метода.

Мне этот кейс нравится тем, что он не про героическую находку, а про инструмент, которого просто не было. Пока его нет, команда обсуждает производительность гипотезами: «наверное, тормозит сериализация», «давай перепишем этот сервис на другом языке». Как только появляется целый профиль, обсуждение сводится к одному вопросу — где горит на самом деле и стоит ли это вообще трогать. Я регулярно вижу команды, которые читали про flame graph, но ни разу его не запускали, и продолжают решать задачи производительности через код-ревью. Flame graph не требует экспертизы. Только привычки открыть его раньше, чем редактор кода.

Три правила, с которых начинается любая работа с производительностью. Первое про порядок. Профиль снимается до изменения и после: без пары «до и после» это не оптимизация, а случайная правка, а сравнение через pprof -base или два снимка в Pyroscope превращает «кажется, стало лучше» в цифру. Профилировать надо прод, а не микробенчмарк на ноутбуке — расположение данных, давление на сборщик мусора, поведение аллокатора и характер ввода-вывода там другие; микробенчмарк остаётся для алгоритмических горячих циклов. И метод USE идёт первым проходом: при невнятном «тормозит» пробежать загрузку, насыщение и ошибки по процессору, памяти, диску и сети раньше, чем открывать код.

Сэмплирование или инструментирование — выбор по контексту, а не по вкусу. Сэмплирующие профайлеры (perf, async-profiler, py-spy) стоят дёшево и спокойно живут в проде. Но у них есть граница: на функциях, которые выполняются очень быстро и очень часто, сэмплирование не работает — интервал больше длительности вызова, и функция в профиль просто не попадает. Инструментирующие (pprof в Go, gprof) дают точные счётчики и времена ценой заметных накладных расходов. Отсюда разделение: прод — сэмплирование, точечный разбор отдельного сценария — инструментирование в staging, где лишние тридцать процентов накладных расходов никого не убьют. Типовая ошибка обратная. Инструментируют прод — и получают искажённую картину из-за той самой конкуренции за блокировки, которую сами же и создали своим профайлером.

Flame graph читается по ширине. Не сверху вниз. Частая ошибка — искать «верхнюю функцию»: наверху обычно рутина вроде аллокатора, сборщика мусора или стандартной библиотеки, потому что процессор находится там буквально сейчас. Ширина функции на любом уровне — это доля времени, проведённая в ней и во всём, что она вызывает. Сначала широкие функции в середине. Потом углубление. Несколько графов с --reverse хорошо дополняют картину снизу вверх.

Непрерывное профилирование меняет не инструмент, а ритм работы. Классический подход простой: сломалось — иди профилируй. Непрерывный — профиль есть всегда, просадка ловится автоматически. Разница в том, когда именно становится известно о регрессии: в день релиза или через два месяца, когда сверху уже легли три релиза и разбирать придётся все. По моим наблюдениям, здоровую культуру производительности от нездоровой отличает именно наличие непрерывного профилирования, а не изысканность инструментов.

Бюджет задержки по составляющим — практический мостик от SLO к профилированию. Сервис обещает 99-й перцентиль меньше 100 мс. Дальше бюджет расписывается по частям: обмен по сети, разбор JSON, запрос в базу, бизнес-логика, сериализация ответа. Когда какая-то часть вылезает за свою долю, у отладки появляется очевидная точка входа. Без такого бюджета профилирование превращается в поиск в темноте, и обычно находят то, что ближе лежит.

Профиль памяти — про скорость выделения, а не про общий объём. Резидентная память растёт всегда: кеши, куча сборщика мусора, отображённые файлы. Настоящий вопрос — сколько мегабайт в секунду выделяется, потому что высокая скорость выделения даёт давление на сборщик, а оно даёт всплески задержек. alloc_objects и alloc_space в pprof показывают ровно это, и смотреть на них надо раньше, чем на график памяти. Команды, которые смотрят на график памяти и недоумевают, почему «память не растёт, а задержки дёргаются», я вижу регулярно — и там почти всегда выделяется больше, чем сборщик успевает переваривать.

Граница у практики тоже есть, и она про цену. Профилирование окупается там, где производительность видна пользователю или заметна в счёте за инфраструктуру. Для внутреннего сервиса с тремя запросами в минуту неделя работы с профилями — выброшенное время. Там дешевле добавить ресурсов. И обратная сторона: сама привычка искать горячие точки не спасает от проблем архитектуры. Если задержка набегает из десяти последовательных сетевых вызовов, профиль честно покажет, что процессор простаивает, а решение будет лежать в схеме взаимодействия, а не в коде.

  • Programming Languages — у каждого языка свои инструменты (pprof для Go, async-profiler для JVM, py-spy для Python); устройство самого языка определяет, какие проблемы производительности вообще возможны.
  • Shell & CLI Craftperf, strace, bpftrace живут в командной строке; работа с производительностью требует беглости в shell.
  • Operating Systems — профилирование на уровне ядра (eBPF, события perf, ftrace) — это внутренности системы; выше L5 без них не получится.
  • Capacity Planning — «хватит ли ресурсов» и «правильно ли мы их тратим» — две стороны одной задачи.
  • SLO Engineering — SLI определяет, что мерить; профилирование отвечает на вопрос, куда копать, когда он просел.
  • Cloud Cost Control — эффективность, найденная профилировщиком, — вход для решений по деньгам; стоимость запроса неплохо отражает качество этой работы.
  • Networking — профилирование со стороны сети (tcpdump, дампы, mtr) — отдельная подобласть со своими инструментами.
  • Как внедрять непрерывное профилирование (TBD) — что делать команде, которая живёт без него, и по чему считать, что внедрение удалось.
  • DB Performance Tuning (TBD) — проектирование индексов, планы запросов, пулы соединений, vacuum и compaction — соседняя практика под Database Reliability.
  • Производительность фронтенда (TBD) — Core Web Vitals, данные из реальных браузеров, профилирование в браузере. Не самая традиционная тема для SRE, но в продуктовых командах она всплывает постоянно.
  • Дисциплина микробенчмарков (TBD) — JMH в Java, testing.B в Go, pytest-benchmark в Python. Как не попасть в стандартные ловушки: непрогретый код, статистический шум, выброшенные компилятором вычисления.
  • Я не уверен, как правильно профилировать машинное обучение: время шага обучения против загрузки GPU. Если у вас есть работающая практика — расскажите через PR.