Django под нагрузкой: где узкое место ищут первым
Сервис на Django начинает отдавать 502 на нескольких сотнях одновременных запросов, а CPU при этом простаивает. Разбираем, где искать узкое место и почему виноват чаще всего не Python.

Типичная картина: приложение на Django 5.x, gunicorn с четырьмя воркерами, всё работало на демо. В прод пришло 300 одновременных запросов — и ответы начали приходить по 8–12 секунд, часть клиентов получила 502 от Nginx по таймауту. При этом top показывает CPU-загрузку 20–30%, память в норме. Процессор простаивает, а очередь растёт. Это почти всегда означает, что воркеры заблокированы на ожидании, а не на вычислениях. Ниже — где именно они ждут и как это увидеть в цифрах, а не на глаз.
Модель обработки: почему воркеры кончаются раньше CPU
Django по умолчанию синхронный. Один воркер gunicorn с синхронным классом обрабатывает ровно один запрос за раз. Пока воркер ждёт ответа от базы или внешнего API, он не берёт следующий запрос — он занят. Четыре воркера — это четыре одновременных запроса, не больше, сколько бы ядер ни было свободно.
Если средний запрос держит воркер 200 мс, четыре воркера дают потолок примерно 20 запросов в секунду. Остальные ждут в очереди сокета. Когда ожидание превышает таймаут Nginx (proxy_read_timeout, по умолчанию 60 секунд), клиент получает 504; когда очередь backlog переполнена — 502. CPU при этом свободен, потому что воркеры не считают, а ждут.
Первое правило разбора нагрузки в Django: если процессор простаивает, а ответы медленные — вы упёрлись не в Python, а в число воркеров и в то, чего они ждут.
Узкое место номер один: база данных
N+1 запросов
Самая частая причина деградации. В шаблоне или сериализаторе вы обращаетесь к связанному объекту в цикле, и ORM делает отдельный запрос на каждую итерацию. На демо-данных из 10 строк это незаметно. На 500 строках это 501 запрос к базе на один HTTP-запрос, и воркер держится за соединение всё это время.
Найти проблему помогает django-debug-toolbar в разработке или логирование числа запросов. В коде лечится select_related для связей «один-ко-многим» через ForeignKey и prefetch_related для «многие-ко-многим» и обратных связей:
# Django 5.x
# Было: на каждый заказ отдельный запрос за клиентом
orders = Order.objects.all()
for order in orders:
print(order.customer.name) # запрос в цикле
# Стало: два запроса вместо N+1
orders = Order.objects.select_related('customer')
for order in orders:
print(order.customer.name) # данные уже загружены
Как проверить эффект: включите логирование SQL и посчитайте запросы до и после. Число должно упасть с сотен до единиц.
Отсутствие индексов
Запрос с фильтром по неиндексированному полю на таблице в сотни тысяч строк заставляет PostgreSQL делать полное сканирование. На маленькой таблице это миллисекунды, на большой — секунды, и воркер занят всё это время. Проверяется через EXPLAIN ANALYZE в psql: если видите Seq Scan на большой таблице в горячем запросе — нужен индекс через db_index=True на поле модели или составной индекс в Meta.indexes.
Исчерпание пула соединений
Django открывает соединение с базой на запрос. Настройка CONN_MAX_AGE позволяет переиспользовать соединения между запросами. Но у PostgreSQL есть лимит max_connections (по умолчанию 100), и если воркеров с учётом всех процессов больше — часть запросов упрётся в отказ на уровне базы. При росте числа воркеров ставьте перед базой пулер вроде PgBouncer, а не увеличивайте max_connections бесконтрольно.
Узкое место номер два: синхронные внешние вызовы
Запрос к платёжному шлюзу, отправка письма, обращение к стороннему API — всё это в синхронном view блокирует воркер на всё время ожидания ответа. Если внешний сервис отвечает за 3 секунды, ваш воркер эти 3 секунды не обслуживает никого.
Правильное лечение — вынести долгие операции в фоновую очередь (Celery, RQ, django-tasks). View ставит задачу и сразу возвращает ответ, а тяжёлую работу делает отдельный процесс. Это не ускоряет саму операцию, но освобождает веб-воркеры для новых запросов.
Узкое место номер три: неправильный расчёт воркеров
Частая ошибка — считать, что больше воркеров всегда лучше. Каждый синхронный воркер gunicorn — это отдельный процесс со своей копией приложения в памяти. На сервере с 4 ГБ RAM и приложением, съедающим 300 МБ на процесс, вы не запустите 30 воркеров — упрётесь в память и получите OOM-killer.
Базовая формула из документации gunicorn для синхронных воркеров — (2 × число ядер) + 1. Но она рассчитана на приложения, ограниченные CPU. Если ваши запросы ждут I/O (база, внешние API), синхронная модель неэффективна в принципе: воркеры простаивают на ожидании. Варианты:
- Больше синхронных воркеров — работает, но упирается в память и в лимит соединений базы.
- gthread-воркеры — потоки внутри процесса, каждый обрабатывает запрос, пока другие ждут I/O. Экономят память по сравнению с процессами.
- Асинхронный стек — ASGI-сервер (uvicorn) и async-view. Один процесс держит тысячи одновременных ожиданий I/O. Требует переписывания view на async и async-совместимого ORM-кода.
Как выбрать модель воркеров
| Профиль нагрузки | Модель | Чем платите |
|---|---|---|
| Тяжёлые вычисления, мало I/O | sync-воркеры, (2×ядра)+1 | Память на каждый процесс |
| Много ожидания базы и внешних API | gthread-воркеры | GIL ограничивает параллельность CPU-кода |
| Тысячи одновременных медленных соединений | ASGI + async-view | Переписывание кода, ловушки sync-в-async |
Критерий простой: замерьте, на что уходит время запроса. Если 80% — ожидание базы и сети, число синхронных воркеров вам не поможет, нужны потоки или async. Если 80% — вычисления в Python, добавляйте процессы по числу ядер.
Что мерить, а не угадывать
Прежде чем крутить настройки, соберите цифры под реальной нагрузкой. Инструмент нагрузочного тестирования — например, ab, wrk или Locust — задаёт контролируемое число одновременных клиентов. Что смотреть:
- Латентность по перцентилям. Средняя лжёт. Смотрите p95 и p99 — именно медленный хвост создаёт таймауты и 502.
- Число SQL-запросов на HTTP-запрос. Через django-debug-toolbar в стейджинге или логирование. Резкий рост — почти всегда N+1.
- Время в базе против времени в Python. APM-инструмент (например, встроенные метрики через
django.db.connection.queriesв дебаге) покажет, где реально тратится время. - Загрузка CPU и памяти на сервере. Простой CPU при медленных ответах = блокировка на I/O. Растущая память при добавлении воркеров = скоро OOM.
Проводите нагрузочные тесты на конфигурации, близкой к проду по объёму данных. Замер на пустой базе бесполезен: N+1 и отсутствие индексов проявляются только на объёме.
Где ещё ломается
- Сессии и кеш в базе. Если backend сессий — база, каждый запрос делает лишний SQL. Перенос сессий и кеша в Redis снимает нагрузку с PostgreSQL.
- Отдача статики через Django. В проде статику и медиа должен отдавать Nginx или CDN, а не воркеры Python. Иначе каждый файл занимает воркер.
- Тяжёлые middleware. Middleware выполняется на каждый запрос. Синхронный внешний вызов в middleware умножает проблему на весь трафик.
- Дебаг в проде.
DEBUG=Trueнакапливает все SQL-запросы в памяти и не очищает их — утечка памяти и замедление. В проде всегдаDEBUG=False.
Что делать дальше
Начните с профилирования одного медленного эндпоинта: посчитайте SQL-запросы и время в базе против времени в Python. В девяти случаях из десяти узкое место — это N+1 или отсутствие индекса, и лечится это в коде, а не добавлением железа. Только исчерпав оптимизацию запросов, переходите к выбору модели воркеров и вопросу sync против async. Настройте мониторинг p95-латентности и числа запросов к базе на прод — деградацию нужно видеть на графике до того, как о ней сообщит клиент.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: Django documentation: Databases and query optimization, Gunicorn: Design — how many workers
Частые вопросы
Мне нужно переходить на async Django, чтобы держать нагрузку?
Почему CPU простаивает, а запросы всё равно медленные?
Сколько воркеров gunicorn ставить?
У меня Django 4.2, а не 5.x — советы подойдут?
Что делать, если после оптимизации запросов нагрузка всё равно не держится?
Можно ли просто увеличить таймаут Nginx, чтобы убрать 502?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.