PythonSeniorSystem design
Как дебажить медленный endpoint в production?
Сначала метрики (p50/p95/p99, RPS, error rate) и distributed tracing — найти, где время уходит. Дальше — таргетированный профайлинг: SQL plan (EXPLAIN ANALYZE), N+1, external API latency, CPU/lock contention. Воспроизвести в staging, чинить корень, добавить guard.
План действий
- Метрики. Откройте dashboard endpoint: p50/p95/p99 latency, RPS, error rate, версия билда. Сравните с baseline. Если деградация коррелирует с релизом — катите назад и дальше уже спокойно ищите.
- Distributed tracing (Jaeger, Tempo, Datadog APM, OpenTelemetry). Откройте slow span: видно цепочку DB → cache → external → CPU. Если tracing нет — это первое, что надо завести.
- Логи. Структурные логи с
request_idи timing-полями. Срез по slow requests за окно. - База данных.
pg_stat_statementsдля топ-N slow queries,EXPLAIN (ANALYZE, BUFFERS)на подозрительный SQL. N+1 — главный убийца ORM endpoint-ов. - External calls. Таймауты, retry, circuit breaker. Один медленный downstream обнуляет SLA endpoint-а.
- CPU/memory.
py-spy top --pid <pid>илиpy-spy recordпрямо на проде (без рестарта). Для async — флейм-граф черезaustinилиscalene. - Lock contention. GIL contention в multi-thread, async loop с blocking call (см.
loop.slow_callback_duration), advisory locks в БД. - Воспроизведение. Соберите curl/locust сценарий, запустите в staging с production-like данными. Без воспроизведения фиксить опасно.
- Фикс + регресс-тест. Performance guard (p95 budget) в CI, чтобы не вернулось.
Инструменты на проде
# py-spy: профайлинг живого процесса без рестарта
py-spy top --pid 12345
py-spy record -o profile.svg --pid 12345 --duration 30
# Postgres: топ медленных запросов
psql -c "SELECT query, mean_exec_time, calls FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 20;"
# EXPLAIN на конкретный план
psql -c "EXPLAIN (ANALYZE, BUFFERS) SELECT ... ;"
# tcpdump до downstream, если подозреваем сеть
sudo tcpdump -i any -nn host downstream.internal and port 5432
Снять локальные метрики в коде
import logging
import time
from contextlib import contextmanager
import structlog
log = structlog.get_logger()
@contextmanager
def timed(step: str):
t0 = time.perf_counter()
try:
yield
finally:
log.info("step.timing", step=step, elapsed_ms=round((time.perf_counter() - t0) * 1000, 2))
# OpenTelemetry — нормальный путь
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
async def handler(req):
with tracer.start_as_current_span("auth"):
user = await authenticate(req)
with tracer.start_as_current_span("db.fetch_orders"):
orders = await fetch_orders(user.id)
with tracer.start_as_current_span("external.pricing"):
prices = await pricing.batch(orders)
return render(orders, prices)
Типичные находки
- N+1 SQL — SQLAlchemy без
selectinload/joinedload. - Сериализация — Pydantic v1 в больших списках; миграция на v2/orjson.
- Sync HTTP-клиент в async endpoint — блокирует loop.
- Тяжёлый JSON parse в response — используйте
orjsonили стриминг. - Кэш промахивает (низкий hit rate в Redis) или сериализация ключа неконсистентна.
- Auth/permission делает запрос к user-service на каждый request без кэша.
Подводные камни
- Смотреть только average latency — она маскирует хвост; всегда p95/p99.
- Оптимизировать без измерений — типично улучшают не то, что тормозит.
- Профайлить prod без guard на overhead (например,
cProfileв hot path даёт 2-3x просадку). - Фикс без regression test — деградация вернётся через релиз.
- Игнорировать инфраструктуру: CPU steal на cloud, throttling диска, сетевые retries.
Common mistakes
- Начинать с переписывания endpoint.
- Не проверять количество SQL-запросов.
- Игнорировать recent deploy/config changes.
What the interviewer is testing
- Предлагает observability-first план.
- Понимает percentiles.
- Разделяет DB/I/O/CPU bottlenecks.