1. 1. 为什么 Django REST Framework 那么容易造成这个问题
  2. 2. 解决 Django 「懒惰」的基本方法
  3. 3. 解决 Django REST Framework 性能问题的标准化模式
  4. 4. 那么怎样编写 setup_eager_loading
  5. 5. 链接

看似简单直观的 Django REST Framework 及其嵌套序列化可能会大大降低你的 API 端的性能。你的服务器的其他部分的响应能力也会被某一个低效的 REST API 影响
问题的根源就是 「N+1 selects problem」;首先查询数据库一次得到表中的数据(例如,Customers),然后每个用户的其他字段又需要循环不止一次地查询数据库(例如 customer.country.Name)。使用 Django 的 ORM,很容易造成这个问题,而使用 DRF,同样会造成这个问题。

幸运的是,目前有修复 Django REST Framework 性能问题的解决方法,而且不需要对代码进行重大重组。它只是需要使用未充分利用的 select_related 和 prefetch_related 方法来执行所谓的「预加载」。

为什么 Django REST Framework 那么容易造成这个问题

当你建立一个 DRF 视图时,你经常需要从多个相关表中返回相应的数据。写这样的功能是很简单的,DRF文档中有详细的介绍。不过不幸的是,只要你在序列化中使用嵌套关系,你就在拿你的性能开玩笑,像很多的性能问题一样,它往往只出现有大型数据集的真实生产环境中。

这种情况发生就是因为 Django 的 ORM 是惰性的,它只取出当前查询所需响应最小的数据。它不知道你是否有成百上千的相同或相似的数据也需要取出来。

况且如今,当我们谈到数据库型网站时,一般情况下,最重要的响应指标就是数据库的访问次数。

在 DRF 视图中,我们每次序列化有嵌套关系的数据时都会出现问题,如下面的例子:

1
2
3
4
5
class CustomerSerializer(serializers.ModelSerializer):
# This can kill performance!
order_descriptions = serializers.StringRelatedField(many=True)
# So can this, same exact problem...
orders = OrderSerializer(many=True, read_only=True) # This can kill performance!

CustomerSerializer 函数里面是这么运行的:

  • 获取所有的 customers (需要往返到数据库)
  • 对于第一个返回的客户,获取他们的 orders (又需要去往返一趟数据库)
  • 对于第二个返回的客户,获取他们的 orders (又需要去往返一趟数据库)
  • 对于第三个返回的客户,获取他们的 orders (又需要去往返一趟数据库)
  • 对于第四个返回的客户,获取他们的 orders (又需要去往返一趟数据库)
  • 对于第五个返回的客户,获取他们的 orders (又需要去往返一趟数据库)
  • 对于第六个返回的客户,获取他们的 orders (又需要去往返一趟数据库)
  • 。。。。终于意识到,千万不要有更多的用户

解决 Django 「懒惰」的基本方法

现在我们解决这个问题的方法就是「预加载」。从本质上讲,就是你提前警告 Django ORM 你要一遍又一遍的告诉它同样无聊的指令。在上面的例子中,在 DRF 开始获取前很简单地加上这句话就搞定了:

1
queryset = queryset.prefetch_related('orders')

当 DRF 调用上述相同序列化 customers 时,出现的是这种情况:

  • 获取所有 customers(执行两个往返数据库操作,第一个是获取 customers,第二个获取相关 customers 的所有相关的 orders。)
  • 对于第一个返回的 customers,获取其 order(不需要访问数据库,我们已经在上一步中获取了所需要的数据)
  • 对于第二个返回的 customers,获取其 order (不需要访问数据库)
  • 对于第三个返回的 customers,获取其 order (不需要访问数据库)
  • 对于第四个返回的 customers,获取其 order (不需要访问数据库)
  • 对于第五个返回的 customers,获取其 order (不需要访问数据库)
  • 对于第六个返回的 customers,获取其 order (不需要访问数据库)
  • 你又意识到,你可以有了很多 customers,已经不需要再继续等待去数据库。

其实 Django ORM 的「预备」是在第1步进行请求,它在本地高速缓存的数据能够提供步骤2+所要求的数据。与之前往返数据库相比从本地缓存数据中读取数据基本上是瞬时的,所以我们在有很多 customers 时就获得了巨大的性能加速。

解决 Django REST Framework 性能问题的标准化模式

我们已经确定了一个优化 Django REST Framework 性能问题的通用模式,那就是每当序列化查询嵌套字段时,我们就添加一个新的 @staticmethod 名叫 setup_eager_loading,像这样:

1
2
3
4
5
6
7
class CustomerSerializer(serializers.ModelSerializer):
orders = OrderSerializer(many=True, read_only=True)
def setup_eager_loading(cls, queryset):
""" Perform necessary eager loading of data. """
queryset = queryset.prefetch_related('orders')
return queryset

这样,不管哪里要用到这个序列化,都只需在调用序列化前简单调用 setup_eager_loading ,就像这样:

1
2
3
customer_qs = Customers.objects.all()
customer_qs = CustomerSerializer.setup_eager_loading(customer_qs) # Set up eager loading to avoid N+1 selects
post_data = CustomerSerializer(customer_qs, many=True).data

或者,如果你有一个 APIView 或 ViewSet,你可以在 get_queryset 方法里调用 setup_eager_loading:

1
2
3
4
5
def get_queryset(self):
queryset = Customers.objects.all()
# Set up eager loading to avoid N+1 selects
queryset = self.get_serializer_class().setup_eager_loading(queryset)
return queryset

那么怎样编写 setup_eager_loading

想要解决 Django 的性能问题,最困难的部分就是要熟悉 select_related ,我们将详细介绍它们在 Django ORM 和 Django REST Framework 中怎样使用。

  • select_related:Django ORM 最简单的预加载工具,对于所有一对一或多对一的数据关系,你都需要从同一个父对象获取数据,如客户的公司名称。这个会被翻译成 SQL 的 join 操作,这样父对象的数据就和子对象的数据一起取回来了。(参见官方文档
  • prefetch_related:对于更复杂的关系,即每个结果有多行(例如 many=True ),像多对一或多对多的数据关系,比如上述客户的订单,这转化一个二级 SQL 查询,通常有很长的 WHERE … IN ,从中只选择相关的行。(参见官方文档
  • Prefetch:用于复杂 prefetch_related 查询,例如过滤子集。它也可以嵌套setup_eager_loading 进行调用。 (参见官方文档

链接

http://blog.oneapm.com/apm-tech/304.html