IT-журнал

Высоконагруженный Django: как выжать максимум из «батареек» без боли

Django часто ругают за «медленность», но правда в том, что большинство проблем на высоких нагрузках — не в самом фреймворке, а в архитектуре вокруг него. Синхронный воркер, неоптимальные запросы к БД и отсутствие кэша убивают производительность быстрее...

Обновлено 19.09.2026

⏱ 5 мин чтения · 👁 прочитали 3

Как выжать максимум из «батареек» без боли

Django часто ругают за «медленность», но правда в том, что большинство проблем на высоких нагрузках — не в самом фреймворке, а в архитектуре вокруг него. Синхронный воркер, неоптимальные запросы к БД и отсутствие кэша убивают производительность быстрее, чем любой WSGI. Эта статья — практическая карта того, как построить Django-приложение, способное держать серьёзный трафик.

Разберём слои: от воркеров и ASGI до БД, кэша, фоновых задач и наблюдаемости.


1. Слой приложений: stateless, горизонтально, за балансировщиком

Главный принцип — приложение должно быть stateless. Если состояние живёт в памяти процесса (локальный кэш, сессии в файлах, загруженные файлы на диск), вы не сможете масштабироваться горизонтально без sticky-сессий и боли.

Сессии и состояние — во внешнее хранилище

# settings.py
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "default"

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": "redis://redis:6379/0",
    }
}

Redis как единая точка состояния — сессии, блокировки, очереди, rate-limit.

WSGI или ASGI?

  • WSGI + Gunicorn/uWSGI — предсказуемо и просто. Классика для CRUD-нагрузок.
  • ASGI + Uvicorn/Gunicorn (uvicorn.workers.UvicornWorker) — если есть async-вьюхи, WebSocket (Channels), SSE, много I/O-bound вызовов.
gunicorn config.wsgi:application \
  --workers $((2 * $(nproc) + 1)) \
  --worker-class uvicorn.workers.UvicornWorker \
  --timeout 30 \
  --max-requests 1000 \
  --max-requests-jitter 100 \
  --bind 0.0.0.0:8000

--max-requests с джиттером помогает бороться с утечками памяти в долгоживущих воркерах. Число воркеров подбирается эмпирически под нагрузку, а не «на глаз»: ориентир 2 * CPU + 1 — только стартовая точка.

Балансировка

Nginx/HAProxy/Traefik или облачный ALB. Приложение должно отвечать идемпотентно на health-проверки, а балансировщик — уметь выкидывать «уставшие» ноды.


2. База данных: 90% узких мест живёт здесь

Реляционная БД почти всегда становится бутылочным горлышком раньше, чем CPU приложения.

Пул соединений

Django не имеет встроенного пула. Два подхода:

  1. CONN_MAX_AGE — переиспользование соединений на процесс:
    python DATABASES["default"]["CONN_MAX_AGE"] = 60 DATABASES["default"]["CONN_HEALTH_CHECKS"] = True

  2. PgBouncer в режиме transaction — обязателен, когда воркеров сотни, а Postgres не переваривает тысячи коннектов. Важно: в transaction-режиме нельзя использовать session-level features (например, prepared statements без настройки, advisory locks).

Реплики для чтения

Разделите трафик: запись — на primary, чтение — на реплики. Есть готовые решения: django-db-read-replica, django-multidb-router, либо собственный router.

class PrimaryReplicaRouter:
    def db_for_read(self, model, **hints):
        if hints.get("instance") and hints["instance"]._state.db == "default":
            return "default"  # read-after-write из primary
        return "replica"

    def db_for_write(self, model, **hints):
        return "default"

Осторожно с read-after-write: если пользователь только что записал данные и сразу читает — читайте с primary, иначе увидит устаревшие данные.

Индексы и запросы

  • EXPLAIN ANALYZE — ваш лучший друг.
  • Составные индексы под реальные WHERE + ORDER BY.
  • Partial indexes и покрывающие (covering) индексы в Postgres.
  • Не забывайте про db_index=True и Meta.indexes.

N+1 и работа с ORM

# Плохо: N+1
for order in Order.objects.all():
    print(order.user.email)

# Хорошо
orders = Order.objects.select_related("user").all()

# Обратная сторона FK / M2M
orders = Order.objects.prefetch_related("items").all()

# Только нужные поля
emails = User.objects.values_list("email", flat=True)

# Стриминг больших выборок — итератор не тянет всё в память
for row in Order.objects.iterator(chunk_size=2000):
    ...

Для массовых вставок:

OrderItem.objects.bulk_create(items, batch_size=1000)
Model.objects.filter(...).update(status="done")  # вместо save() в цикле

Полезно навесить проверки в CI: django-debug-toolbar, nplusone, django-perf-rec в тестах, чтобы N+1 ловился на ревью, а не в проде.


3. Кэширование: на каждом слое своя роль

Кэш — не «серебряная пуля», а система уровней, каждый со своими инвалидациями.

Слой Что кэшируем Инвалидация
Browser / CDN Статика, публичные ответы Cache-Control, версии URL
Reverse proxy Полные страницы, фрагменты TTL / теги
Application (Redis) Дорогие вычисления, объекты Явная / по ключам
Database Query plan, buffers Автоматически

Кэш представлений и фрагментов

from django.views.decorators.cache import cache_page

@cache_page(60, key_prefix="home_v2")
def home(request):
    ...
{% load cache %}
{% cache 300 sidebar request.user.id %}
  ...
{% endcache %}

Ключевые паттерны

  • Cache-aside — читаем из кэша, при промахе идём в БД и записываем. Риск stampede.
  • Локи для пересчёта — чтобы 1000 запросов не пересчитывали одно и то же:
    ```python
    from django.core.cache import cache

def get_expensive(key, producer, ttl=300):
value = cache.get(key)
if value is None:
lock = cache.lock(f"lock:{key}", timeout=10)
with lock:
value = cache.get(key)
if value is None:
value = producer()
cache.set(key, value, ttl)
return value
`` - **Версионирование ключей** (key_prefix,VERSION`) — упрощает инвалидацию при деплое.


4. Всё тяжёлое — в фон

HTTP-запрос должен длиться миллисекунды. Всё, что дольше сотен миллисекунд и не нужно для ответа — в очередь.

from celery import shared_task

@shared_task(bind=True, max_retries=5, autoretry_for=(ConnectionError,))
def send_invoice(self, order_id):
    order = Order.objects.get(pk=order_id)
    render_and_send(order)

Что вынести в Celery/RQ/Dramatiq:

  • Email/SMS/push-уведомления
  • Генерация PDF, отчётов, экспортов
  • Обработка загруженных файлов (thumbnails, antivirus, OCR)
  • Синхронизации с внешними API
  • Периодические задачи (Celery Beat)

Важные правила:

  • Идемпотентность задач — брокеры доставляют «хотя бы раз».
  • Ретраи с backoff и dead-letter queue.
  • Не держите открытые транзакции и не передавайте ORM-объекты в задачи — передавайте pk.
  • Тяжёлые задачи — в отдельные очереди и отдельные пулы воркеров, чтобы медленный отчёт не блокировал отправку писем.

5. Асинхронность там, где она реально помогает

Django поддерживает async-вьюхи, но ORM в синхронном режиме под капотом выполняется через threadpool. Async выигрывает, когда в запросе много параллельных внешних вызовов.

import asyncio, httpx

async def dashboard(request):
    async with httpx.AsyncClient() as client:
        a, b, c = await asyncio.gather(
            client.get("https://api-a.example/data"),
            client.get("https://api-b.example/data"),
            client.get("https://api-c.example/data"),
        )
    return render(request, "dash.html", {"a": a.json(), "b": b.json(), "c": c.json()})

Правило простое: если вьюха ждёт сеть — async поможет; если жрёт CPU (шаблоны, PDF, шифрование) — async не поможет, поможет только вынос в фон или больше воркеров.

WebSocket/SSE — через Django Channels + Redis как channel layer.


6. Статика, медиа, CDN

  • Статика — в S3/GCS + CloudFront/Cloudflare. django-storages + ManifestStaticFilesStorage для кэш-бастинга.
  • Медиа от пользователей — никогда не через Django-воркеры. Прямые presigned-загрузки в объектное хранилище, обработка — фоном.
  • Формат — WebP/AVIF, lazy-loading, HTTP/2 или HTTP/3.
STORAGES = {
    "default": {"BACKEND": "storages.backends.s3.S3Storage"},
    "staticfiles": {"BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage"},
}

7. Защита и деградация под нагрузкой

На пике главное — не «пропустить всё», а не упасть целиком.

  • Rate limiting на входе: Nginx limit_req, или django-ratelimit, или свой middleware поверх Redis (INCR + EXPIRE).
  • Таймауты на все внешние вызовы — иначе ваши воркеры утонут в ожидании.
  • Circuit breaker (например, pybreaker) на нестабильные зависимости.
  • Bulkhead — отдельные пулы/очереди для разных типов трафика.
  • Graceful degradation — отдать частичный ответ или кэш, а не 500.
  • Очереди с ограничением — не накапливать бесконечный бэклог, лучше отдать 429 клиенту.
limit_req_zone $binary_remote_addr zone=api:10m rate=20r/s;

location /api/ {
    limit_req zone=api burst=40 nodelay;
    proxy_pass http://django_upstream;
}

8. Наблюдаемость: без неё оптимизация — гадание

Что должно быть в проде с первого дня:

  • Метрики: p50/p95/p99 latency, RPS, ошибки, saturation БД, длина очередей Celery, hit-rate кэша. Prometheus + Grafana, django-prometheus.
  • Трейсинг: OpenTelemetry — связать HTTP-запрос, SQL, вызовы в Celery и внешние API в один trace.
  • Логи: структурированные (JSON), с request id, без чувствительных данных.
  • Профайлинг: py-spy, django-silk, cProfile под нагрузкой.

Метрика, которую стоит держать на дашборде, — APDEX или бюджет по времени ответа. Всё остальное — следствие.


9. Деплой и масштабирование

  • Контейнеры + Kubernetes (или ECS/Nomad) с HPA по RPS/CPU/латентности.
  • Blue-green или canary деплои — БД-миграции должны быть обратно совместимы (add column → backfill → switch code → drop old).
  • Долгие миграции — отдельными задачами, ALTER TABLE ... CONCURRENTLY, без блокировок.
  • Readiness / liveness пробы отделены друг от друга.
  • Ограничения ресурсов (requests/limits) — иначе один утёкший воркер утащит ноду.

Чек-лист для высоконагруженного Django

  1. Приложение stateless, состояние — в Redis/БД/объектном хранилище.
  2. БД: пул (PgBouncer), реплики, индексы, отсутствие N+1.
  3. Многоуровневый кэш с продуманной инвалидацией.
  4. Всё тяжёлое — в Celery с идемпотентностью и ретраями.
  5. Async — только под I/O-bound задачи.
  6. Статика/медиа — мимо приложения, через CDN.
  7. Rate limit, таймауты, circuit breaker, graceful degradation.
  8. Метрики, трейсинг, логи, профайлинг.
  9. Миграции и деплой без простоя.
  10. Регулярный load testing (k6, Locust, wrk) — иначе нельзя узнать, где предел.

Вывод

Высоконагруженный Django — это не про замену фреймворка, а про архитектурную дисциплину. Django даёт удобные абстракции, но под нагрузкой вы всё равно работаете с теми же законами: узкое место — база, память ограничена, сеть ненадёжна, а состояние нужно централизовать. Если слой приложения stateless, БД грамотно разведена и проиндексирована, тяжёлое ушло в фон, а кэш снимает 80% повторных чтений — Django спокойно держит продакшен любого масштаба.

Начинайте с наблюдаемости и профайлинга: оптимизировать то, чего вы не измеряете, — верный способ потратить квартал впустую.