← Назад к вопросам

Чему научила допущенная ошибка или сложная ситуация

2.0 Middle🔥 121 комментариев
#REST API и HTTP

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Lessons Learned: Ошибки и сложные ситуации

У каждого разработчика есть ошибки, которые научили его больше, чем тысяча удачных проектов. Хочу рассказать о нескольких ситуациях, которые сформировали мой подход к разработке.

Ошибка 1: N+1 Query Problem (которая стоила компании денег)

Что произошло: Я написал код, который выглядел логично, но скрывал серьёзную проблему производительности:

# ❌ ПЛОХО — N+1 query problem
users = db.query(User).all()  # 1 запрос
for user in users:  # N итераций
    # Каждая итерация вызывает отдельный запрос к БД
    print(user.profile.bio)  # N запросов (TOTAL: N+1)

В production с 100,000 пользователями это означало:

  • 100,001 запрос к БД вместо 1–2
  • Время отклика возросло с 100ms до 30 секунд
  • Резко выросло потребление ресурсов RDS
  • Счёт за облако увеличился на $5,000/месяц

Чему я научился:

  1. Тестировать с реальными объёмами данных — багу было видно сразу, если бы я тестировал с 1,000+ записей
  2. Использовать eager loading:
# ✅ ХОРОШО — Eager loading
from sqlalchemy.orm import joinedload

users = (
    db.query(User)
    .options(joinedload(User.profile))  # Загружаем профиль в одном запросе
    .all()
)
for user in users:
    print(user.profile.bio)  # Больше нет дополнительных запросов
  1. Профилировать запросы в development:
from django.db import connection
from django.test.utils import override_settings

@override_settings(DEBUG=True)
def test_user_queries():
    users = User.objects.select_related('profile').all()
    for user in users:
        _ = user.profile.bio
    
    # В development можно видеть все запросы
    print(f"Total queries: {len(connection.queries)}")
    assert len(connection.queries) <= 2  # Проверяем лимит

Ошибка 2: Неправильная обработка исключений (которая скрывала баги)

Что произошло: Я писал слишком общий try-except, который глушил все ошибки:

# ❌ ПЛОХО — слишком общий exception handling
def process_payment(order):
    try:
        payment = PaymentService.charge(order.amount)
        order.status = 'paid'
        db.session.commit()
    except Exception as e:
        # Логируем и... забываем
        logger.error(f"Payment failed: {e}")
        # Но что дальше? Отправили ли деньги? Откатилась ли транзакция?
        return False

Это привело к:

  • Silent failures — платежи проходили, но статус не обновлялся
  • Data inconsistency — в БД были заказы в broken state
  • Lost money — некоторые клиенты платили дважды или не платили совсем
  • Трудно дебажить — логи говорили только "Payment failed", но не почему

Чему я научился:

  1. Ловить специфичные исключения:
# ✅ ХОРОШО — specific exception handling
def process_payment(order):
    try:
        payment = PaymentService.charge(order.amount)
        order.status = 'paid'
        db.session.commit()
    except PaymentGatewayTimeout as e:
        # Известная, временная ошибка — retry позже
        logger.warning(f"Payment gateway timeout: {e}")
        raise RetryableError() from e
    except InsufficientFundsError as e:
        # Ошибка клиента — понятное сообщение
        logger.info(f"Insufficient funds for user {order.user_id}")
        raise BusinessError("Not enough funds") from e
    except Exception as e:
        # Неожиданная ошибка — нужно уведомить девопса
        logger.exception(f"Unexpected error processing payment: {e}")
        send_alert_to_devops()
        raise
  1. Транзакции и откаты:
# ✅ ХОРОШО — атомарные операции
from sqlalchemy import begin_nested

def process_payment(order):
    with db.session.begin_nested():
        try:
            payment = PaymentService.charge(order.amount)
            order.status = 'paid'
            db.session.commit()  # Коммитим только при успехе
        except PaymentError:
            # Автоматический rollback благодаря nested transaction
            db.session.rollback()
            raise
  1. Логирование контекста:
import structlog

logger = structlog.get_logger()

def process_payment(order):
    try:
        logger.info("processing_payment", order_id=order.id, amount=order.amount)
        payment = PaymentService.charge(order.amount)
        logger.info("payment_success", order_id=order.id, payment_id=payment.id)
    except PaymentError as e:
        logger.error(
            "payment_failed",
            order_id=order.id,
            amount=order.amount,
            error=str(e),
            error_code=e.code
        )
        raise

Ошибка 3: Отсутствие мониторинга и алертов

Что произошло: Мы деплоили код, который был хорошо протестирован локально. Но в production с реальным трафиком и нагрузкой:

  • Память медленно растёт (memory leak)
  • Спустя 48 часов сервер падает
  • Клиенты заметили проблему раньше, чем мы

Чему я научился:

  1. Мониторить метрики с самого начала:
from prometheus_client import Histogram, Counter, Gauge
import time

request_duration = Histogram(
    'request_duration_seconds',
    'Time spent processing request'
)
request_count = Counter(
    'requests_total',
    'Total requests',
    ['method', 'endpoint', 'status']
)
memory_usage = Gauge(
    'memory_usage_bytes',
    'Memory usage'
)

def my_endpoint():
    with request_duration.time():
        try:
            result = expensive_operation()
            request_count.labels(method='GET', endpoint='/api/data', status=200).inc()
            return result
        except Exception as e:
            request_count.labels(method='GET', endpoint='/api/data', status=500).inc()
            raise
  1. Настроить алерты:
# prometheus-rules.yml
groups:
  - name: app_alerts
    rules:
      - alert: HighMemoryUsage
        expr: memory_usage_bytes > 1e9  # 1GB
        for: 5m
        annotations:
          summary: "High memory usage detected"
      
      - alert: ErrorRateHigh
        expr: rate(requests_total{status="500"}[5m]) > 0.05  # > 5%
        annotations:
          summary: "Error rate above 5%"
  1. Профилировать память:
import tracemalloc
import linecache

def find_memory_leak():
    tracemalloc.start()
    # ... run code ...
    snapshot = tracemalloc.take_snapshot()
    top_stats = snapshot.statistics('lineno')
    for stat in top_stats[:10]:
        print(stat)

Ошибка 4: Отсутствие версионирования API

Что произошло: Мы просто изменили API endpoint, думая что никто им не пользуется:

# v1 (старый)
@app.get("/api/users/{user_id}")
def get_user(user_id: int):
    return {"id": user_id, "name": "John"}

# Потом мы изменили структуру:
# v1 (новый, сломанный)
@app.get("/api/users/{user_id}")
def get_user(user_id: int):
    return {"user_id": user_id, "full_name": "John", "email": "..."}

Результат:

  • Мобильное приложение (которое мы забыли про него) сломалось
  • Интеграция с партнёрской системой упала
  • Клиенты потеряли доступ

Чему я научился:

  1. Всегда версионировать API:
# v1 — оставляем как есть, поддерживаем долго
@app.get("/api/v1/users/{user_id}")
def get_user_v1(user_id: int):
    return {"id": user_id, "name": "John"}

# v2 — новая версия с изменениями
@app.get("/api/v2/users/{user_id}")
def get_user_v2(user_id: int):
    return {"user_id": user_id, "full_name": "John", "email": "john@example.com"}

# Дефолтный маршрут указывает на latest, но с явным deprecation warning
@app.get("/api/users/{user_id}", deprecated=True)
def get_user(user_id: int):
    # Внутри просто вызываем новую версию
    return get_user_v2(user_id)
  1. Стратегия deprecation:
from datetime import datetime, timedelta

DEPRECATION_DATE = datetime(2024, 12, 31)

def check_deprecated(endpoint_name):
    if datetime.now() > DEPRECATION_DATE:
        logger.warning(f"{endpoint_name} is deprecated, please use v2")
        if datetime.now() > DEPRECATION_DATE + timedelta(days=30):
            raise HTTPException(status_code=410, detail="API endpoint removed")

Общие уроки

  1. Тестировать с реальными объёмами — микро-оптимизации и баги видны только при масштабе
  2. Быть specific в error handling — generic exceptions скрывают проблемы
  3. Мониторить всё — метрики, логи, ошибки
  4. Backwards compatibility важна — не ломай API без версионирования
  5. Code review спасает — дополнительная пара глаз ловит очевидные баги
  6. Documentation спасает — когда ты вернёшься к коду через год, ты поймёшь почему это сделано так

Вывод

Ошибки — это не стыдно. Стыдно делать одну и ту же ошибку дважды. Каждая ошибка — это школа, которая научила меня писать надёжный код и быть предусмотрительнее. Сейчас я работаю так, чтобы эти ошибки (и даже хуже) просто не произойдут благодаря правильным процессам и инструментам.

Чему научила допущенная ошибка или сложная ситуация | PrepBro