• Python异步框架大战:FastAPI、Sanic、Tornado VS Go 的 Gin


    一、前言

    异步编程在构建高性能 Web 应用中起着关键作用,而 FastAPI、Sanic、Tornado 都声称具有卓越的性能。本文将通过性能压测对这些框架与Go的Gin框架进行全面对比,揭示它们之间的差异。

    原文:Python异步框架大战:FastAPI、Sanic、Tornado VS Go 的 Gin

    二、环境准备

    系统环境配置

    编程语言

    语言版本官网/Github
    Python3.10.12https://www.python.org/
    Go1.20.5https://go.dev/

    压测工具

    工具介绍官网/Github
    abApache的压力测试工具,使用简单https://httpd.apache.org/docs/2.4/programs/ab.html
    wrk高性能多线程压力测试工具https://github.com/wg/wrk
    JMeter功能强大的压力/负载测试工具https://github.com/apache/jmeter

    这里选择 wrk 工具进行压测,mac 安装直接通过brew快速安装

    brew install wrk
    
    • 1

    window安装可能要依赖它的子系统才方便安装,或者换成其他的压测工具例如JMeter。

    web框架

    框架介绍压测版本官网/Github
    FastAPI基于Python的高性能web框架0.103.1https://fastapi.tiangolo.com/
    SanicPython的异步web服务器框架23.6.0https://sanic.dev/zh/
    TornadoPython的非阻塞式web框架6.3.3https://www.tornadoweb.org/en/stable/
    GinGo语言的web框架1.9.1https://gin-gonic.com/
    Fibertodotodohttps://gofiber.io/
    Flasktodotodohttps://github.com/pallets/flask
    Djangotodotodohttps://www.djangoproject.com/

    数据库配置

    数据库名介绍压测版本依赖库
    MySQL关系型数据库8.0sqlalchemy+aiomysql
    RedisNoSQL数据库7.2aioredis

    三、wrk 工具 http压测

    FastAPI

    普通http请求压测

    依赖安装

    pip install fastapi==0.103.1
    pip install uvicorn==0.23.2
    
    • 1
    • 2

    编写测试路由

    from fastapi import FastAPI
    
    
    app = FastAPI(summary="fastapi性能测试")
    
    
    @app.get(path="/http/fastapi/test")
    async def fastapi_test():
        return {"code": 0, "message": "fastapi_http_test", "data": {}}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    Uvicorn 运行,这里是起四个进程运行部署

    uvicorn fastapi_test:app --log-level critical --port 8000 --workers 4
    
    • 1

    wrk压测

    开20个线程,建立500个连接,持续请求30s

    wrk -t20 -d30s -c500 http://127.0.0.1:8000/http/fastapi/test
    
    • 1

    压测结果

    ~ wrk -t20 -d30s -c500 http://127.0.0.1:8000/http/fastapi/test
    
    Running 30s test @ http://127.0.0.1:8000/http/fastapi/test
      20 threads and 500 connections
      
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency     3.06ms    2.89ms  36.65ms   85.34%
        Req/Sec     3.85k     3.15k   41.59k    70.05%
        
      2298746 requests in 30.11s, 383.64MB read
      Socket errors: connect 267, read 100, write 0, timeout 0
      
    Requests/sec:  76357.51
    Transfer/sec:     12.74MB
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    Thread Stats 这里是 20、30个压测线程的平均结果指标

    • 平均延迟(Avg Latency):每个线程的平均响应延迟
    • 标准差(Stdev Latency):每个线程延迟的标准差
    • 最大延迟(Max Latency):每个线程遇到的最大延迟
    • 延迟分布(+/- Stdev Latency):每个线程延迟分布情况
    • 每秒请求数(Req/Sec):每个线程每秒完成的请求数
    • 请求数分布(+/- Stdev Req/Sec):每个线程请求数的分布情况

    Socket errors: connect 267, read 100, write 0, timeout 0,是压测过程中socket的错误统计

    • connect:连接错误,表示在压测过程中,总共有 267 次连接异常
    • read:读取错误,表示有 100 次读取数据异常
    • write:写入错误,表示有0次写入异常
    • timeout:超时错误,表示有0次超时

    MySQL数据查询请求压测

    这里在简单试下数据库查询时候的情况

    首先先补充下项目依赖

    pip install hui-tools[db-orm, db-redis]==0.2.0
    
    • 1

    hui-tools是我自己开发的一个工具库,欢迎大家一起来贡献。https://github.com/HuiDBK/py-tools

    #!/usr/bin/python3
    # -*- coding: utf-8 -*-
    # @Author: Hui
    # @Desc: { fastapi性能测试 }
    # @Date: 2023/09/10 12:24
    import uvicorn
    from fastapi import FastAPI
    from py_tools.connections.db.mysql import SQLAlchemyManager, DBManager
    
    app = FastAPI(summary="fastapi性能测试")
    
    
    async def init_orm():
        db_client = SQLAlchemyManager(
            host="127.0.0.1",
            port=3306,
            user="root",
            password="123456",
            db_name="house_rental"
        )
        db_client.init_mysql_engine()
        DBManager.init_db_client(db_client)
    
    
    @app.on_event("startup")
    async def startup_event():
        """项目启动时准备环境"""
    
        await init_orm()
        
    @app.get(path="/http/fastapi/mysql/test")
    async def fastapi_mysql_query_test():
        sql = "select id, username, role from user_basic where username='hui'"
        ret = await DBManager().run_sql(sql)
    
        column_names = [desc[0] for desc in ret.cursor.description]
        result_tuple = ret.fetchone()
        user_info = dict(zip(column_names, result_tuple))
    
        return {"code": 0, "message": "fastapi_http_test", "data": {**user_info}}
    
    • 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

    wrk压测

    wrk -t20 -d30s -c500 http://127.0.0.1:8000/http/fastapi/mysql/test
    
    • 1
    ~ wrk -t20 -d30s -c500 http://127.0.0.1:8000/http/fastapi/mysql/test
    
    Running 30s test @ http://127.0.0.1:8000/http/fastapi/mysql/test
      20 threads and 500 connections
      
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency    38.81ms   19.35ms 226.42ms   76.86%
        Req/Sec   317.65    227.19   848.00     57.21%
        
      180255 requests in 30.09s, 36.95MB read
      Socket errors: connect 267, read 239, write 0, timeout 0
      Non-2xx or 3xx responses: 140
      
    Requests/sec:   5989.59
    Transfer/sec:      1.23MB
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    可以发现就加入一个简单的数据库查询,QPS从 76357.51 降到 5989.59 足足降了有10倍多,其实是单机数据库处理不过来太多请求,并发的瓶颈是在数据库,可以尝试加个redis缓存对比MySQL来说并发提升了多少。

    Redis缓存查询压测

    #!/usr/bin/python3
    # -*- coding: utf-8 -*-
    # @Author: Hui
    # @Desc: { fastapi性能测试 }
    # @Date: 2023/09/10 12:24
    import json
    from datetime import timedelta
    
    import uvicorn
    from fastapi import FastAPI
    from py_tools.connections.db.mysql import SQLAlchemyManager, DBManager
    from py_tools.connections.db.redis_client import RedisManager
    
    app = FastAPI(summary="fastapi性能测试")
    
    
    async def init_orm():
        db_client = SQLAlchemyManager(
            host="127.0.0.1",
            port=3306,
            user="root",
            password="123456",
            db_name="house_rental"
        )
        db_client.init_mysql_engine()
        DBManager.init_db_client(db_client)
    
    
    async def init_redis():
        RedisManager.init_redis_client(
            async_client=True,
            host="127.0.0.1",
            port=6379,
            db=0,
        )
    
    
    @app.on_event("startup")
    async def startup_event():
        """项目启动时准备环境"""
    
        await init_orm()
    
        await init_redis()
    
    
    @app.get(path="/http/fastapi/redis/{username}")
    async def fastapi_redis_query_test(username: str):
        # 先判断缓存有没有
        user_info = await RedisManager.client.get(name=username)
        if user_info:
            user_info = json.loads(user_info)
            return {"code": 0, "message": "fastapi_redis_test", "data": {**user_info}}
    
        sql = f"select id, username, role from user_basic where username='{username}'"
        ret = await DBManager().run_sql(sql)
    
        column_names = [desc[0] for desc in ret.cursor.description]
        result_tuple = ret.fetchone()
        user_info = dict(zip(column_names, result_tuple))
    
        # 存入redis缓存中, 3min
        await RedisManager.client.set(
            name=user_info.get("username"),
            value=json.dumps(user_info),
            ex=timedelta(minutes=3)
        )
    
        return {"code": 0, "message": "fastapi_redis_test", "data": {**user_info}}
    
    
    if __name__ == '__main__':
        uvicorn.run(app)
    
    • 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
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73

    运行

    wrk -t20 -d30s -c500 http://127.0.0.1:8000/http/fastapi/redis/hui
    
    • 1

    结果

    ~ wrk -t20 -d30s -c500 http://127.0.0.1:8000/http/fastapi/redis/hui
    
    Running 30s test @ http://127.0.0.1:8000/http/fastapi/redis/hui
      20 threads and 500 connections
      
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency     9.60ms    5.59ms 126.63ms   88.41%
        Req/Sec     1.22k     0.91k    3.45k    57.54%
        
      730083 requests in 30.10s, 149.70MB read
      Socket errors: connect 267, read 101, write 0, timeout 0
      
    Requests/sec:  24257.09
    Transfer/sec:      4.97MB
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    缓存信息

    添加了redis缓存,并发能力也提升了不少,因此在业务开发中一些查多改少的数据可以适当的做缓存。

    压测结论

    压测类型测试时长线程数连接数请求总数QPS平均延迟最大延迟总流量吞吐量/s
    普通请求30s20500229874676357.513.06ms36.65ms383.64MB12.74MB
    MySQL查询30s205007300835989.5938.81ms226.42ms36.95MB1.23MB
    Redis缓存30s2050073008324257.099.60ms126.63ms149.70MB4.97MB

    给 mysql 查询加了个 redis 缓存 qps 提升了 3倍多,对于一些查多改少的数据,根据业务设置适当的缓存可以大大提升系统的吞吐能力。其他框架我就直接上代码测,就不一一赘述了,直接看结果指标。

    Sanic

    压测方式都是一样的我就不像fastapi一样的一个一个写了,直接写全部压测然后看结果

    环境安装

    pip install sanic==23.6.0
    pip install hui-tools'[db-orm, db-redis]'==0.2.0
    
    • 1
    • 2

    编写测试路由

    #!/usr/bin/python3
    # -*- coding: utf-8 -*-
    # @Author: Hui
    # @Desc: { sanic性能测试 }
    # @Date: 2023/09/10 12:24
    import json
    from datetime import timedelta
    
    from py_tools.connections.db.mysql import SQLAlchemyManager, DBManager
    from py_tools.connections.db.redis_client import RedisManager
    from sanic import Sanic
    from sanic.response import json as sanic_json
    
    app = Sanic("sanic_test")
    
    
    async def init_orm():
        db_client = SQLAlchemyManager(
            host="127.0.0.1",
            port=3306,
            user="root",
            password="123456",
            db_name="house_rental"
        )
        db_client.init_mysql_engine()
        DBManager.init_db_client(db_client)
    
    
    async def init_redis():
        RedisManager.init_redis_client(
            async_client=True,
            host="127.0.0.1",
            port=6379,
            db=0,
        )
    
    
    @app.listener('before_server_start')
    async def server_start_event(app, loop):
        await init_orm()
        await init_redis()
    
    
    @app.get(uri="/http/sanic/test")
    async def fastapi_test(req):
        return sanic_json({"code": 0, "message": "sanic_http_test", "data": {}})
    
    
    @app.get(uri="/http/sanic/mysql/test")
    async def sanic_myql_query_test(req):
        sql = "select id, username, role from user_basic where username='hui'"
        ret = await DBManager().run_sql(sql)
    
        column_names = [desc[0] for desc in ret.cursor.description]
        result_tuple = ret.fetchone()
        user_info = dict(zip(column_names, result_tuple))
    
        return sanic_json({"code": 0, "message": "sanic_mysql_test", "data": {**user_info}})
    
    
    @app.get(uri="/http/sanic/redis/")
    async def sanic_redis_query_test(req, username: str):
        # 先判断缓存有没有
        user_info = await RedisManager.client.get(name=username)
        if user_info:
            user_info = json.loads(user_info)
            return sanic_json({"code": 0, "message": "sanic_redis_test", "data": {**user_info}})
    
        sql = f"select id, username, role from user_basic where username='{username}'"
        ret = await DBManager().run_sql(sql)
    
        column_names = [desc[0] for desc in ret.cursor.description]
        result_tuple = ret.fetchone()
        user_info = dict(zip(column_names, result_tuple))
    
        # 存入redis缓存中, 3min
        await RedisManager.client.set(
            name=user_info.get("username"),
            value=json.dumps(user_info),
            ex=timedelta(minutes=3)
        )
    
        return sanic_json({"code": 0, "message": "sanic_redis_test", "data": {**user_info}})
    
    
    def main():
        app.run()
    
    
    if __name__ == '__main__':
        # sanic sanic_test.app -p 8001 -w 4 --access-log=False
        main()
    
    • 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
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92

    运行

    Sanic 内置了一个生产web服务器,可以直接使用

    sanic python.sanic_test.app -p 8001 -w 4 --access-log=False
    
    • 1

    普通http请求压测

    同样是起了四个进程看看性能如何

    wrk -t20 -d30s -c500 http://127.0.0.1:8001/http/sanic/test
    
    • 1

    压测结果

    ~ wrk -t20 -d30s -c500 http://127.0.0.1:8001/http/sanic/test
    
    Running 30s test @ http://127.0.0.1:8001/http/sanic/test
      20 threads and 500 connections
      
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency     1.93ms    2.20ms  61.89ms   91.96%
        Req/Sec     6.10k     3.80k   27.08k    69.37%
        
      3651099 requests in 30.10s, 497.92MB read
      Socket errors: connect 267, read 163, write 0, timeout 0
      
    Requests/sec: 121286.47
    Transfer/sec:     16.54MB
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    Sanic 果然性能很强,在python中估计数一数二了。

    mysql数据查询请求压测

    运行

    wrk -t20 -d30s -c500 http://127.0.0.1:8001/http/sanic/mysql/test
    
    • 1

    结果

    ~ wrk -t20 -d30s -c500 http://127.0.0.1:8001/http/sanic/mysql/test
    
    Running 30s test @ http://127.0.0.1:8001/http/sanic/mysql/test
      20 threads and 500 connections
      
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency    35.22ms   21.75ms 264.37ms   78.52%
        Req/Sec   333.14    230.95     1.05k    68.99%
        
      198925 requests in 30.10s, 34.72MB read
      Socket errors: connect 267, read 146, write 0, timeout 0
      
    Requests/sec:   6609.65
    Transfer/sec:      1.15MB
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    Redis缓存查询压测

    运行

    wrk -t20 -d30s -c500 http://127.0.0.1:8001/http/sanic/redis/hui
    
    • 1

    结果

    ~ wrk -t20 -d30s -c500 http://127.0.0.1:8001/http/sanic/redis/hui
    
    Running 30s test @ http://127.0.0.1:8001/http/sanic/redis/hui
      20 threads and 500 connections
      
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency     6.91ms    4.13ms 217.47ms   95.62%
        Req/Sec     1.71k     0.88k    4.28k    68.05%
        
      1022884 requests in 30.09s, 178.52MB read
      Socket errors: connect 267, read 163, write 0, timeout 0
      
    Requests/sec:  33997.96
    Transfer/sec:      5.93MB
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    压测结论

    压测类型测试时长线程数连接数请求总数QPS平均延迟最大延迟总流量吞吐量/s
    普通请求30s205003651099121286.471.93ms61.89ms497.92MB16.54MB
    MySQL查询30s205001989256609.6535.22ms264.37ms34.72MB1.15MB
    Redis缓存30s20500102288433997.966.91ms217.47ms178.52MB5.93MB

    Tornado

    环境安装

    pip install tornado==6.3.3
    pip install gunicorn==21.2.0
    pip install hui-tools[db-orm, db-redis]==0.2.0
    
    • 1
    • 2
    • 3

    编写测试路由

    #!/usr/bin/python3
    # -*- coding: utf-8 -*-
    # @Author: Hui
    # @Desc: { tornado 性能测试 }
    # @Date: 2023/09/20 22:42
    import asyncio
    from datetime import timedelta
    import json
    import tornado.web
    import tornado.ioloop
    from tornado.httpserver import HTTPServer
    from py_tools.connections.db.mysql import SQLAlchemyManager, DBManager
    from py_tools.connections.db.redis_client import RedisManager
    
    class TornadoBaseHandler(tornado.web.RequestHandler):
        pass
    
    class TornadoTestHandler(TornadoBaseHandler):
        async def get(self):
            self.write({"code": 0, "message": "tornado_http_test", "data": {}})
    
    class TornadoMySQLTestHandler(TornadoBaseHandler):
        async def get(self):
            sql = "select id, username, role from user_basic where username='hui'"
            ret = await DBManager().run_sql(sql)
    
            column_names = [desc[0] for desc in ret.cursor.description]
            result_tuple = ret.fetchone()
            user_info = dict(zip(column_names, result_tuple))
            self.write({"code": 0, "message": "tornado_mysql_test", "data": {**user_info}})
    
    class TornadoRedisTestHandler(TornadoBaseHandler):
        async def get(self, username):
            user_info = await RedisManager.client.get(name=username)
            if user_info:
                user_info = json.loads(user_info)
                self.write(
                    {"code": 0, "message": "tornado_redis_test", "data": {**user_info}}
                )
                return
    
            sql = f"select id, username, role from user_basic where username='{username}'"
            ret = await DBManager().run_sql(sql)
    
            column_names = [desc[0] for desc in ret.cursor.description]
            result_tuple = ret.fetchone()
            user_info = dict(zip(column_names, result_tuple))
    
            # 存入redis缓存中, 3min
            await RedisManager.client.set(
                name=user_info.get("username"),
                value=json.dumps(user_info),
                ex=timedelta(minutes=3),
            )
            self.write({"code": 0, "message": "tornado_redis_test", "data": {**user_info}})
    
    def init_orm():
        db_client = SQLAlchemyManager(
            host="127.0.0.1",
            port=3306,
            user="root",
            password="123456",
            db_name="house_rental",
        )
        db_client.init_mysql_engine()
        DBManager.init_db_client(db_client)
    
    def init_redis():
        RedisManager.init_redis_client(
            async_client=True,
            host="127.0.0.1",
            port=6379,
            db=0,
        )
    
    def init_setup():
        init_orm()
        init_redis()
    
    def make_app():
        init_setup()
        return tornado.web.Application(
            [
                (r"/http/tornado/test", TornadoTestHandler),
                (r"/http/tornado/mysql/test", TornadoMySQLTestHandler),
                (r"/http/tornado/redis/(.*)", TornadoRedisTestHandler),
            ]
        )
    
    app = make_app()
    
    async def main():
        # init_setup()
        # app = make_app()
        server = HTTPServer(app)
        server.bind(8002)
        # server.start(4) # start 4 worker
        # app.listen(8002)
        await asyncio.Event().wait()
    
    if __name__ == "__main__":
        # gunicorn -k tornado -w=4 -b=127.0.0.1:8002 python.tornado_test:app
        asyncio.run(main())
    
    • 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
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103

    运行tornado服务

    gunicorn -k tornado -w=4 -b=127.0.0.1:8002 python.tornado_test:app
    
    • 1

    wrk 压测

    wrk -t20 -d30s -c500 http://127.0.0.1:8002/http/tornado/test
    
    wrk -t20 -d30s -c500 http://127.0.0.1:8002/http/tornado/mysql/test
    
    wrk -t20 -d30s -c500 http://127.0.0.1:8002/http/tornado/redis/hui
    
    • 1
    • 2
    • 3
    • 4
    • 5

    结果

    ~ wrk -t20 -d30s -c500 http:// 127.0.0.1 : 8002 /http/tornado/test
    Running 30s test @ http://127.0.0.1:8002/http/tornado/test
      20 threads and 500 connections
      
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency     6.54ms    1.92ms  34.75ms   63.85%
        Req/Sec     1.79k     1.07k    3.83k    56.23%
        
      1068205 requests in 30.07s, 280.15MB read
      Socket errors: connect 267, read 98, write 0, timeout 0
      
    Requests/sec:  35525.38
    Transfer/sec:      9.32MB
    
    ➜  ~ wrk -t20 -d30s -c500 http:// 127.0.0.1 : 8002 /http/tornado/mysql/test
    Running 30s test @ http://127.0.0.1:8002/http/tornado/mysql/test
      20 threads and 500 connections
      
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency    41.29ms   16.51ms 250.81ms   71.45%
        Req/Sec   283.47    188.81     0.95k    65.31%
        
      169471 requests in 30.09s, 51.88MB read
      Socket errors: connect 267, read 105, write 0, timeout 0
      
    Requests/sec:   5631.76
    Transfer/sec:      1.72MB
    
    ➜  ~ wrk -t20 -d30s -c500 http:// 127.0.0.1 : 8002 /http/tornado/redis/hui
    Running 30s test @ http://127.0.0.1:8002/http/tornado/redis/hui
      20 threads and 500 connections
      
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency    11.69ms    3.83ms 125.75ms   78.27%
        Req/Sec     1.00k   537.85     2.20k    64.34%
        
      599840 requests in 30.07s, 183.63MB read
      Socket errors: connect 267, read 97, write 0, timeout 0
      Non-2xx or 3xx responses: 2
      
    Requests/sec:  19947.28
    Transfer/sec:      6.11MB
    
    • 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

    Gin

    环境安装

    go get "github.com/gin-gonic/gin"
    go get "github.com/go-redis/redis"
    go get "gorm.io/driver/mysql"
    go get "gorm.io/gorm"
    
    • 1
    • 2
    • 3
    • 4

    代码编写

    package main
    
    import (
        "encoding/json"
        "time"
    
        "github.com/gin-gonic/gin"
        "github.com/go-redis/redis"
        "gorm.io/driver/mysql"
        "gorm.io/gorm"
        "gorm.io/gorm/logger"
    )
    
    var (
        db          *gorm.DB
        redisClient *redis.Client
    )
    
    type UserBasic struct {
        Id       int    `json:"id"`
        Username string `json:"username"`
        Role     string `json:"role"`
    }
    
    func (UserBasic) TableName() string {
        return "user_basic"
    }
    
    func initDB() *gorm.DB {
        var err error
        db, err = gorm.Open(mysql.Open("root:123456@/house_rental"), &gorm.Config{
            // 将LogMode设置为logger.Silent以禁用日志打印
            Logger: logger.Default.LogMode(logger.Silent),
        })
        if err != nil {
            panic("failed to connect database")
        }
    
        sqlDB, err := db.DB()
    
        // SetMaxIdleConns sets the maximum number of connections in the idle connection pool.
        sqlDB.SetMaxIdleConns(10)
    
        // SetMaxOpenConns sets the maximum number of open connections to the database.
        sqlDB.SetMaxOpenConns(30)
    
        // SetConnMaxLifetime sets the maximum amount of time a connection may be reused.
        sqlDB.SetConnMaxLifetime(time.Hour)
    
        return db
    }
    
    func initRedis() *redis.Client {
        redisClient = redis.NewClient(&redis.Options{
            Addr: "localhost:6379",
        })
        return redisClient
    }
    
    func jsonTestHandler(c *gin.Context) {
        c.JSON(200, gin.H{
            "code": 0, "message": "gin json", "data": make(map[string]any),
        })
    }
    
    func mysqlQueryHandler(c *gin.Context) {
    
        // 查询语句
        var user UserBasic
        db.First(&user, "username = ?", "hui")
        //fmt.Println(user)
    
        // 返回响应
        c.JSON(200, gin.H{
            "code":    0,
            "message": "go mysql test",
            "data":    user,
        })
    
    }
    
    func cacheQueryHandler(c *gin.Context) {
        // 从Redis中获取缓存
        username := "hui" // 要查询的用户名
        cachedUser, err := redisClient.Get(username).Result()
        if err == nil {
            // 缓存存在,将缓存结果返回给客户端
            var user UserBasic
            _ = json.Unmarshal([]byte(cachedUser), &user)
            c.JSON(200, gin.H{
                "code":    0,
                "message": "gin redis test",
                "data":    user,
            })
            return
        }
    
        // 缓存不存在,执行数据库查询
        var user UserBasic
        db.First(&user, "username = ?", username)
    
        // 将查询结果保存到Redis缓存
        userJSON, _ := json.Marshal(user)
        redisClient.Set(username, userJSON, time.Minute*2)
    
        // 返回响应
        c.JSON(200, gin.H{
            "code":    0,
            "message": "gin redis test",
            "data":    user,
        })
    }
    
    func initDao() {
        initDB()
        initRedis()
    }
    
    func main() {
        //r := gin.Default()
        r := gin.New()
        gin.SetMode(gin.ReleaseMode) // 生产模式
    
        initDao()
    
        r.GET("/http/gin/test", jsonTestHandler)
    
        r.GET("/http/gin/mysql/test", mysqlQueryHandler)
    
        r.GET("/http/gin/redis/test", cacheQueryHandler)
    
        r.Run("127.0.0.1:8003")
    }
    
    • 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
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133

    wrk 压测

    wrk -t20 -d30s -c500 http: //127.0.0.1:8003/http/gin/test
    
    wrk -t20 -d30s -c500 http: //127.0.0.1:8003/http/gin/mysql/test
    
    wrk -t20 -d30s -c500 http: //127.0.0.1:8003/http/gin/redis/test
    
    • 1
    • 2
    • 3
    • 4
    • 5

    结果

    ~ wrk -t20 -d30s -c500 http:// 127.0.0.1 : 8003 /http/gin/test
    Running 30s test @ http://127.0.0.1:8003/http/gin/test
      20 threads and 500 connections
      
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency     2.45ms    5.68ms 186.48ms   91.70%
        Req/Sec     6.36k     5.62k   53.15k    83.99%
        
      3787808 requests in 30.10s, 592.42MB read
      Socket errors: connect 267, read 95, write 0, timeout 0
      
    Requests/sec: 125855.41
    Transfer/sec:     19.68MB
    
    ➜  ~ wrk -t20 -d30s -c500 http:// 127.0.0.1 : 8003 /http/gin/mysql/test
    Running 30s test @ http://127.0.0.1:8003/http/gin/mysql/test
      20 threads and 500 connections
      
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency    40.89ms   83.70ms   1.12s    90.99%
        Req/Sec   522.33    322.88     1.72k    64.84%
        
      308836 requests in 30.10s, 61.26MB read
      Socket errors: connect 267, read 100, write 0, timeout 0
      
    Requests/sec:  10260.63
    Transfer/sec:      2.04MB
    ➜  ~~ wrk -t20 -d30s -c500 http:// 127.0.0.1 : 8003 /http/gin/redis/test
    Running 30s test @ http://127.0.0.1:8003/http/gin/redis/test
      20 threads and 500 connections
      
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency     7.18ms    1.76ms  79.40ms   81.93%
        Req/Sec     1.63k     1.09k    4.34k    62.59%
        
      972272 requests in 30.10s, 193.79MB read
      Socket errors: connect 267, read 104, write 0, timeout 0
      
    Requests/sec:  32305.30
    Transfer/sec:      6.44MB
    
    • 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

    四、总结

    web框架压测类型测试时长线程数连接数请求总数QPS平均延迟最大延迟总流量吞吐量/s
    FastAPI普通请求30s205002298746(229w)76357.51
    (76k)
    3.06ms36.65ms383.64MB12.74MB
    MySQL查询30s20500180255
    (18w)
    5989.59
    (5.9k)
    38.81ms226.42ms36.95MB1.23MB
    Redis缓存30s20500730083
    (73w)
    24257.09
    (24k)
    9.60ms126.63ms149.70MB4.97MB
    Sanic普通请求30s205003651099(365w)121286.47(120k)1.93ms61.89ms497.92MB16.54MB
    MySQL查询30s20500198925
    (19w)
    6609.65
    (6k)
    35.22ms264.37ms34.72MB1.15MB
    Redis缓存30s205001022884(100w)33997.96
    (33k)
    6.91ms217.47ms178.52MB5.93MB
    Tornado普通请求30s205001068205(106w)35525.38(35k)6.54ms34.75ms280.15MB9.32MB
    MySQL查询30s20500169471
    (16w)
    5631.76
    (5.6k)
    41.29ms250.81ms51.88MB1.72MB
    Redis缓存30s20500599840
    (59w)
    19947.28
    (19k)
    11.69ms125.75ms183.63MB6.11MB
    Gin普通请求30s205003787808(378w)125855.41(125k)2.45ms186.48ms592.42MB19.68MB
    MySQL查询30s20500308836
    (30w)
    10260.63
    (10k)
    40.89ms1.12s61.26MB2.04MB
    Redis缓存30s20500972272
    (97w)
    32305.30(32k)7.18ms79.40ms193.79MB6.44MB

    性能

    从性能角度来看,各个Web框架的表现如下:

    Gin > Sanic > FastAPI > Tornado

    Gin:在普通请求方面表现最佳,具有最高的QPS和吞吐量。在MySQL查询中,性能很高,但最大延迟也相对较高。gin承受的并发请求最高有 1w qps,其他python框架都在5-6k qps,但gin的mysql查询请求最大延迟达到了1.12s, 虽然可以接受这么多并发请求,但单机mysql还是处理不过来。

    还有非常重要的一点,cpython的多线程由于GIL原因不能充分利用多核CPU,故而都是通过开了四个进程来处理请求,资源开销远远大于go的gin,go底层的GMP的调度策略很强,天然支持并发。

    注意:Python使用asyncio语法时切记不要使用同步IO操作不然会堵塞住主线程的事件loop,从而大大降低性能,如果没有异步库支持可以采用线程来处理同步IO。

    综合评价

    除了性能之外,还有其他因素需要考虑,例如框架的社区活跃性、生态系统、文档质量以及团队熟悉度等。这些因素也应该在选择Web框架时考虑。

    最终的选择应该基于具体需求和项目要求。如果性能是最重要的因素之一,那么Sanic和go的一些框架可能是不错的选择。如果您更关注其他方面的因素,可以考虑框架的社区支持和适用性。我个人还是挺喜欢使用FastAPI。

    五、测试源代码

    https://github.com/HuiDBK/WebFrameworkPressureTest

    Github上已经有其他语言的web框架的压测,感兴趣也可以去了解下: https://web-frameworks-benchmark.netlify.app/result

    不知道为啥他们测试的python性能好低,可能异步没用对😄

  • 相关阅读:
    HTML5期末考核大作业,网站——旅游景点。 学生旅行 游玩 主题住宿网页
    Rocky(centos)安装nginx并设置开机自启
    Linux服务器安装部署最新稳定版本mongoDB社区版- Ubuntu-20.04版本
    CSS每日一练——实现可折叠的图片展示
    C语言——函数
    从 0-1 聊聊网络的演进
    小程序商城营销模式怎么去选择?链动2+1举例讲解
    (14.1)Zotero常用功能:导入题录、参考文献
    虚幻引擎:C++网络属性同步
    操作系统:处理机调度与死锁 练习题
  • 原文地址:https://blog.csdn.net/qq_43629857/article/details/133318778