Cherrypy:处理时间长的页面有哪些解决方案
我有一个用cherrypy搭建的网站。有些页面需要处理的时间比较长(比如在一个几百万行的数据库上进行复杂的SQL查询)。这个处理过程有时需要20秒甚至更久,导致浏览器崩溃,因为等待时间太长了。
我在想,有什么好的解决办法吗?
1 个回答
这里的一切都跟网站的访问量有关。CherryPy 是一个多线程的服务器,一旦每个线程都在等待数据库的响应,新请求就无法被处理。还有请求队列的问题,但总的来说就是这样。
简单的解决办法
如果你知道自己的网站流量不大,可以尝试一些变通的方法。可以根据需要增加 response.timeout
的时间(默认是300秒)。还可以增加 server.thread_pool
的大小(默认是10)。如果你在 CherryPy 应用前面使用了反向代理,比如 nginx,也要在那边增加代理的超时时间。
接下来的解决方案需要你重新设计网站。具体来说,就是要让它变成异步的,客户端代码发送一个任务,然后通过拉取或推送的方式获取结果。这需要在客户端和服务器端都做一些改动。
CherryPy 背景任务
你可以利用 cherrypy.process.plugins.BackgroundTask
和一些中间存储(比如在数据库中新建一个表)来处理服务器端的任务。可以用 XmlHttpRequest 来拉取数据,或者用 WebSockets 来推送数据到客户端。CherryPy 都能处理这两种方式。
需要注意的是,因为 CherryPy 是在单个 Python 进程中运行的,所以后台任务的线程也会在这个进程里运行。如果你对 SQL 查询结果进行后处理,就会受到 全局解释器锁(GIL) 的影响。因此,你可能需要考虑改用进程,这样会稍微复杂一些。
工业级解决方案
如果你的网站需要大规模运行,最好考虑使用分布式任务队列,比如 Rq 或 Celery。这样在服务器端的处理会有很大不同。客户端的拉取或推送方式保持不变。
示例
下面是一个使用 XHR 轮询的 BackgroundTags
的简单实现。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import time
import uuid
import cherrypy
from cherrypy.process.plugins import BackgroundTask
config = {
'global' : {
'server.socket_host' : '127.0.0.1',
'server.socket_port' : 8080,
'server.thread_pool' : 8,
}
}
class App:
_taskResultMap = None
def __init__(self):
self._taskResultMap = {}
def _target(self, task, id, arg):
time.sleep(10) # long one, right?
try:
self._taskResultMap[id] = 42 + arg
finally:
task.cancel()
@cherrypy.expose
@cherrypy.tools.json_out()
def schedule(self, arg):
id = str(uuid.uuid1())
self._taskResultMap[id] = None
task = BackgroundTask(
interval = 0, function = self._target, args = [id, int(arg)],
bus = cherrypy.engine)
task.args.insert(0, task)
task.start()
return str(id)
@cherrypy.expose
@cherrypy.tools.json_out()
def poll(self, id):
if self._taskResultMap[id] is None:
return {'id': id, 'status': 'wait', 'result': None}
else:
return {
'id' : id,
'status' : 'ready',
'result' : self._taskResultMap.pop(id)
}
@cherrypy.expose
def index(self):
return '''<!DOCTYPE html>
<html>
<head>
<title>CherryPy BackgroundTask demo</title>
<script type='text/javascript'
src='http://cdnjs.cloudflare.com/ajax/libs/qooxdoo/3.5.1/q.min.js'>
</script>
<script type='text/javascript'>
// Do not structure you real JavaScript application this way.
// This callback spaghetti is only for brevity.
function sendSchedule(arg, callback)
{
var xhr = q.io.xhr('/schedule?arg=' + arg);
xhr.on('loadend', function(xhr)
{
if(xhr.status == 200)
{
callback(JSON.parse(xhr.responseText))
}
});
xhr.send();
};
function sendPoll(id, callback)
{
var xhr = q.io.xhr('/poll?id=' + id);
xhr.on('loadend', function(xhr)
{
if(xhr.status == 200)
{
callback(JSON.parse(xhr.responseText))
}
});
xhr.send();
}
function start(event)
{
event.preventDefault();
// example argument to pass to the task
var arg = Math.round(Math.random() * 100);
sendSchedule(arg, function(id)
{
console.log('scheduled (', arg, ') as', id);
q.create('<li/>')
.setAttribute('id', id)
.append('<span>' + id + ': 42 + ' + arg +
' = <img src="http://sstatic.net/Img/progress-dots.gif" />' +
'</span>')
.appendTo('#result-list');
var poll = function()
{
console.log('polling', id);
sendPoll(id, function(response)
{
console.log('polled', id, '(', response, ')');
if(response.status == 'wait')
{
setTimeout(poll, 2500);
}
else if(response.status == 'ready')
{
q('#' + id)
.empty()
.append('<span>' + id + ': 42 + ' + arg + ' = ' +
response.result + '</span>');
}
});
};
setTimeout(poll, 2500);
});
}
q.ready(function()
{
q('#run').on('click', start);
});
</script>
</head>
<body>
<p>
<a href='#' id='run'>Run a long task</a>, look in browser console.
</p>
<ul id='result-list'></ul>
</body>
</html>
'''
if __name__ == '__main__':
cherrypy.quickstart(App(), '/', config)