История о том как можно сократить время сериализации на 99%!
Перевод статьи Haki Benita: Improve Serialization Performance in Django Rest Framework
Когда разработчик выбирает Python, Django или Django Rest Framework, это обычно происходит не по причине выбора инструментов с невероятно высокой производительности. Python всегда был «удобным» выбором, то есть тем языком, который вы выбираете, когда вам важнее удобство разработки, а не супер высокая скорость выполнения какого-либо процесса.
В этом выборе нет ничего плохого. Большинству проектов на самом деле не нужно микросекундное повышение производительности, им нужнее создание качественного кода.
Все это не означает, что производительность не важна. Как рассказано в этой истории, значительное повышение производительности может быть достигнуто с небольшим вниманием и небольшими изменениями.
Некоторое время назад мы заметили очень низкую производительность на одной из наших основных конечных точек API. Конечная точка извлекала данные из очень большой таблицы, поэтому мы, естественно, предположили, что проблема должна быть в базе данных.
Когда мы заметили, что даже выборка по небольшим наборам данных имеет низкую производительность, мы начали изучать другие части приложения. Это путешествие привело нас к сериализаторам Django Rest Framework (DRF).
Используемые версии:
Python 3.7, Django 2.1.1 и Django Rest Framework 3.9.4.
Сериализаторы используются для преобразования данных в объекты, а объекты в данные. Это простая функция, поэтому давайте напишем такую, которая принимает экземпляр User и возвращает dict:
from typing import Dict, Any from django.contrib.auth.models import User def serialize_user(user: User) -> Dict[str, Any]: return { 'id': user.id, 'last_login': user.last_login.isoformat() if user.last_login is not None else None, 'is_superuser': user.is_superuser, 'username': user.username, 'first_name': user.first_name, 'last_name': user.last_name, 'email': user.email, 'is_staff': user.is_staff, 'is_active': user.is_active, 'date_joined': user.date_joined.isoformat(), }
Создадим пользователя User для использования в бенчмарке:
>>> from django.contrib.auth.models import User >>> u = User.objects.create_user( >>> username='hakib', >>> first_name='haki', >>> last_name='benita', >>> email='me@hakibenita.com', >>> )
Для нашего теста мы используем cProfile. Чтобы устранить внешние воздействия, такие как база данных, мы заранее выбираем пользователя и сериализуем его 5000 раз:
>>> import cProfile >>> cProfile.run('for i in range(5000): serialize_user(u)', sort='tottime') 15003 function calls in 0.034 seconds Ordered by: internal time ncalls tottime percall cumtime percall filename:lineno(function) 5000 0.020 0.000 0.021 0.000 {method 'isoformat' of 'datetime.datetime' objects} 5000 0.010 0.000 0.030 0.000 drf_test.py:150(serialize_user) 1 0.003 0.003 0.034 0.034 <string>:1(<module>) 5000 0.001 0.000 0.001 0.000 __init__.py:208(utcoffset) 1 0.000 0.000 0.034 0.034 {built-in method builtins.exec} 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
Простая функция сериализации заняла 0,034 секунды для сериализации объекта User 5000 раз.
Django Rest Framework (DRF) поставляется с несколькими служебными классами, один из которых ModelSerializer.
ModelSerializer для встроенной пользовательской модели может выглядеть следующим образом:
from rest_framework import serializers class UserModelSerializer(serializers.ModelSerializer): class Meta: model = User fields = [ 'id', 'last_login', 'is_superuser', 'username', 'first_name', 'last_name', 'email', 'is_staff', 'is_active', 'date_joined', ]
Запуск того же теста, что и раньше:
>>> cProfile.run('for i in range(5000): UserModelSerializer(u).data', sort='tottime') 18845053 function calls (18735053 primitive calls) in 12.818 seconds Ordered by: internal time ncalls tottime percall cumtime percall filename:lineno(function) 85000 2.162 0.000 4.706 0.000 functional.py:82(__prepare_class__) 7955000 1.565 0.000 1.565 0.000 {built-in method builtins.hasattr} 1080000 0.701 0.000 0.701 0.000 functional.py:102(__promise__) 50000 0.594 0.000 4.886 0.000 field_mapping.py:66(get_field_kwargs) 1140000 0.563 0.000 0.581 0.000 {built-in method builtins.getattr} 55000 0.489 0.000 0.634 0.000 fields.py:319(__init__) 1240000 0.389 0.000 0.389 0.000 {built-in method builtins.setattr} 5000 0.342 0.000 11.773 0.002 serializers.py:992(get_fields) 20000 0.338 0.000 0.446 0.000 {built-in method builtins.__build_class__} 210000 0.333 0.000 0.792 0.000 trans_real.py:275(gettext) 75000 0.312 0.000 2.285 0.000 functional.py:191(wrapper) 20000 0.248 0.000 4.817 0.000 fields.py:762(__init__) 1300000 0.230 0.000 0.264 0.000 {built-in method builtins.isinstance} 50000 0.224 0.000 5.311 0.000 serializers.py:1197(build_standard_field)
DRF потребовалось 12,8 секунды для сериализации пользователя 5000 раз или 390 мс для сериализации только одного пользователя. Это в 377 раз медленнее, чем обычная функция.
Видно, что значительное количество времени уходит в functions.py. ModelSerializer использует отложенную функцию (lazy) из django.utils.functional для проведение валидации. Она также используется в подробных именах (verbose names) Django и т. д., которые также оцениваются DRF. Кажется что эта функция, значительно отягощает сериализатор.
Проверка полей (валидация) добавляется в ModelSerializer только для доступных для записи полей. Чтобы измерить эффект проверки, мы создадим ModelSerializer и пометим все поля как доступные только для чтения:
from rest_framework import serializers class UserReadOnlyModelSerializer(serializers.ModelSerializer): class Meta: model = User fields = [ 'id', 'last_login', 'is_superuser', 'username', 'first_name', 'last_name', 'email', 'is_staff', 'is_active', 'date_joined', ] read_only_fields = fields
Когда все поля доступны только для чтения, сериализатор нельзя использовать для создания новых экземпляров.
Давайте запустим наш тест с сериализатором только для чтения:
>>> cProfile.run('for i in range(5000): UserReadOnlyModelSerializer(u).data', sort='tottime') 14540060 function calls (14450060 primitive calls) in 7.407 seconds Ordered by: internal time ncalls tottime percall cumtime percall filename:lineno(function) 6090000 0.809 0.000 0.809 0.000 {built-in method builtins.hasattr} 65000 0.725 0.000 1.516 0.000 functional.py:82(__prepare_class__) 50000 0.561 0.000 4.182 0.000 field_mapping.py:66(get_field_kwargs) 55000 0.435 0.000 0.558 0.000 fields.py:319(__init__) 840000 0.330 0.000 0.346 0.000 {built-in method builtins.getattr} 210000 0.294 0.000 0.688 0.000 trans_real.py:275(gettext) 5000 0.282 0.000 6.510 0.001 serializers.py:992(get_fields) 75000 0.220 0.000 1.989 0.000 functional.py:191(wrapper) 1305000 0.200 0.000 0.228 0.000 {built-in method builtins.isinstance} 50000 0.182 0.000 4.531 0.000 serializers.py:1197(build_standard_field) 50000 0.145 0.000 0.259 0.000 serializers.py:1310(include_extra_kwargs) 55000 0.133 0.000 0.696 0.000 text.py:14(capfirst) 50000 0.127 0.000 2.377 0.000 field_mapping.py:46(needs_label) 210000 0.119 0.000 0.145 0.000 gettext.py:451(gettext)
Всего 7,4 секунды. Улучшение на 40% по сравнению с доступным для записи ModelSerializer.
В результатах теста мы видим, что много времени тратится на field_mapping.py и fields.py. Они связаны с внутренней работой ModelSerializer. В процессе сериализации и инициализации ModelSerializer использует много метаданных для создания и проверки полей сериализатора, и это обходится дорого.
В следующем тесте мы хотели точно измерить, сколько ModelSerializer «стоит» нам. Давайте создадим «обычный» сериализатор для модели User:
from rest_framework import serializers class UserSerializer(serializers.Serializer): id = serializers.IntegerField() last_login = serializers.DateTimeField() is_superuser = serializers.BooleanField() username = serializers.CharField() first_name = serializers.CharField() last_name = serializers.CharField() email = serializers.EmailField() is_staff = serializers.BooleanField() is_active = serializers.BooleanField() date_joined = serializers.DateTimeField()
Выполнение того же теста с использованием «обычного» сериализатора:
>>> cProfile.run('for i in range(5000): UserSerializer(u).data', sort='tottime') 3110007 function calls (3010007 primitive calls) in 2.101 seconds Ordered by: internal time ncalls tottime percall cumtime percall filename:lineno(function) 55000 0.329 0.000 0.430 0.000 fields.py:319(__init__) 105000/5000 0.188 0.000 1.247 0.000 copy.py:132(deepcopy) 50000 0.145 0.000 0.863 0.000 fields.py:626(__deepcopy__) 20000 0.093 0.000 0.320 0.000 fields.py:762(__init__) 310000 0.092 0.000 0.092 0.000 {built-in method builtins.getattr} 50000 0.087 0.000 0.125 0.000 fields.py:365(bind) 5000 0.072 0.000 1.934 0.000 serializers.py:508(to_representation) 55000 0.055 0.000 0.066 0.000 fields.py:616(__new__) 5000 0.053 0.000 1.204 0.000 copy.py:268(_reconstruct) 235000 0.052 0.000 0.052 0.000 {method 'update' of 'dict' objects} 50000 0.048 0.000 0.097 0.000 fields.py:55(is_simple_callable) 260000 0.048 0.000 0.075 0.000 {built-in method builtins.isinstance} 25000 0.047 0.000 0.051 0.000 deconstruct.py:14(__new__) 55000 0.042 0.000 0.057 0.000 copy.py:252(_keep_alive) 50000 0.041 0.000 0.197 0.000 fields.py:89(get_attribute) 5000 0.037 0.000 1.459 0.000 serializers.py:353(fields)
Вот прыжок, которого мы ждали!
«Обычный» сериализатор занял всего 2,1 секунды. Это на 60% быстрее, чем доступный только для чтения ModelSerializer, и на 85% быстрее, чем доступный для записи ModelSerializer.
В этот момент становится очевидным, что ModelSerializer не из дешевых.
В доступном для записи ModelSerializer много времени тратиться на проверки. Мы смогли сделать это быстрее, отметив все поля только для чтения. «Обычный» сериализатор не определяет никаких проверок, поэтому не ожидается, что пометка полей как только для чтения будет быстрее. Давайте удостоверимся:
from rest_framework import serializers class UserReadOnlySerializer(serializers.Serializer): id = serializers.IntegerField(read_only=True) last_login = serializers.DateTimeField(read_only=True) is_superuser = serializers.BooleanField(read_only=True) username = serializers.CharField(read_only=True) first_name = serializers.CharField(read_only=True) last_name = serializers.CharField(read_only=True) email = serializers.EmailField(read_only=True) is_staff = serializers.BooleanField(read_only=True) is_active = serializers.BooleanField(read_only=True) date_joined = serializers.DateTimeField(read_only=True)
И запускаем тест:
>>> cProfile.run('for i in range(5000): UserReadOnlySerializer(u).data', sort='tottime') 3360009 function calls (3210009 primitive calls) in 2.254 seconds Ordered by: internal time ncalls tottime percall cumtime percall filename:lineno(function) 55000 0.329 0.000 0.433 0.000 fields.py:319(__init__) 155000/5000 0.241 0.000 1.385 0.000 copy.py:132(deepcopy) 50000 0.161 0.000 1.000 0.000 fields.py:626(__deepcopy__) 310000 0.095 0.000 0.095 0.000 {built-in method builtins.getattr} 20000 0.088 0.000 0.319 0.000 fields.py:762(__init__) 50000 0.087 0.000 0.129 0.000 fields.py:365(bind) 5000 0.073 0.000 2.086 0.000 serializers.py:508(to_representation) 55000 0.055 0.000 0.067 0.000 fields.py:616(__new__) 5000 0.054 0.000 1.342 0.000 copy.py:268(_reconstruct) 235000 0.053 0.000 0.053 0.000 {method 'update' of 'dict' objects} 25000 0.052 0.000 0.057 0.000 deconstruct.py:14(__new__) 260000 0.049 0.000 0.076 0.000 {built-in method builtins.isinstance}
Как и ожидалось, пометка полей как «только для чтения» не имела существенного различия по сравнению с «обычным» сериализатором. Это подтверждает, что время было потрачено на проверки, полученные из определений полей модели.
Вот краткое изложение результатов на данный момент:
SERIALIZER | SECONDS |
---|---|
UserModelSerializer | 12.818 |
UserReadOnlyModelSerializer | 7.407 |
UserSerializer | 2.101 |
UserReadOnlySerializer | 2.254 |
serialize_user | 0.034 |
В интернете можно найти множество статей о производительности сериализации в Python. Как и ожидалось, большинство статей посвящено улучшению доступа к БД с использованием таких методов, как select_related и prefetch_related. Хотя это очень хорошие способы улучшить общее время ответа на запрос API, они не обращаются к самой сериализации. Я подозреваю, что это потому, что никто не ожидает, что сериализация может работать медленно.
Другие статьи, которые фокусируются исключительно на сериализации, обычно избегают попытки исправления DRF и вместо этого описывают новые платформы сериализации, такие как marshmallow и serpy. Есть даже сайт, посвященный сравнению форматов сериализации в Python.
В конце 2013 года Том Кристи, создатель Django Rest Framework, написал статью, в которой обсуждались некоторые недостатки DRF. В его тестах сериализация составляла 12% от общего времени, затрачиваемого на обработку одного запроса. В заключение Том рекомендует, не всегда прибегать к сериализации:
Вам не всегда нужно использовать сериализаторы.
Для критичных к производительности представлений вы можете полностью исключить сериализаторы и просто использовать .values() в запросах к базе данных.
Как мы увидим чуть позже, это серьезный совет.
В первом бенчмарке с использованием ModelSerializer мы увидели что значительное количество времени, тратиться в functions.py, а точнее в функции lazy.
Функция lazy используется Django для многих вещей, таких как подробные имена (verbose names), шаблоны (templates) и т. д. Источник описывает lazy следующим образом:
Эта функция инкапсулирует вызов функции и действует как прокси для методов, которые вызываются в результате выполнения этой функции. Функция не выполняется, пока не будет вызван один из методов инкапсулированной функции.
lazy функция делает свое волшебство, создавая прокси класс результата. Чтобы создать прокси, lazy выполняет итерации по всем атрибутам и функциям результирующего класса (и его суперклассов) и создает класс-оболочку, который выполняет функцию только тогда, когда ее результат фактически используется.
Для больших классов создание прокси может занять некоторое время. Так же, чтобы ускорить процесс, lazy кеширует выполнение прокси. Но, как оказалось, небольшой контроль в коде полностью сломал механизм кеширования, что делало lazy функцию очень и очень медленной.
Чтобы понять, насколько медленным является lazy без надлежащего кэширования, давайте воспользуемся простой функцией, которая возвращает str (класс результата), например, upper. Мы выбираем str, потому что у него много методов, и для его настройки требуется некоторое время.
Чтобы установить базовый уровень, мы тестируем напрямую, используя str.upper, без lazy:
>>> import cProfile >>> from django.utils.functional import lazy >>> upper = str.upper >>> cProfile.run('''for i in range(50000): upper('hello') + ""''', sort='cumtime') 50003 function calls in 0.034 seconds Ordered by: cumulative time ncalls tottime percall cumtime percall filename:lineno(function) 1 0.000 0.000 0.034 0.034 {built-in method builtins.exec} 1 0.024 0.024 0.034 0.034 <string>:1(<module>) 50000 0.011 0.000 0.011 0.000 {method 'upper' of 'str' objects} 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
Теперь та же самая функция, но на этот раз обернутая в lazy:
>>> lazy_upper = lazy(upper, str) >>> cProfile.run('''for i in range(50000): lazy_upper('hello') + ""''', sort='cumtime') 4900111 function calls in 1.139 seconds Ordered by: cumulative time ncalls tottime percall cumtime percall filename:lineno(function) 1 0.000 0.000 1.139 1.139 {built-in method builtins.exec} 1 0.037 0.037 1.139 1.139 <string>:1(<module>) 50000 0.018 0.000 1.071 0.000 functional.py:160(__wrapper__) 50000 0.028 0.000 1.053 0.000 functional.py:66(__init__) 50000 0.500 0.000 1.025 0.000 functional.py:83(__prepare_class__) 4600000 0.519 0.000 0.519 0.000 {built-in method builtins.hasattr} 50000 0.024 0.000 0.031 0.000 functional.py:106(__wrapper__) 50000 0.006 0.000 0.006 0.000 {method 'mro' of 'type' objects} 50000 0.006 0.000 0.006 0.000 {built-in method builtins.getattr} 54 0.000 0.000 0.000 0.000 {built-in method builtins.setattr} 54 0.000 0.000 0.000 0.000 functional.py:103(__promise__) 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
Используя lazy, потребовалось 1,139 секунды, чтобы перевернуть 5 000 строк в верхний регистр. Точно такая же функция, которая использовалась напрямую, заняла всего 0,034 секунды. Это на 33,5 быстрее.
Это явная проблема. Разработчики четко осознавали важность кеширования прокси. PR был выпущен, и вскоре после этого слился (см. Здесь). Предполагается, что после выхода этот патч улучшит общую производительность Django
DRF использует lazy для проверок и полей verbose names Когда все эти отложенные оценки собраны вместе, вы получаете заметное замедление.
Исправление для lazy в Django решило бы эту проблему и для DRF после незначительного исправления, но, тем не менее, было сделано отдельное исправление для DRF, чтобы заменить lazy что то более эффективное.
Чтобы увидеть эффект от изменений, установите последние версии Django и DRF:
(venv) $ pip install git+https://github.com/encode/django-rest-framework (venv) $ pip install git+https://github.com/django/django
После применения обоих патчей мы снова запустили один и тот же тест. Вот результаты рядом:
SERIALIZER | BEFORE | AFTER | % CHANGE |
---|---|---|---|
UserModelSerializer | 12.818 | 5.674 | -55% |
UserReadOnlyModelSerializer | 7.407 | 5.323 | -28% |
UserSerializer | 2.101 | 2.146 | +2% |
UserReadOnlySerializer | 2.254 | 2.125 | -5% |
serialize_user | 0.034 | 0.034 | 0% |
Время сериализации для доступного для записи ModelSerializer было сокращено вдвое. Время сериализации для ModelSerializer только для чтения было сокращено почти на треть. Как и ожидалось, в других методах сериализации нет заметного различия.
Наши выводы из этого эксперимента следующие:
Чтобы разработчики не забыли установить поля только для чтения, мы добавили проверку Django, чтобы все классы ModelSerializer устанавливали read_only_fields:
# common/checks.py import django.core.checks @django.core.checks.register('rest_framework.serializers') def check_serializers(app_configs, **kwargs): import inspect from rest_framework.serializers import ModelSerializer import conf.urls # noqa, force import of all serializers. for serializer in ModelSerializer.__subclasses__(): # Skip third-party apps. path = inspect.getfile(serializer) if path.find('site-packages') > -1: continue if hasattr(serializer.Meta, 'read_only_fields'): continue yield django.core.checks.Warning( 'ModelSerializer must define read_only_fields.', hint='Set read_only_fields in ModelSerializer.Meta', obj=serializer, id='H300', )
С этой проверкой, когда разработчик добавляет сериализатор, он также должен установить read_only_fields. Если сериализатор доступен для записи, read_only_fields может быть задано пустым кортежем. Если разработчик забывает установить read_only_fields, он получает следующую ошибку:
$ python manage.py check System check identified some issues: WARNINGS: <class 'serializers.UserSerializer'>: (H300) ModelSerializer must define read_only_fields. HINT: Set read_only_fields in ModelSerializer.Meta System check identified 1 issue (4 silenced).
В этой статье вы найдете много других полезных проверок о том, как мы используем систему проверки системы Django.
Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…
Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…
Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…
Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…
Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…
Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…
View Comments
Полезная статья, чесно говоря я и не думал что существует такая разница в производительности между ModelSerializer и Serializer