Оптимизация Django

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

Оптимизация базы данных

Я так много раз видел, как люди начинают оптимизацию с запросов к базе данных, которые составляют 5% от всех запросов. К сожалению, большинство разработчиков просто добавляют select_related/prefetch_related в свои Django QuerySets, чтобы уменьшить количество запросов к базе данных.

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

Никогда не пытайтесь оптимизировать запросы на устройстве для разработки

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

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

Оптимизация кода

Наверное, все знают о django-extension с RunProfileServer, но мне кажется, что это решение не очень удобно для работы. Оно предоставляет много данных в трудно читаемом формате.

Вместо этого я использую line_profiler. Этот пакет позволяет проверить производительность определенных частей кода. По сути, вам нужно написать скрипт для оценки нужного кода и поставить декоратор @profile к интересующим вас методам.

В результате вы получите:

  • Количество времени, затраченное каждым методом
  • Общее затраченное время
  • Время на один запрос
  • Количество запросов
  • Время для каждой строки метода, показанное в процентах

Я использую два варианта запуска представлений (view) в проекте Django для проверки производительности. Первый вариант проще, но он не раскрывает промежуточные модули и код Django. Второй вариант немного сложнее, но он дает возможность измерить промежуточные модули.

#!/usr/bin/env python
import os
os.environ.setdefault(
    'DJANGO_SETTINGS_MODULE',
    'django_classifier_profile.settings'
)
import django
django.setup()
from django.test.client import RequestFactory
from django_classifier_profile.apps.account.models import User
from django_classifier_profile.apps.account.views import ProfileEditView
request_factory = RequestFactory()
user = User.objects.get()
request = request_factory.get('/')
request.session = {}
request.user = user
view = ProfileEditView.as_view()
view(request).render()

Здесь я создаю ложный запрос и вызываю представление напрямую. Нам нужно вызвать метод render , чтобы запустить рендеринг шаблона и оценить lazy объекты.

#!/usr/bin/env python
import os
os.environ.setdefault(
    'DJANGO_SETTINGS_MODULE',
    'django_classifier_profile.settings'
)
import django
django.setup()
from django.core.servers.basehttp import get_internal_wsgi_application
from django.test.client import RequestFactory
from django_classifier_profile.apps.account.models import User
request_factory = RequestFactory()
user = User.objects.get()
request = request_factory.get('/')
request.session = {}
request._cached_user = user
#request.user = user
app = get_internal_wsgi_application()
app.get_response(request)

В этом скрипте я использую WSGI-приложение для вызова представления. Это дает возможность оценить весь поток Django с промежуточными компонентами и рендерингом шаблонов.

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

$ kernprof –l <script_name.py>

а вторую – чтобы показать результаты профилирования:

$ python -m line_profiler <script_name.py>.lprof

Результат будет выглядеть следующим образом:

Timer unit: 1e-06 s
Total time: 1.4e-05 s
File: /Users/quard/.pyenv/versions/3.5.3/envs/django-classifier-shop/lib/python3.5/site-packages/django/contrib/auth/middleware.py
Function: process_request at line 17
Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
    17                                               @profile
    18                                               def process_request(self, request):
    19         1            2      2.0     14.3          assert hasattr(request, 'session'), (
    20                                                       "The Django authentication middleware requires session middleware "
    21                                                       "to be installed. Edit your MIDDLEWARE%s setting to insert "
    22                                                       "'django.contrib.sessions.middleware.SessionMiddleware' before "
    23                                                       "'django.contrib.auth.middleware.AuthenticationMiddleware'."
    24                                                   ) % ("_CLASSES" if settings.MIDDLEWARE is None else "")
    25         1           12     12.0     85.7          request.user = SimpleLazyObject(lambda: get_user(request))
Total time: 0.005354 s
File: /Users/quard/Projects/learn/django-classifier-profile/django_classifier_profile/apps/account/views.py
Function: get_object at line 18
Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
    18                                               @profile
    19                                               def get_object(self, queryset=None):
    20                                                   if (
    21         3            9      3.0      0.2              not self.kwargs.get(self.pk_url_kwarg)
    22         1            2      2.0      0.0              and not self.kwargs.get(self.slug_url_kwarg)
    23                                                   ):
    24         1            9      9.0      0.2              self.kwargs[self.pk_url_kwarg] = self.request.user.pk
    25
    26         3         5272   1757.3     98.5          user = super(ProfileEditView, self).get_object(queryset=queryset)
    27
    28         3           60     20.0      1.1          if user != self.request.user and not self.request.user.is_superuser:
    29                                                       raise HttpResponseForbidden
    30
    31         3            2      0.7      0.0          return user
Total time: 0.010449 s
File: /Users/quard/Projects/learn/django-classifier-profile/django_classifier_profile/apps/account/views.py
Function: get_formset at line 59
Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
    59                                               @profile
    60                                               def get_formset(self):
    61                                                   """
    62                                                   create formset of attributes with help of custome formset class
    63                                                   """
    64         1            1      1.0      0.0          FormSetClass = modelformset_factory(
    65         1            1      1.0      0.0              UserAttribute,
    66         1            1      1.0      0.0              formset=UserClassifierFormSet,
    67         1            1      1.0      0.0              form=UserAttributeForm,
    68         1            0      0.0      0.0              can_delete=True,
    69         1          892    892.0      8.5              extra=0
    70                                                   )

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

Перевод статьи Vadym Zakovinko “How to Improve Django Performance. Optimization Tips”.

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *