Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Кейсы оптимизации
case_studies

Кейсы оптимизации

Реальные истории: ускорение API в 10 раз, поиск memory leak, оптимизация batch-задач

Кейсы оптимизации

Теория без практики мертва. Практика без теории слепа.

Три реальных кейса оптимизации с использованием py-spy. От медленного API до утечек памяти.


#Кейс 1: Ускорение API в 10 раз

#Проблема

REST API для получения статистики пользователя отвечал 2.5 секунды вместо целевых 200 мс.

Контекст:

  • Flask-приложение
  • PostgreSQL через SQLAlchemy
  • 10 000+ пользователей
  • Запрос: /api/user/<id>/stats

#Шаг 1: Профилирование

# Запустили нагрузочный тест ab -n 100 -c 10 http://localhost:5000/api/user/123/stats # В другом терминале профилирование py-spy record -o api_slow.svg --rate 20 --duration 30 --pid 12345

#Шаг 2: Анализ flame graph

Открыли api_slow.svg:

┌─────────────────────────────────────────────────────┐
│              get_user_stats (95%)                   │
├─────────────────────────────────────────────────────┤
│         get_all_transactions (75%)                  │
│         calculate_metrics (15%)                     │
│         serialize_response (5%)                     │
└─────────────────────────────────────────────────────┘

Вывод: get_all_transactions занимает 75% времени!

#Шаг 3: Углублённый анализ

Запрофилировали get_all_transactions:

┌─────────────────────────────────────────────────────┐
│         get_all_transactions (75%)                  │
├─────────────────────────────────────────────────────┤
│    Transaction.query.filter (5%)                    │
│    transaction.to_dict() (70%) ← Проблема!          │
└─────────────────────────────────────────────────────┘

Код:

def get_all_transactions(user_id): transactions = Transaction.query.filter_by(user_id=user_id).all() return [t.to_dict() for t in transactions] # N+1! # to_dict() делает запрос для каждой транзакции: def to_dict(self): return { 'id': self.id, 'amount': self.amount, 'category': self.category.name, # ← Запрос к БД! 'merchant': self.merchant.name # ← Ещё запрос! }

Диагноз: N+1 запросов! Для 500 транзакций = 1001 запрос к БД.

#Шаг 4: Оптимизация

Было:

transactions = Transaction.query.filter_by(user_id=user_id).all()

Стало:

from sqlalchemy.orm import joinedload transactions = Transaction.query\ .filter_by(user_id=user_id)\ .options( joinedload(Transaction.category), joinedload(Transaction.merchant) )\ .all()

joinedload делает JOIN и загружает связанные объекты одним запросом.

#Шаг 5: Проверка

# Снова профилируем py-spy record -o api_fast.svg --rate 20 --duration 30 --pid 12345

Новый профиль:

┌─────────────────────────────────────────────────────┐
│              get_user_stats (95%)                   │
├─────────────────────────────────────────────────────┤
│         get_all_transactions (25%) ← Стало лучше!   │
│         calculate_metrics (60%) ← Теперь это узкое  │
│         serialize_response (5%)                     │
└─────────────────────────────────────────────────────┘

#Результат

МетрикаДоПослеУлучшение
Время ответа2500 мс250 мс10×
Запросов к БД100111000×
Запрофилированное время75%25%3×

#Урок

Всегда проверяйте N+1 запросы! py-spy показал, где время тратится, а анализ кода выявил причину.


#Кейс 2: Поиск утечки памяти

#Проблема

Сервис обработки файлов начинал потреблять 4 ГБ памяти через 2 часа работы (стартовал с 200 МБ).

Контекст:

  • Celery воркеры
  • Обработка CSV/Excel файлов
  • Память растёт постепенно, не освобождается

#Шаг 1: Профилирование памяти через py-spy

py-spy не профилирует память напрямую, но можно использовать dump для анализа состояния:

# Снимки каждые 5 минут for i in {1..24}; do py-spy dump --pid 12345 > dump_$(date +%H%M).txt sleep 300 done

#Шаг 2: Анализ снимков

Снимок 1 (200 МБ):

Thread 0x7FF895C3A5C0 (active)
    "MainThread"
    Func: process_file, File: tasks.py, Line: 45
    Func: parse_csv, File: parser.py, Line: 23

Снимок 12 (2 ГБ):

Thread 0x7FF895C3A5C0 (active)
    "MainThread"
    Func: process_file, File: tasks.py, Line: 45
    Func: parse_csv, File: parser.py, Line: 23
    # То же самое!

Вывод: Воркер застрял в обработке одного файла. Но почему память растёт?

#Шаг 3: Дополнительное расследование

Добавили логирование в код:

import tracemalloc import gc tracemalloc.start() def process_file(filepath): # До обработки current, peak = tracemalloc.get_traced_memory() print(f"До: {current / 1024 / 1024:.2f} MB") data = parse_csv(filepath) result = transform(data) # После обработки current, peak = tracemalloc.get_traced_memory() print(f"После: {current / 1024 / 1024:.2f} MB") return result

Лог:

До: 50.23 MB
После: 150.45 MB
До: 150.45 MB  ← Не освободилось!
После: 280.67 MB

#Шаг 4: Поиск причины

Анализ кода parse_csv:

def parse_csv(filepath): rows = [] with open(filepath) as f: for line in f: rows.append(line.strip().split(',')) # ← Сохраняем всё в памяти return rows

Проблема: Файл 500 МБ загружается целиком в память!

#Шаг 5: Оптимизация

Стало:

def parse_csv_streaming(filepath): """Генератор для потоковой обработки""" with open(filepath) as f: for line in f: yield line.strip().split(',') def process_file(filepath): # Обрабатываем по одной строке for row in parse_csv_streaming(filepath): process_row(row) # Не накапливаем в памяти

#Результат

МетрикаДоПослеУлучшение
Потребление памяти4 ГБ за 2 часа250 МБ стабильно16×
Время обработки30 сек/файл25 сек/файл1.2×

#Урок

py-spy dump показал, что воркер застревает в одной функции. Это навело на мысль о проблеме с памятью в этой функции.


#Кейс 3: Оптимизация batch-обработки

#Проблема

Ночной job обработки 100 000 записей выполнялся 3 часа вместо 30 минут.

Контекст:

  • Celery beat запускает job в 02:00
  • Обработка: валидация → трансформация → сохранение
  • 100 000 записей, ~100 записей/сек

#Шаг 1: Профилирование

# Запустили job вручную python -m celery -A myapp worker --loglevel=info # Профилирование с запуском py-spy record -o batch_slow.svg --children -- python batch_job.py

#Шаг 2: Анализ

┌─────────────────────────────────────────────────────┐
│              process_batch (95%)                    │
├─────────────────────────────────────────────────────┤
│         validate_record (40%)                       │
│         transform_record (35%)                      │
│         save_record (20%)                           │
└─────────────────────────────────────────────────────┘

Вывод: validate_record и transform_record занимают 75% времени.

#Шаг 3: Детальный анализ validate_record

┌─────────────────────────────────────────────────────┐
│         validate_record (40%)                       │
├─────────────────────────────────────────────────────┤
│    re.match (email_pattern) (25%) ← Regex!          │
│    re.match (phone_pattern) (10%)                   │
│    check_duplicates (5%)                            │
└─────────────────────────────────────────────────────┘

Код:

import re EMAIL_PATTERN = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' PHONE_PATTERN = r'^\+?1?\d{9,15}$' def validate_record(record): # Компиляция regex КАЖДЫЙ РАЗ! if not re.match(EMAIL_PATTERN, record['email']): return False if not re.match(PHONE_PATTERN, record['phone']): return False return True

Проблема: re.match компилирует regex каждый вызов! 100 000 записей × 2 паттерна = 200 000 компиляций.

#Шаг 4: Оптимизация

Стало:

import re # Компиляция ОДИН РАЗ при загрузке модуля EMAIL_PATTERN = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$') PHONE_PATTERN = re.compile(r'^\+?1?\d{9,15}$') def validate_record(record): if not EMAIL_PATTERN.match(record['email']): return False if not PHONE_PATTERN.match(record['phone']): return False return True

#Шаг 5: Анализ transform_record

┌─────────────────────────────────────────────────────┐
│         transform_record (35%)                      │
├─────────────────────────────────────────────────────┤
│    json.loads (20%) ← Парсинг JSON                  │
│    dict manipulation (10%)                          │
│    date parsing (5%)                                │
└─────────────────────────────────────────────────────┘

Код:

import json def transform_record(record): data = json.loads(record['raw_json']) # Парсинг каждый раз data['processed_at'] = datetime.now().isoformat() data['amount'] = float(data['amount']) / 100 # Конвертация копеек return data

Проблема: json.loads вызывается для каждой записи. Можно кэшировать?

Оптимизация:

from functools import lru_cache import json @lru_cache(maxsize=1000) def parse_json_cached(json_string): return json.loads(json_string) def transform_record(record): # Кэширование одинаковых JSON data = parse_json_cached(record['raw_json']) data['processed_at'] = datetime.now().isoformat() data['amount'] = float(data['amount']) / 100 return data

#Результат

МетрикаДоПослеУлучшение
Время job3 часа25 минут7×
Записей/сек1007007×
validate_record40%5%8×
transform_record35%15%2.3×

#Урок

Компиляция regex в цикле — классическая ошибка. py-spy показал, что re.match занимает 25% времени, что привело к поиску проблемы.


#Общие паттерны оптимизации

#1. N+1 запросы

Симптом: Одна функция занимает 50%+ времени, внутри много вызовов БД.

Решение: joinedload, selectinload, пакетная обработка.

#2. Компиляция в цикле

Симптом: re.match, json.loads, конструкторы классов занимают много времени.

Решение: Компилировать/создавать заранее, кэшировать.

#3. Блокирующие вызовы

Симптом: time.sleep, socket.recv, Lock.acquire в профиле.

Решение: Асинхронность, таймауты, неблокирующие операции.

#4. Избыточные вычисления

Симптом: Одна и та же функция вызывается много раз с одинаковыми аргументами.

Решение: lru_cache, мемоизация.


#Практическое задание

  1. Возьмите свой медленный скрипт
  2. Запрофилируйте с py-spy
  3. Найдите функцию с наибольшим % времени
  4. Проанализируйте, почему она медленная
  5. Оптимизируйте
  6. Запрофилируйте снова, сравните результат

Ключевая идея: py-spy не оптимизирует код за вас. Но он точно показывает, что оптимизировать. А дальше — ваш опыт и здравый смысл.