15、DRF实战总结:2023 DRF框架序列化性能优化和cProfile性能基准测试(附源码)
Python中的序列化性能取决于使用的序列化库、数据大小和序列化/反序列化操作的复杂性。一些流行的Python序列化库包括pickle、json和msgpack。对于大型数据集,Cython和Pypy可以提高序列化性能。
本篇详细解析如何优化提升序列化时间减少到原来的99%,只需稍加注意并进行一些小的改变,就可以显著提高性能。
模型序列化器性能
模型序列化器是DRF框架提供的序列化器之一,在序列化数据库模型对象时非常方便。在处理大型数据集时,模型序列化器的性能可能会受到影响。
情境:一个API接口从一个非常大的表中获取数据,性能非常差,因此自然而然地假设问题一定在数据库中。当注意到即使是很小的数据集也会有很差的性能时,开始查看应用程序的其他部分。这个旅程最终将带向了Django Rest框架(DRF)序列化器。
在本次基准测试中,版本使用Python 3.8、Django 2.2和Django Rest框架3.11。
目前为止最新发布版本为Python 3.11、Django 4.*和Django Rest框架3.14。
扩展阅读
DRF框架提供的序列化器包括:
1. ModelSerializer:用于序列化和反序列化Django模型实例。它自动处理模型的字段和关系,并且可以指定额外的或覆盖默认的序列化器。
2. Serializer:用于序列化和反序列化任何数据。它可以手动指定字段,也可以自动生成字段。
3. ListSerializer:用于对多个对象进行序列化和反序列化。
4. DictSerializer:用于对字典对象进行序列化和反序列化。
5. JSONSerializer:用于将JSON格式的数据进行序列化和反序列化。
6. BaseSerializer:是所有序列化器的基类,提供了通用的序列化和反序列化方法。
详细介绍内容参考:
4、DRF实战总结:序列化器(Serializer)、数据验证、重写序列化器方法详解(附源码)_SteveRocket的博客-CSDN博客
简单的函数
序列化器用于将数据转换为对象,以及将对象转换为数据。这是一个简单的函数,因此来编写一个接受一个User实例并返回一个字典的函数:
from typing import Dict, Any
from django.contrib.auth.models import Userdef 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(),}
创建一个用户以便在基准测试中使用:
如果想创建超级用户,则需要携带密码
数据表数据
对基准测试,将使用 cProfile。为了消除数据库等外部影响,提前获取一个用户,并对其进行5000次序列化:
这个简单的函数花了0.023秒来序列化一个用户对象5000次。
ModelSerializer
Django Rest框架(DRF)附带了一些实用程序类,即ModelSerializer。
ModelSerializer是DRF框架提供的序列化器之一,它可以自动序列化和反序列化Django模型对象。ModelSerializer类中的字段与模型中的字段相匹配,这使得序列化和反序列化变得非常容易。 ModelSerializer还支持与外部数据源(如数据库)的嵌套关系。
内置User模型的一个ModelSerializer可能是这样的:
from rest_framework import serializersclass UserSerializer(serializers.ModelSerializer):class Meta:model = Userfields = ['id','last_login','is_superuser','username','first_name','last_name','email','is_staff','is_active','date_joined',]
和之前一样运行相同的基准测试:
DRF序列化一个用户5000次需要7.340秒。这比普通的函数慢377倍。可以看到在functional.py中花费了大量的时间。ModelSerializer使用了django.utils.functional中的lazy函数来评估验证情况。Django的verbose name等也使用到了lazy,DRF也对它进行了评估。这个函数似乎在拖累序列化器。
只读 ModelSerializer
ModelSerializer仅为可写字段添加字段验证。为了度量验证的效果,创建了一个ModelSerializer,并将所有字段标记为只读(read only):
class UserReadOnlyModelSerializer(serializers.ModelSerializer):class Meta:model = Userfields = ['id','last_login','is_superuser','username','first_name','last_name','email','is_staff','is_active','date_joined',]read_only_fields = fields
当所有字段是只读时,则不能使用序列化器创建新的实例。来运行这个只读序列化器的基准测试:
只有7.032秒。与可写的ModelSerializer相比,提升了40%。
在基准测试的输出中,可以看到在field_mapping.py和fields.py中花费了大量时间。这些都与ModelSerializer的内部工作方式有关。在序列化和初始化过程中,ModelSerializer使用大量元数据来构造和验证序列化器字段,当然这是有代价的。
“一般”Serializer
在下一个基准测试中,希望准确地测量ModelSerializer“花费”了多少时间。先为User模型创建一个“一般”Serializer:
class UserSerializer2(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()
对这个"一般" 序列化器运行同样的基准测试:
“一般”序列化器只花了1.875秒。这比只读的ModelSerializer快60%,比可写的ModelSerializer惊人地快85%。此时,可以很明显地看到ModelSerializer并不“便宜”!
只读“一般”Serializer
在可写的ModelSerializer中,验证过程花费了大量的时间。通过将所有字段标记为只读,可以使它更快。“一般”序列化器并不定义任何的验证,因此将字段标记为只读并不会使它更快。要确保:
class UserReadOnlySerializer3(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)
并对一个用户实例运行基准测试:
和预期的一样,与“一般”序列化器相比,将字段标记为只读并没有带来太大区别。这就再一次肯定了时间主要花在从模型的字段定义派生的验证部分上。
结果摘要
以下是迄今为止的运行结果的摘要:
序列化器 |
秒数 |
UserModelSerializer |
7.340 |
UserReadOnlyModelSerailizer |
7.032 |
UserSerializer |
1.875 |
UserReadOnlySerailizer |
2.033 |
serialize_user |
0.023 |
序列化的问题
很多关于Python中的序列化性能的文章都关注于使用select_related和prefetch_related等技术来改进DB访问。虽然这两种方法都可以有效地提高API请求的总体响应时间,但它们并没有解决序列化本身的问题。怀疑这是因为没有人想到序列化会很慢。
其他只关注序列化的文章通常会避免修复DRF,而是去激发新的序列化框架,如marshmallow和serpy。甚至有一个站点专门比较Python中的序列化格式。为了节省的点击,DRF总是排在最后。
Django REST Framework Read & Write Serializers 2013 年底,Django Rest Framework 的创建者 Tom Christie 写了一篇文章讨论了 DRF 的一些缺点。在他的基准测试中,序列化占处理单个请求的总时间的 12%。在总结中,Tom 建议不要总是求助于序列化:你并不总是需要使用序列化器。REST 框架中的序列化程序的工作方式与 Django 的 Form 和 ModelForm 类非常相似。我们提供了一个 Serializer 类,它为您提供了一种强大的通用方法来控制您的响应的输出,以及一个 ModelSerializer 类,它为创建处理模型实例和查询集的序列化器提供了一种有用的快捷方式。
"不需要总是使用序列化器。" 对于性能关键的视图,可以考虑完全删除序列化器,并在数据库查询中简单地使用.values()。正如在前面看到的,这是一个可靠的建议。
序列化问题改进
在第一个使用ModelSerializer的基准测试中,看到大量的时间花费在functional.py中,更具体地说是在lazy函数中。
修复Django中的lazy
Django在内部使用lazy函数来处理许多事情,比如verbose name(冗长的名称)、模板等。其源代码中将lazy描述如下:
对一个函数调用进行封装,并将其作为一个在该函数的结果上进行调用的方法的代理。在调用结果上的一个方法之前,不会对函数进行计算。
lazy函数通过创建一个结果类的代理来实现它的魔力。要创建这个代理,lazy函数会遍历这个结果类(及其超类)的所有属性和函数,并创建一个包装器类,该类仅在实际使用函数结果时才会对函数进行计算。
对于大型结果类,创建代理可能需要一些时间。因此,为了加快速度,lazy会缓存该代理。但事实证明,代码中的一个小疏忽会完全破坏这个缓存机制,使得lazy函数非常非常慢。
为了了解在没有适当缓存的情况下,lazy函数有多慢,让使用一个简单的函数,它返回一个str(结果类),比如upper。选择str是因为它有很多方法,所以为它设置一个代理需要一段时间。
为了建立一个基线,直接使用str.upper进行基准测试,不使用lazy函数:
现在就是惊人的部分,完全相同的函数,但这次使用lazy进行了包装:
没有任何错误! 使用lazy时,将5000个字符串转换为大写需要1.139秒,而直接使用相同的函数只需要0.034秒。快将近33.5倍。
升级到新版本Python环境的测试效率
这显然是一个疏忽。开发人员清楚地意识到缓存代理的重要性。因此,他们发布了一个PR,并在不久后进行了合并。一旦发布,这个补丁将使Django的整体性能更好。
修复Django Rest 框架
DRF对验证和字段冗长名称使用了lazy函数。当所有这些惰性评估结果放在一起时,会明显感觉运行要慢。
Django中对lazy的修复,在进行微小修复后本来也可以解决DRF的这个问题,但尽管如此,开发人员还是对DRF进行了一个单独的修复,用更有效的东西替代lazy。
要查看更改的效果,请安装Django和DRF的最新版本:
在应用了这两个补丁之后,再一次运行同样的基准测试,以及升级Python环境版本。这些是并列的结果:
序列化器 |
第一次 |
第二次 |
UserModelSerializer |
12.818 |
5.674 |
UserReadOnlyModelSerailizer |
7.407 |
5.323 |
UserSerializer |
2.101 |
1.875 |
UserReadOnlySerailizer |
2.254 |
2.033 |
serialize_user |
0.034 |
0.023 |
在本次基准测试中,版本使用Python 3.8、Django 2.2和Django Rest框架3.11。
目前为止最新发布版本为Python 3.11、Django 4.*和Django Rest框架3.14,建议升级项目到最新版本。
来总结一下Django和DRF的变化结果:
- 可写 ModelSerializer的序列化时间被降低了一半。
- 只读 ModelSerializer的序列化时间被降低了三分之一。
- 和预期的一样,在其它的序列化方法中没有明显的差异。
结论
从这个实验中得出的结论是:
1.一旦这些补丁正式发布,就升级DRF和Django。
2.在性能关键的接口中,使用“一般”序列化器,或者根本不使用。
如果多个客户端正在使用API来获取大量数据,API只用于从服务器读取数据,因此决定根本不使用Serializer,而是使用内联序列化进行替代。
3.不用于写入或验证的Serializer字段应该是只读的。
正如在基准测试中所看到的,验证的实现方式使它们变得昂贵,而将字段标记为只读可以消除不必要的额外成本。
扩展阅读:
内联序列化:内联序列化是在内存中序列化和反序列化对象,而不是将其写入磁盘或网络。这可以提升序列化性能,特别是在处理大型数据集时。
为了确保开发人员不会忘记设置只读字段,添加了一个Django检查,以确保所有的ModelSerializer都设置了read_only_fields:
# common/cecks.py
import inspect
import django.core.checks
from rest_framework.serializers import ModelSerializer
import django_rest_framework_pro.urls # noqa force import of all serializer@django.core.checks.register('rest_framework.serializers')
def check_serializer(app_config, **kwargs):for serializer in ModelSerializer.__subclasses__():# skip third-party appspath = inspect.getfile(serializer)if path.find('site-packages') > -1:continueif hasattr(serializer.Meta, 'read_only_fields'):continueyield 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,将得到以下错误:
经常使用Django检查,以确保没有遗漏任何内容。可以在《如何使用Django系统检查框架》这篇文章中找到更多的其他有用的检查。
输入才有输出,吸收才能吐纳。——码字不易