使用Python和Eventlet实现多核并行处理

7
我有一个Python Web应用程序,其中客户端(Ember.js)通过WebSocket与服务器通信(我使用Flask-SocketIO)。除了WebSocket服务器外,后端还执行两个值得一提的操作:
  • 进行一些图像转换(使用graphicsmagick
  • 对来自客户端的图像进行OCR处理(使用tesseract

当客户端提交图像时,它的实体将在数据库中创建,并将其ID放入图像转换队列中。工作线程会抓取并进行图像转换。之后,工作线程将其放入OCR队列中,由OCR队列工作线程处理。

到目前为止,WS请求在单独的线程中同步处理(Flask-SocketIO使用Eventlet进行处理),而重型计算操作则以异步方式发生(也在单独的线程中进行处理)。

现在问题来了:整个应用程序运行在树莓派3上。如果我不利用它的4个核心,我只有一个时钟频率为1.2 GHz的ARMv8核心。这对OCR来说非常弱。因此,我决定找出如何使用Python的多个核心。虽然我读到了关于GIL的问题,但我发现了multiprocessing,其中写道:多进程包同时提供本地和远程并发,通过使用子进程而不是线程有效地绕过全局解释器锁。正是我想要的。所以我立即取代了

from threading import Thread
thread = Thread(target=heavy_computational_worker_thread)
thread.start()

通过

from multiprocessing import Process
process = Process(target=heavy_computational_worker_thread)
process.start()

队列需要由多个核心处理,因此我不得不进行更改。
from queue import Queue
queue = multiprocessing.Queue()

import multiprocessing
queue = multiprocessing.Queue()

同时存在的问题:队列和线程库被Eventlet 猴子补丁,如果我停止使用经过猴子补丁的Thread和Queue并改用multiprocessing中的版本,则由Eventlet启动的请求线程在访问队列时会永久阻塞。
现在我的问题是: 有没有办法让这个应用程序在单独的核心上执行OCR和图像转换? 如果可能的话,我想继续使用WebSocket和Eventlet。我拥有的优势是进程之间唯一的通信接口是队列。
我已经考虑过的想法: - 不使用Python实现的队列,而是使用I/O。例如,不同的子进程将访问专用Redis - 更进一步:将每个队列工作者作为单独的Python进程启动(例如python3 wsserver | python3 ocrqueue | python3 imgconvqueue)。然后我必须自己确保对队列和数据库的访问是非阻塞的。
最好的方法是保持单个进程,并使其与multiprocessing一起工作。
非常感谢您的帮助。

我不熟悉Evenlet库,所以我的回答可能不适用。在“主”程序中使用多线程,在该主应用程序的每个“进程”中使用“子进程”调用子程序(Python程序调用Python程序,即使这似乎很奇怪)。仅在这些子进程中使用“Eventlet”库(或任何非进程安全库)。不要在主程序中使用它们。您将无法使用“队列”,但可以通过文件传递数据(例如:程序A编写图像文件然后终止,启动程序B并读取此文件)。 - Sci Prog
1个回答

5

Eventlet目前与多进程包不兼容。这项工作有一个开放的问题:https://github.com/eventlet/eventlet/issues/210

我认为在您的情况下,可行的替代方案是使用Celery来管理您的队列。 Celery将启动一组工作进程,等待主进程通过消息队列(RabbitMQ和Redis都受支持)提供的任务。

Celery工作者不需要使用eventlet,只有主服务器需要使用它,因此这使它们可以自由地执行任何需要做的事情,而不受eventlet所施加的限制。

如果您有兴趣探索这种方法,我有一个完整的示例使用它:https://github.com/miguelgrinberg/flack


谢谢,这个很好用。我现在唯一面临的问题是,我已经将socketio实例放在了全局变量中,当然不能在多个进程之间共享。但是可以通过使用专用存储来解决这个问题,或者将变量传递给任务函数。 - Schnodderbalken
1
请查看我在答案中提到的项目。如果您需要从工作进程发出信号,则在每个进程中创建一个socketio实例。不同之处在于,Celery工作进程中的socketio实例将不会与Flask应用程序实例关联,因此它们只能发出信号。您无需在进程之间共享任何内容,所有通信都通过消息队列完成。 - Miguel Grinberg
我按照您在https://github.com/miguelgrinberg/Flask-SocketIO/blob/master/docs/index.rst文档中记录的说明进行操作 - 使用消息队列='redis://'作为参数的构造函数解决了问题 :) 这样,我仍然只有一个socketio实例,从而允许我从工作进程发出发射。 - Schnodderbalken
@MiguelGrinberg 这可能是一个简单的问题,但它一直困扰着我。当我们在另一个进程中创建一个Flask SocketIO服务器实例而不传递应用程序对象时,这个socketio如何“知道”要发射到哪里?这是通过消息队列完成的吗?具有应用程序上下文的socketio只需发射到连接的客户端。外部进程中的socketio没有任何连接的客户端(至少在我看来是这样的)。 - jacob
1
@jacob 是的,服务器和外部进程通过在消息队列中传递消息来协调发射操作。即使您从外部进程发射,实际的发射也发生在服务器进程中,因为这是拥有与客户端的套接字连接的进程。 - Miguel Grinberg

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接