龙空技术网

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

高可用架构 1360

前言:

此刻咱们对“python webserver框架”都比较着重,兄弟们都需要知道一些“python webserver框架”的相关知识。那么小编也在网上网罗了一些关于“python webserver框架””的相关资讯,希望各位老铁们能喜欢,看官们快快来了解一下吧!

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

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

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

基准结果

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

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

一些注意事项:

表现最好的是同步框架

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

表现差的全都是异步框架

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

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

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

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

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

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

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

为什么工作进程 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 的效率较低。

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

我还需要提到的一件事是,在设置这些基准的过程中,每一个 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 非常好,但它鼓励大家写一些负载变大后出现灾难性结果的东西。

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

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

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

Notes on structured concurrency

Some thoughts on asynchronous API design in a post-async/await world

Control-C handling in Python and Trio

Timeouts and cancellation for humans

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

英文原文:

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

高可用架构

改变互联网的构建方式

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

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

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

基准结果

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

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

一些注意事项:

表现最好的是同步框架

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

表现差的全都是异步框架

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

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

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

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

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

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

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

为什么工作进程 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 的效率较低。

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

我还需要提到的一件事是,在设置这些基准的过程中,每一个 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 非常好,但它鼓励大家写一些负载变大后出现灾难性结果的东西。


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

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

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

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

Notes on structured concurrency

Some thoughts on asynchronous API design in a post-async/await world

Control-C handling in Python and Trio

Timeouts and cancellation for humans

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

英文原文:


参考阅读:

谈谈PHP8新特性Attributes

如何做好Code Review? 分享一份我们团队的 Checklist分布式算法 Paxos 的直观解释 (TL;DR)
重构,还是重写?(2020版)深入浅出Rust异步编程之Tokio

深入理解同步/异步与阻塞/非阻塞区别

本文由高可用架构翻译,技术原创及架构实践文章,欢迎通过公众号菜单「联系我们」进行投稿。
高可用架构改变互联网的构建方式
长按二维码 关注「高可用架构」公众号

标签: #python webserver框架