本篇文章主要介绍了"测男测女 Python Django性能测试与优化指南",主要涉及到测男测女方面的内容,对于软件测试感兴趣的同学可以参考一下:
本文通过一个简单的实例一步一步引导读者对其进行全方位的性能优化。唐纳德·克努特(Donald Knuth)曾经说过:“不成熟的优化方案...
本文通过一个简单的实例一步一步引导读者对其进行全方位的性能优化。
唐纳德·克努特(Donald Knuth)曾经说过:“不成熟的优化方案是万恶之源。”然而,任何一个承受高负载的成熟项目都不可避免地需要进行优化。在本文中,我想谈谈 优化Web项目代码的五种常用方法 。虽然本文是以Django为例,但其他框架和语言的优化原则也是类似的。通过使用这些优化方法,文中例程的查询响应时间从原来的77秒减少到了3.7秒。

本文用到的例程是从一个我曾经使用过的真实项目改编而来的,是性能优化技巧的典范。如果你想自己尝试着进行优化,可以在 GitHub 上获取优化前的初始代码,并跟着下文做相应的修改。我使用的是Python 2,因为一些第三方软件包还不支持Python 3。
示例代码介绍
这个Web项目只是简单地跟踪每个地区的房产价格。因此,只有两种模型:
# houses/models.pyfrom utils.hash import Hasher
classHashableModel(models.Model):"""Provide a hash property for models."""classMeta: abstract = True @propertydefhash(self):return Hasher.from_model(self)
classCountry(HashableModel):"""Represent a country in which the house is positioned.""" name = models.CharField(max_length=30)
def__unicode__(self):return self.name
classHouse(HashableModel):"""Represent a house with its characteristics."""# Relations country = models.ForeignKey(Country, related_name='houses')
# Attributes address = models.CharField(max_length=255)
sq_meters = models.PositiveIntegerField()
kitchen_sq_meters = models.PositiveSmallIntegerField()
nr_bedrooms = models.PositiveSmallIntegerField()
nr_bathrooms = models.PositiveSmallIntegerField()
nr_floors = models.PositiveSmallIntegerField(default=1)
year_built = models.PositiveIntegerField(null=True, blank=True)
house_color_outside = models.CharField(max_length=20)
distance_to_nearest_kindergarten = models.PositiveIntegerField(null=True, blank=True)
distance_to_nearest_school = models.PositiveIntegerField(null=True, blank=True)
distance_to_nearest_hospital = models.PositiveIntegerField(null=True, blank=True)
has_cellar = models.BooleanField(default=False)
has_pool = models.BooleanField(default=False)
has_garage = models.BooleanField(default=False)
price = models.PositiveIntegerField()
def__unicode__(self):return'{} {}'.format(self.country, self.address)
抽象类 HashableModel
提供了一个继承自模型并包含 hash
属性的模型,这个属性包含了实例的主键和模型的内容类型。 这能够隐藏像实例ID这样的敏感数据,而用散列进行代替。如果项目中有多个模型,而且需要在一个集中的地方对模型进行解码并要对不同类的不同模型实例进行处理时,这可能会非常有用。 请注意,对于本文的这个小项目,即使不用散列也照样可以处理,但使用散列有助于展示一些优化技巧。
这是 Hasher
类:
# utils/hash.pyimport basehash
classHasher(object): @classmethoddeffrom_model(cls, obj, klass=None):if obj.pk isNone:
returnNonereturn cls.make_hash(obj.pk, klass if klass isnotNoneelse obj)
@classmethoddefmake_hash(cls, object_pk, klass): base36 = basehash.base36()
content_type = ContentType.objects.get_for_model(klass, for_c>False)
return base36.hash('%(contenttype_pk)03d%(object_pk)06d' % {
'contenttype_pk': content_type.pk,
'object_pk': object_pk
})
@classmethoddefparse_hash(cls, obj_hash): base36 = basehash.base36()
unhashed = '%09d' % base36.unhash(obj_hash)
contenttype_pk = int(unhashed[:-6])
object_pk = int(unhashed[-6:])
return contenttype_pk, object_pk
@classmethoddefto_object_pk(cls, obj_hash):return cls.parse_hash(obj_hash)[1]
由于我们想通过API来提供这些数据,所以我们安装了Django REST框架并定义以下序列化器和视图:
# houses/serializers.pyclassHouseSerializer(serializers.ModelSerializer):"""Serialize a `houses.House` instance."""
id = serializers.ReadOnlyField(source="hash")
country = serializers.ReadOnlyField(source="country.hash")
classMeta: model = House
fields = (
'id',
'address',
'country',
'sq_meters',
'price' )
# houses/views.pyclassHouseListAPIView(ListAPIView): model = House
serializer_class = HouseSerializer
country = Nonedefget_queryset(self): country = get_object_or_404(Country, pk=self.country)
queryset = self.model.objects.filter(country=country)
return queryset
deflist(self, request, *args, **kwargs):# Skipping validation code for brevity country = self.request.GET.get("country")
self.country = Hasher.to_object_pk(country)
queryset = self.get_queryset()
serializer = self.serializer_class(queryset, many=True)
return Response(serializer.data)
现在,我们将用一些数据来填充数据库(使用 factory-boy
生成10万个房屋的实例:一个地区5万个,另一个4万个,第三个1万个),并准备测试应用程序的性能。
性能优化其实就是测量
在一个项目中我们需要测量下面这几个方面:
- 执行时间
- 代码的行数
- 函数调用次数
- 分配的内存
- 其他
但是,并不是所有这些都要用来度量项目的执行情况。一般来说,有两个指标比较重要:执行多长时间、需要多少内存。
在Web项目中, 响应时间 (服务器接收由某个用户的操作产生的请求,处理该请求并返回结果所需的总的时间)通常是最重要的指标,因为过长的响应时间会让用户厌倦等待,并切换到浏览器中的另一个选项卡页面。
在编程中,分析项目的性能被称为 profiling 。为了分析API的性能,我们将使用 Silk 包。在安装完这个包,并调用 /api/v1/houses/?country=5T22RI
后,可以得到如下的结果:
200 GET
/api/v1/houses/77292ms overall
15854ms on queries
50004 queries
整体响应时间为77秒,其中16秒用于查询数据库,总共有5万次查询。这几个数字很大,提升空间也有很大,所以,我们开始吧。
1. 优化数据库查询
性能优化最常见的技巧之一是对数据库查询进行优化,本案例也不例外。同时,还可以对查询做多次优化来减小响应时间。
1.1 一次提供所有数据
仔细看一下这5万次查询查的是什么:都是对 houses_country
表的查询: