Python 使用 Gunicorn + Nginx 在 Flask 请求中进行多处理
Python multiprocessing within Flask request with Gunicorn + Nginx
我想构建一个能够处理的服务:
- 请求量低
- 每个请求的高计算成本
- 但是高计算成本可以并行化。
我对预分叉服务器的理解是会发生如下情况:
- 服务器启动
- Gunicorn 创建多个 OS 进程,也称为 worker,准备接受请求
- 请求进来。Nginx 转发给 Gunicorn。 Gunicorn 发送给其中一名工人。
我想了解的是,如果在我的 Flask 代码中处理请求时会发生什么:
from multiprocessing import pool as ProcessPool
with ProcessPool(4) as pool:
pool.map(some_expensive_function, some_data)
特别是:
- 是否会启动其他 OS 进程?加速会达到我的预期吗? (也就是说,如果我 运行 Flask 生产上下文之外的 ProcessPool 类似?)如果 Gunicorn 创建了 4 个网络工作者,那么现在是否会有 7 个 OS 进程 运行? 9?是否存在制造过多的风险? Gunicorn 是假设每个 worker 不会分叉还是不关心?
- 如果 web-worker 在启动 ProcessPool 后死亡或被杀死,它会被上下文管理器正确关闭吗?
- 这样做明智吗?有哪些替代方案?
好问题!对于 Python 多处理,可以使用 3 种“启动方法”,它们都对您的问题有影响。 As the docs explain,他们是:
'spawn'
: The parent process starts a fresh python interpreter process. The child process will only inherit those resources necessary to run the process object’s run()
method. In particular, unnecessary file descriptors and handles from the parent process will not be inherited. Starting a process using this method is rather slow compared to using fork or forkserver. Available on Unix and Windows. The default on Windows and macOS.
'fork'
: The parent process uses os.fork()
to fork the Python interpreter. The child process, when it begins, is effectively identical to the parent process. All resources of the parent are inherited by the child process. Note that safely forking a multithreaded process is problematic. Available on Unix only. The default on Unix.
'forkserver'
When the program starts and selects the forkserver start method, a server process is started. From then on, whenever a new process is needed, the parent process connects to the server and requests that it fork a new process. The fork server process is single threaded so it is safe for it to use os.fork()
. No unnecessary resources are inherited. Available on Unix platforms which support passing file descriptors over Unix pipes.
至于Gunicorn的pre-fork模型,你已经解释的很好了。每个工人都在自己的进程中 运行ning。由于您尝试在 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
对象创建一个进程池,就像您所做的那样。此代码必须位于工作进程中仅 called/used 的 function/module 内。如果在服务器进程中使用它可能会导致问题。
- Will additional OS processes be started? Will the speedup be what I expect? (I.e., similar to if I ran the
ProcessPool
outside of a Flask production context?)
这里有很多问题。将启动其他 OS 个进程。加速可能会有很大差异,并且取决于许多因素,例如:
- 还有多少其他进程正在 运行ning? Gunicorn 运行ning 有多少个工作处理器?
- 服务器负载是否过重?
- 处理器有多少个核心?
- 这项工作的并行化程度如何?
some_expensive_function(data_1)
是否必须等待 some_expensive_function(data_2)
才能开始工作?
要弄清楚使用多处理是否更快,以及它会快多少,您必须对其进行测试。在此之前,您可以做的最好的事情是根据上面列出的因素进行粗略估计。
- (cont.) If Gunicorn created 4 web workers, will there now be 7 OS processes running? 9? Is there a risk of making too many? Does Gunicorn assume that each worker will not fork or does it not care?
如果有 4 个 Gunicorn 工作进程,并且每个进程都在完成一个使用 4 个进程的多处理的请求,那么将有 1 个 Gunicorn 父进程 + 4 个工作进程 + 4 * 4 个工作子进程 = 21 个进程,更不用说 Nginx 使用的进程了。
Gunicorn 建议您创建 (2 * num_cores) + 1
个工作进程,但在您的情况下,您可能希望减少它,也许可以将其除以 4,以说明您的工作进程本身在使用多核时工作得最好.要找到最有效的配置,您必须对各种配置进行基准测试以找出最适合您的配置。
- If a web-worker dies or is killed after starting the ProcessPool, will it be closed by the context manager properly?
这取决于工人是如何死亡的。如果它通过 SIGKILL 被杀死,或者遇到分段错误,或者其他一些严重错误,那么它会在没有到达 运行 任何最终代码的情况下突然死亡。上下文管理器只能在 try-finally 块能够执行 'finally' 块的情况下完成它的工作。有关更多信息,请查看此答案:
- Is this a sane thing to do? What are the alternatives?
它本身并不疯狂,但它不是我通常推荐的那种方法。一种替代方法是使用自己的服务器实现 some_expensive_function
。您的 Gunicorn worker 可以使用 IPC 或网络通信将工作发送到 some_expensive_function
服务器进程,它会处理将此工作分配给子进程。像这样的设计的一个优点是,如果性能需要,some_expensive_function
服务器进程可以很容易地移动到另一台计算机上的 运行。
这类似于数据库通常 运行 作为它们自己的服务器进程,并且可以位于同一台计算机上或单独的计算机上(可能在只读查询的负载平衡器后面,或者分片配置)取决于必须满足的性能要求。
如果您决定走那条路,您可能会发现 Python 包 Celery 对于分发 Gunicorn 工作人员的工作很有用。
如果你想这样做,你可能应该 运行ning Gunicorn 和 preload_app=True
。
我想构建一个能够处理的服务:
- 请求量低
- 每个请求的高计算成本
- 但是高计算成本可以并行化。
我对预分叉服务器的理解是会发生如下情况:
- 服务器启动
- Gunicorn 创建多个 OS 进程,也称为 worker,准备接受请求
- 请求进来。Nginx 转发给 Gunicorn。 Gunicorn 发送给其中一名工人。
我想了解的是,如果在我的 Flask 代码中处理请求时会发生什么:
from multiprocessing import pool as ProcessPool
with ProcessPool(4) as pool:
pool.map(some_expensive_function, some_data)
特别是:
- 是否会启动其他 OS 进程?加速会达到我的预期吗? (也就是说,如果我 运行 Flask 生产上下文之外的 ProcessPool 类似?)如果 Gunicorn 创建了 4 个网络工作者,那么现在是否会有 7 个 OS 进程 运行? 9?是否存在制造过多的风险? Gunicorn 是假设每个 worker 不会分叉还是不关心?
- 如果 web-worker 在启动 ProcessPool 后死亡或被杀死,它会被上下文管理器正确关闭吗?
- 这样做明智吗?有哪些替代方案?
好问题!对于 Python 多处理,可以使用 3 种“启动方法”,它们都对您的问题有影响。 As the docs explain,他们是:
'spawn'
: The parent process starts a fresh python interpreter process. The child process will only inherit those resources necessary to run the process object’srun()
method. In particular, unnecessary file descriptors and handles from the parent process will not be inherited. Starting a process using this method is rather slow compared to using fork or forkserver. Available on Unix and Windows. The default on Windows and macOS.'fork'
: The parent process usesos.fork()
to fork the Python interpreter. The child process, when it begins, is effectively identical to the parent process. All resources of the parent are inherited by the child process. Note that safely forking a multithreaded process is problematic. Available on Unix only. The default on Unix.'forkserver'
When the program starts and selects the forkserver start method, a server process is started. From then on, whenever a new process is needed, the parent process connects to the server and requests that it fork a new process. The fork server process is single threaded so it is safe for it to useos.fork()
. No unnecessary resources are inherited. Available on Unix platforms which support passing file descriptors over Unix pipes.
至于Gunicorn的pre-fork模型,你已经解释的很好了。每个工人都在自己的进程中 运行ning。由于您尝试在 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
对象创建一个进程池,就像您所做的那样。此代码必须位于工作进程中仅 called/used 的 function/module 内。如果在服务器进程中使用它可能会导致问题。
- Will additional OS processes be started? Will the speedup be what I expect? (I.e., similar to if I ran the
ProcessPool
outside of a Flask production context?)
这里有很多问题。将启动其他 OS 个进程。加速可能会有很大差异,并且取决于许多因素,例如:
- 还有多少其他进程正在 运行ning? Gunicorn 运行ning 有多少个工作处理器?
- 服务器负载是否过重?
- 处理器有多少个核心?
- 这项工作的并行化程度如何?
some_expensive_function(data_1)
是否必须等待some_expensive_function(data_2)
才能开始工作?
要弄清楚使用多处理是否更快,以及它会快多少,您必须对其进行测试。在此之前,您可以做的最好的事情是根据上面列出的因素进行粗略估计。
- (cont.) If Gunicorn created 4 web workers, will there now be 7 OS processes running? 9? Is there a risk of making too many? Does Gunicorn assume that each worker will not fork or does it not care?
如果有 4 个 Gunicorn 工作进程,并且每个进程都在完成一个使用 4 个进程的多处理的请求,那么将有 1 个 Gunicorn 父进程 + 4 个工作进程 + 4 * 4 个工作子进程 = 21 个进程,更不用说 Nginx 使用的进程了。
Gunicorn 建议您创建 (2 * num_cores) + 1
个工作进程,但在您的情况下,您可能希望减少它,也许可以将其除以 4,以说明您的工作进程本身在使用多核时工作得最好.要找到最有效的配置,您必须对各种配置进行基准测试以找出最适合您的配置。
- If a web-worker dies or is killed after starting the ProcessPool, will it be closed by the context manager properly?
这取决于工人是如何死亡的。如果它通过 SIGKILL 被杀死,或者遇到分段错误,或者其他一些严重错误,那么它会在没有到达 运行 任何最终代码的情况下突然死亡。上下文管理器只能在 try-finally 块能够执行 'finally' 块的情况下完成它的工作。有关更多信息,请查看此答案:
- Is this a sane thing to do? What are the alternatives?
它本身并不疯狂,但它不是我通常推荐的那种方法。一种替代方法是使用自己的服务器实现 some_expensive_function
。您的 Gunicorn worker 可以使用 IPC 或网络通信将工作发送到 some_expensive_function
服务器进程,它会处理将此工作分配给子进程。像这样的设计的一个优点是,如果性能需要,some_expensive_function
服务器进程可以很容易地移动到另一台计算机上的 运行。
这类似于数据库通常 运行 作为它们自己的服务器进程,并且可以位于同一台计算机上或单独的计算机上(可能在只读查询的负载平衡器后面,或者分片配置)取决于必须满足的性能要求。
如果您决定走那条路,您可能会发现 Python 包 Celery 对于分发 Gunicorn 工作人员的工作很有用。
如果你想这样做,你可能应该 运行ning Gunicorn 和 preload_app=True
。