我想构建一个能够处理以下问题的服务:
- 请求量少
- 每个请求的计算成本很高
- 但高计算成本可以并行化。
我对预分叉服务器的理解是会发生类似以下的事情:
- 服务器启动
- Gunicorn 创建多个操作系统进程,也称为 worker ,准备接受请求
- 请求进来了。Nginx转发给Gunicorn。独角兽发送给其中一名工人。
我想了解的是,如果在我的 Flask 代码中,在处理请求时,我有以下内容,会发生什么:
from multiprocessing import pool as ProcessPool
with ProcessPool(4) as pool:
pool.map(some_expensive_function, some_data)
特别:
- 是否会启动其他操作系统进程?加速会是我所期望的吗?(即,类似于我在 Flask 生产上下文之外运行 ProcessPool?如果 Gunicorn 创建了 4 个 Web Worker ,现在还会有 7 个操作系统进程在运行吗?9?有没有制造太多的风险?独角兽是假设每个工人都不会分叉还是不在乎?
- 如果 Web worker 在启动 ProcessPool 后死亡或被杀死,上下文管理器会正确关闭它吗?
- 这是理智的事情吗?有哪些替代方案?
好问题!使用 Python 多处理,可以使用 3 种"启动方法",它们都对您的问题有影响。正如文档所解释的那样,它们是:
'spawn'
:父进程启动一个新的python解释器进程。子进程将仅继承运行进程对象的run()
方法所需的资源。特别是,不会继承父进程中不必要的文件描述符和句柄。与使用 fork 或 forkserver 相比,使用此方法启动进程相当慢。在 Unix 和 Windows 上可用。Windows 和 macOS 上的默认值。'fork'
:父进程使用os.fork()
来分叉 Python 解释器。子进程开始时实际上与父进程相同。父进程的所有资源都由子进程继承。请注意,安全分叉多线程进程是有问题的。仅在 Unix 上可用。Unix 上的默认值。'forkserver'
当程序启动并选择 forkserver 启动方法时,将启动服务器进程。从那时起,每当需要新进程时,父进程都会连接到服务器并请求它派生一个新进程。fork 服务器进程是单线程的,因此使用os.fork()
是安全的。不会继承不必要的资源。在支持通过 Unix 管道传递文件描述符的 Unix 平台上可用。
至于Gunicorn的pre-fork模型,你已经解释得很好了。每个工作线程都在自己的进程中运行。由于您尝试在worker中使用多处理,而不是与Gunicorn一起使用,这应该是可行的,但仍然有点容易出错。
import multiprocessing
mp = multiprocessing.get_context('spawn')
这段代码为我们提供了mp
对象,该对象与多处理模块具有相同的 API,但具有设置的启动方法。在上述代码的情况下,它设置为'spawn'
.这是在 Gunicorn worker 中使用多处理的最安全的途径,因为它与创建它的流程最隔离,并且不太可能遇到意外共享资源的问题。
with mp.Pool(processes=4) as pool:
pool.map(some_expensive_function, some_data)
然后,我们使用mp
对象创建进程池,就像您所做的那样。此代码必须位于仅在工作进程中调用/使用的函数/模块内。如果在服务器进程中使用它,可能会导致问题。
- 是否会启动其他操作系统进程?加速会是我所期望的吗?(即,类似于我在 Flask 生产上下文之外运行
ProcessPool
?
这里挤满了不少问题。将启动其他操作系统进程。加速可能会有很大差异,并且取决于许多因素,例如:
- 还有多少其他进程正在运行?独角兽运行多少个工作处理器?
- 服务器是否负载过重?
- 处理器有多少个内核?
- 工作的并行化程度如何?
some_expensive_function(data_1)
是否必须等待some_expensive_function(data_2)
才能完成工作?
要弄清楚使用多处理是否更快,以及它会快多少,你必须对其进行测试。在此之前,您能做的最好的事情是根据上面列出的因素进行粗略估计。
- (续) 如果 Gunicorn 创建了 4 个 Web Worker ,现在会不会有 7 个操作系统进程在运行?9?有没有制造太多的风险?独角兽是假设每个工人都不会分叉还是不在乎?
如果有 4 个 Gunicorn 工作进程,并且每个进程都在满足使用 4 个进程的多处理的请求,那么将有 1 个 Gunicorn 父进程 + 4 个工作进程 + 4 * 4 个工作子进程 = 21 个进程,更不用说 Nginx 正在使用的进程了。
Gunicorn 建议您创建(2 * num_cores) + 1
个工作线程,但在您的情况下,您可能希望减少它,也许通过将其除以 4,以说明您的工作进程本身在使用多个内核时效果最佳。要找到最有效的配置,您必须对各种配置进行基准测试,以找出最适合您的配置。
- 如果一个 Web 工作者在启动 ProcessPool 后死亡或被杀死,上下文管理器会正确关闭它吗?
这取决于工人是如何死亡的。如果它通过SIGKILL被杀死,或者遇到分段错误,或者其他一些严重错误,那么它将突然死亡,而不会运行任何最终代码。上下文管理器只能在 try-finally 块能够执行"final"块的情况下完成其工作。有关此的更多信息,请查看此答案:"最终"总是在Python中执行吗?
这是
- 理智的事情吗?有哪些替代方案?
这本身并不疯狂,但这不是我通常推荐的那种方法。一种替代方法是使用自己的服务器实现some_expensive_function
。您的 Gunicorn 工作人员可以使用 IPC 或网络通信将工作发送到some_expensive_function
服务器进程,它将处理在子进程之间划分此工作。这种设计的一个优点是,如果性能需要,可以轻松地将some_expensive_function
服务器进程移动到另一台计算机上运行。
它类似于数据库通常作为自己的服务器进程运行的方式,可以位于同一台计算机上,也可以位于单独的计算机上(可能位于用于只读查询的负载均衡器或分片配置后面),具体取决于必须满足的性能要求。
如果你决定走这条路,你可能会发现Python包Celery对于分发Gunicorn工人的工作很有用。
如果你想这样做,你可能应该用preload_app=True
运行Gunicorn。