Я часто сталкиваюсь с повторяющейся ситуацией, когда разработчики получают задание оптимизировать производительность 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”.