百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

Python Web框架并发只有12个请求秒(python高并发socket)

toyiye 2024-04-04 11:41 26 浏览 0 评论

真实看一下Python Web框架

如果围绕Python Web框架的各种基准浏览博客圈,您可能会开始对自己的设置感到不满。或者,或者,对可能性大肆宣传。

考虑,例如,在球员令人难以置信的工作魔术栈,获得每秒100,000个请求从uvloop在一个单独的线程。这与Go的性能等编译语言相当。

但是该基准测试并未真正涵盖完善的Web框架,对吗?除了读写字节外,我们的框架还需要更多的功能和结构。用python充实的Web框架怎么样?

Sanic是这样的框架之一,它又被证明具有类似的性能:每秒100,000个请求。或有Vibora。这不仅声称可以替代Flask,而且还具有自己的模板引擎。每秒处理350,000个请求

Japronto更加令人赞叹,它声称在单个线程中每秒有120万个疯狂的请求破坏了其他语言和框架的性能:


最近,我们已经做了很多工作来提高Python API的性能。当前,我们正在运行Flask,最初我们有一个问题:如何从单个工作线程处理更多请求?但是,看看这些基准,我们提出了更多的要求:

  1. 我们可以将它们与我们的设置进行有意义的比较吗?
  2. 它们对于完整的生产应用程序有多现实?
  3. 我们在Flask上使用这些框架之一会更好吗?

换句话说,我们应该信任这些基准吗?它们应在多大程度上影响我们对技术的选择?

为了回答这些问题,在本文中,我对一个实际的Flask应用程序及其Sanic等效文件进行了基准测试。我猜想大多数读者都来自具有较“传统” Python框架之一(Flask或Django)的背景,这肯定与Suade Labs的开发人员更相关。出于这个原因,我以多种不同的方式运行Flask应用程序,以了解对我们而言最有利的一面:在代码几乎(几乎)零更改的情况下,我们如何才能使应用程序性能更高?在此过程中,我们将为原始问题选择一些技巧:如何从单个工作线程处理更多请求?

旁注:如果您不熟悉Python的Web框架或其异步库,请快速阅读本文底部的附录中的[1]。这篇文章主要假设您知道这些事情。

基线

首先,让我们运行一些简单的“ Hello,World!” 我们系统上的基准,以获得有意义的比较基准。作为参考,Flask在techempower上的基准每秒提供25,000个请求。

这是我们的Flask应用程序:

app = Flask(__name__)

@app.route("/", methods=["GET", "POST"])
def hello():
    if request.method == "GET":
        return "Hello, World!"

    data = request.get_json(force=True)
    try:
        return "Hello, {id}".format(**data)
    except KeyError:
        return "Missing required parameter 'id'", 400

我在各种条件下运行它。首先是“原始”通过python app.py,然后在Gunicorn下与一个单一的syncworker通过gunicorn -k sync app:app,最后在Gunicorn下与一个单一gevent工作者via gunicorn -k gevent app:app。从理论上讲,Gunicorn应该比原始python处理并发和断开连接要好得多,并且使用gevent worker应该允许我们执行异步IO而无需更改代码[2a]。我们还在PyPy下运行了这些基准测试,该基准测试理论上应该在不做任何更改的情况下加快任何与CPU绑定的代码的速度(如果您还没有听说过PyPy,请参见下面的附录中的[2b],以获取快速说明和一些术语)。

那Sanic呢?好吧,这是我们应用程序的“重写”:

app = Sanic(__name__)

@app.route("/", methods=["GET", "POST"])
async def hello(request):
    if request.method == "GET":
        return text("Hello, World!")

    data = request.json
    try:
        return text("Hello, {id}".format(**data))
    except KeyError:
        raise InvalidUsage("Missing required parameter 'id'")

结果如下:

一些技术细节:我将Python 3.7与常规CPython解释器一起使用,并将Python 3.6与PyPy 7.3.3一起使用。在撰写本文时,运行3.6是最新的PyPy解释器,在某些极端情况下,他们的Python 2.7解释器更快,但是由于Python 2正式死亡,我认为这样做对基准测试没有帮助。我的系统详细信息可在附录[3]中找到。我使用wrk来实际执行基准测试。

我将结果分为两部分。第一:Sanic占主导地位,每秒处理23,000次请求,尽管在Guncorn + gevent和PyPy下运行我们的Flask应用程序在保持速度方面做得很好。第二:Flask应用程序的性能范围如何?

在CPython下,我们看到使用Gunicorn将每秒Flask请求的数量增加了三倍,从1,000到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内核之间没有切换(该过程已变得“棘手”),单个内核的利用率更高。

关键要点:Sanic获胜。PyPy很快。在Gunicorn下运行您的“传统”应用程序。

现实的基准

上面的基准测试虽然很有趣,但对于现实世界的应用程序而言却毫无意义。让我们为我们的应用程序添加更多功能!

首先,我们将允许用户将数据实际存储在数据库中,该数据库将通过ORM(在我们的示例中为SQLAlchemy,即python中的事实上的独立ORM)进行检索。其次,我们将添加输入验证,以确保我们的用户收到有意义的错误消息,并且我们不接受使应用程序崩溃的垃圾邮件。最后,我们将添加一个响应编组器以自动化将数据库对象转换为JSON的过程。

我们将为出版社编写一个简单的书店应用程序。我们有许多作者,每本书都以几种类型写作零本书或更多本书。为简单起见,每本书只有一位作者,但可以有多种类型-例如,我们可以有一本书同时属于“现存小说”和“贝特尼克诗歌”两类。我们将在数据库中增加100万名作者和大约1000万本书。[4]

我们的SQLAlchemy模型看起来像这样:

class Author(db.Model):
    id = db.Column(UUIDType, primary_key=True)
    name = 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", backref="books")
    ... # snip!

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

class Author(Schema):
    id = fields.Str(dump_only=True)
    name = fields.Str(required=True)
    country_code = EnumField(CountryCodes, required=True)
    email = fields.Str(required=True)
    phone = fields.Str(required=True)
    contact_address = fields.Str(required=True)
    contract_started = fields.DateTime(format="iso")
    contract_finished = fields.DateTime(format="iso")
    contract_value = fields.Integer()

在我们的端点中,这些用于验证输入和返回结果,如下所示:

@bp.route("/author", methods=["GET", "POST"])
def author():
    """View all authors, or create a new one."""

    if request.method == "GET":
        args = validate_get(marshallers.LimitOffsetSchema())
        limit = args["limit"]
        offset = args["offset"]

        authors = Author.query.limit(limit).offset(offset).all()
        return jsonify(marshallers.authors.dump(authors))

    if request.method == "POST":
        author = Author(**validate_post(marshallers.author))

        db.session.add(author)
        db.session.commit()

        return jsonify({"id": author.id})

完整的源代码可以在GitHub repo中查看。在这里,需要注意的是棉花糖模式marshallers.foo的实例,它既可以用于验证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语法。因此,如果我们要非阻塞数据库请求,我们有三个选择:

  1. 用不同的ORM重写我们的模型和查询(Tortoise看起来很有趣)
  2. 选择一个像数据库这样的库,它使我们能够保留模型/ SQLAlchemy核心进行查询,但会失去很多功能
  3. 跳过所有这些,仅将原始SQL插入asyncpg驱动程序

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

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

使用新的ORM,我们的代码如下:

@bp.route("/author", methods=["GET", "POST"])
async def author(request):
    """View all authors, or create a new one."""

    if request.method == "GET":
        args = validate_get(request, marshallers.LimitOffsetSchema())
        limit = args["limit"]
        offset = args["offset"]

        authors = await Author.all().prefetch_related(
            "country_code"
        ).limit(limit).offset(offset)
        return json(marshallers.authors.dump(authors))

    if request.method == "POST":
        author = Author(**validate_post(marshallers.author))
        await author.save()

        return json({"id": author.id})

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

关键要点:转换框架要求您评估和选择整个库生态系统及其特性。Sanic和Tortoise真的很不错,并且具有很好的人体工程学设计asyncio。没有ORM的工作很乏味。

结果

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

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

@property
def genres(self):
    result = set()
    for book in self.books:
        result.update(book.genres)

    return sorted(result)

本质上说,要返回作者的体裁,我们必须提取他们所有的书genres,然后合并成经过重复数据删除和排序的列表。

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

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

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

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

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

现在,我们两个同步工作人员都达到了每秒**12个** 请求的速度。使用异步工作程序似乎有很大帮助,但奇怪的是Sanic在这里挣扎。我认为Sanic的结果更多与前面提到的Tortoise ORM代码中的额外联接有关。我希望它会给数据库带来一点额外的负载。这是交换框架方面的宝贵经验:要保持性能,您还必须选择,评估和调整多个库,而不仅仅是一个。

作为参考,在异步基准测试期间,数据库的CPU使用率达到1050%,而API的使用率达到了50%。如果我们想为更多的用户提供服务,那就很清楚了:我们将需要升级数据库!希望我们没有其他使用该数据库的应用程序,因为它们可能会遇到麻烦!

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

结论

实际上,除少数利基用例外,大多数“超快速”基准的意义很小。如果仔细查看代码,您会发现它们要么是简单的“ Hello,World!”,要么是“ 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文档)是FastAPI。

现在,我感到满意的是,在PyPy下运行的Flask,Gunicorn和gevent的组合几乎可以在所有情况下实现最快的速度。我们将在不久的将来积极开发FastAPI,而不是针对其基准测试,而是针对其功能。

喜欢研究有趣的问题并深入研究技术吗?我们正在招聘:https : //suade.org/lead/

附录

(1)大多数“传统” Python Web框架都属于WSGI标准,该标准按顺序处理请求:请求进入,处理,发送答复,下一个请求进入,等等。大多数“新学校” Python框架使用Python的asyncio库和另一个称为ASGI的标准,这意味着在等待IO(例如,字节通过Web到达)时,应用程序可以切换到处理其他请求。从理论上讲,这允许处理更多的请求,因为我们的应用程序不会处于等待状态-但是该应用程序仍然可以在任何时间仅处理一个请求,因此这不是并行处理。请参阅这篇很棒的博客文章以获取解释。

(2a)Gunicorn是WSGI应用程序的赛跑者,它处理了很好地运行Web应用程序的许多棘手的网络部分。它具有许多“工作者类型”,这些改变了它处理请求的方式。尽管WSGI框架仅支持同步连接,但gevent和eventlet工作者使用绿色线程的概念在任何IO期间切换上下文-从而提供隐式异步。

(2b)在日常用语中,我们使用Python来描述语言和用于运行该语言的解释器。但是还有其他解释器可以运行您的python代码。标准的Python解释器被众所周知的人称为CPython(因为它是用C编写的)。PyPy是一个不同的解释器,它在python的子集中实现python!它包括一个即时编译器,它实际上可以使您的代码运行得更快。

(3)我在配备12核2.20GHz Core i7 i7-8750H和32GB RAM的Dell XPS 15上运行了该基准测试。

(4)作为参考,亚马逊上的图书数量估计在1到2千万之间。这并不是说我们希望拥有一个亚马逊大小的网站,尽管如果我们在性能方面苦苦挣扎,也许这是一个很好的用例!但这在数据库内生成大量工作负载的相当简单的方法。

(5)实际上问题出在psycopg驱动程序上,该驱动程序使用纯C编码的套接字,而不是Python的socket库。这意味着gevent无法在IO期间修补套接字和切换上下文。解决方法是使用psycogreen修补psycopg。没有这个,gevent worker最终可能会像sync worker一样执行更多的工作。如果使用其中之一eventletpg8000则不需要。在PyPy下,我们需要按顺序应用几层补丁:首先是gevent补丁,然后是psycopg2cffi,然后是psycogreen。

(6)在此处查看电子表格。

(7)实际上,此曲线以相当平坦的水平继续延伸到1000个并发连接。为了更好地了解这种负载,应该使用带有类似NGINX之类的单独的应用程序服务器和基准服务器。

Ref: https://suade.org/dev/12-requests-per-second-with-python/

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码