昂贵的阻塞 Python 进程的 HTTP 服务优化

36 阅读4分钟

我们有一个提供大型 MP3 文件的小而任意的片段服务的 web 服务。MP3 文件是由一个 Python 应用程序动态生成的。这种模式是,向一个指定要分段的 URL 发出 GET 请求,并以 audio/mpeg 流的形式接收响应。这是一个昂贵的过程。

我们使用 Nginx 作为前端请求处理程序。Nginx 会负责缓存常见请求的响应。

我们最初尝试使用 Tornado 在后端处理来自 Nginx 的请求。正如你所料,阻塞 MP3 操作使 Tornado 无法执行其任务(异步 I/O)。所以,我们使用多线程,这解决了阻塞问题,并且性能非常好。然而,它引入了一个微妙的竞争条件(在实际负载下),我们还没有能够诊断或重现。竞争条件破坏了我们的 MP3 输出。

因此,我们决定将我们的应用程序设置为 Apache/mod_wsgi 后面的一个简单的 WSGI 处理程序(仍然使用 Nginx)。这消除了阻塞问题和竞争条件,但在实际条件下却给服务器带来了级联负载(即 Apache 创建了太多进程)。我们目前正在调整 Apache/mod_wsgi,但仍处于反复试验阶段。(更新:我们已经切换回 Tornado。)

最后,疑问是:我们是否遗漏了什么?有没有一种更好的方法通过 HTTP 服务成本高昂的 CPU 资源?

更新:多亏了 Graham 见多识广的文章,我相当确定这是一个 Apache 调整问题。与此同时,我们已经改回使用 Tornado,并正在尝试解决数据损坏问题。

对于那些急于用更多资源来解决这个问题的人来说,Tornado 和一点多线程(尽管线程引入的数据完整性问题)在一个小型(单核)Amazon EC2 实例上能够很好地处理负载。

2.解决方案

1. 使用 Spawning

Spawning 是一个具有灵活的多线程模式选择范围的 WSGI 服务器。

2. 使用队列系统和 AJAX 通知方法

当需要生成昂贵资源的请求出现时,将该请求添加到队列中(如果尚未添加)。队列操作应该返回一个对象的 ID,你可以查询该 ID 以获取其状态。

接下来,你需要编写一个后台服务来启动工作线程。这些工作线程只需从队列中取出请求,生成数据,然后将数据的地址保存到请求对象中。

网页可以向你的服务器发出 AJAX 调用以了解生成的进度,并在文件可用后提供一个指向该文件的链接。这是大型媒体网站的工作方式——那些不得不处理视频的网站。然而,这对于你的 MP3 工作来说可能有些过犹不及。

或者,你可以考虑运行多台机器来分配负载。Apache 上的线程仍然会被阻塞,但至少你不会消耗 Web 服务器上的资源。

3. 确定使用 Apache/mod_wsgi 的模式

请务必确定你没有错误地使用 Apache/mod_wsgi 的嵌入模式。阅读:

blog.dscpl.com.au/2009/03/loa…

确保使用守护程序模式(daemon mode),如果使用 Apache/mod_wsgi。

4. 优化 Apache 进程数量

请定义“级联负载”,因为它没有普遍的含义。

你最可能遇到的问题是运行了太多 Apache 进程。

对于这样的负载,请确保你使用的是 prefork mpm,并确保你将自己限制在适当数量的进程中(不少于每个 CPU 一个,不超过两个)。

代码例子

1. 使用 Spawning

from spawning import SpawnProcess
from werkzeug.wrappers import Request, Response

@Request.application
def application(request):
    # Process the request here

    # Spawn a new process to handle the expensive operation
    process = SpawnProcess('expensive_operation.py', args=(request.args,))

    # Wait for the process to finish and get the result
    result = process.join()

    # Return the result to the client
    return Response(result, mimetype='text/plain')

2. 使用队列系统和 AJAX 通知方法

import queue
import threading
import time

# Create a queue to store the requests
queue = queue.Queue()

# Create a worker thread to handle the requests
def worker():
    while True:
        # Get a request from the queue
        request = queue.get()

        # Process the request
        result = expensive_operation(request)

        # Send a notification to the client
        send_notification(request.id, result)

# Start the worker thread
worker_thread = threading.Thread(target=worker)
worker_thread.start()

# Handle the client requests
@app.route('/expensive-operation', methods=['POST'])
def expensive_operation_handler():
    # Get the request parameters
    request_data = request.get_json()

    # Add the request to the queue
    queue.put(request_data)

    # Return a response to the client
    return jsonify({
        'id': request_data['id'],
        'status': 'processing'
    })

# Handle the client requests for the status of the operation
@app.route('/expensive-operation-status', methods=['GET'])
def expensive_operation_status_handler():
    # Get the request parameters
    request_id = request.args.get('id')

    # Check the status of the request
    status = get_status(request_id)

    # Return a response to the client
    return jsonify({
        'id': request_id,
        'status': status
    })