Автоматизация тестирования производительности в Django
Неэффективный запрос к базе данных - одна из наиболее распространенных проблем с производительностью в Django. В частности, запросы N+1 могут негативно сказаться на производительности вашего приложения на ранних стадиях. Они возникают, когда вы выбираете записи из связанной таблицы, используя отдельный запрос для каждой записи, а не собираете все записи в одном запросе. К сожалению, такие недостатки довольно легко устранить с помощью ORM Django. Тем не менее, их можно быстро выявить и предотвратить с помощью автоматизированного тестирования.
В этой статье рассматривается, как:
- Проверьте количество запросов, выполняемых запросом, а также продолжительность запросов
- Предотвратите N+1 запрос с помощью пакета nplusone
N+1 запросов
Пример приложения, с которым мы будем работать на протяжении всей этой статьи, можно найти на GitHub.
Допустим, например, вы работаете с приложением Django, которое имеет следующие модели:
# courses/models.py
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=100)
def __str__(self):
return self.name
class Course(models.Model):
title = models.CharField(max_length=100)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
def __str__(self):
return self.title
Теперь, если перед вами стоит задача создать новое представление для возврата JSON-ответа по всем курсам с названием и именем автора, вы могли бы написать следующий код:
# courses/views.py
from django.http import JsonResponse
from courses.models import Course
def all_courses(request):
queryset = Course.objects.all()
courses = []
for course in queryset:
courses.append(
{"title": course.title, "author": course.author.name}
)
return JsonResponse(courses, safe=False)
Этот код будет работать, но он очень неэффективен, так как будет выполняться слишком много запросов к базе данных:
- 1 запрос для получения всех курсов
- N запросов для получения ветви на каждой итерации
Прежде чем обратиться к этому вопросу, давайте посмотрим, сколько всего запросов выполняется, и измерим время выполнения.
Промежуточное программное обеспечение для метрик
Вы заметите, что проект включает пользовательское промежуточное программное обеспечение, которое вычисляет и регистрирует время выполнения каждого запроса:
# core/middleware.py
import logging
import time
from django.db import connection, reset_queries
def metric_middleware(get_response):
def middleware(request):
reset_queries()
# Get beginning stats
start_queries = len(connection.queries)
start_time = time.perf_counter()
# Process the request
response = get_response(request)
# Get ending stats
end_time = time.perf_counter()
end_queries = len(connection.queries)
# Calculate stats
total_time = end_time - start_time
total_queries = end_queries - start_queries
# Log the results
logger = logging.getLogger("debug")
logger.debug(f"Request: {request.method} {request.path}")
logger.debug(f"Number of Queries: {total_queries}")
logger.debug(f"Total time: {(total_time):.2f}s")
return response
return middleware
Запустите начальную команду базы данных, чтобы добавить в базу данных 10 авторов и 100 курсов:
$ python manage.py seed_db
Когда сервер разработки Django будет запущен, перейдите по ссылке http://localhost:8000/courses/ в вашем браузере. Вы должны увидеть ответ в формате JSON. Вернувшись в свой терминал, обратите внимание на показатели:
Request: GET /courses/
Number of Queries: 101
Total time: 0.10s
Это слишком много запросов! Это очень неэффективно. Для каждого добавленного автора и курса потребуется дополнительный запрос к базе данных, поэтому производительность будет продолжать снижаться по мере роста базы данных. К счастью, исправить это довольно просто: вы можете добавить метод select_related
для создания SQL-соединения, которое будет включать авторов в исходный запрос к базе данных.
queryset = Course.objects.select_related("author").all()
Прежде чем вносить какие-либо изменения в код, давайте сначала проведем несколько тестов.
Тесты производительности
Начните со следующего теста, в котором используется django_assert_num_queries функция pytest, чтобы убедиться, что доступ к базе данных осуществляется только один раз, когда в базе данных присутствует одна или несколько записей автора и курса:
import json
import pytest
from faker import Faker
from django.test import override_settings
from courses.models import Course, Author
@pytest.mark.django_db
def test_number_of_sql_queries_all_courses(client, django_assert_num_queries):
fake = Faker()
author_name = fake.name()
author = Author(name=author_name)
author.save()
course_title = fake.sentence(nb_words=4)
course = Course(title=course_title, author=author)
course.save()
with django_assert_num_queries(1):
res = client.get("/courses/")
data = json.loads(res.content)
assert res.status_code == 200
assert len(data) == 1
author_name = fake.name()
author = Author(name=author_name)
author.save()
course_title = fake.sentence(nb_words=4)
course = Course(title=course_title, author=author)
course.save()
res = client.get("/courses/")
data = json.loads(res.content)
assert res.status_code == 200
assert len(data) == 2
Не используете pytest? Используйте метод тестирования assertNumQueries вместо
django_assert_num_queries
.
Более того, мы также можем использовать nplusone, чтобы предотвратить появление в будущем N+1 запросов. После установки пакета и добавления его в файл настроек вы можете добавить его в свои тесты с помощью @override_settings
декоратора:
...
@pytest.mark.django_db
@override_settings(NPLUSONE_RAISE=True)
def test_number_of_sql_queries_all_courses(client, django_assert_num_queries):
...
Или, если вы хотите автоматически включить nplusone для всего набора тестов, добавьте в свой корневой каталог тестов следующее conftest.py файл:
from django.conf import settings
def pytest_configure(config):
settings.NPLUSONE_RAISE = True
Вернитесь к образцу приложения и запустите тесты. Вы должны увидеть следующую ошибку:
nplusone.core.exceptions.NPlusOneError: Potential n+1 query detected on `Course.author`
Теперь внесите рекомендуемые изменения - добавьте метод select_related
- и снова запустите тесты. Теперь они должны пройти успешно.
Заключение
В этой статье рассматривается, как предотвратить автоматическое выполнение N+1 запросов в вашей кодовой базе с помощью пакета nplusone и протестировать количество выполненных запросов с помощью django_assert_num_queries
pytest fixture.
Это должно помочь избежать снижения производительности по мере роста числа ваших приложений и привлечения большего числа пользователей. Если вы добавляете это в существующую кодовую базу, вам, вероятно, потребуется потратить некоторое время на исправление ошибочных запросов, чтобы они имели постоянное количество обращений к базе данных. Если после исправлений и оптимизации вы по-прежнему сталкиваетесь с проблемами производительности, возможно, вам потребуется добавить дополнительные уровни кэширования, денормализовать части базы данных и/или настроить индексы базы данных.
Вернуться на верх