Команда Python for Devs подготовила перевод статьи о том, как сделать Django-сайты быстрее. Автор разбирает два пути — «делать больше» (масштабирование инфраструктуры) и «делать меньше» (уменьшение задержек через оптимизацию кода и запросов). В статье — практические примеры, баг N+1, кэширование и инструменты вроде Django Debug Toolbar, Locust и APM.


В этой статье мы разберёмся с производительностью. Как сделать сайт на Django быстрее?

Теория производительности

Есть два пути ускорить работу сайта:

  • делать больше;

  • делать меньше.

Как именно мы делаем больше или меньше, зависит от типа работы, которую сайт выполняет.

Когда я говорю «делать больше», на самом деле речь идёт об увеличении пропускной способности сайта. Пропускная способность — это объём работы за определённое время. Увеличивая её, сайт может обслуживать больше пользователей одновременно.

На Django-сайте естественной метрикой пропускной способности являются запросы в секунду. Один просмотр страницы может включать несколько запросов, поэтому эта метрика не идеально отражает количество пользователей, которых может выдержать сайт, но она даёт полезное представление о производительности.

А что значит «делать меньше» для повышения производительности?

Самый быстрый код — тот, которого нет.

Каждая строка, которую вы пишете, должна быть выполнена компьютером. Если код неэффективен или его слишком много, это неизбежно приведёт к увеличению времени отклика.

Время от получения входных данных до выдачи результата называется задержкой (latency). Если пользователь кликает по ссылке на сайте, через сколько секунд он получает ответ? Это и есть задержка.

И «делать меньше» не ограничивается только количеством кода! На задержку влияют и другие факторы:

  • Неэффективный код — ошибки в разработке замедляют работу

  • Объём данных — чем больше данных нужно передать, тем выше нагрузка

  • Географическое расположение — скорость света реальна и ограничивает сетевые коммуникации

  • и многое другое

Снижая задержку, вы улучшаете пользовательский опыт. В оставшейся части статьи вы узнаете, как можно и «делать больше», и «делать меньше», чтобы сделать сайт лучше для пользователей.

Сначала измеряем

Прежде чем оптимизировать, нужно понять, что именно тормозит приложение. Иными словами, какой ресурс ограничивает его производительность?

Измерять работу приложения, особенно под реальной нагрузкой, может быть непросто. Анализ можно вести как с «птичьего полёта», так и вблизи.

Я бы начал с общей картины и поиска закономерностей. Обычно узкие места попадают в две категории:

  • ограничение по вводу-выводу (I/O bound)

  • ограничение по процессору (CPU bound)

I/O bound значит, что систему сдерживают операции ввода-вывода. Конкретнее: система ждёт, когда появятся данные. Классические примеры:

  • ожидание ответа от базы данных,

  • ожидание содержимого из файловой системы,

  • ожидание передачи данных по сети,

  • и так далее.

Оптимизация здесь сводится к сокращению времени ожидания.

CPU bound — это когда процессор не справляется с объёмом вычислений. Примеры:

  • вычисления для машинного обучения,

  • обработка и рендеринг изображений,

  • запуск больших тестовых наборов.

Оптимизация в этом случае сосредоточена на ускорении и удешевлении вычислений.

Чтобы понять, к какой категории относится проблема, первым делом стоит посмотреть на загрузку CPU. Если процессор сильно загружен, значит, приложение CPU bound. Но для веб-приложений это редкость. Чаще они I/O bound, так как в основном получают данные из базы и показывают пользователю, а вычислений относительно немного.

Если система не упирается в процессор, следующий шаг — выяснить, где именно приложение тратит время на ожидание. Для этого нужны инструменты.

Начнём с простого: как узнать загрузку CPU? Это могут показать инструменты хостинг-провайдера. Например, Heroku отображает метрики (CPU, память, диск и пр.) на одной странице. У Digital Ocean или AWS есть свои средства.

Если таких инструментов нет, придётся использовать сервер напрямую (через ssh). На сервере можно запустить программу top — она показывает, какие процессы потребляют больше всего ресурсов. Список обновляется каждую секунду. (Подсказка: нажмите q, чтобы выйти.)

Хотя top и полезен, он не самый удобный инструмент. Альтернатива — htop, который считается более дружелюбным.

Ещё одна категория инструментов — APM (Application Performance Monitoring). Это сервисы, которые отслеживают работу приложения, если вы добавите их в проект. Они помогают выявлять как проблемы с CPU, так и с I/O. Попробовать можно, например, Datadog. Популярны также Scout APM и New Relic.

Таким образом, у вас есть большой выбор инструментов, чтобы диагностировать узкие места. Теперь давайте посмотрим, как решать найденные проблемы.

Делать больше

Увеличить пропускную способность можно разными способами. Чаще всего это сводится к двум видам масштабирования:

  • вертикальное

  • горизонтальное

Чтобы понять разницу, представим пример. Допустим, вам нужно перенести сотни мешков земли весом 18 кг каждый из грузовика в сад. Вы зовёте друзей.

Один вариант — позвать самых сильных. Их мало, но они быстро справятся. Это вертикальное масштабирование.

Другой вариант — позвать много друзей послабее. Каждый перенесёт меньше, но вместе они сделают работу быстрее. Это горизонтальное масштабирование.

Те же подходы применимы и к компьютерам.

Вертикальное масштабирование

При вертикальном масштабировании вы запускаете приложение на более мощной машине. Облачные провайдеры предлагают множество вариантов. Разумеется, чем мощнее компьютер, тем дороже.

Когда стоит подумать об этом? Например, если приложение ограничено CPU. Более быстрый процессор позволит обрабатывать запросы быстрее.

Современные мощные серверы обычно имеют не только более быстрые, но и более многочисленные CPU. Поэтому, если вы просто увеличите мощность, но не настроите приложение (например, не увеличите число воркеров в Gunicorn), то выгоды почти не будет.

Таким образом, вертикальное масштабирование подразумевает «большую» машину, а горизонтальное — больше машин.

Горизонтальное масштабирование

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

Нужен центральный узел, который будет распределять трафик между машинами в горизонтально масштабируемой системе. Такой узел называется балансировщиком нагрузки (load balancer). У него несколько задач. Чаще всего балансировщик используется для:

  • распределения трафика между серверами приложений;

  • управления TLS-сертификатами, которые обеспечивают работу HTTPS.

Так как балансировщик не выполняет основную работу по обработке запроса, система может увеличить пропускную способность, просто добавив больше серверов приложений. В такой конфигурации каждый сервер «думает», что он является основным и обрабатывает запросы напрямую. Балансировщик действует как клиент, отправляющий запросы от имени реальных пользователей. Такая конфигурация называется прокси-настройкой.

Хотите больше узнать о горизонтальном масштабировании и балансировке нагрузки? Обратите внимание на NginxHAProxy (от «high availability proxy») или AWS ALB (Application Load Balancer). Эти инструменты широко применяются и зарекомендовали себя как надёжные балансировщики.

Что лучше?

Какие плюсы и минусы у горизонтального и вертикального масштабирования?

Когда вы добавляете новые элементы в систему, её сложность возрастает. Поэтому вертикальное масштабирование часто обеспечивает более простую архитектуру на первых этапах. Лично я, если бы запускал сервис на VPS вроде Digital Ocean или AWS, сначала попробовал бы вертикальное масштабирование. Более мощная машина позволила бы запустить больше параллельных рабочих процессов и увеличить пропускную способность без лишней сложности, связанной с развёртыванием нескольких серверов приложений.

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

Тем не менее, вертикальное масштабирование имеет и недостатки.

Во-первых, в этой модели сбой сервера может привести к полной недоступности сервиса. В индустрии это называют проблемой доступности: сайт либо доступен, либо нет. Если всё приложение завязано на один крупный сервер, он становится «единой точкой отказа».

Во-вторых, вертикальное масштабирование может быть дороже. Большинство сайтов имеют пики и спады активности в течение суток. Например, моя компания в сфере телемедицины в США видит естественное снижение нагрузки ночью, когда большинство людей спит.

Популярная стратегия оптимизации затрат — уменьшать ресурсы в периоды низкой активности. В случае вертикального масштабирования быстро менять размеры машины трудно. В итоге вы оплачиваете ресурсы, даже если сервисом почти никто не пользуется. Горизонтальное масштабирование решает эту проблему с помощью автоматического масштабирования (auto-scaling).

Auto-scaling позволяет динамически подстраивать инфраструктуру под нагрузку. Когда пользователей много, автоматически добавляются новые серверы. Когда нагрузка снижается, лишние машины отключаются. Это помогает экономить и использовать только необходимые ресурсы.

Правда в том, что на больших масштабах вопрос «горизонтальное или вертикальное» становится ложной дихотомией. Зрелые системы обычно комбинируют оба подхода, чтобы достичь нужных характеристик, например высокой доступности.

Надеюсь, теперь у вас есть набор инструментов для размышлений. С ними вы сможете понять, как справляться с ростом нагрузки, если ваш сайт внезапно станет популярным ?

В этом разделе мы говорили о том, как увеличить пропускную способность, меняя инфраструктуру сервиса для обработки большего числа запросов. Теперь давайте переключимся с макроуровня на микроуровень и посмотрим, как повысить пропускную способность, выполняя меньше работы.

Делать меньше

Как заставить сайт на Django выполнять меньше работы? Всегда нужно измерять, но, так как большинство сайтов упирается в I/O, сосредоточимся на техниках оптимизации именно в этом направлении.

Оптимизация запросов к базе данных

Самая частая проблема производительности, которую я встречал в Django-приложениях, — это баг N+1 (иногда его называют 1+N).

Этот баг возникает, когда код обращается к базе данных внутри цикла.

from application.models import Movie

movies = Movie.objects.all()
for movie in movies:
    print(movie.director.name)

Сколько запросов будет в этом примере?

На самом деле, это каверзный вопрос. В простейшем случае: один запрос на выборку фильмов и по одному запросу на каждого режиссёра.

Причина в том, что Django выполняет запросы лениво. ORM «не знает», что нужно сделать join таблиц фильмов и режиссёров, чтобы сразу получить все данные. Первый запрос к таблице фильмов выполняется при итерации цикла. Когда Python доходит до print и обращается к полю director, ORM понимает, что данных о режиссёре нет в памяти, и делает отдельный запрос к базе, чтобы их получить.

В итоге:

  • 1 запрос к таблице фильмов,

  • N запросов к таблице режиссёров (по одному для каждой строки).

Отсюда и название — «N+1».

Почему это плохо? Потому что запросы к базе данных гораздо медленнее, чем доступ к данным в памяти Python. И чем больше строк обрабатывается, тем сильнее растёт проблема.

Исправить это можно, подсказав Django, что данные из связанной таблицы нужны заранее. Для этого используют select_related.

from application.models import Movie

movies = Movie.objects.select_related(
    "director").all()
for movie in movies:
    print(movie.director.name)

В переработанном примере ORM «знает», что нужно получить данные о режиссёре. Благодаря этой дополнительной информации фреймворк сделает выборку сразу из таблиц фильмов и режиссёров одним запросом при начале итерации цикла.

Под капотом Django выполняет более сложный SQL-запрос с JOIN двух таблиц. База возвращает все данные сразу, и Django кэширует их в памяти Python. Теперь, когда выполнение доходит до строки print, атрибут director.name берётся из памяти, а не вызывает новый запрос к базе.

Экономия производительности здесь может быть колоссальной, особенно если ваш код работает с большим количеством строк.

Хотя select_related отлично справляется со своей задачей, он подходит не для всех сценариев. Например, связи «многие ко многим» нельзя получить одним запросом. В таких случаях стоит использовать prefetch_related. Этот метод выполняет меньшее число запросов (обычно по одному на таблицу) и объединяет результаты в памяти. На практике prefetch_related во многом работает похоже на select_related. За подробностями стоит обратиться к документации Django.

Кэширование затратных операций

Если вы знаете, что:

  • выполнение операции будет происходить много раз,

  • она дорогая по ресурсам,

  • и её результат не будет меняться,

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

Хороший пример — новостной сайт. Такой сайт в основном «читабельный»: пользователи чаще просматривают информацию, чем пишут или сохраняют что-то. Кроме того, статьи на новостном сайте читают многие пользователи, и их содержание фиксировано.

Django включает инструменты, которые упрощают работу с кэшем для оптимизации контента — как в нашем примере с новостным сайтом.

Самый простой инструмент — декоратор cache_page. Этот декоратор кэширует результат целого Django-представления на определённое время. Если страница не содержит персонализации, это быстрый и эффективный способ отдавать готовый HTML. Найти декоратор можно в django.views.decorators.cache.

Иногда требуется более тонкий уровень, чем вся страница. Например, если на сайте есть авторизация и у каждого пользователя своя навигационная панель с фотографией профиля. В такой ситуации нельзя кэшировать всю страницу и отдавать её разным пользователям, иначе каждый будет видеть панель первого зашедшего. Здесь пригодится шаблонный тег cache.

Вот пример использования тега cache в шаблоне:

{% load cache %}

Hi {{ user.username }}, this part won't be cached.

{% cache 600 my_cache_key_name %}
    Everything inside of here will be cached.
    The first argument to `cache` is how long this should be cached
    in seconds. This cache fragment will cache for 10 minutes.
    Cached chunks need a key name to help the cache system
    find the right cache chunk.

    This cache example usage is a bit silly because this is static text
    and there is no expensive computation in this chunk.
{% endcache %}

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

Наконец, есть возможность работать напрямую с интерфейсом кэша. Вот базовый пример:

# application/views.py
from django.core.cache import cache

from application.complex import calculate_expensive_thing

def some_view(request):
    expensive_result = cache.get(
        "expensive_computation")
    if expensive_result is None:
        expensive_result = calculate_expensive_thing()
        cache.set(
            "expensive_computation",
            expensive_result
        )

    # Handle the rest of the view.
    ...

При первом запросе к этому представлению в кэше ещё не будет expensive_result, поэтому результат будет вычислен и сохранён. При последующих запросах его можно будет извлечь из кэша. В этом примере используется значение тайм-аута по умолчанию, но вы можете задать его самостоятельно, если требуется больше контроля. В системе кэширования есть и другие полезные возможности, так что стоит заглянуть в документацию.

Важно учитывать: кэширование обычно требует дополнительных инструментов и настроек. Django умеет работать с популярными системами кэша вроде Redis и Memcached, но вам придётся настроить их самостоятельно. Документация Django поможет, но будьте готовы к дополнительным усилиям.

Оптимизация базы данных и кэширование — это основные техники повышения производительности. Но как понять, что вы действительно оптимизируете правильно? Какой прирост вы получаете? Давайте рассмотрим инструменты, которые помогут ответить на эти вопросы.

Инструменты для измерения изменений

Мы рассмотрим инструменты с возрастающим уровнем сложности. Первый из них чрезвычайно полезен именно при разработке на Django. Остальные носят более универсальный характер, но о них тоже стоит знать, чтобы понимать, когда к ним стоит прибегнуть.

Каждый из этих инструментов помогает получить реальные данные о производительности. Сравнивая результаты «до» и «после» изменений, вы сможете понять, действительно ли оптимизация принесла ожидаемый эффект.

Django Debug Toolbar

Django Debug Toolbar — это ключевой инструмент, который я всегда добавляю в свои проекты. Панель отображается поверх вашего сайта и открывается в виде набора различных категорий диагностической информации о представлениях.

Каждая категория информации собрана в отдельную «панель», и вы можете переключаться между ними, чтобы получить данные, которые помогут при оптимизации.

Среди панелей можно найти:

  • SQL

  • Шаблоны

  • Запросы

  • Время

Большую часть времени при оптимизации я провожу именно в панели SQL. Она показывает все запросы, которые выполняет страница. Для каждого запроса можно увидеть, какой участок кода его вызвал, и даже полный SQL SELECT. При необходимости можно получить и EXPLAIN-запрос, чтобы детально разобраться, что именно делает база данных.

С небольшой практикой вы научитесь замечать ошибки типа N+1, потому что они проявляются в повторяющихся запросах, «каскадом» уходящих вниз.

Я часто использую debug toolbar, когда добавляю select_related, чтобы визуально убедиться, что количество запросов на странице сократилось. Debug toolbar — это open source и совершенно бесплатный инструмент. Настроить его для следующего проекта на Django однозначно стоит.

hey / ab

Есть два очень похожих инструмента, к которым я обращаюсь, когда нужно получить грубую оценку производительности сайта. Это hey и ab (Apache Bench). Оба они являются генераторами нагрузки и позволяют измерить базовые характеристики производительности.

На практике я предпочитаю hey, но упоминаю и ab, потому что это хорошо известный инструмент, который вы наверняка встретите, если начнёте изучать тему нагрузочного тестирования.

Работать с ними предельно просто:

$ hey https://www.example.com

В этом примере hey откроет большое количество параллельных соединений с указанным URL и выполнит серию запросов. По завершении вы получите отчёт с количеством успешных запросов, временем выполнения и другой статистикой. Такой генератор нагрузки позволяет синтезировать трафик и понять, как ваш сайт поведёт себя под нагрузкой.

Стоит быть осторожным с тем, куда именно вы направляете эти инструменты. Если неосторожно ими пользоваться, можно устроить атаку типа «отказ в обслуживании» (DoS) на собственный сервер. Поток запросов способен «забить» ресурсы и сделать сайт недоступным для других пользователей. Подумайте дважды, прежде чем запускать такие тесты на живом сайте!

Locust

Упомянутые выше инструменты — это довольно примитивные решения, потому что они позволяют тестировать только один URL за раз. А что делать, если нужно смоделировать реальный пользовательский сценарий? Тут на помощь приходит Locust. Это не тот инструмент, который стоит запускать «просто так», но он действительно впечатляет и определённо заслуживает внимания.

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

В Locust вы кодируете эти модели поведения, после чего инструмент имитирует большое количество пользователей, которые будут вести себя так, как вы задумали (с элементом случайности, чтобы тест был максимально близок к реальности).

Продвинутое нагрузочное тестирование может никогда не понадобиться вашему сайту, но круто знать, что в Python есть такой инструмент, который поможет глубоко изучить производительность и пределы вашего проекта.

Мониторинг производительности приложений (APM)

Ранее в статье я упоминал, что инструменты APM позволяют увидеть загрузку процессора и использование памяти вашим сайтом. Но это обычно лишь верхушка айсберга.

На деле APM выходит далеко за рамки измерения аппаратных ресурсов. Я думаю о таких системах как о «турбоверсии» debug toolbar.

Во-первых, APM обычно используется на «боевых» сайтах. Инструмент собирает данные о реальных запросах. Это даёт возможность выявлять настоящие проблемы с производительностью, которые затрагивают реальных пользователей.

Например, New Relic собирает информацию о медленных запросах в виде «трейсов». Эти трейсы агрегируются в набор, показывающий, какие страницы на вашем сайте работают хуже всего. Вы можете углубиться в этот список, рассмотреть конкретный трейс и расследовать проблему.

Возможно, у вас ошибка N+1. Возможно, в одной из таблиц базы данных отсутствует индекс по важному полю, и в результате при выполнении SELECT происходит избыточное сканирование записей. Такие трейсы (или как их называют в других сервисах) помогают расставить приоритеты и понять, что стоит исправлять в первую очередь.

Именно в этом проявляется главная ценность измерений. Если оставить вам одну мысль об оптимизации, то вот она: оптимизируйте там, где это действительно важно.

Простой мысленный эксперимент: у вас есть система, выполняющая два вида задач многократно:

  • Задача А — это 90% всей активности на сайте.

  • Задача B — оставшиеся 10%.

Если нужно что-то оптимизировать, потому что производительность недостаточна, что выберете?

Предположим, для каждой задачи есть оптимизация, которая позволяет выполнять её вдвое быстрее. И внедрение обеих требует одинаковых усилий. Тогда выбор очевиден:

  • Оптимизировать A: 90% * 50% = экономия 45% времени работы всей системы.

  • Оптимизировать B: 10% * 50% = экономия 5%.

В большинстве случаев направляйте усилия на то, что принесёт наибольший эффект (то есть выбирайте задачу A, когда только возможно). Иногда самое сложное — понять, что именно является задачей A, а что — задачей B. Инструменты мониторинга вроде APM помогают выявить главные узкие места, чтобы вы могли сосредоточить ограниченное время на действительно важных точках.

Русскоязычное сообщество про Python

Друзья! Эту статью перевела команда Python for Devs — канала, где каждый день выходят самые свежие и полезные материалы о Python и его экосистеме. Подписывайтесь, чтобы ничего не пропустить!

Итоги

В этой статье мы разобрались, как ускорять Django-приложения. Мы рассмотрели:

  • мысленную модель для подхода к оптимизации производительности;

  • типы узких мест;

  • как заставить систему «делать больше» с помощью горизонтального и вертикального масштабирования;

  • как заставить приложение «делать меньше», оптимизируя запросы к БД и используя кэширование;

  • инструменты, помогающие во всей этой работе.

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


  1. ManBitXe
    02.09.2025 09:10

    Довольно интересная статья! Хоть я сам лично не фанат Django (здесь я субъективен, т.к. на практике сталкивался больше с негативными кейсами), но думаю, что данный обзор поможет другим разрабам с улучшением производительности.