Django设计模式用于计算耗时较长的网页分析界面
我有一个“分析仪表盘”页面,用户在我的django网页应用中可以看到,但计算起来非常耗时。这个页面需要检查数据库中每个用户的所有交易,并给出一些相关的指标。
我希望这个过程能够实时进行,但对于活跃用户来说,计算时间可能需要20到30秒(不能分页,因为它需要给出交易的平均值)。
我想到的解决办法是通过一个manage.py的批处理命令在后台进行计算,然后把计算好的结果缓存起来,直接展示给用户。请问有没有什么Django的设计模式可以帮助实现这种模型或展示方式?
4 个回答
你需要看看 Django的缓存框架。
对于这种情况,最简单而且我认为正确的解决方案是,在数据更新时提前计算好所有内容,这样当用户查看仪表盘时,就不需要再计算,只需显示已经计算好的值。
实现这个目标的方法有很多,但基本的思路是,当某个需要计算的东西发生变化时,后台会自动触发一个计算的功能。
为了在后台触发这样的计算,我通常会使用celery。比如说,用户在视图view_foo
中添加了一个项目foo
,我们就会调用一个celery任务update_foo_count
,这个任务会在后台运行并更新foo
的数量。或者,你也可以设置一个celery定时器,每10分钟更新一次数量,检查是否需要重新计算。重新计算的标志可以在用户更新数据的不同地方设置。
你要找的是离线处理和缓存的结合。这里的“离线”是指计算逻辑在请求和响应的循环之外进行。而“缓存”则是指你那些耗时的计算结果在一段时间内是有效的,这段时间内你不需要重新计算来显示结果。这是一种非常常见的做法。
离线处理
有两种常用的方法可以在请求和响应循环之外进行工作:
- 定时任务(通常通过自定义管理命令来简化)
- Celery
相对来说,定时任务的设置更简单,而Celery则更强大和灵活。不过,Celery有很好的文档和全面的测试套件。我在几乎每个项目中都用过它,虽然它有一些要求,但设置起来并不复杂。
定时任务
定时任务是一种传统的方法。如果你只需要运行一些逻辑并将结果存储到数据库中,定时任务没有任何依赖。定时任务唯一麻烦的地方是让你的代码在Django项目的上下文中运行——也就是说,你的代码必须正确加载settings.py,以便知道你的数据库和应用程序。对于初学者来说,这可能会让人感到困惑,尤其是在确定正确的PYTHONPATH
时。
如果你选择定时任务,一个好的方法是编写自定义管理命令。这样你可以很方便地从终端测试你的命令(并编写测试),而且在管理命令的开头不需要做任何特别的设置来配置Django环境。在生产环境中,你只需运行path/to/manage.py yourcommand
。我不确定这种方法是否在没有virtualenv的帮助下能否正常工作,但无论如何你都应该使用它。
另一个需要考虑的方面是定时任务:如果你的逻辑运行时间不固定,定时任务对此一无所知。每小时运行一个需要两小时的定时任务可能会让你的服务器崩溃。你可以自己实现一个锁机制来防止这种情况,但要注意——最初看似短的定时任务,随着数据量的增加或数据库的异常,可能会变得不那么短。
在你的情况下,定时任务似乎不太适用,因为你需要定期为每个用户计算图表,而不考虑谁在使用系统。这就是Celery可以帮助你的地方。
Celery
…是个非常棒的工具。通常人们会被它需要一个AMQP代理的“默认”要求吓到。设置RabbitMQ并不复杂,但确实需要你稍微走出Python的舒适区。对于许多任务,我只是用redis作为Celery的任务存储。设置非常简单:
CELERY_RESULT_BACKEND = "redis"
REDIS_HOST = "localhost"
REDIS_PORT = 6379
REDIS_DB = 0
REDIS_CONNECT_RETRY = True
这样就不需要AMQP代理了。
Celery相比简单的定时任务有很多优势。像定时任务一样,你可以安排定期任务,但你也可以在不阻塞请求/响应循环的情况下响应其他事件来触发任务。
如果你不想为每个活跃用户定期计算图表,你需要按需生成它。我假设查询最新的可用平均值是便宜的,而计算新的平均值是昂贵的,并且你是在客户端使用类似flot的工具生成实际的图表。以下是一个示例流程:
- 用户请求一个包含平均值图表的页面。
- 检查缓存——是否有存储的、未过期的查询集包含该用户的平均值?
- 如果有,就使用它。
- 如果没有,触发一个Celery任务来重新计算,重新查询并缓存结果。由于查询现有数据是便宜的,如果你想在此期间向用户显示过时的数据,可以运行查询。
- 如果图表过时,可以选择提供一些指示,表明图表是过时的,或者做一些ajax的花样,定期向Django询问更新的图表是否准备好了。
你可以结合一个定期任务,每小时为有活跃会话的用户重新计算图表,以防止显示真的过时的图表。这并不是唯一的解决方案,但它为你提供了所有必要的控制,以确保数据的新鲜,同时降低计算任务的CPU负载。最棒的是,定期任务和“按需”任务共享相同的逻辑——你只需定义一次任务,然后从两个地方调用它,以保持代码的简洁。
缓存
Django缓存框架为你提供了所有需要的钩子,可以缓存你想要的任何内容,持续的时间也由你决定。大多数生产网站依赖于memcached作为它们的缓存后端,而我最近开始使用redis和django-redis-cache后端,但我不确定我是否会信任它用于大型生产网站。
以下是一些代码,展示了如何使用低级缓存API来实现上述工作流程:
import pickle
from django.core.cache import cache
from django.shortcuts import render
from mytasks import calculate_stuff
from celery.task import task
@task
def calculate_stuff(user_id):
# ... do your work to update the averages ...
# now pull the latest series
averages = TransactionAverage.objects.filter(user=user_id, ...)
# cache the pickled result for ten minutes
cache.set("averages_%s" % user_id, pickle.dumps(averages), 60*10)
def myview(request, user_id):
ctx = {}
cached = cache.get("averages_%s" % user_id, None)
if cached:
averages = pickle.loads(cached) # use the cached queryset
else:
# fetch the latest available data for now, same as in the task
averages = TransactionAverage.objects.filter(user=user_id, ...)
# fire off the celery task to update the information in the background
calculate_stuff.delay(user_id) # doesn't happen in-process.
ctx['stale_chart'] = True # display a warning, if you like
ctx['averages'] = averages
# ... do your other work ...
render(request, 'my_template.html', ctx)
编辑:值得注意的是,序列化一个查询集会将整个查询集加载到内存中。如果你的平均值查询集包含大量数据,这可能不是最佳选择。无论如何,使用真实数据进行测试是明智的。