Gevent 还是 asyncio 这一直是个经典的问题,在这里我们直接用数据来帮助大家做一下决策
开篇 Lin Wei 老师珠玉在前
给出了 asyncio 和 Gevnet 的极限性能。 在这里我们看到了 asyncio 配合 uvloop 基本上是 Gevent 的 double 了
那么在在 Web 框架下是否如此呢?
我们来做一下实验吧
首先说一下负载机器的配置,这里我选用了 Azure 上 D8as_v5 的机器,该机器配置如下:
8Core32G 的配置
底座硬件基于 EPYC 7763 系列处理器
共计4个节点,分配给 Django/Flast/FastAPI/Starlette 四个不同的框架
我们压测框架选择 locust,同样基于 Kuberntes 集群,因为我账户的 D8as_v5 机器的 Quota 不太够,所以压测框架我们选了不同机器的混合部署
4个 D8as_v5,共计 32 Core 算力
4个 D8as_v3,共计 32 Core 算力
4个 D4as_v2,共计 16 Core 算力
我们测试的主要目的是模拟在生产环境下的吞吐,所以我选择的测试方式如下
准备一台 16Core 64G 的 MySQL 实例,用于存储数据
创建一张表,随机写入100万数据
在框架代码中进行 SQL 查询,返回查询结果
MySQL 表结构如下
1 2 3 4 5 6 7 8 9 10 create table if not exists `demo_data`( `id` bigint (20 ) not null auto_increment, `name` varchar (255 ) not null , `create_time` timestamp default CURRENT_TIMESTAMP , `update_time` timestamp default CURRENT_TIMESTAMP , primary key (`id`), index (`name`) ) charset = utf8mb4 engine = innodb;
Django 代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import randomfrom django.core import serializersfrom django.shortcuts import HttpResponsefrom .models import DemoDataTEMP = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+=-" def demo_views (request ): result = DemoData.objects.filter ( name="" .join(random.choices(TEMP, k=random.randrange(1 , 254 ))) ) return HttpResponse( serializers.serialize("json" , result if result else []), content_type="application/json" , )
Flask 代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import jsonimport randomimport osimport datasetfrom flask import Flask, Responseapp = Flask(__name__) DATABASE_URL = f"mysql://{os.getenv('DATABASE_USER' )} :{os.getenv('DATABASE_PASSWORD' )} @{os.getenv('DATABASE_HOST' )} :3306/demo" db = dataset.connect(DATABASE_URL, engine_kwargs={"pool_size" : 10000 }) TEMP = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+=-" @app.route("/demo" , methods=["GET" ] ) def demo_code (): return Response( response=json.dumps( list ( db.query( f"select * from demo_data where name='{'' .join(random.choices(TEMP, k=random.randrange(1 , 254 )))} '" ) ), default=str ), status=200 , content_type="application/json" , ) if __name__ == "__main__" : app.run(debug=True )
FastAPI 代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 import randomimport osfrom typing import List import databasesimport pymysqlimport sqlalchemyimport jsonfrom fastapi import FastAPIfrom fastapi.responses import Responsefrom pydantic import BaseModelpymysql.install_as_MySQLdb() AYSNC_DATABASE_URL = f"mysql+aiomysql://{os.getenv('DATABASE_USER' )} :{os.getenv('DATABASE_PASSWORD' )} @{os.getenv('DATABASE_HOST' )} :3306/demo" SYNC_DATABASE_URL = f"mysql+mysqldb://{os.getenv('DATABASE_USER' )} :{os.getenv('DATABASE_PASSWORD' )} @{os.getenv('DATABASE_HOST' )} :3306/demo" database = databases.Database(AYSNC_DATABASE_URL, max_size=10000 ) metadata = sqlalchemy.MetaData() demo_data = sqlalchemy.Table( "demo_data" , metadata, sqlalchemy.Column("id" , sqlalchemy.Integer, primary_key=True ), sqlalchemy.Column("name" , sqlalchemy.String), sqlalchemy.Column("create_time" , sqlalchemy.DATETIME), sqlalchemy.Column("update_time" , sqlalchemy.DATETIME), ) engine = sqlalchemy.create_engine(SYNC_DATABASE_URL) metadata.create_all(engine) TEMP = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+=-" class DemoData (BaseModel ): id : int name: str app = FastAPI() init = False @app.get("/demo" , response_model=List [DemoData] ) async def demo_code (): global init if not init: await database.connect() init = True query = demo_data.select().where( demo_data.c.name == "" .join(random.choices(TEMP, k=random.randrange(1 , 254 ))) ) data = await database.fetch_all(query) response = json.dumps(data, default=str ) return Response(content=response, status_code=200 , media_type="application/json" )
Starlette 代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 import randomimport osfrom typing import List import databasesimport pymysqlimport jsonimport sqlalchemyfrom starlette.applications import Starlettefrom starlette.responses import Responsefrom starlette.routing import Routefrom pydantic import BaseModelpymysql.install_as_MySQLdb() AYSNC_DATABASE_URL = f"mysql+aiomysql://{os.getenv('DATABASE_USER' )} :{os.getenv('DATABASE_PASSWORD' )} @{os.getenv('DATABASE_HOST' )} :3306/demo" SYNC_DATABASE_URL = f"mysql+mysqldb://{os.getenv('DATABASE_USER' )} :{os.getenv('DATABASE_PASSWORD' )} @{os.getenv('DATABASE_HOST' )} :3306/demo" database = databases.Database(AYSNC_DATABASE_URL, max_size=10000 ) metadata = sqlalchemy.MetaData() demo_data = sqlalchemy.Table( "demo_data" , metadata, sqlalchemy.Column("id" , sqlalchemy.Integer, primary_key=True ), sqlalchemy.Column("name" , sqlalchemy.String), sqlalchemy.Column("create_time" , sqlalchemy.DATETIME), sqlalchemy.Column("update_time" , sqlalchemy.DATETIME), ) engine = sqlalchemy.create_engine(SYNC_DATABASE_URL) metadata.create_all(engine) TEMP = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+=-" class DemoData (BaseModel ): id : int name: str init = False async def demo_code (request ): global init if not init: await database.connect() init = True query = demo_data.select().where( demo_data.c.name == "" .join(random.choices(TEMP, k=random.randrange(1 , 254 ))) ) data = await database.fetch_all(query) return Response(content=json.dumps(data, default=str ), status_code=200 , media_type="application/json" ) routes = [ Route("/demo" , demo_code, methods=["GET" ]), ] app = Starlette(debug=False , routes=routes)
然后部署方式如下
各服务都部署在 K8S 上,POD 类型为 Guaranteed
所有镜像都基于 3.12 构建
服务限制 6Core 的 CPU
Django 和 Flask 基于 Gevent + Gunicorn 进行部署,利用 Greenify 对二进制进行 Patch
FastAPI 和 Starlette 基于 uvicorn 进行部署,使用 uvloop 作为 event loop
OK, 我们现在来公布测试结果
标准操作下的测试结果 Django:
FastAPI
Flask
Starlette
Django 毫无疑问的最后,其余三者的性能是 Flask + Gevent > Starlette > FastAPI,后三个框架 CPU 占用率均 > 90%
空转测试 为了保险起见,我们将后续三个框架进行空转测试
Flask
FastAPI
Starlette
Starlette > FastAPI > Flask + Gevent
总结 目前来看,整体结论是这样
在空转情况下,asyncio 的性能要搞出 Gevent 不少,加上框架因素后,也有百分之10-20% 的提升
在 ORM + MySQL Driver 的情况下,Gevent 的生态要好于 asyncio 的生态
如果换成 ORM + PGSQL 的生态结论会不会更好一些呢?有点期待下一轮测试的结果