Python社区  »  Python

基准测试表明, Async Python 远不如同步方式

高可用架构 • 1 年前 • 348 次点击  

大多数人都知道 async Python 具有更高的并发性。这意味着对于常见的任务如动态网站或 Web API, async 性能更好。


但遗憾的是,async 对于 Python 解释器来说,并不是一个加速条。


在现实条件下的数据(见下图),异步网络框架的吞吐量(请求量/秒)更差,响应延迟也大得多。


基准结果


我测试了各种不同的同步和异步的 Web 服务器配置。



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


一些注意事项:


  1. 表现最好的是同步框架

    1. 但 Flask 的吞吐量比其他的要低

  2. 表现差的全都是异步框架

  3. 异步框架的响应延迟也差很多

  4. 基于 Uvloop 的循环比内置的 asyncio 循环做得更好。

    1. 如果不得不使用 asyncio,请选择 uvloop。


这些基准测试有代表性吗?


我认为如此,我尽量让基准运行的场景贴近真实,下面是使用的架构。


我尽可能地模拟真实世界的部署:一个反向代理,中间是 Python 代码,后面一个数据库。我还使用了数据库连接池,这是真实的 Web 应用部署中常见的做法(至少对于 postgresql 来说是这样)。


测试的应用程序通过随机 key 查询数据库某一行,并以 JSON 形式返回。完整的源代码可以参看 github :

https://github.com/calpaterson/python-web-perf


为什么工作进程 worker 数设置不一样?


决定最佳 worker 数量是多少的规则很简单:对于每个框架,我从 1 个 worker 开始,连续增加数量,直到性能变差。


Async 和 sync 框架的最佳 worker 数量在有所不同,原因很简单,async 框架由于其 IO 并发性,一个 worker 进程就能让一个 CPU 跑满。


而同步 worker 就不一样了,它们做 IO 时会调用阻塞,直到 IO 完成。因此,它们需要有更多的 worker,以确保在负载时所有 CPU 核心始终处于满负荷状态。


关于这方面的更多信息,请参见 gunicorn 文档。


一般来说,我们建议 (2 x $num_cores) + 1  作为开始的 worker 数量。虽然这个公式并不太科学,但它是基于这样的假设:对于一个给定的 core,当一个 worker 在处理请求时,另外一个 worker 可以从套接字中读写数据。


机器规格


我在 Hetzner 的 CX31 机器类型上运行了基准测试,它是一个4 vCPU / 8 GB 内存的机器,运行在 Ubuntu 20.04 上。在另一个(较小的)虚拟机上运行了施压程序。


为什么 async 表现更差?


吞吐量


吞吐量(即:请求量/秒)最主要的因素不是 async 还是 sync,而是有多少 Python 代码被替换成了本地代码。简单的说,你能替换的对性能敏感的 Python 代码越多,性能就越好。这是 Python 性能战术,历史悠久(另见:numpy)。


Meinheld 和 UWSGI(每个约 5.3k请求量/秒)包含了大量的 C 代码。标准 Gunicorn(约 3.4k请求量/秒)基于纯 Python。


Uvicorn + Starlette(~4.9k请求/秒)比 AIOHTTP 的默认服务器(~4.5k请求量/秒)替换了更多的 Python 代码(尽管 AIOHTTP 也安装了它的可选 "加速")。


延时


在响应延迟上,问题更复杂。在请求负载下,async 的表现很糟糕,延迟开始飙升,比传统的同步部署,延迟的程度要大得多。


为什么会这样呢?在 async Python 中,多线程是合作式(co-operative)的,简单来说就是线程不被中央治理者(比如内核)打断,而是要主动把执行时间让给别人。在 asyncio 中,执行时间是在三个语言关键词上让渡的:await、async for 和 async with。


这意味着执行时间并不是 "公平 "分配的,一个线程在工作时可能会无意中饿死另一个线程的 CPU 时间。这就是为什么延迟比较不稳定的原因。


相比之下,传统的同步 Python webservers,比如 UWSGI,使用的是内核调度器的抢占式(Pre-emptive)的多进程,它的工作原理是通过周期性地将进程从执行中交换出来,以保证公平性。这意味着时间的分配更加公平,延迟差异更低。


为什么其他基准显示的结果不同?


大多数其他基准(尤其是那些来自 async 框架作者的基准)根本没有为同步框架配置足够的 worker。这意味着,这些同步框架实际上无法合理使用真正可用的大部分 CPU 时间。


下面是 Vibora 项目的一个样本基准(我没有测试这个框架,因为它是一个不太流行的框架)。



Vibora 声称比 Flask 高出 500% 的吞吐量。然而,当我审查他们的基准代码时,发现他们错误地将 Flask 配置为每个 CPU 使用一个 worker。当我纠正这个问题时,得到了以下结果。



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


另一个问题是,许多基准都会去掉响应延迟的统计数据,而倾向于吞吐量结果(例如 Vibora 的基准甚至没有提到它)。然而,增加吞吐量其实可以通过简单增加机器来提高,但在高负载下的延迟不佳的话并没有直接的解决办法。


只有在延迟在可接受的范围内,提高吞吐量才真正有意义。


进一步的推理、假设和传闻


虽然基准测试在设计方面尽量接近现实,但它仍然比现实生活中的工作负载要单调得多 —— 所有的请求都会做一个数据库查询,都会用这个查询做同样的事情。真实的应用通常会有更丰富的变化:会有一些慢的以及快的操作,一些请求做了很多 IO,另外一些使用了很多 CPU。似乎有理由假设(根据我的经验也是如此),在真实的应用中,延迟变化实际上要高得多。


在这种情况下,我的预感 async 应用的性能会更有问题。公开的传闻与这个想法一致。


Dan McKinley 分享了他在 Etsy 管理一个基于 Twisted 系统的经历。似乎那个系统受到了延迟变大的困扰。


[Twisted的顾问]说,虽然 Twisted 在整体吞吐量上很好,但冷僻的访问请求可能会出现严重的延迟,这对 [Etsy的系统] 来说是个问题,因为 PHP 前端的使用方式是每个 web 请求都会访问几百或几千次。


SQLAlchemy 的作者 Mike Bayer 在几年前写了《异步 Python 和数据库》(1),他在书中从一个稍微不同的角度考虑了异步的问题。他还进行了基准测试,发现 asyncio 的效率较低。


  1. https://techspot.zzzeek.org/2015/02/15/asynchronous-python-and-databases/


Rachel by the Bay 写了一篇文章《我们必须谈谈 Python、Gunicorn、Gevent 这件事》(1),文章中描述了基于 gevent 配置所产生的操作混乱。我也曾在生产中遇到过 gevent 的麻烦(虽然与性能无关)。


  1. https://rachelbythebay.com/w/2020/03/07/costly/


我还需要提到的一件事是,在设置这些基准的过程中,每一个 async 实现都最终以一种令人讨厌的方式挂掉。


Uvicorn 的父进程在没有终止任何子进程的情况下就退出了,这意味着我不得不去寻找那些还在 8001 端口的子进程。有一次,AIOHTTP 抛出了一个与文件描述符有关的内部严重错误,但它并没有退出 (因此任何进程监控脚本都不会重新启动它 —— 这可是大罪!)。Daphne 也在本地遇到了麻烦,但我忘了具体是怎么遇到的。


所有这些错误都是短暂的,用 SIGKILL 很容易解决。但实际我不想在生产环境中负责基于这些库的代码。相比之下,我在使用 Gunicorn 或 UWSGI 时没有遇到任何问题 —— 除了UWSGI 在应用没有正确加载时不会退出。


总结


我的建议是:出于性能的考虑,使用普通的、同步的 Python 即可,但尽量使用 native 代码。对于 webserver 来说,如果吞吐量是最重要的,值得考虑 Flask 以外的框架,但即使是 UWSGI 下的 Flask, 也有最好的延迟特性。


感谢 Tudor Munteanu 帮忙检查了文章中的数据。


参阅


Flask 作者已经写过几篇文章,表达了他对 async 的担忧,第一篇是《我不理解 Python 的 asyncio》(1),对 async 技术做了非常好的解释,最近又发了《我感觉不到 async 的压力》(2),里面提到


async/await 非常好,但它鼓励大家写一些负载变大后出现灾难性结果的东西。

  1. https://lucumr.pocoo.org/2016/10/30/i-dont-understand-asyncio/

  2. https://lucumr.pocoo.org/2020/1/1/async-pressure/


《你的函数是什么颜色》这篇文章解释了一个语言如果同时存在同步和异步,开发起来比较痛苦的一些原因。

https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/


函数着色是 Python 中的一个大问题,现在社区很悲哀地分成了写同步代码的人和写 async 代码的人 —— 他们不能共享同一个库。更糟糕的是,一些异步库还与另外一些异步库不兼容,所以异步 Python 社区更加分裂。


Chris Wellons 最近写了一篇文章,其中也提到了延迟问题和 asyncio 标准库中的一些注脚。不幸的是,这是一种让异步程序更难搞好的问题。

https://nullprogram.com/blog/2020/05/24/


Nathaniel J. Smith 有一系列关于 async 的精彩文章,推荐给有兴趣的读者。


  • Notes on structured concurrency https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/

  • Some thoughts on asynchronous API design in a post-async/await world https://vorpus.org/blog/some-thoughts-on-asynchronous-api-design-in-a-post-asyncawait-world/

  • Control-C handling in Python and Trio https://vorpus.org/blog/control-c-handling-in-python-and-trio/

  • Timeouts and cancellation for humans https://vorpus.org/blog/timeouts-and-cancellation-for-humans/


他认为 asyncio 库的概念是错误的。我担心的是,如果讨论 PEPs 规范的那些前辈们都搞不清楚,像我这样的普通开发者就更没戏了。


英文原文:

http://calpaterson.com/async-python-is-not-faster.html


参考阅读:


本文由高可用架构翻译,技术原创及架构实践文章,欢迎通过公众号菜单「联系我们」进行投稿。

高可用架构
改变互联网的构建方式

长按二维码 关注「高可用架构」公众号
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/70310
 
348 次点击  
分享到微博