Flask 请求中的 Python 多处理与 Gunicorn + Nginx



我想构建一个能够处理以下问题的服务:

  • 请求量少
  • 每个请求的计算成本很高
  • 但高计算成本可以并行化。

我对预分叉服务器的理解是会发生类似以下的事情:

  1. 服务器启动
  2. Gunicorn 创建多个操作系统进程,也称为 worker ,准备接受请求
  3. 请求进来了。Nginx转发给Gunicorn。独角兽发送给其中一名工人。

我想了解的是,如果在我的 Flask 代码中,在处理请求时,我有以下内容,会发生什么:

from multiprocessing import pool as ProcessPool
with ProcessPool(4) as pool:
pool.map(some_expensive_function, some_data)

特别:

  1. 是否会启动其他操作系统进程?加速会是我所期望的吗?(即,类似于我在 Flask 生产上下文之外运行 ProcessPool?如果 Gunicorn 创建了 4 个 Web Worker ,现在还会有 7 个操作系统进程在运行吗?9?有没有制造太多的风险?独角兽是假设每个工人都不会分叉还是不在乎?
  2. 如果 Web worker 在启动 ProcessPool 后死亡或被杀死,上下文管理器会正确关闭它吗?
  3. 这是理智的事情吗?有哪些替代方案?

好问题!使用 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对象创建进程池,就像您所做的那样。此代码必须位于仅在工作进程中调用/使用的函数/模块内。如果在服务器进程中使用它,可能会导致问题。

  1. 是否会启动其他操作系统进程?加速会是我所期望的吗?(即,类似于我在 Flask 生产上下文之外运行ProcessPool

这里挤满了不少问题。将启动其他操作系统进程。加速可能会有很大差异,并且取决于许多因素,例如:

  • 还有多少其他进程正在运行?独角兽运行多少个工作处理器?
  • 服务器是否负载过重?
  • 处理器有多少个内核?
  • 工作的并行化程度如何?some_expensive_function(data_1)是否必须等待some_expensive_function(data_2)才能完成工作?

要弄清楚使用多处理是否更快,以及它会快多少,你必须对其进行测试。在此之前,您能做的最好的事情是根据上面列出的因素进行粗略估计。

  1. (续) 如果 Gunicorn 创建了 4 个 Web Worker ,现在会不会有 7 个操作系统进程在运行?9?有没有制造太多的风险?独角兽是假设每个工人都不会分叉还是不在乎?

如果有 4 个 Gunicorn 工作进程,并且每个进程都在满足使用 4 个进程的多处理的请求,那么将有 1 个 Gunicorn 父进程 + 4 个工作进程 + 4 * 4 个工作子进程 = 21 个进程,更不用说 Nginx 正在使用的进程了。

Gunicorn 建议您创建(2 * num_cores) + 1个工作线程,但在您的情况下,您可能希望减少它,也许通过将其除以 4,以说明您的工作进程本身在使用多个内核时效果最佳。要找到最有效的配置,您必须对各种配置进行基准测试,以找出最适合您的配置。

  1. 如果一个 Web 工作者在启动 ProcessPool 后死亡或被杀死,上下文管理器会正确关闭它吗?

这取决于工人是如何死亡的。如果它通过SIGKILL被杀死,或者遇到分段错误,或者其他一些严重错误,那么它将突然死亡,而不会运行任何最终代码。上下文管理器只能在 try-finally 块能够执行"final"块的情况下完成其工作。有关此的更多信息,请查看此答案:"最终"总是在Python中执行吗?

    这是
  1. 理智的事情吗?有哪些替代方案?

这本身并不疯狂,但这不是我通常推荐的那种方法。一种替代方法是使用自己的服务器实现some_expensive_function。您的 Gunicorn 工作人员可以使用 IPC 或网络通信将工作发送到some_expensive_function服务器进程,它将处理在子进程之间划分此工作。这种设计的一个优点是,如果性能需要,可以轻松地将some_expensive_function服务器进程移动到另一台计算机上运行。

它类似于数据库通常作为自己的服务器进程运行的方式,可以位于同一台计算机上,也可以位于单独的计算机上(可能位于用于只读查询的负载均衡器或分片配置后面),具体取决于必须满足的性能要求。

如果你决定走这条路,你可能会发现Python包Celery对于分发Gunicorn工人的工作很有用。


如果你想这样做,你可能应该用preload_app=True运行Gunicorn。

最新更新