异步Python的速度并不快

2020-06-12 17:23:37

在现实基准下,异步Python比同步Python慢。更令人担忧的是,异步框架在负载下会有点不稳定。

大多数人都知道异步Python具有更高级别的并发性。对于像服务动态网站或WebAPI这样的常见任务来说,这意味着更高的性能,这在一定程度上是有意义的。

在现实条件下(见下文),异步Web框架的吞吐量(请求数/秒)略差,延迟差异大得多。

第50和第99个百分位响应时间以毫秒为单位,吞吐量以每秒请求数为单位。该表是按第99页排序的,我认为这可能是现实世界中最重要的统计数据。

我也这么想。我试着让它们尽可能的逼真。以下是我使用的架构:

我已经尽我所能为真实世界的部署建模。这里有一个反向代理、python代码(即:变量)和一个数据库。我还包括了一个外部数据库连接池,因为我认为这是实际部署Web应用程序的一个非常常见的特性(至少,它是针对PostgreSQL的)。

有问题的应用程序通过随机键查询一行,并以JSON的形式返回值。完整的源代码可以在GitHub上找到。

我用来决定最佳工作进程数的规则很简单:对于每个框架,我从一个工作进程开始,然后连续增加工作进程计数,直到性能变差。

异步框架和同步框架的最佳工作进程数各不相同,原因很简单。异步框架由于其IO并发性,能够用单个工作进程使单个CPU饱和。

对于同步工作进程则不是这样:当它们执行IO时,它们将阻塞,直到IO完成。因此,他们需要有足够的工作人员来确保所有CPU核心在负载时始终处于充分使用状态。

通常,我们建议(2x$num_cores)+1作为开始时的工作人员数量。虽然不太科学,但该公式是基于这样的假设:对于给定的内核,一个工作者将从套接字中读取或写入,而另一个工作者则在处理请求。

我在Hetzner的CX31机器类型上运行了基准测试,该机器基本上是4/8 GB内存的vCPU机器。它是在Ubuntu20.04下运行的。我在另一个(较小的)VM上运行了负载生成器。

关于吞吐量(即:请求/秒),主要因素不是异步与同步,而是有多少Python代码被本机代码替换。简单地说,可以替换的Python代码对性能越敏感,效果就越好。这是一种历史悠久的Python性能策略(另见:Numpy)。

Meinhold和UWSGI(分别约5.3k个请求/秒)是大量的C代码。标准Gunicorn(约3.4k请求/秒)是纯Python。

Uvicorn+Starlette(约4.9k请求/秒)替换了比AIOHTTP';的默认服务器(约4.5k请求/秒)多得多的Python代码(尽管AIOHTTP也安装了可选的加速功能)。

在延迟方面,问题更为严重。与传统的同步部署相比,在负载情况下,异步性能很差,延迟开始激增。

这是为什么?在异步Python中,多线程是协作的,这仅仅意味着线程不会被中央调控器(如内核)中断,而是必须自愿将其执行时间让给其他调控器。在Asyncio中,根据三个语言关键字执行:Await、Async For和Async With。

这意味着执行时间不是公平分配的,并且一个线程在工作时可能会不经意地占用另一个线程的CPU时间。这就是延迟更不稳定的原因。

相比之下,传统的同步Python Web服务器(如UWSGI)使用内核调度器的抢占式多处理,它通过定期交换执行中的进程来确保公平性。这意味着时间分配更公平,延迟差异更小。

大多数其他基准测试(特别是来自异步框架作者的基准测试!)。简单地说,不要配置具有足够工作人员的同步框架。这意味着有效地阻止了这些同步框架访问大部分真正可用的CPU时间。

以下是Vibora项目的示例基准测试。(我没有测试这个框架,因为它是不太受欢迎的框架之一。)。

Vibora声称比Flask的吞吐量高出500%。然而,当我检查他们的基准代码时,我发现他们错误地将Flask配置为每个CPU使用一个工作者。当我更正它时,我得到以下数字:

使用Vibora而不是Flask的吞吐量优势实际上只有18%。Flask是我测试过的吞吐量较低的同步框架之一,所以我预计更好的同步设置会比Vibora快得多,尽管它的图形看起来令人印象深刻。

另一个问题是,许多基准测试将延迟结果打乱优先级,而更看重吞吐量结果(例如,Vibora;s甚至没有提到这一点)。然而,虽然可以通过增加机器来提高吞吐量,但是当您这样做时,负载下的延迟并不会变得更好。

虽然我的基准测试在涉及的方面相当现实,但它仍然比现实生活中的工作负载同质化得多-所有请求都执行数据库查询,并且它们都对该查询执行相同的操作。真实的应用程序通常有更多固有的变化:会有一些慢的操作、一些快的操作、一些执行大量IO的操作和一些使用大量CPU的操作。似乎可以合理地假设(根据我的经验,这是正确的),在实际的应用程序中,延迟差异实际上要高得多。

我的预感是,在这种情况下,异步应用程序的性能将会更成问题。公开的轶事与这一观点一致:

丹·麦金利(Dan McKinley)写道,他在Etsy操作基于Twisted的系统的经历。该系统似乎受到了慢性延迟变化影响:

[Twisted顾问]说,虽然Twisted在总体吞吐量方面很好,但外围请求可能会经历严重的延迟。这对[Etsy‘s系统]来说是个问题,因为PHP前端使用它的方式是每个web请求数百/数千次。

SQLAlChemy的作者Mike Bayer在几年前编写了异步Python和数据库,其中他从略微不同的角度看待异步。他还进行了基准测试,发现异步通信效率较低。

雷切尔在海湾边写了一篇文章,名为“我们必须讨论一下巨蟒,Gunicorn,Gevent的事情”,她在文中描述了基于Gevent的配置引起的操作混乱。我在生产中也遇到过Gevent的麻烦(虽然与性能无关)。

我应该提到的另一件事是,在设置这些基准的过程中,每个异步实现都以一种恼人的方式失败了。

Uvicorn让它的父进程终止,而没有终止它的任何子进程,这意味着我必须进行PID搜索,寻找仍在保留端口8001的子进程。AIOHTTP一度引发了一个与文件描述符有关的内部关键错误,但没有退出(因此任何进程管理程序都不会重新启动-这是一个重大错误!)。达芙妮在当地也遇到了麻烦,但我忘了到底是怎么回事。

所有这些错误都是暂时的,使用SIGKILL很容易解决。然而,事实仍然是,我不想在生产环境中负责基于这些库的代码。相比之下,我对Gunicorn或UWSGI没有任何问题--只是我真的不喜欢UWSGI在你的应用没有正确加载的情况下不退出。

我的建议:出于性能目的,只使用普通的同步Python,但尽可能多地使用本机代码。对于web服务器,如果吞吐量是最重要的,那么值得考虑使用Flask以外的框架,但即使是UWSGI下的Flask也有最好的延迟特性。

Flask的原作者几次发帖表达了他对异步技术的担忧,第一次发帖时,我不理解Python的Asyncio,它实际上很好地解释了这项技术,最近他在帖子中说,我并没有感受到异步的压力。他在帖子中说,我不理解Python的Asyncio,它实际上对这项技术做出了相当好的解释。最近,他在文章中说,我并没有感受到异步的压力。他在帖子中说,我不理解Python的Asyncio,它实际上对这项技术做出了相当好的解释。

你的功能是什么颜色的?解释了为什么同时使用同步和异步的语言会更痛苦的一些原因。

函数着色是Python中的一个大问题,令人遗憾的是,社区现在分成了编写同步代码的人和编写异步代码的人-他们不能共享相同的库。更糟糕的是,一些异步库也与其他异步库不兼容,因此异步Python社区甚至进一步分裂。

Chris Wellons最近写了一篇文章,也谈到了延迟问题和asyncio标准库中的一些问题。不幸的是,这类问题使得异步程序更难正确运行。

纳撒尼尔·J·史密斯(Nathaniel J.Smith)有一系列关于异步通信的重要文章,我推荐给任何想要掌握它的人:

他争辩说异步图书馆的想法是错误的。我担心的是,如果就PEP进行辩论的大脑袋鼠都做不对,那么像我这样的凡人还有什么希望呢?