V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX 提问指南
LeeReamond
V2EX  ›  问与答

2021 年,用 Python 部署异步网络服务的最佳实践是什么?

  •  
  •   LeeReamond · 2021-01-24 19:41:19 +08:00 · 2971 次点击
    这是一个创建于 1179 天前的主题,其中的信息可能已经有所发展或是发生改变。

    先说几句题外话,前两天看见一个帖子,提到异步框架的,里面很多人推荐 fastapi 。我个人说来很惭愧,学习 python 的 web 框架,入门是 flask,异步是 aiohttp,一直与 django 和 fastapi 这类主流的、用的人比较多的框架无缘。

    所以这次也是想学习一下 fastapi,看看相对于一直使用的原生 aiohttp 有什么区别。

    根据我个人理解,异步从 python3.4 版本提出以来,现在已经不是像 3.6 版本时候那样大家都不会用,现在用异步的人应该越来越多了。目前主流不管是公司内部服务,还是生产级服务,如果上 python 的话,如果要用异步的话,应该是很多人使用 django 的 asgi,一些人使用 fastapi,几乎没有人使用 aiohttp 这样。tornado 我不太了解,因为我最初接触异步是 3.5 时代,彼时 tornado 的异步是用猴子补丁实现的,所以一直也没做接触,不知道现在是怎么样了。

    使用异步框架当然第一步还是看性能,我去 fastapi 官网看了一下教学,教学写的很友好,直接就推荐了 fastapi+uvicorn 的部署方案。

    官网上写了 fastapi 是最快的框架之一,我们都知道 python 异步刚出的时候有很多昙花一现的框架,比如 Vibora,japronto 这些,性能做的都非常夸张,单例可以达到十万 qps,实际上是用 py 胶水封装了一下 c 框架而已,性能高也很正常,可惜这些开发社区做了 demo 出来以后都不怎么活跃了,bug 不修,没法投入生产级。

    倒是 aiohttp 这个一上来看起来就很弱的,表现也不怎么亮眼的,一直更新到现在,投入生产级也完全没问题了,说句题外话,我个人使用起来主要优势就是用的熟,想实现什么效果几乎以前都做过,很快都能找到解决方案,所以学习 fastapi 对我来说倒是要考虑学习成本问题。

    =====================================================================

    说回正题,关于压力测试,我在虚拟机上用 wrk 进行压测,测试结果 fastapi 其实表现并不好,想问一下各位 fastapi 用的比较熟练的大佬,是我部署错误,还是它的性能表现就是这样的。

    另外想问一下切换到生产级服务的话,fastapi 这条路线目前坑度怎么样,比如 web 部署里的一些常用插件,cors,basic auth,jwt 等等,还有中间件开发,支持 ws 协议等等,目前这些坑都踩的差不多了吗?这个框架从名字来看就可以看出是为 api 设计的,如果用来一体化部署 spa 之类的,有额外的坑吗?

    谢谢大家

    =====================================================================

    附一些压测数据

    #笔记本随手测一下,虚拟机给了 8 核心,所以用 16 线程 500 并发进行测试
    
    # fastapi + uvicorn 部署,单进程
    Running 20s test @ http://127.0.0.1:8000
      16 threads and 500 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency    20.88ms    3.51ms  50.90ms   95.22%
        Req/Sec     0.96k    66.29     1.21k    83.54%
      95737 requests in 20.01s, 13.70MB read
    Requests/sec:   4784.35
    Transfer/sec:    700.83KB
    
    # aiohttp + 自带服务部署,单进程
    Running 20s test @ http://127.0.0.1:8000
      16 threads and 500 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency    12.90ms    2.94ms  58.01ms   94.81%
        Req/Sec     1.57k   156.69     1.82k    80.90%
      156446 requests in 20.05s, 24.32MB read
    Requests/sec:   7803.19
    Transfer/sec:      1.21MB
    
    # aiohttp + gunicorn(uvloop 模式) ,单进程
    Running 20s test @ http://127.0.0.1:8000
      16 threads and 500 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency     6.12ms    1.08ms  23.27ms   90.35%
        Req/Sec     3.28k   216.93     5.05k    72.67%
      327908 requests in 20.10s, 50.97MB read
    Requests/sec:  16315.63
    Transfer/sec:      2.54MB
    

    一般来说这些框架都会自带一个 web 服务,可以用来做测试什么的,一般因为稳定性,性能等等原因,都不会用在生产环境部署。但是根据这个单线程测试,fastapi 实际上单进程只有 aiohttp 的 60%,如果用 gunicorn 部署的话(值得吐槽的是 gunicorn 似乎本身也是 python 中不算快的部署方式。。),fastapi+uvicorn 的组合只有 aiohttp+gunicorn 25%左右的性能

    然后是多进程 prefork 测试,采用 8 线程部署服务。

    # fastapi 8 线程
    Running 20s test @ http://127.0.0.1:8000
      16 threads and 500 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency     6.53ms    1.94ms  33.58ms   79.44%
        Req/Sec     3.09k   476.62     4.02k    60.60%
      307858 requests in 20.07s, 28.48MB read
    Requests/sec:  15341.09
    Transfer/sec:      1.42MB
    
    # fastapi 增大 echo 报文长度
    Running 20s test @ http://127.0.0.1:8000
      16 threads and 500 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency     9.56ms    6.59ms  51.53ms   61.89%
        Req/Sec     2.18k     1.18k    6.39k    87.10%
      217184 requests in 20.05s, 24.85MB read
    Requests/sec:  10834.00
    Transfer/sec:      1.24MB
    
    # aiohttp 8 线程
    Running 20s test @ http://127.0.0.1:8000
      16 threads and 500 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency     2.77ms    1.34ms  16.60ms   65.92%
        Req/Sec     7.30k     2.28k   19.59k    73.53%
      726936 requests in 20.10s, 123.40MB read
    Requests/sec:  36170.63
    Transfer/sec:      6.14MB
    

    可以看到同样地,fastapi 性能只有 aiohttp 的三成左右。另外值得吐槽的是使用长报文测试下,fastapi 的 echo 性能衰退又有点厉害啊,直接掉三成。

    26 条回复    2021-01-28 03:38:35 +08:00
    TypeError
        1
    TypeError  
       2021-01-24 21:09:48 +08:00
    https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=fortune&l=zijzen-1r

    看这个 benchmark,FastAPI 比 aiohttp 高不少,
    我感觉 Python 异步框架没必要追求极致性能,我倾向顺手和生态成熟的,
    目前线上生产环境 Tornado 、aiohttp 都有,更喜欢 Tornado,
    接下来可能迁到 Go 了,性能方面还是静态语言优势大
    LeeReamond
        2
    LeeReamond  
    OP
       2021-01-24 22:36:27 +08:00
    @TypeError 不知道它这个怎么测试的,aiohttp 倒是和我测的差不多,即使 prefork 也有一个上限,大概四万 qps 左右,并不能 8 线程就 8 倍可用性。fastapi 我已经用 uvicorn 部署了,理论上已经是最佳配置了,不知道它这个怎么搞出来五万 qps 的。

    go 和 py 这种属于无谓争论,用 py 上生产肯定还是看重开发效率,py 内部换个框架都要问一问有没有坑的问题,换 go 就更苦了,go 目前这个生态。。。
    Carry0317
        3
    Carry0317  
       2021-01-24 23:02:36 +08:00
    请教 我想提供一个 gpu 的服务 用哪种方式性能最高
    so1n
        4
    so1n  
       2021-01-25 00:07:57 +08:00
    可能有些配置没写对吧.
    fastapi 的底子就是 starlette, 但为了做类型转化和参数校验,性能会比 starlette 略差一点. 然后 aio 的库相比其他同类的库都不太好(可能是出现太早的原因)
    LeeReamond
        5
    LeeReamond  
    OP
       2021-01-25 01:46:12 +08:00
    @Carry0317 这篇帖子跟 GPU 没什么关系吧。你的意思是想提供一个高可用的 gpu 服务接口?
    LeeReamond
        6
    LeeReamond  
    OP
       2021-01-25 01:52:46 +08:00
    @so1n 就是很简单的按照 quick start 定义了一个异步函数,绑定到'/',返回一个 echo,没有其他任何东西。部署方面,8 个 fork,关闭 log 。uvicorn main:app --worker 8 --log-level error,不知道有哪里配置还能提高性能的。

    aio 库性能差我觉得应该没有这个说法。python 的异步从一开始就没有什么黑魔法,当初 dableaz 在 pycon 花半小时就实现一个功能完整的 eventloop,可以完全替代原生进行 basic tcp socket programming 的。所以 eventloop 相同的情况下,实现方面完全可追溯,封装程度其实区别不大,影响也不大,理论上无所谓 aio 库慢与否,实践当中我也从没听说过有人说 aiolibs 里面的东西比同类慢。
    ManjusakaL
        7
    ManjusakaL  
       2021-01-25 02:00:39 +08:00 via iPhone
    直接 Gevent 不香么...
    asyncio 那么多💩,活着不好么😂😂🐶🐶
    LeeReamond
        8
    LeeReamond  
    OP
       2021-01-25 02:04:39 +08:00
    @ManjusakaL 我觉得你对屎可能有些误解。即使在 python3.5 时代,原生异步也并不屎,这种用户态完全可控、可预测的状态显然是更优的设计。gevent 无法做到以上任何一项,用户用脚投票也说明了这点。
    ManjusakaL
        9
    ManjusakaL  
       2021-01-25 02:38:52 +08:00 via iPhone
    @LeeReamond 很抱歉,可能用💩来形容不太合适,不过依旧只能用糟糕来形容.
    顺便指出几个误区

    1. Gevent 也是完全可控,可预测
    2. 原生 asyncio bug 到现在为止 bug 太多,随手举几个例子,BPO-30698 和 BPO-29406 这两个横跨 asyncio 到现在的会导致一些 https 链接泄漏的 bug 到现在依旧没有修
    3. 生态一如即往的糟糕,随手举个例子,aiomysql,目前 asyncio 生态中的 mysql lib,三个月没更新可以说是个 dead project 了. 当然你要说我用 thread Future 封个 mysqlclient 当我没说

    我自己应该是最早一批在国内推动 asyncio 上生产的人( 17-18 年)在给予厚望后,我写下了这篇文章 https://manjusaka.itscoder.com/posts/2018/10/05/why-i-dont-use-async/

    我可以很负责任的说,我这篇文章中写的大部分弊端依旧适用于 2021 年的现在
    ManjusakaL
        10
    ManjusakaL  
       2021-01-25 03:00:06 +08:00 via iPhone
    @LeeReamond BTW 你对于 asyncio 的可预测存在误解
    event loop 是无法真正意义上做到“可预测”的
    此处“可预测”指 A task 执行完后能预测下一个 task 是 B 还是 C
    无论是 asyncio 还是 Gevent 我们都只能做到一个基础的保证,即正常情况下,我能在一个切出点后能切入执行后续代码
    但是在生产环境中,配合 Python 的 GIL,能做到这点也是奢望
    随手举例时间,我们在请求 www.v2ex.com 的时候,会通过 gethostbyname ( Linux 下,参见 https://man7.org/linux/man-pages/man3/gethostbyname.3.html )来做 DNS 解析,而这个函数是不可调度函数,所以那么一旦 DNS 解析出现问题,那么可能炸整个 event loop 导致所有 task 不可调度. 而这样复杂的可能阻塞整个 event loop 调度情况还有很多,此处不一一列举. 诚然我们可以通过很多额外的手段来尽可能规避这种情况. 但是就其本身而言,无论 asyncio 还是 Gevent,其所要面对的问题都是一样的
    LeeReamond
        11
    LeeReamond  
    OP
       2021-01-25 03:31:16 +08:00
    @ManjusakaL 认真看完了,大佬确实经验丰富。我因为学习异步的时候已经出现原生异步了,所以对猴子补丁天生有不信任,承认错误。我们在简单的生产环境(非内部管理平台)中使用原生异步体验良好,可能有些过于信任。

    搜了一下你说的 BPO-30698,ssl 链接泄露应该如何理解,似乎不是一个导致明文泄露的恶性 bug,而是导致内存不能回收的问题,不知是否理解正确。在 17/18 年左右倒是听说过有人 aiohttp 框架出现 ssl 内存泄露,我从未遇到过类似问题,以为在新版中已经修好了。看了这个 issue,不理解如何复现。

    你在帖子中提到的同步异步混合,以及生态不支持 c 插件等问题,我个人理解这两个目前已经不是问题,我的理解中异步代码中首先不应存在同步内容,我从未体验过同步异步同时维护的复杂度。另外生态方面主要是接入后端,python 本身的阻塞实现倒是能用附带线程池的方式梭掉,顺带 cython 还能解决掉 gil,而后端方面,mysql 和 redis,oracle 也有异步连接方式,我使用 aiolibs 的库体验良好,可能是接入服务数少,我个人而言这方面没什么不满。

    另外大佬这么推崇猴子补丁,有没有 gevent 系列比较合适的入门文章,我想完整评估一下 gevent 相对于原生异步方案的性能和稳定性
    wdhwg001
        12
    wdhwg001  
       2021-01-25 04:15:11 +08:00 via iPhone
    fastapi 不是要用 gunicorn 套 uvicorn 吗? techempower 是开源的,可以去看他们的部署和代码。
    wdhwg001
        13
    wdhwg001  
       2021-01-25 04:34:19 +08:00 via iPhone
    另外 techempower 里的 fastapi 代码是有轻微作弊的,主要是 ujson,不过也不严重,你甚至可以用 orjson 跑的更高一点。

    我的观点是常量级差距都不用太在意的,fastapi 还有完善的 openapi 支持什么的,那些要更吸引人一点。
    wdhwg001
        14
    wdhwg001  
       2021-01-25 04:49:43 +08:00 via iPhone
    另外 asyncio 应该是大势所趋了,生态在逐步完善,但是距离 wsgi 时代还是有差距的,然而依旧是好兆头。
    其实单说 fastapi 也是问题多多的,缺少 session 支持是一个,不完全遵守 asgi 是一个,中间件还有闭包引用不可靠的问题,自带路由是遍历而不是树优化也是一个问题。但即使如此,fastapi 的设计也依然是比 flask 好一些的。
    而且其实更大的坑是 orm,gino 和 tortoise 都有各自的坑,django 的 async orm 还在难产,我这边项目用的 tortoise,设计上基本就是抄 django 了,没什么创新点。
    LeeReamond
        15
    LeeReamond  
    OP
       2021-01-25 05:00:33 +08:00
    @wdhwg001 gunicorn 套 uvicorn 怎么实现,感觉这两个不能互相套啊

    楼上说的 ssl 泄露的 issue 我看倒是确实没人理。印象中 17 年左右 stackoverflow 的 asyncio 区还是极端冷清的,不知道现在怎么样。我个人体验来讲,倒是 3.5 时代感觉原生异步的学习过程很底层,从生成器概念一步步概念学上来,最近两年倒是完全感觉在使用高级 api,完全没有底层的感觉了,基本和写同步代码没有任何区别,只是外面要套一层扳机而已。
    LeeReamond
        16
    LeeReamond  
    OP
       2021-01-25 05:02:18 +08:00
    @wdhwg001 orm 方面我是完全不做任何希望了,我觉得以 python 社区的生产能力 orm 大概是要永远难产下去了。我个人使用体验上倒是没体会到 orm 对开发速度有多大帮助,都是直接操作数据库,所以倒是感觉不很有所谓
    spcharc
        17
    spcharc  
       2021-01-25 05:35:05 +08:00
    aiohttp 库 contributor 路过,很惭愧只做了一点微小的工作(大概+366 −48 这样子)
    我感觉 aiohttp,一般不是配合 aiodns 来用吗?
    官方都提供了 pip install aiohttp[speedups]这种安装方式来捆绑销售 aiodns,不就是因为一旦 dns 服务不稳定,就可能阻塞整个 loop 嘛?
    另外官方也推荐搭配使用 uvloop,比 asyncio 自带 loop 速度快,也没有上面提到的 https 泄露之类的问题
    而且 loop (不管是 python 官方的还是第三方的)都提供了 run_in_executor 的吧,有可能长时间阻塞的函数都应该用这个来运行来避免 loop 阻塞啊
    LeeReamond
        18
    LeeReamond  
    OP
       2021-01-25 06:15:38 +08:00 via Android
    @spcharc 我印象中确实是有见过生产级部署以后出现莫名泄露的问题的帖子,大概几年前。我自己没遇到过。另外我对楼上说的猴子布丁原理上性能高于 libuv 仿品很好奇
    so1n
        19
    so1n  
       2021-01-25 09:24:04 +08:00 via Android
    @LeeReamond 我不是说性能,比如 aiohttp 的 client 就一堆隐藏坑,aioredis 停止更新,集群到现在都没支持等等
    Carry0317
        20
    Carry0317  
       2021-01-25 09:41:27 +08:00
    @LeeReamond 是的 高可用的 gpu 服务接口
    wdhwg001
        21
    wdhwg001  
       2021-01-25 12:02:44 +08:00 via iPhone
    @LeeReamond Django 系的 orm 大多数时候够用了,可以不用去手写 crud 的,除非你用到了很复杂的用法或者特殊的函数。totorise 作为 django orm 的残品其实也勉强够用,觉得遗憾的地方只是没有新东西,没有 flask 到 fastapi 的那种换代感。
    另外你倒是看一下源码啊。
    wdhwg001
        22
    wdhwg001  
       2021-01-25 12:05:06 +08:00 via iPhone
    @LeeReamond 另外这个泄露的问题如果用 uvloop 是不会出现的,同时只能通过重写 asyncio 解决,而重写现在还在进行中。
    LeeReamond
        23
    LeeReamond  
    OP
       2021-01-25 17:45:45 +08:00 via Android
    @so1n redis 集群不是自身特性,为什么需要客户端支持呢
    @Carry0317 目前按照我这个帖子,纯 py 的方法就是 uvloop+aiohttp+gunicorn 。楼上有老哥提到 fastapi 部署是 uvicorn+gunicorn 但他没说怎么做,我暂不理解。另外如果你的应用层封装简单,也可以试试 vibora,japronto 这类 c 库,应该可以获得最大转发效率,比较基础的使用上应该也没什么坑,大概吧。不过你这个 gpu 业务本身需要 python+http 转发本身就挺奇怪的
    wdhwg001
        24
    wdhwg001  
       2021-01-28 00:31:18 +08:00   ❤️ 1
    LeeReamond
        25
    LeeReamond  
    OP
       2021-01-28 01:11:11 +08:00
    @wdhwg001 感谢,我不太熟悉这个 benchmark 的项目,你不指路的话我都不知道你指的源码是这个。我按他的部署测了一下,确实是速度快一些,整体单进程和 prefork 的性能和 aiohttp+gunicorn+uvloop 几乎相当,可能 gunicorn 自己是 python 写的,性能极限就这么多了吧,在我的机器上测试 qps 并没有超过四万。

    大概是这样

    Thread Stats Avg Stdev Max +/- Stdev
    Latency 2.79ms 1.39ms 17.14ms 68.74%
    Req/Sec 7.31k 2.07k 12.89k 67.10%
    727813 requests in 20.02s, 85.37MB read
    Requests/sec: 36348.24
    Transfer/sec: 4.26MB

    作为对比,后面用 pyston 跑了一下,加入 jit 以后 fastapi 确实是更快一些
    fastapi+python3.8: Requests/sec: 36348.24
    fastapi+pyston2.1: Requests/sec: 45435.96

    aiohttp+python3.8: Requests/sec: 36170.63
    aiohttp+pyston2.1: Requests/sec: 42089.11


    所以 fastapi 相对于 aiohttp 或者 tornado 这些传统异步框架没有性能劣势,那么使用 fastapi 的优势是什么呢,除了比较火以外,用来做后端服务器似乎很合适,因为可以自动生成文档,不过这个其实也还好,不是那么的决定性,不太清楚还有没有什么其他优势。不过不管怎么说 2021 年感觉部署大型 web 应用,如果使用 py 的话,起码语言不应作为性能瓶颈看待了。不知道当初知乎、豆瓣这些网站,都是 py2 时代码出来的项目,后面都传出来过重构的新闻,如果用现在的 python 的话应该是不会重构了吧
    wdhwg001
        26
    wdhwg001  
       2021-01-28 03:38:35 +08:00
    @LeeReamond FastAPI 主要是省事和优雅,它的 API 总体上是 Flask 风格的,可以提供 OpenAPI,也可以用 Pydantic 做自动的输入验证,还可以用依赖注入的方式比较方便地实现鉴权,总的来说就是好用,并且性能代价不高。

    不过大型 Web 应用估计还是不适合用 Python,但是能在被迫用 Java 或者 Go 重写之前撑更久。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   3670 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 30ms · UTC 04:55 · PVG 12:55 · LAX 21:55 · JFK 00:55
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.