Gunicorn是用于Python应用程序的通用WSGI服务器,但是大多数在Docker容器中使用的gunicorn配置都是错误的。在容器中运行gunicorn与在虚拟机或物理服务器上运行不同,并且还需要考虑Linux环境之间的差异。

因此,为了保持你的Gunicorn设置正确和高效,在本文中,我将介绍:

  • 防止由于heartbeats心跳而导致缓慢。
  • 正确配置worker数。
  • 正确输出日志到标准输出。

为什么Gunicorn“有时会挂半分钟”

Gunicorn的master进程启动一个或多个worker进程,如果worker进程死亡,则master负责将其重新启动。为了确保worker一直工作,Gunicorn有一个心跳系统-通过使用文件系统上的文件来工作。因此,Gunicorn建议将此文件存储在文件系统的仅内存部分中。

正如Gunicorn常见问题解答所述,心跳文件的默认目录位于/tmp,在某些Linux发行版中,该目录通过tmpfs文件系统存储在内存中。但是,Docker容器默认情况下不启用tmpfs的/tmp:

$ docker run --rm -it ubuntu:18.04 df
Filesystem       1K-blocks     Used Available Use% Mounted on
overlay           31263648 25656756   3995732  87% /
tmpfs                65536        0     65536   0% /dev
tmpfs              4026608        0   4026608   0% /sys/fs/cgroup
/dev/mapper/root  31263648 25656756   3995732  87% /etc/hosts
shm                  65536        0     65536   0% /dev/shm

如您所见,/tmp正在使用标准的Docker overlay 文件系统:它由计算机正在使用的普通块设备或硬盘驱动器提供支持。

这可能会导致性能问题-引用常见问题解答:“在AWS中,EBS根实例卷有时可能会挂起半分钟,在此期间Gunicorn worker可能会完全阻塞。”

您不会想让您的Gunicorn worker阻塞30秒,那么您应该怎么办?一种选择是使用Docker的卷支持将一个内存中的tmpfs或ramfs文件系统挂载到/tmp上。这将起作用,但并非在所有地方都起作用:并非所有运行Docker容器的环境都支持任意卷。

一个更通用的解决方案是告诉Gunicorn将其临时文件存储在其他位置。特别是,如果您在上面看,您会看到/dev/shm使用了shm文件系统-共享内存和内存文件系统。

因此,您所需要做的就是告诉Gunicorn使用/dev/shm而不是/tmp。 (当19.9.0之后的版本发布时,这至少会在Gunicorn FAQ中有所记录,但是您仍然必须记住要这样做。)

在命令行上的操作方法如下:

$ gunicorn --worker-tmp-dir /dev/shm ...

正确配置worker数

如果您是直接在物理机或虚拟机上运行Gunicorn,则通常希望单个Gunicorn实例利用所有可用的CPU。由于Python不能很好地使用多个CPU,因此通常您需要启动多个worker进程,每个worker进程都不同,以便利用所有CPU。

但是,在容器中运行时,通常会处在可以通过运行更多容器来扩大规模的分布式环境中。Heroku,AWS Elastic Beanstalk和Kubernetes:它们全部都隐藏了硬件,并希望通过拆分多个容器来利用多个CPU。

因此,可能只需要一个worker就可以启动Gunicorn。但是,这些系统中的许多系统还包括心跳机制,该机制通过向服务器发送周期性的查询来检查服务器是否处于活动状态。

如果您只有一个worker,并且在处理慢查询的时候遇到了问题,则心跳查询将超时。 届时,负载平衡器将确定容器已卡住,并停止向其发送查询。在某些环境中,它也可能会重新启动。(感谢杰里米·瑟古德(Jeremy Thurgood)向我介绍了这个问题。)

解决方案:启动至少两个worker进程,并可能还使用gthread工作线程后端启动多个线程。 这样,每个worker进程都可以处理多个查询,只要花一些时间等待(例如,返回数据库查询)即可。这样可确保容器获得最大的CPU利用率(不缩放时),并减少了无法响应心跳查询的机会。

$ gunicorn --workers=2 --threads=4 --worker-class=gthread ...
为了在使用Gunicorn时提高性能,我们必须牢记3种并发方式。
第一种并发方式(worker进程,又名UNIX进程)

每个工作程序都是一个加载Python应用程序的UNIX进程。工作人员之间没有共享内存。

建议的worker数量为:(2*CPU)+1。

对于双核(2 CPU)计算机,建议workers值为5 。

gunicorn --workers=5 main:app
第二种并发方式(worker进程 + 线程)

Gunicorn还允许每个worker都有多个线程。在这种情况下,每个worker进程会加载一次Python应用程序,并且同一worker进程产生的每个线程都共享相同的内存空间。
要将线程与Gunicorn一起使用,请使用该threads设置。每次使用时threads,worker类都设置为gthread:

gunicorn --workers=5 --threads=2 main:app

在使用worker进程+线程时,建议的最大并发请求数仍为(2*CPU)+1。
因此,如果我们使用的是四核(4 CPU)计算机,并且要使用worker进程和线程的混合,则可以使用3个worker进程和3个线程,以获取9个最大的并发请求。

gunicorn --workers=3 --threads=3 main:app
第三种并发方式(“伪线程”)

有一些Python库,例如gevent和Asyncio,它们通过使用用协程实现的“伪线程”在Python中启用并发。
Gunicorn通过设置它们的相应工作程序类(worker-class),允许使用这些异步Python库。
在这里,这些设置适用于我们要使用gevent以下命令运行的单核计算机:

gunicorn --worker-class=gevent --worker-connections=1000 --workers=3 main:app

worker-connections是gevent worker类的特定设置。

workers=(2CPU)+1仍然建议使用,因为我们只有1个核心,我们将使用3个工作进程。
在这种情况下,最大并发请求数为3000(3个worker进程
每个worker进程1000个连接)

正确输出日志

容器调度程序通常期望日志在stdout/stderr上输出,因此您应该配置Gunicorn来做到这一点:

$ gunicorn --log-file=- ...

如果您的nginx在Gunicorn前面,而想使用它的日志,则可以不修改。

nginx并非总是必要的

说到nginx,您并不总是需要在Gunicorn前面使用nginx或其他代理。许多容器部署系统已经内置了HTTP负载平衡器/反向代理,在这种情况下,无论如何Gunicorn都不会直接暴露给HTTP客户端。

在容器中运行应用程序的特殊性

在容器中运行应用程序与在计算机或VM中运行有一点不同:您具有不同级别的控制(通常无法从正在运行的容器内部安装文件系统),不同的缩放模型以及通常不同的网络配置。

不要只是复制您的旧配置,请确保对其进行适当的自定义以在容器中运行。

ref: https://pythonspeed.com/articles/gunicorn-in-docker/
ref: https://medium.com/building-the-system/gunicorn-3-means-of-concurrency-efbb547674b7

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐