Профилирование с cProfile и py-spy: измерьте продакшен-нагрузку до правок кода
cProfile перехватывает каждый вызов: оверхед 1.3-2x раздувает мелкие горячие функции и не видит внутренностей C. py-spy сэмплирует стеки снаружи через ptrace за ~1%: top, record-флеймграфы, dump для зависших процессов. timeit льстит. Профилируйте прод-нагрузку и перемеряйте p99.
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 перестал разбирать очередь и ни на что не отвечает. Первый диагностический ход?
- 01Сопоставьте cProfile и py-spy: механизм, оверхед и что каждый не способен увидеть.
- 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-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.