唐纳德·克努特(Donald Knuth)曾经说过:“不成熟的优化方案是万恶之源。”然而,任何一个承受高负载的成熟项目都不可避免地需要进行优化。在本文中,我想谈谈优化Web项目代码的五种常用方法。虽然本文是以Django为例,但其他框架和语言的优化原则也是类似的。通过使用这些优化方法,文中例程的查询响应时间从原来的77秒减少到了3.7秒。
?
本文用到的例程是从一个我曾经使用过的真实项目改编而来的,是性能优化技巧的典范。如果你想自己尝试着进行优化,可以在GitHub上获取优化前的初始代码,并跟着下文做相应的修改。我使用的是Python 2,因为一些第三方软件包还不支持Python 3。
?
示例代码介绍
?
这个Web项目只是简单地跟踪每个地区的房产价格。因此,只有两种模型:
# houses/models.pyfrom utils.hash import Hasherclass HashableModel(models.Model): """Provide a hash property for models.""" class Meta:
abstract = True @property def hash(self):
return Hasher.from_model(self)class Country(HashableModel): """Represent a country in which the house is positioned.""" name = models.CharField(max_length=30) def __unicode__(self):
return self.nameclass House(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 basehashclass Hasher(object): @classmethod def from_model(cls, obj, klass=None):
if obj.pk is None:
return None
return cls.make_hash(obj.pk, klass if klass is not None else obj) @classmethod def make_hash(cls, object_pk, klass):
base36 = basehash.base36()
content_type = ContentType.objects.get_for_model(klass, for_concrete_model=False)
return base36.hash('%(contenttype_pk)03d%(object_pk)06d' % {
'contenttype_pk': content_type.pk,
'object_pk': object_pk
}) @classmethod def parse_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 @classmethod def to_object_pk(cls, obj_hash):
return cls.parse_hash(obj_hash)[1]
?
由于我们想通过API来提供这些数据,所以我们安装了Django REST框架并定义以下序列化器和视图:
# houses/serializers.pyclass HouseSerializer(serializers.ModelSerializer): """Serialize a `houses.House` instance.""" id = serializers.ReadOnlyField(source="hash") country = serializers.ReadOnlyField(source="country.hash") class Meta:
model = House
fields = (
'id',
'address',
'country',
'sq_meters',
'price'
)
?
# houses/views.pyclass HouseListAPIView(ListAPIView): model = House serializer_class = HouseSerializer country = None def get_queryset(self):
country = get_object_or_404(Country, pk=self.country)
queryset = self.model.objects.filter(country=country)
return queryset def list(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万个),并准备测试应用程序的性能。
?
性能优化其实就是测量
?
在一个项目中我们需要测量下面这几个方面:
?
执行时间代码的行数函数调用次数分配的内存其他
但是,并不是所有这些都要用来度量项目的执行情况。一般来说,有两个指标比较重要:执行多长时间、需要多少内存。
?