open atlas
↑ К треку
Python для JS/TS-разработчиков PY · 08 · 01

Профилирование с cProfile и py-spy: измерьте продакшен-нагрузку до правок кода

cProfile перехватывает каждый вызов: оверхед 1.3-2x раздувает мелкие горячие функции и не видит внутренностей C. py-spy сэмплирует стеки снаружи через ptrace за ~1%: top, record-флеймграфы, dump для зависших процессов. timeit льстит. Профилируйте прод-нагрузку и перемеряйте p99.

PY Senior ◷ 18 min
Уровень
ОсновыJuniorMiddleSenior

p99 у API чекаута держался на 480 мс при SLO в 200 мс. cProfile на дев-машине указал на compute_discounts — 38% кумулятивного времени, — и команда потратила спринт на мемоизацию и переписывание. Микробенчмарк показал ускорение в 6 раз. Деплой сдвинул p99 с 480 до 465 мс. Потом кто-то запустил py-spy top --pid на живом продакшен-воркере: 61% сэмплов сидели внутри JSON-сериализации двухмегабайтного ответа — стоимость, которую дев-профиль не видел вовсе, потому что фикстура была синтетической, 2 КБ, а похуковый оверхед cProfile раздул тысячи мелких вызовов хелперов compute_discounts, записав работу C-сериализатора одной непрозрачной строкой. Замена сериализатора и урезание ответа — и p99 стал 170 мс. Спринт ушёл впустую из-за профилировщика, которому поверили, запустив его не на той нагрузке и не в том процессе.

Через десять минут ты будешь знать, почему cProfile может указать не на ту функцию — и как py-spy на живом процессе находит то, что cProfile не видел никогда.

cProfile: детерминированная трассировка и что искажает оверхед

cProfile — детерминированный трассировщик: он регистрирует C-уровневый профильный хук (механика lsprof за sys.setprofile), срабатывающий на каждом вызове и возврате функции, и копит счётчики и таймеры по функциям. Ничего не пропускается — в этом привлекательность, — но каждый вызов платит фиксированный налог на учёт, поэтому программа целиком обычно работает в 1.3-2 раза медленнее. Искажение неравномерно. Фиксированная цена на вызов огромна относительно хелпера на 200 нс, вызванного десять миллионов раз, и ничтожна относительно 50-миллисекундного запроса к базе, вызванного дважды. Поэтому мелкие горячие Python-функции выглядят хуже, чем есть, и рейтинг «главных виновников» гнётся к числу вызовов, а не к реальной стоимости.

Чтение вывода — отдельный навык: tottime — собственное время (без вызываемых), cumtime — включающее. Сортируйте по cumtime, чтобы найти дорогое поддерево, затем по tottime внутри него, чтобы найти, где реально горят циклы. pstats режет программно; snakeviz показывает те же данные интерактивно.

import cProfile, pstats

cProfile.run("handle_request(req)", "out.prof")
p = pstats.Stats("out.prof")
p.sort_stats("cumulative").print_stats(12)  # найти дорогое поддерево
p.sort_stats("tottime").print_stats(12)     # найти собственную стоимость внутри

Две слепые зоны структурны. Первая: внутренности C невидимыjson.dumps или операция numpy выглядят одной непрозрачной строкой: общее время есть, разбивки внутри нет. Вторая: потоки — по умолчанию cProfile наблюдает только поток, в котором запущен; рабочие потоки идут непрофилированными, пока каждый не установит свой профилировщик. Под asyncio метод запуска цикла событий проглатывает всё в один cumtime, и пер-корутинная атрибуция мутнеет.

Викторина

cProfile показывает `validate_row` (хелпер на 15 строк, 10 млн вызовов) первым по tottime в профиле сервиса. Какой дисциплинированный следующий шаг?

py-spy: сэмплирование снаружи процесса

py-spy переворачивает сделку. Он работает отдельным процессом: читает память цели — process_vm_readv/ptrace на Linux, mach-API на macOS, — находит состояния потоков интерпретатора и обходит структуры фреймов, восстанавливая каждый Python-стек примерно 100 раз в секунду. Без изменения кода, без импорта, без рестарта: подключение по PID к процессу, запущенному задолго до того, как вы узнали о проблеме. Цель приостанавливается на микросекунды на сэмпл, так что оверхед — около 1%. Безопасно против продакшена.

Три подкоманды покрывают полевой процесс:

  • py-spy top --pid 4137 — живой вид в стиле htop: какие функции владеют сэмплами прямо сейчас.
  • py-spy record -o prof.svg --pid 4137 --duration 60 — флеймграф; добавьте --native, чтобы вплести C-фреймы (именно так сериализатор из хука стал видимым), --idle — чтобы включить заблокированные потоки.
  • py-spy dump --pid 4137 — одномоментный стек каждого потока: инструмент для зависшего процесса. Воркер на 0% CPU, переставший разбирать очередь, диагностируется за секунды — видно, на каком именно захвате лока или чтении сокета припаркован каждый поток.

Честная оговорка: сэмплирование статистично. Всплеск на 5 мс раз в минуту на 100 Гц может не попасть в сэмплы вовсе; нужно достаточно стенового времени на исследуемом поведении, а латентность редких событий требует трейсинга, не профилирования.

timeit и ложь микробенчмарков

timeit отключает GC и сообщает лучший из N повторов — он по замыслу меряет тёпло-кэшевый, разогнанный турбо-бустом идеал изолированного сниппета. Реальный сервис гоняет тот же код с холодными кэшами, перемешанной работой, вытесняющей предсказатель ветвлений, частотным скейлингом и фрагментированным аллокатором. Выигрыш в 30% по timeit может быть невидим на p99. Используйте его, чтобы сравнить две реализации одной маленькой операции при одинаковой лжи, — и никогда, чтобы предсказать латентность сервиса.

Дисциплина

Прежде чем потянуться к профилировщику, спроси себя: совпадает ли эта среда с продакшеном? Реальные размеры payload, реальная конкурентность, реальные распределения данных, реальные хитрейты кэшей. Синтетический цикл на дев-машине — это другая программа, случайно разделяющая исходники. Рабочий порядок: py-spy против продакшена (или реплея прод-трафика), чтобы найти где; cProfile и timeit локально, чтобы итерироваться над почему; затем перемерить p99 после деплоя — профилировщик это улика, метрика SLO — приговор.

Викторина

Продакшен-воркер на 0% CPU перестал разбирать очередь и ни на что не отвечает. Первый диагностический ход?

Вспомните перед уходом
  1. 01
    Сопоставьте cProfile и py-spy: механизм, оверхед и что каждый не способен увидеть.
  2. 02
    Микробенчмарк говорит, что оптимизированная функция в 6 раз быстрее, но продакшен-p99 не сдвинулся. Перечислите ошибки измерения, дающие такой исход.
Итог

cProfile и py-spy отвечают на один вопрос противоположными контрактами. Трассировщик хукает каждый вызов изнутри: полно, детерминированно, но в 1.3-2 раза медленно, со смещением против мелких горячих функций пропорционально числу вызовов, слеп внутри C и однопоточен по умолчанию — читайте его как cumtime для поддеревьев и tottime для собственной стоимости, через pstats или snakeviz. Сэмплер читает структуры фреймов интерпретатора из другого процесса через ptrace: ~1% оверхеда, без рестарта, безопасно для продакшена, C-фреймы доступны под —native, с top для живой атрибуции, record для флеймграфов и dump как рентгеном зависшего процесса — его единственный налог статистический, так что редкие события требуют терпения или трейсинга. timeit меряет тёпло-кэшевый лучший случай с выключенным GC и честен лишь при сравнении двух сниппетов в одинаковых условиях. Мета-навык важнее всех трёх инструментов: профилируйте продакшен-форму — payload, конкурентность, распределение данных, — потому что синтетический цикл это другая программа, и судите успех по метрике SLO после деплоя, а не по бенчмарку, оправдавшему изменение.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 6 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

Примени это

Примени этот урок в реальном проекте.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.