Django性能优化

性能优化其实就是测量。我们在一个项目中测量以下几个方面:

  • 执行时间
  • 代码的行数
  • 函数调用次数
  • 分配的内存
  • 其他

一般来说,两个指标比较重要:执行时间,需要的内存。

在Web项目中,响应时间(服务器接收由某个用户的操作产生的请求,处理该请求并返回结果所需的总的时间)通常是最重要的指标,因为过长的响应时间会让用户厌倦等待,并切换到浏览器中的另一个选项卡页面。

在编程中,分析项目的性能被称为profiling。为了分析API的性能,我们将使用Silk包。在安装完这个包,并调用/api/v1/houses/?country=5T22RI后,可以得到如下的结果:

1
2
3
4
5
6
200 GET 
/api/v1/houses/

77292ms overall
15854ms on queries
50004 queries

优化数据库的查询

性能优化最常见的技巧之一是对数据库查询进行优化,同时,还可以对查询做多次优化来减小响应时间。

1. 一次性提供所有数据

Django中的查询是惰性的。这意味着在你真正需要获取数据之前它不会访问数据库。同时,它只获取你指定的数据,如果需要其他附加数据,则要另外发出请求。
这时候可以在查询集上使用两个方法:

  1. select_related(),他将返回和外键有关的QuerySet,在执行查询时选择其他相关的数据。
    这是一个性能提升,导致一个复杂的查询,但是意味着稍后使用外键关系不会要求数据库查询。select_related通过创建SQL连接并在SELECT语句中包含相关对象的字段来工作。select_related仅限于单值关系 - 外键和一对一。
  2. prefetch_related()。prefetch_related为每个关系进行单独的查找,并在Python中进行“连接”。这使得它可以预取多对多和多对一的对象,也支持GenericRelation和GenericForeignKey的预取,但是它必须被限制在一组同样的结果中。

使用了select_related()后:

1
2
3
4
5
6
200 GET
/api/v1/houses/

35979ms overall
102ms on queries
4 queries

总体响应时间降至36秒,在数据库中花费的时间约为100ms,只有4个查询!

2. 仅提取相关的数据

默认情况下,Django会从数据库中提取所有字段。但是,当表有很多列很多行的时候,告诉Django提取哪些特定的字段就非常有意义了,这样就不会花时间去获取根本用不到的信息。
Django可以使用defer()only()这两个查询方法来实现这一点。第一个用于指定哪些字段不要加载,第二个用于指定只加载哪些字段。

1
2
Person.objects.defer("age", "biography")   # 取age,biography之外的字段信息
Person.objects.only("name") # 只取name字段的信息

使用only()后:

1
2
3
4
5
6
200 GET
/api/v1/houses/

33111ms overall
52ms on queries
4 queries

减少了一半的查询时间,总体时间也略有下降,还有提升的空间。

3. 其他

  • 使用 filter and exclude 过滤不需要的记录,这两个是最常用语句,相当是SQL的where
  • 同一实体里使用F()表达式过滤其他字段
  • 使用annotate对数据库做聚合运算,不要用python语言对以上类型数据过滤筛选,同样的结果,python处理复杂度要高,而且效率不高, 白白浪费内存
  • 使用QuerySet.extra() extra虽然扩展性不太好,但功能很强大,如果实体里需要需要增加额外属性,不得已时,通过extra来实现,也是个好办法

代码优化

你不能无限制地优化数据库查询,并且上面的结果也证明了这一点。即使把查询时间减少到0,我们仍然会面对需要等待半分钟才能得到应答这个现实。现在是时候转移到另一个优化级别上来了,那就是:业务逻辑

1. 简化代码

有时,第三方软件包对于简单的任务来说有着太大的开销。可以自定义完成简单的任务,不使用框架写好的方法。

2. 更新或替代第三方软件包

想要再减少时间,就需要分析代码了。
你可以自己使用Python内置的分析器来进行分析,也可以使用一些第三方软件包。由于我们已经使用了silk,它可以分析代码并生成一个二进制的分析文件,因此,我们可以做进一步的可视化分析。有好几个可视化软件包可以将二进制文件转换为一些友好的可视化视图。本文将使用snakeviz
这是上文一个请求的二进制分析文件的可视化图表:
二进制分析文件的可视化图表

从上到下是调用堆栈,显示了文件名、函数名及其行号,以及该方法花费的时间。可以很容易地看出,时间大部分都用在计算散列上(紫罗兰色的init.py和primes.py矩形)。
目前,这是代码的主要性能瓶颈,但同时,这不是我们自己写的代码,而是用的第三方包。
在这种情况下,我们可以做的事情将非常有限:

  • 检查包的最新版本(希望能有更好的性能)。
  • 寻找另一个能够满足我们需求的软件包。
  • 我们自己写代码,并且性能优于目前使用的软件包。

幸运的是,我们找到了一个更新版本的basehash包。原代码使用的是v.2.1.0,而新的是v.3.0.4。
更新之后:

1
2
3
4
5
6
200 GET
/api/v1/houses/

7738ms overall
59ms on queries
4 queries

响应时间从17秒缩短到了8秒以内。太棒了!但还有一件事我们应该来看看。

3. 重构代码

到目前为止,我们已经改进了查询、用自己特定的函数取代了第三方复杂而又泛型的代码、更新了第三方包,但是我们还是保留了原有的代码。但有时,对现有代码进行小规模的重构可能会带来意想不到的结果。但是,为此我们需要再次分析运行结果。
二进制分析文件的可视化图表
仔细看一下,你可以看到散列仍然是一个问题(毫不奇怪,这是我们对数据做的唯一的事情),虽然我们确实朝这个方向改进了,但这个绿色的矩形表示init.py花了2.14秒的时间,同时伴随着灰色的init.py:54(hash)。这意味着初始化工作需要很长的时间。
我们来看看basehash包的源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# basehash/__init__.py

# Initialization of `base36` class initializes the parent, `base` class.
class base36(base):
def __init__(self, length=HASH_LENGTH, generator=GENERATOR):
super(base36, self).__init__(BASE36, length, generator)


class base(object):
def __init__(self, alphabet, length=HASH_LENGTH, generator=GENERATOR):
if len(set(alphabet)) != len(alphabet):
raise ValueError('Supplied alphabet cannot contain duplicates.')

self.alphabet = tuple(alphabet)
self.base = len(alphabet)
self.length = length
self.generator = generator
self.maximum = self.base ** self.length - 1
self.prime = next_prime(int((self.maximum + 1) * self.generator)) # `next_prime` call on each initialized instance

正如你所看到的,一个base实例的初始化需要调用next_prime函数,这是太重了,我们可以在上面的可视化图表中看到左下角的矩形。

我们再来看看Hash类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class 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() # <-- initializing on each method call
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() # <-- initializing on each method call
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]

正如你所看到的,我已经标记了这两个方法初始化base36实例的方法,这并不是真正需要的。
由于散列是一个确定性的过程,这意味着对于一个给定的输入值,它必须始终生成相同的散列值,因此,我们可以把它作为类的一个属性。让我们来看看它将如何执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Hasher(object):
base36 = basehash.base36() # <-- initialize hasher only once

@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):
content_type = ContentType.objects.get_for_model(klass, for_concrete_model=False)
return cls.base36.hash('%(contenttype_pk)03d%(object_pk)06d' % {
'contenttype_pk': content_type.pk,
'object_pk': object_pk
})

@classmethod
def parse_hash(cls, obj_hash):
unhashed = '%09d' % cls.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]

再看结果:

1
2
3
4
5
6
7
**200 GET**

/api/v1/houses/

3766ms overall
38ms on queries
4 queries

使用缓存

在python开发中,如果使用django进行编写,为了提升效率,常常需要优化缓存。
官方文档

settings.py中CACHES配置格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 此为开始调试用,实际内部不做任何操作
# 配置:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.dummy.DummyCache', # 引擎
'TIMEOUT': 300, # 缓存超时时间(默认300,None表示永不过期,0表示立即过期)
'OPTIONS':{
'MAX_ENTRIES': 300, # 最大缓存个数(默认300)
'CULL_FREQUENCY': 3, # 缓存到达最大个数之后,剔除缓存个数的比例,即:1/CULL_FREQUENCY(默认3)
},
'KEY_PREFIX': '', # 缓存key的前缀(默认空)
'VERSION': 1, # 缓存key的版本(默认1)
'KEY_FUNCTION' 函数名 # 生成key的函数(默认函数会生成为:【前缀:版本:key】)
}
}

Django中提供了6种缓存方式:

  • 开发调试
  • 内存
  • 文件
  • 数据库
  • Memcache缓存(python-memcached模块)
  • Memcache缓存(pylibmc模块)

缓存的3中应用:
1、全站应用

1
2
3
4
5
6
7
8
9
10
11
# 使用中间件,经过一系列的认证等操作,如果内容在缓存中存在,则使用FetchFromCacheMiddleware获取内容并返回给用户,当返回给用户之前,判断缓存中是否已经存在,如果不存在则UpdateCacheMiddleware会将缓存保存至缓存,从而实现全站缓存

MIDDLEWARE = [
'django.middleware.cache.UpdateCacheMiddleware',
# 其他中间件...
'django.middleware.cache.FetchFromCacheMiddleware',
]

CACHE_MIDDLEWARE_ALIAS = "" #用于存储的高速缓存别名。
CACHE_MIDDLEWARE_SECONDS = "" #每页应该被缓存的秒数。
CACHE_MIDDLEWARE_KEY_PREFIX = ""

查看详情

2、单独视图缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
#方式一:
from django.views.decorators.cache import cache_page

@cache_page(60 * 15)
def my_view(request):
...

#方式二:
from django.views.decorators.cache import cache_page

urlpatterns = [
url(r'^foo/([0-9]{1,2})/$', cache_page(60 * 15)(my_view)),
]

查看更多
3、模板片段缓存(局部视图)

1
2
3
4
{% load cache %}
{% cache 500 sidebar %}
.. sidebar ..
{% endcache %}

查看更多

参考博客

原文: A Guide to Performance Testing and Optimization With Python and Django
作者:IULIAN GULEA
翻译:雁惊寒
个人cnblogs