Cherrypy:处理时间长的页面有哪些解决方案

3 投票
1 回答
2305 浏览
提问于 2025-04-17 23:47

我有一个用cherrypy搭建的网站。有些页面需要处理的时间比较长(比如在一个几百万行的数据库上进行复杂的SQL查询)。这个处理过程有时需要20秒甚至更久,导致浏览器崩溃,因为等待时间太长了。

我在想,有什么好的解决办法吗?

1 个回答

8

这里的一切都跟网站的访问量有关。CherryPy 是一个多线程的服务器,一旦每个线程都在等待数据库的响应,新请求就无法被处理。还有请求队列的问题,但总的来说就是这样。

简单的解决办法

如果你知道自己的网站流量不大,可以尝试一些变通的方法。可以根据需要增加 response.timeout 的时间(默认是300秒)。还可以增加 server.thread_pool 的大小(默认是10)。如果你在 CherryPy 应用前面使用了反向代理,比如 nginx,也要在那边增加代理的超时时间。

接下来的解决方案需要你重新设计网站。具体来说,就是要让它变成异步的,客户端代码发送一个任务,然后通过拉取或推送的方式获取结果。这需要在客户端和服务器端都做一些改动。

CherryPy 背景任务

你可以利用 cherrypy.process.plugins.BackgroundTask 和一些中间存储(比如在数据库中新建一个表)来处理服务器端的任务。可以用 XmlHttpRequest 来拉取数据,或者用 WebSockets 来推送数据到客户端。CherryPy 都能处理这两种方式。

需要注意的是,因为 CherryPy 是在单个 Python 进程中运行的,所以后台任务的线程也会在这个进程里运行。如果你对 SQL 查询结果进行后处理,就会受到 全局解释器锁(GIL) 的影响。因此,你可能需要考虑改用进程,这样会稍微复杂一些。

工业级解决方案

如果你的网站需要大规模运行,最好考虑使用分布式任务队列,比如 RqCelery。这样在服务器端的处理会有很大不同。客户端的拉取或推送方式保持不变。

示例

下面是一个使用 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)

撰写回答