每秒12个请求–现实的Python Web框架

2021-02-19 10:35:39

如果您在Python Web框架的各种基准上查看博客圈,您可能会开始对自己的设置感到非常糟糕。或者,或者,超级估量关于可能性。

例如,考虑魔术堆栈中的人员的难以置信的工作,从单个线程中从UVLoop获得每秒100,000个请求。这与编译语言相似,如Go' s表现。

但那个基准并不真的覆盖一个完全充实的Web框架,对吗?我们需要我们框架的更多功能和结构,而不是读写字节。在Python中完全充实的网页框架怎么样?

一个这样的框架是SANIC,它再次被证明具有类似的性能:每秒100,000个请求。或者' s vibora。这一声称不仅是烧瓶的替代品,而且还有自己的模板发动机。它每秒处理350,000个请求!

更令人兴奋的是日本吹嘘,其中索赔了一个疯狂的120万次在一个线程中的请求♪促进其他语言和框架的表现:

最近我们一直在做很多工作,提高了我们Python API的表现。目前我们'重新运行烧瓶,我们最初有一个问题:我们如何从单个工人线程提供更多请求?但是看着这些基准让我们询问更多:

换句话说,我们应该相信这些基准多少钱?他们应该在多大程度上影响我们的技术选择?

为了回答这些问题,在这篇文章中,我将一个现实的烧瓶申请与它一起基准。我将猜测大多数读者来自一个&#34之一的背景;传统" Python框架(烧瓶或Django),它'在Suade Labs的Devs肯定更相关。出于这个原因,我以多种不同的方式运行烧瓶应用程序,看看我们的降价最好的爆炸是:我们如何使用(几乎)对代码进行零变化的应用程序?沿着我们' ll为原始问题提取了一些提示:我们如何从单个工人线程提供更多请求?

Sidenote:如果你'重新到Python'它的Web框架或其异步库,从此帖子底部的Addenda看看[1]以获得快速解释者。这篇文章主要假设您知道这些事情。

首先让'跑了一些简单的"你好,世界!"我们系统的基准测试以获得有意义的基线进行比较。供参考,TechEmpower上的烧瓶基准将每秒提供25,000个请求。

app = flask(__ name __)@ app.route(" /"方法= [" get" post"])def hello():如果请求.Method ==" Get&#34 ;:返回"你好,世界!" data = request.get_json(force = true)尝试:返回" hello,{id}​​" .format(**数据)除keyerror之外:返回"缺少必需参数' id&# 39;&#34 ;,400

我在各种情况下跑了它。第一个"生"通过Python app.py,然后在Gunicorn下通过Gunicorn -k同步应用程序:应用程序和最后枪手通过Gunicorn -K Gevent应用程序:应用程序的枪手,并终于使用单个Gevent工作者在理论上,丘陵应该处理并发性和掉落的连接,比原始Python好得多,并且使用Gevent Worlt应该让我们在不改变代码[2a]的情况下进行异步IO。我们也在小明课程下运行了这些基准,从理论上应该加快任何CPU绑定的代码而不进行任何变化(如果您没有听到Pypy在下面的addenda中看到[2b],以便快速解释和一些术语) 。

app = sanic(__ name __)@ app.route(" /"方法= [" get" post" post"])async def hello(请求):如果请求..ethod ==" get&#34 ;:返回文本("您好,世界!")data = request.json try:返回文本(" hello,{id} " .format(**数据))除了KeyError之外:提高Invalidusage("缺少所需参数' id'")

一些技术细节:我使用Python 3.7与常规CPython解释器和Python 3.6,具有Pypy 7.3.3。在撰写本文时,运行3.6是最新的Pypy解释器,他们的Python 2.7解释器在一些边缘案件中更快,但随着Python 2正式死亡,我不相信它为基准提高了它。我的系统细节可在附录[3]中提供。我用WRK实际执行基准。我' ll在两部分中断结果。第一:Sanic占主导地位,每秒有23,000个请求,虽然在枪击+ Gevent和Pypy下运行我们的烧瓶应用程序,但在跟上持续很好的工作。第二:什么'我们的烧瓶应用程序的性能范围?

在CPython下,我们看到使用Gunicorn将每秒Flask请求的数量增加了三倍,从1000增至4,000,而使用gevent worker增加了适度(低于10%)的速度。 PyPy的结果更令人印象深刻。在原始测试中,它每秒处理3,000个请求。它获得了Gunicorn相同的4倍速度提升,使我们每秒达到12,000个请求;最后,加上gevent,它每秒处理多达17,000个请求,比原始CPython版本高出17倍,而无需更改任何代码行。

gevent对CPython进程的影响很小,这让我感到非常震惊-可能是因为此时CPU已达到极限。另一方面,PyPy的速度似乎更快,这意味着即使在Gunicorn的支持下,PyPy仍在花时间等待系统调用/ IO。在混合中添加gevent意味着它可以在并发连接之间切换,并以CPU允许的速度进行处理。

为了真正理解这一点,我在监视CPU使用率的同时运行了基准测试。以下是针对PyPy下原始应用的简短测试:

您可以看到该程序在CPU内核之间跳转,很少使用给定内核的100%。另一方面,这是在PyPy下针对Gunicorn gevent工作者进行的更长测试的一部分:

现在,很明显,CPU内核之间没有切换(该过程变得“粘滞”),并且单个内核的使用程度更高。

上面的基准测试虽然很有趣,但对于现实世界的应用程序却毫无意义。让我们为我们的应用添加更多功能!首先,我们将允许用户将数据实际存储在数据库中,并通过ORM(在我们的示例中为SQLAlchemy,实际上是python中的独立ORM)检索数据库。其次,我们将添加输入验证,以确保我们的用户收到有意义的错误消息,并且我们不接受使应用程序崩溃的垃圾邮件。最后,我们将添加一个响应编组器以自动化将数据库对象转换为JSON的过程。

我们将为出版社编写一个简单的书店应用。我们有许多作者,每本书写作几类或零本或更多本。为简单起见,每本书只有一位作者,但可以有多种体裁-例如,我们可以有一本书同时存在于" Existential Fiction"中。和"贝特尼克诗歌"类别。我们将向数据库中增加100万名作者和大约1000万本书。 [4]

类Author(db.Model):id = db.Column(UUIDType,primary_key = True)名称= db.Column(db.String,nullable = False)...#snip!class Book(db.Model):author_id = db.Column(UUIDType,db.ForeignKey(" author.id"),nullable = False,index = True)author = db.relationship(" Author&#34 ;, backref =&#34 ; books")...#剪!

要封送这些文件,我们使用棉花糖,这是一个流行的Python封送库。这是作者概述的棉花糖模型的示例:

类Author(Schema):id =字段.Str(dump_only = True)名称=字段.Str(required = True)country_code = EnumField(CountryCodes,required = True)email =字段.Str(required = True)phone =字段。 Str(required = True)contact_address =字段.Str(required = True)contract_started =字段.DateTime(format =" iso")contract_finished =字段.DateTime(format =" iso") contract_value = fields.Integer()

@ bp.route(" / author&#34 ;, methods = [" GET&#34 ;," POST"])def author():"" "查看所有作者,或创建一个新作者。"""如果request.method ==" GET&#34 ;: args = validate_get(marshallers.LimitOffsetSchema())limit = args [" limit"] offset = args [" offset" ] authors = Author.query.limit(limit).offset(offset).all()如果request.method ==" POST"则返回jsonify(marshallers.authors.dump(authors)):author = Author (** validate_post(marshallers.author))db.session.add(作者)db.session.commit()返回jsonify({" id&#34 ;: author.id})

完整的源代码可以在GitHub存储库中查看。在这里,需要注意的是marshallers.foo是Marshmallow模式的实例,可用于验证Foo输入(例如在POST请求中)以及封送准备作为JSON返回的Foo实例。

为了实际执行异步数据库请求,修补库需要花哨的步伐,这取决于您使用的postgres连接器。 SQLAlchemy不支持此功能,实际上,它的主要开发人员在一篇很棒的文章中指出,异步ORM并不总是一个好主意。附录[5]中有许多技术细节,但是请注意,仅使用Gunicorn gevent worker并不一定能为您提供所需的东西。

当使用C扩展名和库而不是纯python时,PyPy往往会遭受性能下降,相反,CPython应该从基于C的库中获得性能提升。考虑到这一点,我测试了两个不同的基础数据库连接器:psycopg2和一个纯Python对应的pg8000,以及两个不同类的异步gunicorn worker:gevent和一个纯python对应的eventlet。

Sanic重写我们的应用程序怎么样?好吧,如上所述,SQLAlchemy并不是真正异步的,它绝对不支持python的await语法。因此,如果我们要非阻塞数据库请求,我们有三个选择:

选择一个像数据库这样的库,它使我们能够保留模型/ SQLAlchemy核心进行查询,但会失去很多功能

我们将从1中获得最好的代码,但它也将涉及最多的思考和重写。它引入了许多其他考虑因素:例如,架构迁移,测试,如何处理缺少的功能(SQLAlchemy只是做了许多其他ORM不会做的高级事情)。最快的应用程序可能来自3,但也有技术欠佳,痛苦和不透明性。

最后,我选择了2,几乎立即希望我完成1。部分原因是各个库之间存在一些不兼容性。但是,这也使加入变得非常乏味和hacky,无法正确地进行封送。经过短暂的转移之后,我切换到了Tortoise ORM,相比之下,这真的很令人愉快!

@ bp.route(" / author&#34 ;, Methods = [" GET&#34 ;," POST"])异步定义作者(请求):"&# 34;"查看所有作者,或创建一个新作者。"""如果request.method ==" GET&#34 ;: args = validate_get(request,marshallers.LimitOffsetSchema())limit = args [" limit"] offset = args [" offset&# 34;] authors = await Author.all()。prefetch_related(" country_code").limit(limit).offset(offset)如果请求request.method =,则返回json(marshallers.authors.dump(authors))。 =" POST&#34 ;:作者= Author(** validate_post(marshallers.author))等待author.save()返回json({" id&#34 ;: author.id})

请注意,在上文中,我必须" prefetch" (即加入)国家/地区代码表。这与表达我想要外键约束而不是Tortoise ORM中的关系/联接很困难。毫无疑问,我可以采取一些伏都教来解决此问题,但这并不是很明显。国家/地区代码表仅由300个左右的ISO 3166国家/地区代码组成,因此可能在内存中,任何开销都是很小的。

关键要点:转换框架要求您评估和选择整个库生态系统及其特性。 Sanic和Tortoise真的很棒,并且在使用asyncio方面具有出色的人体工程学。没有ORM的工作很乏味。

让我们从/ author /< author_id>开始端点。在这里,我们通过主键从数据库中选择一位作者-收集他们每本书的摘要,并将全部书籍打包以返回给用户。

由于我希望在我们的应用程序中至少包含一些业务逻辑,因此我在Author模型和AuthorDetail marshaller中添加了我认为是有趣的字段:

从本质上讲,要返回作者的体裁,我们必须拿出他们所有的书。种类,然后合并为已删除重复数据和排序的列表。

不出所料,纯python库在PyPy下的性能比其基于C的同类要好一些,而在CPython下则要差一些。因为微基准测试之外没有什么是完全整洁的,所以情况并非总是如此,实际上差异完全是微不足道的,因此我没有包括所有结果。有关完整结果,请参见附录[6]。

无论我们在这里使用什么库或设置,与最差的“ Hello,World!”相比,我们执行的请求都更少。简介中的示例。更有什者,异步PyPy worker似乎比具有高并发性的同步PyPy worker更糟-这在某种程度上颠覆了原始基准!哪一个最终可以回答我们遇到的其他问题:  Hello,World!"基准并不现实,与我们的实际应用关系不大。

我们可以得出的另一个结论很明确:如果数据库是快速的,也可以使用PyPy来使Python应用程序快速。无论您选择哪种解释器,异步工作者和同步工作者之间的区别并不是太大:当然,在每种情况下我们都可以选择性能最好的,但是它可能是噪音[7]。 Sanic的性能不到CPython + Flask的两倍,这是令人印象深刻的,但如果我们可以在PyPy下免费获得它,则可能不值得重写应用程序。

/ author概述端点给出的结果几乎相同。但是,让我们看看如果对数据库施加更多的负载会发生什么。为了模拟复杂的查询,我们将命中/ author?limit = 20& offset = 50000,这应该使数据库除通过主键查找外还可以做其他事情。还需要完成一些Python工作,以验证参数并编组20位作者。结果如下:

这次很明显,与PyPy一起使用异步gunicorn工作者或Sanic之类的异步框架对于加速我们的应用程序大有帮助。这就是异步的口头禅:如果您在应用程序中发出长时间/不定期的请求,请使用异步,以便您可以在等待回复的同时执行其他工作。在某个时刻,我们的数据库达到最大容量,并且每秒的请求数量停止增加。通过将偏移量增加到500,000,我们可以将这一点发挥到极致:

现在,我们两个同步工作人员都达到了每秒12个请求的速度。😅使用异步工作程序似乎有很大帮助,但奇怪的是Sanic在这里挣扎。我认为Sanic的结果更多与前面提到的Tortoise ORM代码中的额外联接有关。我希望它会给数据库带来一点额外的负载。这是交换框架方面的宝贵一课:要保持性能,您还必须选择,评估和调整多个库,而不仅仅是一个。作为参考,在异步基准测试期间,数据库的CPU使用率达到1050%,而API的使用率达到了50%。如果我们想为更多的用户提供服务,那就很清楚了:我们将需要升级数据库!希望我们没有其他任何使用此数据库的应用程序,因为它们可能会遇到麻烦!

关键要点:PyPy获胜。 Sanic很快,但没有那么快。您可能应该运行传统的"带有异步工作程序的应用程序。

实际上,大多数" super-fast"除了一些利基用例之外,基准测试几乎没有意义。如果您仔细查看代码,您会发现它们要么很简单,要么是Hello,World !!或echo服务器,并且所有人都花费大量时间调用带有Python绑定的手工C代码。

这意味着如果您想构建代理或提供静态内容(甚至可能用于流式传输),这些工具也非常有用。但是,一旦您将任何实际的Python工作引入代码中,您就会发现这些数字急剧下降。如果您依靠这些框架的速度,那么在没有诸如此类的情况下很难保持该性能水平。 cythonize您的所有代码。如果您打算几乎不编写Python,那么选择这些框架是最好的选择。但是想必您正在用Python编写应用程序,因为您需要的不仅仅是一个简单的  Hello,World!"。并且您实际上想编写很多Python,非常感谢!

如果您的服务每秒接收100,000个请求,则您使用的特定Python框架可能不会成为瓶颈。特别是如果您的API是无状态的,并且您可以通过Kubernetes或类似的方法进行扩展。到那时,拥有良好的数据库,良好的架构设计和良好的体系结构将变得至关重要。话虽如此,如果您确实想要更多的处理能力,请使用PyPy。

如果数据库或服务请求可能不是即时的,则具有以某种异步功能运行的能力提供了明显的优势。即使请求通常是瞬时的,选择异步运行程序也是一种使应用程序免受间歇性延迟影响的低成本方法。虽然像Sanic这样的异步优先框架为您提供了开箱即用的功能,但您可以轻松地在Flask或Django应用中使用其他Gunicorn worker。

我们在基准测试中看到的是架构设计,数据库选择和体系结构将成为瓶颈。仅出于速度考虑,使用一种新的完全异步框架可能不会像仅使用PyPy和异步Gunicorn工人那样有效。我还发现它给我带来了决策瘫痪,它提出了许多其他问题,例如:是否可以保持较低的延迟,使用C编写的同步Foo客户端还是纯Python编写的异步Foo客户端,或多或少地表现出性能? ?

这并不意味着这些框架不是很好的工程,也不是说用它们编写代码并不是一件很有趣的事情!实际上,与将某些东西与SQLAlchemy核心和数据库混合在一起相比,我最终爱上了Tortoise ORM的可用性,并且我喜欢在隐式查询队列和连接池上编写await Foo.all()的明确性。

对我而言,所有这些都强调了一个事实,除非您牢记一些超级小众用例,否则根据人体工程学和功能而非速度来选择框架实际上是一个更好的主意。我没有提到的一个框架似乎具有针对工业应用程序的下一级人体工程学(请求解析,编组,自动API文档)。

现在,我感到满意的是,在PyPy下运行的Flask,Gunicorn和gevent的组合几乎可以在所有情况下实现最快的速度。我们将在不久的将来积极开发FastAPI,这不是因为它的基准,而是因为它的功能。喜欢研究有趣的问题并深入研究技术吗?我们正在招聘:https://suade.org/lead/

(1)大部分"传统" Python Web框架属于称为WSGI的标准,其中按顺序处理请求:请求进入,处理,发送答复,下一个请求进入,等等。大多数" new-school" Python框架使用Python的asyncio库和另一个称为ASGI的标准,这意味着在等待IO(例如,字节通过Web到达)时,应用程序可以切换到在d上工作

......