py-spy в проде: профилирование Python без остановки сервиса
Сервис ест CPU, но приложить strace или добавить профайлер в код нельзя — на нём висят живые запросы. py-spy снимает стек работающего процесса извне, без рестарта и без правок кода.

Ситуация: воркер ест 100% CPU, а перезапускать нельзя
Gunicorn с четырьмя воркерами, один из них стабильно держит 100% одного ядра. Latency p99 уехала с 200 мс до 3 секунд, но воспроизвести на стенде не выходит — проблема только под живым трафиком. Добавить cProfile в код и выкатить релиз — это полчаса на CI, ещё рестарт с обрывом соединений, и не факт, что попадёте в тот же паттерн нагрузки. Классический профайлер требует, чтобы вы заранее обернули подозрительный участок. А вы не знаете, какой участок подозрительный — за этим и пришли.
py-spy читает стек уже запущенного процесса по PID, не внедряясь в него и не останавливая. Он написан на Rust, работает как отдельный процесс, читает память целевого процесса через системные вызовы и разбирает структуры интерпретатора CPython. Целевое приложение не знает, что его профилируют, и продолжает обслуживать запросы. Ниже — как снять флейм-граф с боевого воркера, что означают цифры и где py-spy врёт.
Актуально для py-spy 0.3.x (проверялось на 0.3.14) и CPython 3.8–3.12. Для более новых минорных версий Python сверяйте поддержку в README проекта: разбор внутренних структур интерпретатора завязан на конкретную версию, и свежий CPython может потребовать обновления py-spy.
Что делает py-spy и чем платит за отсутствие остановки
Инструмент периодически (по умолчанию 100 раз в секунду) заглядывает в память процесса и восстанавливает текущий стек вызовов каждого потока. Это сэмплирующий профайлер: он не считает каждый вызов, а делает моментальные снимки и по частоте попаданий оценивает, где код проводит время. Отсюда его главное свойство — почти нулевой оверхед на целевой процесс, потому что сам процесс ничего не исполняет ради профилирования.
Сэмплирующий профайлер отвечает на вопрос «где CPU проводит время», а не «сколько раз вызвана функция». Для поиска горячей точки в проде это ровно то, что нужно — и без штрафа за инструментирование.
Цена такого подхода — статистическая природа результата. Редкая, но дорогая функция может не попасть в выборку, если вы сняли слишком короткий отрезок. И py-spy показывает, где поток находится в момент снимка, включая ожидание в C-коде — но по умолчанию он считает и время, проведённое в ожидании (idle), если не указать обратное.
py-spy против встроенных профайлеров
| Свойство | py-spy | cProfile / profile |
|---|---|---|
| Правки в коде | не нужны | обернуть участок или запустить весь процесс под профайлером |
| Рестарт сервиса | не нужен | нужен, если профилировать весь процесс |
| Оверхед на целевой процесс | близок к нулю | заметный, детерминированный профайлер тормозит вызовы |
| Тип данных | сэмплы стеков | точные счётчики вызовов и времени |
| Видит время в C-расширениях | да, как нативный кадр | ограниченно |
Как снять профиль с боевого процесса
Установка
py-spy ставится как обычный пакет, но он самодостаточный бинарник — его можно поставить в системное окружение, а не в venv приложения:
pip install py-spyПроверьте, что бинарник виден:
py-spy --versionДолжна вывестись строка вида py-spy 0.3.14.
Найти PID
Для gunicorn это PID конкретного воркера, а не мастер-процесса. Мастер сам код запроса не исполняет.
# дерево процессов gunicorn: мастер и воркеры
ps -ef | grep gunicornЖивой режим top
Первое, что стоит запустить, — интерактивный режим, похожий на top, но по функциям Python:
sudo py-spy top --pid 12345В таблице появятся функции, отсортированные по доле CPU. Колонка %Own — время в самой функции, %Total — вместе с вызванными из неё. Если наверху висит ваша функция сериализации или регулярка — горячая точка найдена.
Флейм-граф на отрезок времени
Для отчёта, который можно приложить к тикету, снимите SVG за фиксированный интервал:
# 60 секунд сэмплирования, только активное время потоков
sudo py-spy record \
--pid 12345 \
--duration 60 \
--idle \
--output profile.svgФлаг --idle в текущих версиях включает учёт неактивных потоков; чтобы отфильтровать ожидание и увидеть только реальную работу CPU, поведение флагов сверяйте с py-spy record --help вашей версии — набор опций между релизами менялся. Откройте profile.svg в браузере: ширина полосы — доля времени, вложенность сверху вниз — цепочка вызовов. Широкий кадр внизу — то, что греет процессор.
Дамп стеков прямо сейчас
Если воркер завис и вы хотите понять, на чём именно, снимите текущие стеки всех потоков одним снимком:
sudo py-spy dump --pid 12345Это аналог «покажи, где застрял поток» без отладчика и без остановки процесса.
Права, контейнеры и почему py-spy не видит процесс
py-spy читает память чужого процесса, а это привилегированная операция. На хосте обычно достаточно sudo. В контейнерах и под ограничениями безопасности начинаются нюансы.
- Docker. Профилировать процесс внутри контейнера с хоста или из соседнего контейнера получится, только если разрешён нужный доступ к трассировке — обычно это capability
SYS_PTRACE(запуск с--cap-add SYS_PTRACE) либо профилирование изнутри того же контейнера. - ptrace_scope. На многих Linux-хостах чтение памяти чужого процесса ограничено параметром ядра
kernel.yama.ptrace_scope. Если он равен 1, py-spy сможет смотреть только на потомков своего процесса. Значение проверяется черезsysctl kernel.yama.ptrace_scope. - Kubernetes. Профилирование пода обычно делают эфемерным debug-контейнером в том же namespace процессов, с добавленным
SYS_PTRACE. Класть py-spy в основной образ ради этого не обязательно.
Если не получилось
- «Permission denied» или «Failed to open process». Нет прав на трассировку. Запустите через
sudo, а в контейнере проверьтеSYS_PTRACEи значениеptrace_scope. - «Failed to find python version from target process». Целевой процесс не Python или версия CPython новее, чем поддерживает ваша сборка py-spy. Обновите py-spy до свежего релиза и проверьте по README поддержку вашей версии Python.
- Флейм-граф — сплошное ожидание, работы почти нет. Поток простаивает на I/O или в GIL. Пересоберите профиль, исключив idle-время, и снимайте именно в момент нагрузки, а не в затишье.
- Наверху висит только нативный кадр без Python-функций. Время уходит в C-расширение (например, парсинг или математика в numpy). py-spy покажет это как нативный кадр, но глубже в C он не разбирает — это ожидаемо.
- Пустой или почти пустой профиль за короткий интервал. Слишком мало сэмплов. Увеличьте
--durationи снимайте под реальным трафиком.
Что делать после
Найденную горячую функцию подтвердите точечно: оберните её локально cProfile на стенде и сравните счётчики вызовов — py-spy показал где, cProfile покажет сколько раз. Для постоянного наблюдения за прод-воркерами имеет смысл настроить периодический сбор флейм-графов, а не дёргать py-spy руками при каждом инциденте. Если горячая точка оказалась в C-расширении, дальше профилируйте уже нативным инструментом уровня perf — py-spy своё дело сделал, показав направление. И держите версию py-spy в актуальном состоянии: совместимость с новыми минорными релизами CPython добавляется именно в обновлениях.
Редактор изображений: Убрать лишнее, заменить фон, добавить объект, расширить кадр — на вашем изображении.
Спросить за 20 ₽Источники: py-spy — репозиторий проекта на GitHub
Частые вопросы
py-spy остановит мой сервис на время профилирования?
У меня Python 3.13, py-spy будет работать?
Получаю Permission denied, хотя запускаю от нужного пользователя. Что не так?
Чем py-spy отличается от cProfile, зачем он мне?
Флейм-граф показал время только в нативном кадре, Python-функций почти нет. Это баг?
Можно ли профилировать под в Kubernetes без пересборки образа?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.