TL;DR,对于受 CPU 限制的应用程序,增加工作人员和/或内核。对于 I/O 有界应用程序,请使用“伪线程”。
Gunicorn是一个 Python WSGI HTTP 服务器,它通常位于反向代理(例如,Nginx)或负载均衡器(例如,AWS ELB)和 Web 应用程序(例如 Django 或 Flask)之间。
Gunicorn 架构
Gunicorn 实现了一个 UNIX 预分叉网络服务器。
太好了,什么意思?
- Gunicorn 启动一个单独的主进程,它被分叉,产生的子进程是工作进程。
- 主进程的作用是确保工人数量与设置中定义的数量相同。因此,如果任何一个工人死亡,主进程通过再次分叉自己来启动另一个进程。
- worker 的作用是处理 HTTP 请求。
- 的预在预分叉单元,其主处理过程的任何HTTP请求之前创建的工人。
- 操作系统内核处理工作进程之间的负载平衡。
为了在使用 Gunicorn 时提高性能,我们必须牢记 3 种并发方式。
第一种并发方式(工人,又名 UNIX 进程)
每个 worker 都是一个加载 Python 应用程序的 UNIX 进程。工人之间没有共享内存。
对于双核(2 个 CPU)机器,建议workers
值为5 。
gunicorn --workers=5 main:app
具有默认工作类(同步)的 Gunicorn。请注意图像中的第 4 行:“Using worker:sync”。
第二种并发方式(线程)
Gunicorn 还允许每个 worker 拥有多个线程。在这种情况下,Python 应用程序为每个 worker 加载一次,并且由同一个 worker 产生的每个线程共享相同的内存空间。
要在 Gunicorn 中使用线程,我们使用threads
设置。每次我们使用时threads
,worker 类都设置为gthread
:
gunicorn --workers=5 --threads=2 main:app
带有线程设置的 Gunicorn,它使用 gthread 工作类。请注意图像中的第 4 行:“Using worker:threads”。
上一条命令与以下命令相同:
gunicorn --workers=5 --threads=2 --worker-class=gthread main:app
workers * threads
在我们的例子中,最大并发请求数为10。
使用工作线程和线程时建议的最大并发请求数仍为(2*CPU)+1
.
因此,如果我们使用四核(4 个 CPU)机器,并且想要混合使用 worker 和线程,我们可以使用 3 个 worker 和 3 个线程,以获得 9 个最大并发请求。
gunicorn --workers=3 --threads=3 main:app
第三种并发方式(“伪线程”)
有一些 Python 库,例如gevent和Asyncio,它们通过使用协程实现的“伪线程”在 Python 中启用并发。
Gunicorn 允许通过设置它们相应的工作类来使用这些异步 Python 库。
这里的设置适用于我们要使用的单核机器gevent
:
gunicorn --worker-class=gevent --worker-connections=1000 --workers=3 main:app
worker-connections 是 gevent worker 类的特定设置。
(2*CPU)+1
仍然是建议的,workers
因为我们只有 1 个核心,我们将使用 3 个工人。
在这种情况下,最大并发请求数为 3000(3 个工作人员 * 每个工作人员 1000 个连接)
并发与并行
- 并发是指同时执行 2 个或更多任务,这可能意味着只有 1 个任务正在处理而其他任务暂停。
- 并行是指同时执行 2 个或更多任务。
在 Python 中,线程和伪线程是并发的手段,但不是并行的;而工人是并发和并行的手段。
这些都是很好的理论,但是我应该在我的程序中使用什么?
实际用例
通过调整 Gunicorn 设置,我们希望优化应用程序性能。
- 如果应用程序受I/O 限制,则最佳性能通常来自使用“伪线程”(gevent 或 asyncio)。正如我们所见,Gunicorn 通过设置适当的工作类并调整
workers
to的值来支持这种编程范式(2*CPU)+1
。 - 如果应用程序受CPU 限制,则应用程序处理多少并发请求并不重要。唯一重要的是并行请求的数量。由于Python 的 GIL,线程和“伪线程”不能并行运行。实现并行性的唯一方法是增加到
workers
建议值(2*CPU)+1
,理解并行请求的最大数量是内核数。 - 如果有一个关于应用程序的关注内存占用,使用
threads
和其对应的g线程工人阶级有利于workers
产生更好的性能,因为应用程序加载每个工人和工人股运行一些内存每个线程一次,这涉及到一些费用额外的 CPU 消耗。 - 如果你不知道你在做什么,从最简单的配置开始,也就是只设置
workers
为(2*CPU)+1
,不要担心threads
。从那时起,所有的基准测试都是反复试验。如果瓶颈是内存,就开始引入线程。如果瓶颈是 I/O,请考虑不同的 Python 编程范式。如果瓶颈是CPU,考虑使用更多的内核并调整workers
值。
构建系统
我们软件开发人员通常认为每个性能瓶颈都可以通过优化应用程序代码来解决,但事实并非总是如此。
有时,调整 HTTP 服务器的设置、使用更多资源或重新构建应用程序以使用不同的编程范式是我们提高整体应用程序性能所需的解决方案。
在这种情况下,构建系统意味着了解我们可用于部署高性能应用程序的计算资源类型(进程、线程和“伪线程”)。
通过使用正确的资源理解、构建和实施正确的技术解决方案,我们可以避免陷入试图通过优化应用程序代码来提高性能的陷阱。