Чему научила допущенная ошибка или сложная ситуация
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
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,000+ записей
- Использовать 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) # Больше нет дополнительных запросов
- Профилировать запросы в 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", но не почему
Чему я научился:
- Ловить специфичные исключения:
# ✅ ХОРОШО — 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
- Транзакции и откаты:
# ✅ ХОРОШО — атомарные операции
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
- Логирование контекста:
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 часов сервер падает
- Клиенты заметили проблему раньше, чем мы
Чему я научился:
- Мониторить метрики с самого начала:
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
- Настроить алерты:
# 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%"
- Профилировать память:
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": "..."}
Результат:
- Мобильное приложение (которое мы забыли про него) сломалось
- Интеграция с партнёрской системой упала
- Клиенты потеряли доступ
Чему я научился:
- Всегда версионировать 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)
- Стратегия 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")
Общие уроки
- Тестировать с реальными объёмами — микро-оптимизации и баги видны только при масштабе
- Быть specific в error handling — generic exceptions скрывают проблемы
- Мониторить всё — метрики, логи, ошибки
- Backwards compatibility важна — не ломай API без версионирования
- Code review спасает — дополнительная пара глаз ловит очевидные баги
- Documentation спасает — когда ты вернёшься к коду через год, ты поймёшь почему это сделано так
Вывод
Ошибки — это не стыдно. Стыдно делать одну и ту же ошибку дважды. Каждая ошибка — это школа, которая научила меня писать надёжный код и быть предусмотрительнее. Сейчас я работаю так, чтобы эти ошибки (и даже хуже) просто не произойдут благодаря правильным процессам и инструментам.