Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Оптимизация производительности
performance_tuning

Оптимизация производительности

Profiling, bottleneck analysis, database optimization, CDN, scaling strategies

Производительность Django: измерение, бюджет и проверка

Статус: актуально для Django 5.2/6.0. Инструменты наблюдения меняются, но порядок работы стабилен: воспроизвести, измерить, найти ограничение, изменить одну вещь и проверить результат.

«Страница медленная» — не диагноз. Пользователь может ждать DNS, TLS, queue web worker, database lock, внешний API, serialization или browser rendering. Оптимизация без baseline легко ускоряет не тот участок и добавляет сложность без заметного эффекта.

Для важного endpoint заранее задают service-level цель, например p95 latency, error rate и минимальную throughput при ожидаемой concurrency. Среднее время скрывает длинный хвост, поэтому смотрят p50/p95/p99, а не одну среднюю цифру.

#Начните с воспроизводимого сценария

Зафиксируйте:

  • operation и тип пользователя;
  • объём и распределение данных;
  • холодный или прогретый cache;
  • concurrency и длительность теста;
  • версии приложения, Python, Django и database;
  • p50/p95/p99, error rate, throughput;
  • CPU, memory, database time, query count и downstream time.

Локальная база с десятью строками не выявит плохой index или offset в миллион строк. Используйте синтетические данные боевого масштаба без персональной информации. Сначала снимите baseline, после изменения повторите тот же сценарий.

#Бюджет одного request

Полезно разложить latency:

queue + middleware + view/service + database + downstream + render

Если 700 из 800 ms уходит во внешний payment API, переписывание Django template ничего не даст. Distributed trace показывает путь между services, APM — длительность операций, profiler — CPU stack, database statistics — медленные запросы и waits.

django-debug-toolbar и Django test client удобны в development. connection.queries требует DEBUG=True и сам добавляет overhead, поэтому production диагностируют через APM и database telemetry. Profiler включают на контролируемой доле traffic: постоянный подробный profiling тоже имеет цену.

#Сначала посчитайте SQL queries

Самая частая Django-регрессия — N+1:

posts = Post.objects.order_by("-created_at")[:50] for post in posts: print(post.author.email) # дополнительный query на каждый post

Для ForeignKey и OneToOne используется SQL JOIN через select_related(). Для many-to-many и reverse relation Django выполняет отдельную выборку и связывает результаты в Python через prefetch_related().

from django.db.models import Prefetch posts = ( Post.objects .select_related("author") .prefetch_related( Prefetch( "comments", queryset=Comment.objects.filter(is_public=True) .select_related("author") .order_by("id"), to_attr="public_comments", ) ) .order_by("-created_at")[:50] )

Не добавляйте prefetch «на всякий случай»: он расходует память и может загрузить коллекцию, которую response не использует. План зависит от фактически сериализуемых полей.

Закрепите бюджет тестом:

def test_post_list_query_budget(self): with self.assertNumQueries(2): response = self.client.get("/posts/") self.assertEqual(response.status_code, 200)

Число 2 не универсально, но осознанная граница предотвращает незаметное превращение двух queries в двести.

#QuerySet вычисляется лениво

QuerySet обычно не обращается к БД до iteration, list(), len(), bool(), slicing с шагом или serialization. После вычисления результат кэшируется внутри этого экземпляра QuerySet. Повторное создание QuerySet выполняет новый query.

Практические правила:

  • exists() отвечает на вопрос о наличии, но если строки сразу понадобятся, отдельный exists() добавит лишний round trip;
  • count() считает в БД, а не загружает все objects;
  • values()/values_list() полезны для projection, когда model methods не нужны;
  • only()/defer() могут вызвать дополнительный query при обращении к отложенному полю;
  • iterator(chunk_size=...) снижает память для stream processing, но не делает тяжёлую работу коротким HTTP request;
  • bulk update()/bulk_create() уменьшают round trips, но обходят save() и часть signal lifecycle.

Оптимизация должна сохранять семантику. Переход с model instance на dict или bulk operation способен пропустить validation, audit и domain method.

#Читайте план database

queryset = ( Order.objects .filter(account_id=account_id, status="paid") .order_by("-created_at", "-id") ) print(queryset.explain())

Обычный EXPLAIN показывает предполагаемый план без выполнения SELECT. EXPLAIN ANALYZE даёт фактические строки и время, но действительно запускает запрос; применяйте его осторожно и не копируйте production data-changing SQL.

Смотрите не только на Seq Scan — для маленькой таблицы он может быть оптимален. Важны оценка и фактическое число rows, loops, sort, heap fetches, disk reads и lock waits.

#Index следует форме запроса

from django.db import models class Order(models.Model): account = models.ForeignKey("accounts.Account", on_delete=models.PROTECT) status = models.CharField(max_length=30) created_at = models.DateTimeField() class Meta: indexes = [ models.Index( fields=["account", "status", "-created_at", "-id"], name="order_feed_idx", ), ]

Порядок полей, selectivity, сортировка и условия важнее самого факта наличия index. Каждый index занимает место и удорожает insert/update. Проверяйте реальный plan после migration. На большой PostgreSQL table index часто создают concurrent migration из django.contrib.postgres.operations, чтобы не держать длительную блокировку записи.

#Pagination без глубокого OFFSET

OFFSET 1000000 заставляет database найти и отбросить множество строк. Для бесконечной ленты лучше keyset/cursor pagination с устойчивым уникальным порядком:

page = Order.objects.order_by("-id") if before_id is not None: page = page.filter(id__lt=before_id) page = list(page[:51])

51-я строка показывает наличие следующей страницы; клиент получает первые 50 и cursor последней. Для сортировки по времени используйте пару (created_at, id), потому что timestamp может совпасть.

Offset остаётся нормальным для небольших admin lists и перехода на произвольный номер страницы. Выбор зависит от продукта, а не от догмы.

#Transactions и locks тоже входят в latency

Медленный query иногда лишь ждёт другую transaction. Держите atomic() коротким, не вызывайте внешний HTTP внутри database transaction и обновляйте строки в детерминированном порядке. Отслеживайте lock wait, deadlock и время transaction отдельно от чистого execution time.

ATOMIC_REQUESTS=True удобен не всегда: он расширяет transaction на каждый view и может увеличить contention. Transaction boundary лучше привязывать к конкретному use case.

#Cache: сначала модель свежести

Cache ускоряет повторяемое дорогое чтение, но добавляет ещё одно состояние. До реализации ответьте:

  • какой допустим stale interval;
  • что входит в key: tenant, user, language, permissions, query version;
  • когда запись инвалидируется;
  • что произойдёт при cache miss или outage;
  • как предотвратить stampede на популярном key.

Персональный response нельзя кэшировать только по URL. Инвалидацию после записи выполняют через transaction.on_commit(), чтобы cache не опередил database commit. LocMemCache изолирован по process; для общего production cache нужен сетевой backend, например Redis или Memcached.

Подробные cache-aside, sentinel для cached None, sessions и race conditions разобраны в отдельной главе о cache.

#Templates и serialization

В Django 5.2/6.0 cached template loader автоматически используется, если TEMPLATES[...]["OPTIONS"]["loaders"] не задан явно. Старый совет обязательно настраивать его вручную больше не является общей оптимизацией.

Обычно template медленнее не из-за синтаксиса, а из-за hidden queries в properties и template tags. Подготовьте данные во view/service, проверьте query count и не вызывайте дорогую работу в цикле.

Для JSON измеряйте:

  • число объектов и полей;
  • время serializer/resolver;
  • размер несжатого и сжатого payload;
  • память на построение response;
  • возможность streaming для export.

Выбор более быстрого JSON encoder имеет смысл только после профиля. Удаление ненужных полей и pagination часто дают больший выигрыш.

#Connections: три разных механизма

  • CONN_MAX_AGE переиспользует connection в thread/process; это не общий pool.
  • Django с psycopg 3 может использовать встроенный pool через database OPTIONS["pool"] при установленном psycopg[pool].
  • PgBouncer — отдельный proxy; transaction pooling требует учёта server-side cursors и session state.

При ASGI Django рекомендует отключать persistent connections (CONN_MAX_AGE=0) и при необходимости использовать backend pool. Размер pool согласуют с суммой web/Celery processes и лимитом database. Слишком большой pool не ускоряет БД, а увеличивает contention.

#Async и background work

Async view повышает concurrency для ожидания async-compatible I/O под ASGI. Он не ускоряет CPU-bound код и не делает один SQL query быстрее. Смешение sync middleware или sync ORM adapters может съесть выигрыш переключениями контекста.

Фоновая очередь нужна, когда пользователь не обязан ждать завершения: генерация report, обработка image, отправка notification. Это меняет контракт — нужны job status, idempotency, retry policy и наблюдаемость. Нельзя просто перенести критичную ошибку в Celery и считать latency исправленной.

#Edge и static assets

Static/media обычно отдаёт object storage/CDN или reverse proxy, а не Django worker. Cache-Control, hashed filenames, image dimensions и compression уменьшают network cost. Dynamic compression чаще выполняют на edge/proxy; обязательно измеряют CPU и размер, а чувствительные responses проверяют на compression side-channel risk.

HTTP caching через ETag, Last-Modified, Cache-Control может вернуть 304 или обслужить public response на edge без обращения к приложению. Но Vary и приватность должны соответствовать identity и content negotiation.

#Load test и capacity

Нагрузочный тест должен моделировать traffic mix, think time, authentication, cache state и downstream limits. Короткий тест показывает burst, длительный — memory leak, pool exhaustion и queue growth.

Увеличивайте нагрузку постепенно и наблюдайте saturation:

  • web queue и active workers;
  • CPU throttling и memory/GC;
  • database connections, locks, I/O и replication lag;
  • cache hit ratio и evictions;
  • task queue lag;
  • downstream rate limits.

Горизонтальное масштабирование помогает только до следующего общего bottleneck. Добавление web workers к уже перегруженной database может ухудшить p99.

#Практический цикл оптимизации

  1. Определить пользовательский сценарий и SLO.
  2. Снять trace/profile/query plan на realistic data.
  3. Сформулировать конкретную гипотезу.
  4. Изменить минимальный участок.
  5. Повторить тот же benchmark и проверить correctness.
  6. Добавить regression test, query budget или performance threshold.
  7. Выпустить постепенно и сравнить production telemetry.

Оптимизация завершена не тогда, когда код «выглядит быстрее», а когда измерение улучшилось, поведение сохранилось, а operational cost понятен.

#Legacy и исправления

  • Ручное включение cached template loader больше не даётся как обязательный production шаг: при стандартной конфигурации он включён автоматически.
  • CONN_MAX_AGE отделён от настоящего connection pool.
  • Термин master/slave заменён на primary/replica.
  • Async больше не предлагается как универсальное ускорение.
  • iterator() не выдаётся за способ сократить время тяжёлого request.
  • Масштабирование не ставится после абстрактной «полной оптимизации кода»: bottleneck определяет измерение.
  • Cache invalidation через безусловный post_save заменена явным commit-aware подходом.

#Материалы

  • Django: database access optimization
  • Django: database connections
  • Django: template loaders
  • Django: cache framework

Далее: Финальный проект: SaaS